Configurações

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, obsoletas 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).
  • fazer um bake melhor padrão de regras (como criar //my:android_app automaticamente com um SDK especificado).

e muito mais, tudo isso em arquivos .bzl, sem precisar lançar versões no Bazel. Consulte o repositório bazelbuild/examples para exemplos.

Configurações de build definidas pelo usuário

Uma configuração de build é uma informação de configuração (link em inglês). 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 são definidas e os valores são definidos dentro do código Java nativo do Bazel. Os usuários do Bazel só podem ler e gravar por meio da linha de comando e de outras APIs mantidas de forma 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 do Bazel para registrar mudanças. Elas também podem ser definidas por meio da linha de comando (se forem 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

Exemplo completo

O parâmetro build_setting rule()

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 de 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. Mais informações 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 de criação poderá ser definida na linha de comando pelos usuários, bem como internamente pelos criadores de regras por meio de valores e transições padrão. Nem todas as configurações devem ser definidas pelos usuários. Por exemplo, se você, como um gravador de regras, tiver um modo de depuração que gostaria de ativar nas regras de teste, não convém 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 do 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 de regras de configuração do build. Esses métodos de implementação podem encaminhar diretamente o valor das configurações de compilação ou realizar outros trabalhos nele, como verificação de tipo ou criação de structs 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 sinalizações de string com 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/buildsettings/BUILD
load("//example/buildsettings:build_settings.bzl", "allow_multiple_flag")
allow_multiple_flag(
    name = "roasts",
    build_setting_default = "medium"
)

Cada configuração da sinalização é tratada como um valor único:

$ bazel build //my/target --//example:roasts=blonde \
    --//example:roasts=medium,dark

O exemplo acima é analisado para {"//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 usa 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 você pode instanciar sem precisar escrever 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 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 para esse idioma dependem. Embora o conceito nativo de fragments não exista mais como um objeto fixado no código no ambiente de configuração do Starlark, 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 flags. 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 sintaxe booleana especial:

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

Como usar aliases de configuração do build

Você pode definir um alias para o caminho de destino da configuração do build 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 indesejados.

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

Exemplo completo

Ao contrário de outras configurações do build, as configurações tipadas de 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 está definida. label_flag e label_setting podem ser lidos/gravados por transições e label_flag podem ser definidos pelo usuário da mesma forma que outras regras de build_setting. A única diferença é que eles não podem ser definidos de maneira personalizada.

As configurações tipadas de rótulo vão substituir a funcionalidade dos padrões limitados tardias. Os atributos padrão vinculados tardios são atributos 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()

Exemplo completo

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 do 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.

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, é muito fácil explodir o tamanho do gráfico de build. Isso define uma lista de permissões para os pacotes em que é possível criar destinos dessa regra. O valor padrão no bloco de código acima inclui tudo na lista de permissões. No entanto, se você quiser restringir quem usa essa regra, defina o atributo para apontar para sua própria lista de permissões personalizada. Entre em contato com bazel-discuss@googlegroups.com se precisar de orientação ou ajuda para entender como as transições podem afetar o desempenho do seu build.

Definição

As transições definem mudanças de configuração entre as regras. Por exemplo, uma solicitação como "compilar minha dependência para uma CPU diferente da mãe" é processada por uma transição.

Oficialmente, 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() 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 na resolução pós-select(). Quando anexado como uma transição de borda de entrada, o attr não inclui nenhum atributo que use um seletor para resolver o valor. Se uma transição de borda de entrada em --foo ler o atributo bar e depois selecionar o --foo para definir o atributo bar, haverá uma chance de 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 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 do dicionário retornados precisam conter exatamente o conjunto de configurações de compilação transmitidas ao parâmetro outputs da função de transição. Isso acontece mesmo que uma configuração de build não seja mudada ao longo da transição. O valor original precisa ser transmitido explicitamente no dicionário retornado.

Como definir transições 1:2+

Exemplo completo

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 multiarquitetura.

As transições 1:2 e posteriores 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 regras 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 a dois locais: bordas de entrada e bordas de saída. Efetivamente, 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 das dependências (transição de borda de saída).

OBSERVAÇÃO: atualmente, não há como anexar transições Starlark a regras nativas. Se você precisar fazer isso, entre em contato com bazel-discuss@googlegroups.com para receber ajuda 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 de entrada precisam ser individuais.

Transições de borda de saída

As transições de borda de saída são ativadas ao anexar 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

Exemplo completo

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 em --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 flag de "expansão" que se expande para outras flags.

É importante que --config inclua sinalizações que não afetem a configuração do build, como --spawn_strategy . Por padrão, o Bazel 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, você pode discriminar explicitamente as sinalizações que fazem parte da configuração na transição. Isso requer a manutenção da expansão da --config em dois locais, o que é uma falha conhecida na interface.

As transições permitem 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 independentes

Se uma transição retornar {}, [] ou None, essa é uma abreviação para manter todas as configurações nos valores originais. Isso pode ser mais conveniente do que configurar explicitamente cada saída para si mesmo.

# 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",
    ]
)

Acessar atributos com transições

Exemplo completo

Ao anexar uma transição a uma borda de saída, independentemente de a transição ser 1:1 ou 1:2+, ctx.attr será forçada a ser uma lista, caso isso ainda não tenha sido feito. A ordem dos elementos nessa lista não foi 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. Confira abaixo um exemplo de como uma transição pode criar um crescimento exponencial do gráfico de build.

Builds de mau comportamento: um estudo de caso

Gráfico de escalonabilidade

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. 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 exige \(2n+2\) destinos:

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

Imagine que você implement uma flag --//foo:owner=<STRING> e //pkg:i_b se aplicam

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.

Sugestões de leitura

Para saber mais sobre como modificar configurações do build, consulte: