Configurações

Informar um problema Ver fonte Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

Esta página aborda os benefícios e o uso básico das configurações do Starlark, a API do Bazel para personalizar a forma como seu projeto é criado. Ele inclui como definir configurações de build e fornece exemplos.

Isso permite:

  • definir flags personalizadas para seu projeto, eliminando a necessidade de --define
  • Escreva transições para configurar dependências em configurações diferentes das principais (como --compilation_mode=opt ou --cpu=arm).
  • incorporar padrões melhores às regras, como criar automaticamente //my:android_app com um SDK especificado;

e muito mais, tudo completamente de arquivos .bzl (nenhuma versão do Bazel é necessária). Confira o repositório bazelbuild/examples 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 usando a linha de comando e outras APIs mantidas nativamente. Para mudar flags nativas e as APIs que as expõem, é necessário um lançamento 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, veja mais abaixo), mas também podem ser definidas por transições definidas pelo usuário.

Como definir configurações de build

Exemplo completo

O parâmetro build_setting rule()

As configurações de build são regras como qualquer outra e são diferenciadas usando o build_setting atributo 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 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. Saiba mais sobre isso abaixo.

As funções do módulo config usam um parâmetro booleano opcional, flag, que é definido como "false" por padrão. Se flag for definido como "true", a configuração de build poderá ser definida na linha de comando pelos usuários e internamente pelos criadores 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 gravador de regras, tiver um modo de depuração que queira ativar em regras de teste, não vai querer dar aos usuários a capacidade de ativar indiscriminadamente esse recurso em 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 de build pode ser acessado pelo método ctx.build_setting_value. Esse método só está disponível para objetos ctx 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. Veja 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)
)

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/buildsettings/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 exemplo 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 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/buildsettings/BUILD
load("//example/buildsettings:build_settings.bzl", "flavor")
flavor(
    name = "favorite_flavor",
    build_setting_default = "APPLE"
)

Configurações predefinidas

Exemplo completo

A biblioteca Skylib inclui um conjunto de configurações predefinidas que podem ser instanciadas sem precisar escrever 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 uma lista completa, consulte Regras comuns de configuração de build.

Como usar as configurações de build

Dependendo das configurações de build

Se um destino quiser ler uma parte das informações de configuração, ele poderá depender diretamente da configuração de build usando uma dependência de atributo regular.

# 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 de que todas as regras para esse idioma dependem. 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

Assim como a maioria das flags nativas, você pode usar a linha de comando para definir configurações de build marcadas como flags. O nome da configuração de build é o caminho de destino completo 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 é aceita:

$ bazel build //my/target --//example:boolean_flag
$ bazel build //my/target --no//example:boolean_flag

Como usar aliases de configuração de build

Você pode 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.

Adicione --flag_alias=ALIAS_NAME=TARGET_PATH ao seu .bazelrc para definir um alias . 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 indesejados.

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 com tipo de rótulo

Exemplo completo

Ao contrário de outras configurações de build, as configurações do tipo 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 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 de build_setting. A única diferença é que eles não podem ser definidos de forma personalizada.

As configurações com tipo de 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 "Label" cujos valores finais 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"
)

Build settings and select()

Exemplo completo

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 um String e depois 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.

As regras que os definem precisam incluir um atributo especial:

  "_allowlist_function_transition": attr.label(
      default = "@bazel_tools//tools/allowlists/function_transition_allowlist"
  )

Ao adicionar transições, é possível aumentar facilmente o tamanho do gráfico de build. Isso define uma lista de permissões nos pacotes em que é possível criar destinos dessa regra. O valor padrão no bloco de código acima permite tudo. Mas se você quiser restringir quem está usando sua regra, defina esse atributo para apontar para sua própria lista de permissão personalizada. Entre em contato com bazel-discuss@googlegroups.com se quiser conselhos ou ajuda para entender como as transições podem afetar o desempenho do build.

Definição

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 é de 1:1, como "substituir a configuração de entrada por --cpu=ppc". Também podem existir transições de 1:2 ou mais, mas com restrições especiais.

No Starlark, as transições são definidas de maneira muito parecida com as regras, com uma função de definição transition() 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 uma 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 aresta 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 aresta de entrada ler o valor errado 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ções 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 parâmetro outputs da função de transição. Isso é válido mesmo que uma configuração de build não seja alterada durante a transição. O valor original precisa ser transmitido explicitamente no dicionário retornado.

Como definir transições de 1:2 ou mais

Exemplo completo

A transição de aresta 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 de 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"]
)

Eles também podem definir chaves personalizadas que a função de implementação da 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

Exemplo completo

As transições podem ser anexadas em dois lugares: arestas de entrada e de saída. Isso significa que as regras podem fazer a transição da própria configuração (transição de aresta de entrada) e das configurações das dependências (transição de aresta 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 cfg de 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 entrada precisam ser de um para um.

Transições 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 saída podem ser de 1:1 ou 1:2+.

Consulte Como acessar atributos com transições para saber como ler essas chaves.

Transições em opções nativas

Exemplo completo

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 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 não compatíveis

O Bazel não é compatível com a transição em --define com "//command_line_option:define". Em vez disso, use uma configuração de build personalizada. Em geral, não é recomendável usar --define. Em vez disso, use as configurações de build.

O Bazel não é compatível com a transição em --config. Isso acontece porque --config é uma flag de "expansão" que se expande para outras flags.

É importante lembrar que --config pode incluir flags que não afetam a configuração do build, como --spawn_strategy . Por design, o Bazel não pode vincular essas flags a destinos individuais. Isso significa que não há uma maneira coerente de aplicá-los em transições.

Como solução alternativa, você pode listar explicitamente as flags que fazem parte da configuração na sua transição. Isso exige manter a expansão do --config em dois lugares, o que é uma falha conhecida da interface.

Transições em "Permitir 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 sem operação

Se uma transição retornar {}, [] ou None, isso significa 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

Exemplo completo

Ao anexar uma transição a uma aresta de saída (seja uma transição 1:1 ou 1:2+), ctx.attr será 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)
    })

Confira um exemplo completo.

Integração com plataformas e conjuntos de ferramentas

Muitas flags nativas hoje, como --cpu e --crosstool_top, estão relacionadas à resolução da cadeia 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 performance

Adicionar 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 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

Gráfico de escalonabilidade

Figura 1. Gráfico de escalonabilidade mostrando um destino de nível superior e suas dependências.

Esse gráfico mostra um destino de nível superior, //pkg:app, que depende de dois destinos, //pkg:1_0 e //pkg:1_1. Ambos dependem de dois destinos, //pkg:2_0 e //pkg:2_1. Ambos 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.

Para criar //pkg:app, são necessários destinos \(2n+2\) :

  • //pkg:app
  • //pkg:dep
  • //pkg:i_0 e //pkg:i_1 para \(i\) em \([1..n]\)

Imagine que você implemente) uma flag --//foo:owner=<STRING> e //pkg:i_b seja aplicado

depConfig = myConfig + depConfig.owner="$(myConfig.owner)$(b)"

Em outras palavras, //pkg:i_b adiciona 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"
...

O //pkg:dep produz destinos \(2^n\) configurados: config.owner="\(b_0b_1...b_n\)" para todos os \(b_i\) em \(\{0,1\}\).

Isso torna o gráfico de build exponencialmente maior que o gráfico de destino, com consequências correspondentes na memória e no desempenho.

TODO: adicionar estratégias para medição e mitigação desses problemas.

Leitura adicional

Para mais detalhes sobre como modificar configurações de build, consulte: