Esta página aborda os benefícios e o uso básico das configurações do Starlark, a API do Bazel para personalizar a maneira como seu projeto é criado. Ela inclui como definir configurações de build e fornece exemplos.
Isso permite:
- definir flags personalizadas para seu projeto, tornando desnecessário o uso de
--define - escrever
transições para configurar dependências em
configurações diferentes das dos pais
(como
--compilation_mode=optou--cpu=arm) - incorporar padrões melhores às regras (como criar automaticamente
//my:android_appcom um SDK especificado)
e muito mais, tudo completamente em arquivos .bzl (nenhuma versão do Bazel é necessária). Consulte o
bazelbuild/examples repo para
exemplos.
Configurações de build definidas pelo usuário
Uma configuração de build é uma única informação de
configuração. Pense em uma configuração como um mapa de chave-valor. Definir --cpu=ppc
e --copt="-DFoo" produz uma configuração semelhante a
{cpu: ppc, copt: "-DFoo"}. Cada entrada é uma configuração de build.
Flags tradicionais como cpu e copt são configurações nativas —
as chaves são definidas e os valores são definidos no código Java nativo do Bazel.
Os usuários do Bazel só podem ler e gravar esses valores pela linha de comando
e outras APIs mantidas nativamente. A mudança de flags nativas e das APIs
que as expõem exige 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 do Bazel para
registrar mudanças. Elas também podem ser definidas pela linha de comando
(se forem designadas como flags, consulte mais informações abaixo), mas também podem ser
definidas por transições definidas pelo usuário.
Como definir configurações de build
O parâmetro build_setting rule()
As configurações de build são regras como qualquer outra e são diferenciadas usando a
função rule() do Starlark build_setting
attribute.
# 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 de build. O tipo é limitado a um conjunto de tipos básicos do Starlark, como
bool e string. Consulte a documentação do módulo config
para mais detalhes. Tipos mais complicados podem ser
feitos na função de implementação da regra. Mais informações abaixo.
As funções do módulo config usam um parâmetro booleano opcional, flag,
que é definido como "false" por padrão. Se flag estiver definido como "true", a configuração de build
poderá ser definida na linha de comando pelos usuários, bem como internamente pelos autores de regras
usando valores padrão e transições.
Nem todas as configurações podem ser definidas pelos usuários. Por exemplo, se você, como autor de regras
, tiver algum modo de depuração que gostaria de ativar nas regras de teste,
não vai querer dar aos usuários a capacidade de ativar esse
recurso indiscriminadamente em outras regras que não sejam de teste.
Como usar ctx.build_setting_value
Como todas as regras, as regras de configuração de build têm funções de implementação.
O valor do tipo Starlark básico das configurações de build pode ser acessado pelo método
ctx.build_setting_value. Esse método só está disponível para
ctx objetos de regras de configuração de build. Esses métodos de implementação podem encaminhar diretamente o valor das configurações de build ou fazer um trabalho adicional nele, como verificação de tipo ou criação de struct mais complexa. Confira como
implementar 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 flags de string de vários conjuntos
As configurações de string têm um parâmetro allow_multiple adicional que permite que a
flag 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 único valor:
$ bazel build //my/target --//example:roasts=blonde \
--//example:roasts=medium,dark
O código acima é analisado como {"//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 obrigatório implícito
build_setting_default. Esse atributo assume o mesmo tipo as
declarado pelo build_setting parâmetro.
# 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 Skylib Skylib inclui um conjunto de configurações predefinidas que podem ser instanciadas sem precisar escrever um Starlark personalizado.
Por exemplo, para definir uma configuração que aceite 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 de build
Se um destino quiser ler uma informação de configuração, ele poderá depender diretamente da configuração de 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 querer criar um conjunto canônico de configurações de build de que todas as regras
dessa linguagem dependam. Embora o conceito nativo de fragments não exista mais como um objeto codificado no mundo da configuração do Starlark, uma maneira de traduzir esse conceito seria usar conjuntos de atributos implícitos comuns. Por
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
Semelhante à maioria das flags nativas, é possível usar a linha de comando para definir configurações de build
marcadas como flags. O nome da configuração de build é o caminho completo do 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
A sintaxe booleana especial é compatível:
$ bazel build //my/target --//example:boolean_flag
$ bazel build //my/target --no//example:boolean_flag
Como usar aliases de configuração de build
É possível definir um alias para o caminho de destino da configuração de build para facilitar a leitura na linha de comando. Os aliases funcionam de maneira semelhante às flags nativas e também usam a sintaxe de opção de traço duplo.
Defina um alias adicionando --flag_alias=ALIAS_NAME=TARGET_PATH
ao seu .bazelrc . Por exemplo, para definir um alias para coffee:
# .bazelrc
build --flag_alias=coffee=//experimental/user/starlark_configurations/basic_build_setting:coffee-temp
Prática recomendada: definir um alias várias vezes resulta na precedência do mais recente. Use nomes de alias exclusivos para evitar resultados de análise não intencionais.
Para usar o alias, digite-o no lugar do caminho de destino da configuração de 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 confusão na linha de comando.
Configurações de build do tipo marcador
Ao contrário de outras configurações de build, as configurações do tipo marcador não podem ser definidas usando o
build_setting parâmetro de regra. 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 de build está definida. label_flag e
label_setting podem ser lidos/gravados por transições, e label_flag pode ser definido
pelo usuário, assim como outras regras build_setting. A única diferença é que elas
não podem ser definidas de forma personalizada.
As configurações do tipo marcador vão substituir a funcionalidade dos padrões de vinculação tardia. Os atributos padrão de vinculação tardia são atributos do tipo marcador cujos
valores finais podem ser afetados pela configuração. No Starlark, isso vai substituir
a configuration_field
API.
# 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 de build e select()
Os usuários podem configurar atributos nas configurações de build usando
select(). Os destinos de configuração de build podem ser transmitidos ao atributo flag_values de
config_setting. O valor a ser correspondido à configuração é transmitido como a
String e, em seguida, analisado para o tipo da 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 no gráfico de build.
Como definir
As transições definem mudanças de configuração entre 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
por --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 maneira muito semelhante às regras, com uma função
transition()
de definição
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() usa 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 um
a transição de borda de saída, os valores desses
atributos são todos configurados após a resolução de select(). Quando anexado como
uma transição de borda de entrada, attr não
inclui atributos que usam um seletor para resolver o valor. Se uma
transição de borda de entrada em --foo ler o atributo bar e também
selecionar em --foo para definir o atributo bar, há uma chance de a
transição de borda de entrada ler o valor incorreto de bar na transição.
A função de implementação precisa retornar um dicionário (ou uma lista de
dicionários, no caso de
transições com várias configurações de saída)
de novos valores de configuração de build a serem aplicados. Os conjuntos de chaves de dicionário retornados precisam
conter exatamente o conjunto de configurações de build transmitidas ao outputs
parâmetro da função de transição. Isso é verdadeiro mesmo que uma configuração de build não seja realmente alterada durante a transição. O valor original precisa ser transmitido explicitamente no dicionário retornado.
Como definir transições 1:2+
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 agrupam código de várias arquiteturas.
As transições 1:2+ são definidas retornando uma lista de dicionários na função de implementação de 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"]
)
Elas 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 lugares: bordas de entrada e bordas de saída. Isso significa que as regras podem fazer a transição da própria configuração (transição de borda de entrada ) e das configurações de dependências (transição de borda de saída ).
OBSERVAÇÃO: no momento, não é possível anexar transições do Starlark a regras nativas. Se você precisar fazer isso, entre em contato com bazel-discuss@googlegroups.com para receber ajuda com soluções alternativas.
Transições de borda de entrada
As transições de borda de entrada são ativadas anexando um objeto transition
(criado por transition()) ao parâmetro rule()'s cfg:
# 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 de entrada precisam ser transições 1:1.
Transições de borda de saída
As transições de borda de saída são ativadas anexando um transition objeto
(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 em opções de configuração de build nativas usando um prefixo especial para o 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 não compatíveis
O Bazel não oferece suporte à transição em --define com
"//command_line_option:define". Em vez disso, use uma configuração de build personalizada
. Em geral, novos usos de
--define são desencorajados em favor das configurações de build.
O Bazel não oferece suporte à transição em --config. Isso ocorre porque --config é
uma flag de "expansão" que se expande para outras flags.
É importante ressaltar que --config pode incluir flags que não afetam a configuração de build,
como
--spawn_strategy
. O Bazel, por design, não pode vincular essas flags a destinos individuais. Isso significa
que não há uma maneira coerente de aplicá-las em transições.
Como solução alternativa, é possível detalhar explicitamente as flags que fazem parte da
configuração na transição. Isso exige a manutenção da expansão de --config's
em dois lugares, o que é uma falha conhecida da interface.
Transições em configurações de build que permitem várias
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 sem operação
Se uma transição retornar {}, [] ou None, isso será uma abreviação 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
se ainda não for. 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 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)
})
Consulte o exemplo completo aqui.
Integração com plataformas e toolchains
Muitas flags nativas hoje, como --cpu e --crosstool_top, estão relacionadas à
resolução do toolchain. 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 performance
A adição de transições e, portanto, novas configurações ao build tem um custo: gráficos de build maiores, menos compreensíveis e builds mais lentos. Vale a pena considerar esses custos ao considerar usar transições nas regras de build. Confira abaixo um exemplo de como uma transição pode criar um crescimento exponencial do gráfico de build.
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.
Esse gráfico mostra um destino de nível superior, //pkg:app, que depende de dois destinos, a
//pkg:1_0 e //pkg:1_1. Esses dois destinos dependem de dois destinos, //pkg:2_0 e
//pkg:2_1. Esses 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 requer \(2n+2\) destinos:
//pkg:app//pkg:dep//pkg:i_0e//pkg:i_1para \(i\) em \([1..n]\)
Imagine que você implemente uma flag
--//foo:owner=<STRING> e //pkg:i_b se aplique
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\) os \(\{0,1\}\)em.
Isso torna o gráfico de build exponencialmente maior que o gráfico de destino, com consequências correspondentes de memória e performance.
TODO: adicionar estratégias para medição e mitigação desses problemas.
Leitura adicional
Para mais detalhes sobre como modificar as configurações de build, consulte: