Configurações

Informar um problema Ver a fonte Nightly · 7.3 · 7.2 · 7.1 · 7.0 · 6.5

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

Isso possibilita:

  • definir flags personalizadas para seu projeto, tornando obsoleta a necessidade de --define
  • gravação transições para configurar dependências em configurações diferentes das dos pais (como --compilation_mode=opt ou --cpu=arm)
  • incorporar padrões melhores em regras, como a criação automática de //my:android_app. com um SDK especificado)

e muito mais, tudo completamente de arquivos .bzl (sem versão do Bazel necessária). Consulte o repo bazelbuild/examples para exemplos.

Configurações de build definidas pelo usuário

Uma configuração de criação é uma parte configuração informações imprecisas ou inadequadas. Pense em uma configuração como um mapa de chave-valor. A configuração de --cpu=ppc e --copt="-DFoo" produz uma configuração semelhante a {cpu: ppc, copt: "-DFoo"}. Cada entrada é uma configuração de build.

As sinalizações 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 Bazel nativo. Os usuários do Bazel só podem ler e gravar na linha de comando. e outras APIs mantidas de modo nativo. A mudança de flags nativas e das APIs que as expõem exige uma versão do Bazel. Build definido pelo usuário são definidas em arquivos .bzl (e, portanto, não precisam de uma versão do Bazel para registrar alterações). 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

Exemplo completo

O parâmetro build_setting rule()

As configurações de criação são regras como qualquer outra regra e são diferenciadas usando o build_setting da função rule() do Starlark atributo.

# 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 de Starlark, como bool e string. Consulte a documentação do módulo config para mais detalhes. É possível fazer digitações mais complicadas na função de implementação da regra. Confira mais informações 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 build podem ser definidas 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 devem ser definidas pelos usuários. Por exemplo, se você, como um programador de regras, tiver um 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 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 do build. Essas implementações podem encaminhar diretamente o valor das configurações de compilação ou realizar trabalhos adicionais como verificação de tipo ou criação de structs mais complexos. Veja como você faria 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 extra que permite a ser definida várias vezes na linha de comando ou em bazelrcs. O padrão da pessoa value 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 sinalização é tratada como um único valor:

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

O código 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 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

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 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 acessar uma lista completa, consulte Regras comuns de configuração do 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á dependem diretamente da configuração do build usando uma dependência de atributos 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 que todas as regras dessa linguagem dependerão. Embora o conceito nativo de fragments não exista mais como um objeto codificado no mundo de configuração do Starlark, uma maneira de transformar 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 as 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 build O nome da configuração é 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 da versão a fim de 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 dois traços.

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 resulta na versão mais recente a outra tenha precedência. 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 das configurações 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, deixar-os em um .bazelrc reduz a desorganização na linha de comando.

Configurações de build com tipo de marcador

Exemplo completo

Ao contrário de outras configurações de build, as configurações do tipo 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 de build está definida. label_flag e label_setting pode ser lido/gravado por transições e label_flag pode ser definido pelo usuário, assim como outras regras de build_setting. A única diferença é que elas não podem ser definidas de forma personalizada.

As configurações digitadas em rótulos vão substituir a funcionalidade de padrões de limite tardio. Os atributos padrão com prazo final são atributos do tipo rótulo com os valores finais podem ser afetados pela configuração. No Starlark, isso substituirá 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()

Exemplo completo

Os usuários podem definir atributos nas configurações de build usando select() Os destinos de configuração do build podem ser transmitidos para o atributo flag_values do config_setting. O valor para corresponder à configuração é passado como um Em seguida, o String analisou 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 configuração transição mapeia a transformação de um destino configurado para outro dentro do gráfico de build.

As regras que as definem precisam incluir um atributo especial:

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

Ao adicionar transições, você pode facilmente explodir o tamanho de seu gráfico de compilação. Isso define uma lista de permissões nos pacotes em que você pode criar destinos dessa regra. O valor padrão no bloco de código acima coloca tudo na lista de permissões. No entanto, se você quiser restringir quem usa a regra, você pode configurar esse 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 assistência 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" é processado por um a 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". Transições 1:2+ também podem existir, mas têm restrições especiais.

No Starlark, as transições são definidas como regras, com uma transition() funçã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 anexado como uma transição de borda de saída, os valores desses atributos são todos configurados com a resolução post-select(). Quando anexado como uma transição de borda de entrada, attr não incluir quaisquer atributos que usem um seletor para resolver seu valor. Se um a transição de borda de entrada em --foo lê o atributo bar e também selecionar em --foo para definir o atributo bar, haverá uma chance para o transição de borda de entrada para ler o valor errado de bar na transição.

A função de implementação deve retornar um dicionário (ou lista de dicionários, no caso transições com várias configurações de saída) novos valores de configurações de build para aplicar. Os conjuntos de chaves de 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 se uma configuração de build for realmente não mudaram durante a transição - seu valor original deve ser explicitamente passados no dicionário retornado.

Como definir transições de 1:2+

Exemplo completo

A transição de borda de saída pode mapear uma única entrada. a duas ou mais configurações de saída. Isso é útil para definir regras que agrupam códigos de multiarquitetura.

As transições de 1:2 ou mais 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 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 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 (o transição de borda) e fazer a transição das dependências configurações (saída transição de borda).

OBSERVAÇÃO: atualmente, não é possível 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 recebidas

As transições de borda recebidas são ativadas ao anexar um objeto transition (criado por transition()) para o 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 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 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 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 Starlark também podem declarar leituras e gravações no build nativo por um prefixo especial ao 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 sem suporte

O Bazel não oferece suporte à transição em --define com "//command_line_option:define". Em vez disso, use um do build. 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 está uma "expansão" que se expande para outras sinalizações.

É crucial que o --config inclua flags que não afetam a configuração do build. como --spawn_strategy do Google. O Bazel não pode vincular essas flags a destinos individuais. Isso significa que não há uma maneira coerente de aplicá-los nas transições.

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

As transições permitem várias configurações de build

Ao definir configurações de build que permitir vários valores, o valor do atributo precisa ser definida 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, isso é 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

Exemplo completo

Ao anexar uma transição a uma borda de saída (independentemente de a transição ser uma transição de 1:1 ou 1:2+), ctx.attr é forçado a ser uma lista. caso ainda não tenha feito isso. A ordem dos elementos na 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 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 o exemplo completo neste link.

Integração com plataformas e conjuntos de ferramentas

Muitas flags nativas atuais, 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. 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 as dependências dele.

Este gráfico mostra uma meta de nível superior, //pkg:app, que depende de duas metas, uma //pkg:1_0 e //pkg:1_1. Ambas as metas dependem de duas metas, //pkg:2_0 e //pkg:2_1. Ambas as metas dependem de duas metas, //pkg:3_0 e //pkg:3_1. Isso vai continuar até //pkg:n_0 e //pkg:n_1, que dependem do mesmo destino, //pkg:dep.

A criação de //pkg:app requer \(2n+2\) destinos:

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

Imagine que você implementa 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 todos dependências.

Isso gera as seguintes metas configuradas:

//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 \(2^n\) destinos configurados: config.owner= "\(b_0b_1...b_n\)" para todos os \(b_i\) em \(\{0,1\}\).

Isso torna o gráfico de criação exponencialmente maior que o gráfico de destino, com consequências correspondentes de memória e desempenho.

TODO: adicionar estratégias para medir e mitigar esses problemas.

Leitura adicional

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