Esta página aborda os benefícios e o uso básico das configurações do Starlark, a API do Bazel para personalizar a criação do seu projeto. Ele inclui como definir as configurações do build e fornece exemplos.
Isso permite:
- definir flags personalizadas para seu projeto, eliminando a necessidade de
--define
. - transições de gravação para configurar dependências em configurações diferentes dos pais (como
--compilation_mode=opt
ou--cpu=arm
). - usar padrões melhores para aplicar regras (como criar
//my:android_app
automaticamente com um SDK especificado);
e muito mais, tudo de arquivos .bzl (nenhuma versão do Bazel é necessária). Consulte o
repositório bazelbuild/examples
para
exemplos.
Configurações de build definidas pelo usuário
Uma configuração de build é uma peça única de
informações de
configuração. Pense em uma configuração como um mapa de chave-valor. Definir --cpu=ppc
e --copt="-DFoo"
produz uma configuração parecida com
{cpu: ppc, copt: "-DFoo"}
. Cada entrada é uma configuração de build.
Sinalizações tradicionais, como cpu
e copt
, são configurações nativas.
As chaves deles são definidas e os valores são definidos dentro do código Java bazel nativo.
Os usuários do Bazel só podem ler e gravar por meio da linha de comando
e outras APIs mantidas de maneira nativa. A alteração de flags nativas e as APIs
que as expõem requer uma versão do Bazel. As configurações de build
definidas pelo usuário são definidas em arquivos .bzl
e, portanto, não precisam de uma versão Bazel para
registrar mudanças. Elas também podem ser definidas por meio da linha de comando
(se estiverem designadas como flags
, veja mais abaixo), mas também podem ser
definidas por meio de transições definidas pelo usuário.
Como definir configurações de build
O parâmetro rule()
build_setting
As configurações do build são regras como qualquer outra e são diferenciadas usando o
atributo build_setting
da função rule()
do Starlark.
# example/buildsettings/build_settings.bzl
string_flag = rule(
implementation = _impl,
build_setting = config.string(flag = True)
)
O atributo build_setting
usa uma função que designa o tipo da
configuração do build. O tipo é limitado a um conjunto de tipos básicos de Starlark, como
bool
e string
. Consulte a documentação do módulo config
para mais detalhes. Uma digitação mais complicada pode ser feita na função de implementação da regra. Saiba mais sobre isso abaixo.
As funções do módulo config
usam um parâmetro booleano opcional, flag
, que é definido como falso por padrão. Se flag
for definido como verdadeiro, a configuração do build poderá ser definida na linha de comando pelos usuários e internamente pelos criadores de regras com valores padrão e transições.
Nem todas as configurações devem ser definidas pelos usuários. Por exemplo, se você, como um editor de regras, tiver um modo de depuração que gostaria de ativar dentro das regras de teste, não vai querer dar aos usuários a capacidade de ativar indiscriminadamente esse recurso dentro de outras regras que não sejam de teste.
Como usar ctx.build_setting_value
Como todas as regras, as de configuração de build têm funções de implementação.
O valor básico do tipo Starlark das configurações do build pode ser acessado pelo
método ctx.build_setting_value
. Esse método só está disponível para objetos
ctx
das regras de configuração do build. Esses métodos de
implementação podem encaminhar diretamente o valor das configurações do build ou realizar outras tarefas nele, como verificação de tipo ou criação de structs mais complexa. Confira como você
implementaria uma configuração de build do tipo enum
:
# example/buildsettings/build_settings.bzl
TemperatureProvider = provider(fields = ['type'])
temperatures = ["HOT", "LUKEWARM", "ICED"]
def _impl(ctx):
raw_temperature = ctx.build_setting_value
if raw_temperature not in temperatures:
fail(str(ctx.label) + " build setting allowed to take values {"
+ ", ".join(temperatures) + "} but was set to unallowed value "
+ raw_temperature)
return TemperatureProvider(type = raw_temperature)
temperature = rule(
implementation = _impl,
build_setting = config.string(flag = True)
)
Como definir sinalizações de string de vários conjuntos
As configurações de string têm um parâmetro allow_multiple
extra que permite que a
sinalização seja definida várias vezes na linha de comando ou em bazelrcs. O valor padrão
ainda é definido com um atributo do tipo string:
# example/buildsettings/build_settings.bzl
allow_multiple_flag = rule(
implementation = _impl,
build_setting = config.string(flag = True, allow_multiple = True)
)
# example/BUILD
load("//example/buildsettings:build_settings.bzl", "allow_multiple_flag")
allow_multiple_flag(
name = "roasts",
build_setting_default = "medium"
)
Cada configuração da flag é tratada como um valor único:
$ bazel build //my/target --//example:roasts=blonde \
--//example:roasts=medium,dark
O exemplo acima é analisado em relação a {"//example:roasts": ["blonde", "medium,dark"]}
, e
ctx.build_setting_value
retorna a lista ["blonde", "medium,dark"]
.
Como instanciar configurações de build
As regras definidas com o parâmetro build_setting
têm um atributo build_setting_default
obrigatório implícito. Esse atributo assume o mesmo tipo
declarado pelo parâmetro build_setting
.
# example/buildsettings/build_settings.bzl
FlavorProvider = provider(fields = ['type'])
def _impl(ctx):
return FlavorProvider(type = ctx.build_setting_value)
flavor = rule(
implementation = _impl,
build_setting = config.string(flag = True)
)
# example/BUILD
load("//example/buildsettings:build_settings.bzl", "flavor")
flavor(
name = "favorite_flavor",
build_setting_default = "APPLE"
)
Configurações predefinidas
A biblioteca Skylib inclui um conjunto de configurações predefinidas que você pode instanciar sem precisar escrever um Starlark personalizado.
Por exemplo, para definir uma configuração que aceita um conjunto limitado de valores de string:
# example/BUILD
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
string_flag(
name = "myflag",
values = ["a", "b", "c"],
build_setting_default = "a",
)
Para conferir uma lista completa, consulte Regras comuns de configuração de build.
Como usar configurações de build
Dependendo das configurações do build
Se um destino quiser ler uma informação de configuração, ele poderá depender diretamente da definição do build por uma dependência de atributo normal.
# example/rules.bzl
load("//example/buildsettings:build_settings.bzl", "FlavorProvider")
def _rule_impl(ctx):
if ctx.attr.flavor[FlavorProvider].type == "ORANGE":
...
drink_rule = rule(
implementation = _rule_impl,
attrs = {
"flavor": attr.label()
}
)
# example/BUILD
load("//example:rules.bzl", "drink_rule")
load("//example/buildsettings:build_settings.bzl", "flavor")
flavor(
name = "favorite_flavor",
build_setting_default = "APPLE"
)
drink_rule(
name = "my_drink",
flavor = ":favorite_flavor",
)
As linguagens podem criar um conjunto canônico de configurações de build do qual todas as regras
desse idioma dependem. O conceito nativo de fragments
não existe mais
como um objeto fixado no código no universo de configuração do Starlark, mas uma maneira de
converter esse conceito é usando conjuntos de atributos implícitos comuns. Exemplo:
# kotlin/rules.bzl
_KOTLIN_CONFIG = {
"_compiler": attr.label(default = "//kotlin/config:compiler-flag"),
"_mode": attr.label(default = "//kotlin/config:mode-flag"),
...
}
...
kotlin_library = rule(
implementation = _rule_impl,
attrs = dicts.add({
"library-attr": attr.string()
}, _KOTLIN_CONFIG)
)
kotlin_binary = rule(
implementation = _binary_impl,
attrs = dicts.add({
"binary-attr": attr.label()
}, _KOTLIN_CONFIG)
Como usar configurações de build na linha de comando
Assim como na maioria das sinalizações nativas, você pode usar a linha de comando para definir configurações de build
marcadas como sinalizações. O nome da configuração
de build é o caminho completo de destino usando a sintaxe name=value
:
$ bazel build //my/target --//example:string_flag=some-value # allowed
$ bazel build //my/target --//example:string_flag some-value # not allowed
É possível usar uma sintaxe booleana especial:
$ bazel build //my/target --//example:boolean_flag
$ bazel build //my/target --no//example:boolean_flag
Como usar aliases de configuração de compilação
Você pode definir um alias para o caminho de destino da configuração da versão para facilitar a leitura na linha de comando. Os aliases funcionam de maneira semelhante às sinalizações nativas e também usam a sintaxe da opção de traço duplo.
Defina um alias adicionando --flag_alias=ALIAS_NAME=TARGET_PATH
ao .bazelrc
. Por exemplo, para definir um alias como coffee
:
# .bazelrc
build --flag_alias=coffee=//experimental/user/starlark_configurations/basic_build_setting:coffee-temp
Prática recomendada: definir um alias várias vezes faz com que o mais recente tenha precedência. Use nomes de alias exclusivos para evitar resultados de análise acidentais.
Para usar o alias, digite-o no lugar do caminho de destino da configuração do build.
Com o exemplo acima de coffee
definido no .bazelrc
do usuário:
$ bazel build //my/target --coffee=ICED
em vez de
$ bazel build //my/target --//experimental/user/starlark_configurations/basic_build_setting:coffee-temp=ICED
Prática recomendada: embora seja possível definir aliases na linha de comando, deixá-los em um .bazelrc
reduz a sobrecarga da linha de comando.
Configurações de build com tipo de rótulo
Ao contrário de outras configurações do build, as configurações digitadas por rótulo não podem ser definidas usando o
parâmetro de regra build_setting
. Em vez disso, o Bazel tem duas regras integradas:
label_flag
e label_setting
. Essas regras encaminham os provedores do
destino real para o qual a configuração do build é definida. label_flag
e
label_setting
podem ser lidos/gravados por transições e label_flag
pode ser definido
pelo usuário da mesma forma que outras regras de build_setting
. A única diferença é que eles
não podem ser definidos de forma personalizada.
As configurações tipadas de rótulo vão substituir a funcionalidade dos padrões
vinculados tardias. Os atributos padrão finais são do tipo rótulo com valores finais que podem ser afetados pela configuração. No Starlark, isso vai substituir
a API
configuration_field
.
# example/rules.bzl
MyProvider = provider(fields = ["my_field"])
def _dep_impl(ctx):
return MyProvider(my_field = "yeehaw")
dep_rule = rule(
implementation = _dep_impl
)
def _parent_impl(ctx):
if ctx.attr.my_field_provider[MyProvider].my_field == "cowabunga":
...
parent_rule = rule(
implementation = _parent_impl,
attrs = { "my_field_provider": attr.label() }
)
# example/BUILD
load("//example:rules.bzl", "dep_rule", "parent_rule")
dep_rule(name = "dep")
parent_rule(name = "parent", my_field_provider = ":my_field_provider")
label_flag(
name = "my_field_provider",
build_setting_default = ":dep"
)
Configurações do build e select()
Os usuários podem definir atributos nas configurações do build usando
select()
. Os destinos de configuração do build podem ser transmitidos para o atributo flag_values
de
config_setting
. O valor para corresponder à configuração é transmitido como um
String
e, em seguida, analisado para o tipo de configuração de build para correspondência.
config_setting(
name = "my_config",
flag_values = {
"//example:favorite_flavor": "MANGO"
}
)
Transições definidas pelo usuário
Uma transição de configuração mapeia a transformação de um destino configurado para outro dentro do gráfico do build.
Definição
As transições definem mudanças de configuração entre as regras. Por exemplo, uma solicitação como "compile minha dependência para uma CPU diferente da mãe" é processada por uma transição.
Formalmente, uma transição é uma função de uma configuração de entrada para uma ou mais
configurações de saída. A maioria das transições é 1:1, como "substituir a configuração
de entrada com --cpu=ppc
". As transições 1:2+ também podem existir, mas têm
restrições especiais.
No Starlark, as transições são definidas de forma semelhante às regras, com uma
função
transition()
definida e uma função de implementação.
# example/transitions/transitions.bzl
def _impl(settings, attr):
_ignore = (settings, attr)
return {"//example:favorite_flavor" : "MINT"}
hot_chocolate_transition = transition(
implementation = _impl,
inputs = [],
outputs = ["//example:favorite_flavor"]
)
A função transition()
recebe uma função de implementação, um conjunto de
configurações de build para leitura(inputs
) e um conjunto de configurações de build para gravação
(outputs
). A função de implementação tem dois parâmetros, settings
e
attr
. settings
é um dicionário {String
:Object
} de todas as configurações declaradas
no parâmetro inputs
para transition()
.
attr
é um dicionário de atributos e valores da regra a que a
transição está anexada. Quando anexados como uma
transição de borda de saída, os valores desses
atributos são todos configurados na resolução pós-select(). Quando anexado como
uma transição de entrada, a attr
não
inclui nenhum atributo que usa um seletor para resolver o valor. Se uma
transição de borda de entrada em --foo
ler o atributo bar
e, em seguida,
selecionar o --foo
para definir o atributo bar
, haverá uma chance para a
transição de borda de entrada ler o valor errado de bar
na transição.
A função de implementação precisa retornar um dicionário (ou lista de
dicionários, no caso de
transições com várias configurações de saída)
de novos valores de configurações de build a serem aplicados. Os conjuntos de chaves do dicionário retornados precisam conter exatamente o conjunto de configurações de build transmitido para o parâmetro outputs
da função de transição. Isso é válido mesmo que uma configuração de compilação não seja realmente alterada durante a transição. O valor original precisa ser explicitamente transmitido no dicionário retornado.
Como definir transições 1:2 ou mais
A transição de borda de saída pode mapear uma única configuração de entrada para duas ou mais configurações de saída. Isso é útil para definir regras que agrupem código de multiarquitetura.
As transições 1:2+ são definidas com o retorno de uma lista de dicionários na função de implementação da transição.
# example/transitions/transitions.bzl
def _impl(settings, attr):
_ignore = (settings, attr)
return [
{"//example:favorite_flavor" : "LATTE"},
{"//example:favorite_flavor" : "MOCHA"},
]
coffee_transition = transition(
implementation = _impl,
inputs = [],
outputs = ["//example:favorite_flavor"]
)
Eles também podem definir chaves personalizadas que a função de implementação de regra pode usar para ler dependências individuais:
# example/transitions/transitions.bzl
def _impl(settings, attr):
_ignore = (settings, attr)
return {
"Apple deps": {"//command_line_option:cpu": "ppc"},
"Linux deps": {"//command_line_option:cpu": "x86"},
}
multi_arch_transition = transition(
implementation = _impl,
inputs = [],
outputs = ["//command_line_option:cpu"]
)
Como anexar transições
As transições podem ser anexadas em dois locais: bordas de entrada e bordas de saída. Na prática, isso significa que as regras podem fazer a transição da própria configuração (transição de entrada) e fazer a transição das configurações das dependências (transição da borda de saída).
OBSERVAÇÃO: atualmente, não há como anexar transições do Starlark a regras nativas. Se precisar fazer isso, entre em contato com bazel-discuss@googlegroups.com para encontrar soluções alternativas.
Transições de borda de entrada
As transições de borda recebidas são ativadas anexando um objeto transition
(criado por transition()
) ao parâmetro cfg
do rule()
:
# example/rules.bzl
load("example/transitions:transitions.bzl", "hot_chocolate_transition")
drink_rule = rule(
implementation = _impl,
cfg = hot_chocolate_transition,
...
As transições de borda recebidas precisam ser de 1:1.
Transições de borda de saída
As transições de borda de saída são ativadas anexando um objeto transition
(criado por transition()
) ao parâmetro cfg
de um atributo:
# example/rules.bzl
load("example/transitions:transitions.bzl", "coffee_transition")
drink_rule = rule(
implementation = _impl,
attrs = { "dep": attr.label(cfg = coffee_transition)}
...
As transições de borda de saída podem ser 1:1 ou 1:2+.
Consulte Como acessar atributos com transições para saber como ler essas chaves.
Transições em opções nativas
As transições do Starlark também podem declarar leituras e gravações nas opções de configuração do build nativas usando um prefixo especial no nome da opção.
# example/transitions/transitions.bzl
def _impl(settings, attr):
_ignore = (settings, attr)
return {"//command_line_option:cpu": "k8"}
cpu_transition = transition(
implementation = _impl,
inputs = [],
outputs = ["//command_line_option:cpu"]
Opções nativas incompatíveis
O Bazel não oferece suporte à transição de --define
com
"//command_line_option:define"
. Em vez disso, use uma
configuração de build personalizada. Em geral, novos usos de
--define
não são recomendados em favor das configurações de build.
O Bazel não oferece suporte à transição em --config
. Isso ocorre porque --config
é
uma sinalização de "expansão" que se expande para outras sinalizações.
É importante destacar que --config
pode incluir sinalizações que não afetam a configuração do build,
como
--spawn_strategy
. Por padrão, o Bazel não pode vincular essas sinalizações a destinos individuais. Isso significa
que não há uma maneira coerente de aplicá-las em transições.
Como solução alternativa, você pode discriminar explicitamente as sinalizações que fazem parte da
configuração na transição. Isso exige manter a expansão da --config
em dois locais, que é uma falha conhecida na interface.
As transições ao permitirem várias configurações de build
Ao definir configurações de build que permitem vários valores, o valor da configuração precisa ser definido com uma lista.
# example/buildsettings/build_settings.bzl
string_flag = rule(
implementation = _impl,
build_setting = config.string(flag = True, allow_multiple = True)
)
# example/BUILD
load("//example/buildsettings:build_settings.bzl", "string_flag")
string_flag(name = "roasts", build_setting_default = "medium")
# example/transitions/rules.bzl
def _transition_impl(settings, attr):
# Using a value of just "dark" here will throw an error
return {"//example:roasts" : ["dark"]},
coffee_transition = transition(
implementation = _transition_impl,
inputs = [],
outputs = ["//example:roasts"]
)
Transições de ambiente autônomo
Se uma transição retornar {}
, []
ou None
, essa é uma forma abreviada para manter todas
as configurações nos valores originais. Isso pode ser mais conveniente do que definir explicitamente
cada saída para si mesma.
# example/transitions/transitions.bzl
def _impl(settings, attr):
_ignore = (attr)
if settings["//example:already_chosen"] is True:
return {}
return {
"//example:favorite_flavor": "dark chocolate",
"//example:include_marshmallows": "yes",
"//example:desired_temperature": "38C",
}
hot_chocolate_transition = transition(
implementation = _impl,
inputs = ["//example:already_chosen"],
outputs = [
"//example:favorite_flavor",
"//example:include_marshmallows",
"//example:desired_temperature",
]
)
Como acessar atributos com transições
Ao anexar uma transição a uma borda de saída
(independentemente de a transição ser 1:1 ou 1:2+), ctx.attr
é forçado a ser uma lista,
caso ainda não seja. A ordem dos elementos nessa lista não é especificada.
# example/transitions/rules.bzl
def _transition_impl(settings, attr):
return {"//example:favorite_flavor" : "LATTE"},
coffee_transition = transition(
implementation = _transition_impl,
inputs = [],
outputs = ["//example:favorite_flavor"]
)
def _rule_impl(ctx):
# Note: List access even though "dep" is not declared as list
transitioned_dep = ctx.attr.dep[0]
# Note: Access doesn't change, other_deps was already a list
for other_dep in ctx.attr.other_deps:
# ...
coffee_rule = rule(
implementation = _rule_impl,
attrs = {
"dep": attr.label(cfg = coffee_transition)
"other_deps": attr.label_list(cfg = coffee_transition)
})
Se a transição for 1:2+
e definir chaves personalizadas, ctx.split_attr
poderá ser usado
para ler as dependências individuais de cada chave:
# example/transitions/rules.bzl
def _impl(settings, attr):
_ignore = (settings, attr)
return {
"Apple deps": {"//command_line_option:cpu": "ppc"},
"Linux deps": {"//command_line_option:cpu": "x86"},
}
multi_arch_transition = transition(
implementation = _impl,
inputs = [],
outputs = ["//command_line_option:cpu"]
)
def _rule_impl(ctx):
apple_dep = ctx.split_attr.dep["Apple deps"]
linux_dep = ctx.split_attr.dep["Linux deps"]
# ctx.attr has a list of all deps for all keys. Order is not guaranteed.
all_deps = ctx.attr.dep
multi_arch_rule = rule(
implementation = _rule_impl,
attrs = {
"dep": attr.label(cfg = multi_arch_transition)
})
Veja um exemplo completo aqui.
Integração com plataformas e conjuntos de ferramentas
Atualmente, muitas sinalizações nativas, como --cpu
e --crosstool_top
, estão relacionadas à
resolução do conjunto de ferramentas. No futuro, as transições explícitas nesses tipos de
flags provavelmente serão substituídas pela transição na
plataforma de destino.
Considerações sobre memória e desempenho
Adicionar transições e, portanto, novas configurações ao build tem um custo: gráficos de build maiores, gráficos de build menos compreensíveis e builds mais lentos. Considere esses custos ao considerar o uso de transições nas regras de build. Veja abaixo um exemplo de como uma transição pode criar um crescimento exponencial do gráfico de compilação.
Builds com comportamento inadequado: um estudo de caso
Figura 1. Gráfico de escalonabilidade mostrando um destino de nível superior e as dependências dele.
Este gráfico mostra um destino de nível superior, //pkg:app
, que depende de dois destinos, //pkg:1_0
e //pkg:1_1
. Os dois destinos dependem de dois destinos, //pkg:2_0
e
//pkg:2_1
. Os dois destinos dependem de dois destinos, //pkg:3_0
e //pkg:3_1
.
Isso continua até //pkg:n_0
e //pkg:n_1
, que dependem de um único
destino, //pkg:dep
.
A criação de //pkg:app
exige \(2n+2\) destinos:
//pkg:app
//pkg:dep
//pkg:i_0
e//pkg:i_1
para \(i\) em \([1..n]\)
Imagine que você implement uma sinalização
--//foo:owner=<STRING>
e //pkg:i_b
se aplica
depConfig = myConfig + depConfig.owner="$(myConfig.owner)$(b)"
Em outras palavras, //pkg:i_b
anexa b
ao valor antigo de --owner
para todas as dependências.
Isso produz os seguintes destinos configurados:
//pkg:app //foo:owner=""
//pkg:1_0 //foo:owner=""
//pkg:1_1 //foo:owner=""
//pkg:2_0 (via //pkg:1_0) //foo:owner="0"
//pkg:2_0 (via //pkg:1_1) //foo:owner="1"
//pkg:2_1 (via //pkg:1_0) //foo:owner="0"
//pkg:2_1 (via //pkg:1_1) //foo:owner="1"
//pkg:3_0 (via //pkg:1_0 → //pkg:2_0) //foo:owner="00"
//pkg:3_0 (via //pkg:1_0 → //pkg:2_1) //foo:owner="01"
//pkg:3_0 (via //pkg:1_1 → //pkg:2_0) //foo:owner="10"
//pkg:3_0 (via //pkg:1_1 → //pkg:2_1) //foo:owner="11"
...
//pkg:dep
produz \(2^n\) destinos configurados: config.owner=
"\(b_0b_1...b_n\)" para todos \(b_i\) em \(\{0,1\}\).
Isso torna o gráfico de build exponencialmente maior do que o de destino, com consequências de memória e desempenho correspondentes.
O que fazer: adicionar estratégias para medir e atenuar esses problemas.
Leia mais
Para saber mais sobre como modificar as configurações do build, consulte:
- Configuração do build do Starlark
- Roteiro de configuração do Bazel
- Conjunto completo de exemplos completos