Nesta página, descrevemos o framework do conjunto de ferramentas, que é uma maneira de os autores de regras desacoplarem a lógica de regras da seleção de ferramentas baseada na plataforma. É recomendável ler as páginas regras e plataformas antes de continuar. Nesta página, explicamos por que os conjuntos de ferramentas são necessários, como os definir e usar e como o Bazel seleciona um conjunto adequado com base nas restrições da plataforma.
Motivação
Vamos ver primeiro as cadeias de ferramentas que foram projetadas para resolver problemas. Suponha que você esteja escrevendo regras para dar suporte à linguagem de programação "bar". Sua regra bar_binary
compilaria arquivos *.bar
usando o compilador barc
, uma ferramenta
criada como outro destino no espaço de trabalho. Como os usuários que escrevem destinos
bar_binary
não precisam especificar uma dependência no compilador, você a torna uma
dependência implícita adicionando-a à definição da regra como um atributo particular.
bar_binary = rule(
implementation = _bar_binary_impl,
attrs = {
"srcs": attr.label_list(allow_files = True),
...
"_compiler": attr.label(
default = "//bar_tools:barc_linux", # the compiler running on linux
providers = [BarcInfo],
),
},
)
O //bar_tools:barc_linux
agora é uma dependência de cada destino bar_binary
. Portanto, ele será criado antes de qualquer destino bar_binary
Ele pode ser acessado pela função de
implementação da regra como qualquer outro atributo:
BarcInfo = provider(
doc = "Information about how to invoke the barc compiler.",
# In the real world, compiler_path and system_lib might hold File objects,
# but for simplicity they are strings for this example. arch_flags is a list
# of strings.
fields = ["compiler_path", "system_lib", "arch_flags"],
)
def _bar_binary_impl(ctx):
...
info = ctx.attr._compiler[BarcInfo]
command = "%s -l %s %s" % (
info.compiler_path,
info.system_lib,
" ".join(info.arch_flags),
)
...
O problema aqui é que o rótulo do compilador está fixado no código em bar_binary
. No entanto,
destinos diferentes podem precisar de compiladores distintos, dependendo da plataforma
para a qual estão sendo criados e da plataforma em que estão sendo criados, o que é chamado de
plataforma de destino e plataforma de execução, respectivamente. Além disso, o autor da regra nem necessariamente conhece todas as ferramentas e plataformas disponíveis, portanto, não é viável fixá-las na definição da regra.
Uma solução não ideal seria transferir a carga para os usuários, tornando
o atributo _compiler
não particular. Em seguida, os destinos individuais podem ser
fixados no código para criar para uma plataforma ou outra.
bar_binary(
name = "myprog_on_linux",
srcs = ["mysrc.bar"],
compiler = "//bar_tools:barc_linux",
)
bar_binary(
name = "myprog_on_windows",
srcs = ["mysrc.bar"],
compiler = "//bar_tools:barc_windows",
)
É possível melhorar essa solução usando select
para escolher a compiler
com base na plataforma:
config_setting(
name = "on_linux",
constraint_values = [
"@platforms//os:linux",
],
)
config_setting(
name = "on_windows",
constraint_values = [
"@platforms//os:windows",
],
)
bar_binary(
name = "myprog",
srcs = ["mysrc.bar"],
compiler = select({
":on_linux": "//bar_tools:barc_linux",
":on_windows": "//bar_tools:barc_windows",
}),
)
No entanto, isso é tedioso e exige um pouco de cada usuário do bar_binary
.
Se esse estilo não for usado de forma consistente em todo o espaço de trabalho, isso resultará em
builds que funcionam bem em uma única plataforma, mas falharão quando estendidos para
cenários de várias plataformas. Ele também não resolve o problema de adicionar suporte
a novas plataformas e compiladores sem modificar as regras ou os destinos existentes.
O framework do conjunto de ferramentas resolve esse problema adicionando um nível extra de indireção. Essencialmente, você declara que a regra tem uma dependência abstrata em algum membro de uma família de destinos (um tipo de conjunto de ferramentas). O Bazel resolve isso automaticamente para um destino específico (um conjunto de ferramentas) com base nas restrições de plataforma aplicáveis. Nem o autor da regra nem o de destino precisam saber o conjunto completo de plataformas e conjuntos de ferramentas disponíveis.
Como escrever regras que usam conjuntos de ferramentas
No framework do conjunto de ferramentas, em vez de as regras dependerem diretamente das ferramentas, elas dependem dos tipos de conjunto de ferramentas. Um tipo de conjunto de ferramentas é um destino simples que representa uma classe de ferramentas que atendem ao mesmo papel em diferentes plataformas. Por exemplo, você pode declarar um tipo que representa o compilador de barras:
# By convention, toolchain_type targets are named "toolchain_type" and
# distinguished by their package path. So the full path for this would be
# //bar_tools:toolchain_type.
toolchain_type(name = "toolchain_type")
A definição da regra na seção anterior foi modificada para que, em vez de
tomar o compilador como um atributo, ele declare que consome um
conjunto de ferramentas //bar_tools:toolchain_type
.
bar_binary = rule(
implementation = _bar_binary_impl,
attrs = {
"srcs": attr.label_list(allow_files = True),
...
# No `_compiler` attribute anymore.
},
toolchains = ["//bar_tools:toolchain_type"],
)
A função de implementação agora acessa essa dependência em ctx.toolchains
em vez de ctx.attr
, usando o tipo de conjunto de ferramentas como chave.
def _bar_binary_impl(ctx):
...
info = ctx.toolchains["//bar_tools:toolchain_type"].barcinfo
# The rest is unchanged.
command = "%s -l %s %s" % (
info.compiler_path,
info.system_lib,
" ".join(info.arch_flags),
)
...
ctx.toolchains["//bar_tools:toolchain_type"]
retorna o
provedor ToolchainInfo
de qualquer destino que o Bazel tenha resolvido na dependência do conjunto de ferramentas. Os campos do objeto
ToolchainInfo
são definidos pela regra da ferramenta. Na próxima
seção, essa regra é definida para que haja um campo barcinfo
que envolve
um objeto BarcInfo
.
O procedimento do Bazel para resolver conjuntos de ferramentas para destinos é descrito
abaixo. Somente o destino do conjunto de ferramentas resolvido é, na verdade,
uma dependência do destino bar_binary
, não todo o espaço dos conjuntos de ferramentas
candidatos.
Conjuntos de ferramentas obrigatórios e opcionais
Por padrão, quando uma regra expressa uma dependência de tipo de conjunto de ferramentas usando um rótulo básico (como mostrado acima), o tipo de conjunto de ferramentas é considerado obrigatório. Se o Bazel não encontrar um conjunto de ferramentas correspondente (consulte Resolução do conjunto de ferramentas abaixo) para um tipo obrigatório, a análise será interrompida.
Em vez disso, é possível declarar uma dependência de tipo de conjunto de ferramentas opcional desta maneira:
bar_binary = rule(
...
toolchains = [
config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False),
],
)
Quando um tipo de conjunto de ferramentas opcional não pode ser resolvido, a análise continua e o
resultado de ctx.toolchains["//bar_tools:toolchain_type"]
é None
.
A função config_common.toolchain_type
é obrigatória por padrão.
Os seguintes formulários podem ser usados:
- Tipos de conjunto de ferramentas obrigatórios:
toolchains = ["//bar_tools:toolchain_type"]
toolchains = [config_common.toolchain_type("//bar_tools:toolchain_type")]
toolchains = [config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = True)]
- Tipos de conjunto de ferramentas opcionais:
toolchains = [config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False)]
bar_binary = rule(
...
toolchains = [
"//foo_tools:toolchain_type",
config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False),
],
)
Também é possível misturar e combinar formulários na mesma regra. No entanto, se o mesmo tipo de conjunto de ferramentas for listado várias vezes, ela usará a versão mais rigorosa, em que a obrigatória é mais rigorosa do que a opcional.
Como gravar aspectos que usam conjuntos de ferramentas
Os aspectos têm acesso à mesma API de conjunto de ferramentas que as regras. É possível definir os tipos de conjuntos de ferramentas necessários, acessá-los por contexto e usá-los para gerar novas ações.
bar_aspect = aspect(
implementation = _bar_aspect_impl,
attrs = {},
toolchains = ['//bar_tools:toolchain_type'],
)
def _bar_aspect_impl(target, ctx):
toolchain = ctx.toolchains['//bar_tools:toolchain_type']
# Use the toolchain provider like in a rule.
return []
Como definir conjuntos de ferramentas
Para definir alguns conjuntos de ferramentas para determinado tipo, são necessários três itens:
Uma regra específica da linguagem que representa o tipo de ferramenta ou pacote de ferramentas. Por convenção, o nome dessa regra é sufixado com "_dataset".
- Observação:a regra
\_toolchain
não pode criar nenhuma ação de build. Em vez disso, ele coleta artefatos de outras regras e os encaminha para a regra que usa o conjunto de ferramentas. Essa regra é responsável por criar todas as ações de build.
- Observação:a regra
Vários destinos desse tipo de regra, representando versões do pacote de ferramentas ou do pacote de ferramentas para diferentes plataformas.
Para cada um desses destinos, um destino associado da regra genérica
toolchain
para fornecer metadados usados pelo framework do conjunto de ferramentas. Esse destinotoolchain
também se refere aotoolchain_type
associado a esse conjunto de ferramentas. Isso significa que uma determinada regra_toolchain
pode ser associada a qualquertoolchain_type
e que somente em uma instânciatoolchain
que use essa regra_toolchain
, a regra será associada a umtoolchain_type
.
Para o exemplo em execução, veja uma definição para uma regra bar_toolchain
. Nosso exemplo tem apenas um compilador, mas outras ferramentas, como um vinculador, também podem ser agrupadas abaixo dele.
def _bar_toolchain_impl(ctx):
toolchain_info = platform_common.ToolchainInfo(
barcinfo = BarcInfo(
compiler_path = ctx.attr.compiler_path,
system_lib = ctx.attr.system_lib,
arch_flags = ctx.attr.arch_flags,
),
)
return [toolchain_info]
bar_toolchain = rule(
implementation = _bar_toolchain_impl,
attrs = {
"compiler_path": attr.string(),
"system_lib": attr.string(),
"arch_flags": attr.string_list(),
},
)
A regra precisa retornar um provedor ToolchainInfo
, que se torna o objeto que
a regra de consumo recupera usando ctx.toolchains
e o rótulo do
tipo de conjunto de ferramentas. ToolchainInfo
, como struct
, pode conter pares de campo-valor arbitrários. A especificação exata de quais campos são adicionados ao ToolchainInfo
precisa ser claramente documentada no tipo de conjunto de ferramentas. Neste exemplo, os valores
retornam unidos em um objeto BarcInfo
para reutilizar o esquema definido acima. Esse
estilo pode ser útil para validação e reutilização do código.
Agora você pode definir destinos para compiladores barc
específicos.
bar_toolchain(
name = "barc_linux",
arch_flags = [
"--arch=Linux",
"--debug_everything",
],
compiler_path = "/path/to/barc/on/linux",
system_lib = "/usr/lib/libbarc.so",
)
bar_toolchain(
name = "barc_windows",
arch_flags = [
"--arch=Windows",
# Different flags, no debug support on windows.
],
compiler_path = "C:\\path\\on\\windows\\barc.exe",
system_lib = "C:\\path\\on\\windows\\barclib.dll",
)
Por fim, crie definições de toolchain
para os dois destinos bar_toolchain
.
Essas definições vinculam os destinos específicos da linguagem ao tipo de conjunto de ferramentas e
fornecem as informações de restrição que informam ao Bazel quando o conjunto de ferramentas é
apropriado para uma determinada plataforma.
toolchain(
name = "barc_linux_toolchain",
exec_compatible_with = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
target_compatible_with = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
toolchain = ":barc_linux",
toolchain_type = ":toolchain_type",
)
toolchain(
name = "barc_windows_toolchain",
exec_compatible_with = [
"@platforms//os:windows",
"@platforms//cpu:x86_64",
],
target_compatible_with = [
"@platforms//os:windows",
"@platforms//cpu:x86_64",
],
toolchain = ":barc_windows",
toolchain_type = ":toolchain_type",
)
O uso da sintaxe de caminho relativo acima sugere que essas definições estejam todas no
mesmo pacote, mas não há motivo para o tipo de conjunto de ferramentas, os destinos de conjunto de ferramentas
específicos da linguagem e os destinos de definição de toolchain
não podem estar todos em pacotes
separados.
Consulte go_toolchain
para ver um exemplo real.
Conjuntos de ferramentas e configurações
Uma pergunta importante para autores de regras é: quando um destino bar_toolchain
é analisado, qual configuração ele vê e quais transições precisam ser usadas para dependências? O exemplo acima usa atributos de string, mas
o que aconteceria com um conjunto de ferramentas mais complicado que depende de outros destinos
no repositório do Bazel?
Vamos conferir uma versão mais complexa de bar_toolchain
:
def _bar_toolchain_impl(ctx):
# The implementation is mostly the same as above, so skipping.
pass
bar_toolchain = rule(
implementation = _bar_toolchain_impl,
attrs = {
"compiler": attr.label(
executable = True,
mandatory = True,
cfg = "exec",
),
"system_lib": attr.label(
mandatory = True,
cfg = "target",
),
"arch_flags": attr.string_list(),
},
)
O uso de attr.label
é o mesmo usado para uma regra padrão,
mas o significado do parâmetro cfg
é um pouco diferente.
A dependência de um destino (chamado de "pai") para um conjunto de ferramentas pela resolução dele usa uma transição de configuração especial chamada "transição do conjunto de ferramentas". A transição do conjunto de ferramentas mantém a mesma configuração, exceto
que ela força a plataforma de execução a ser a mesma para o conjunto de ferramentas e para
o pai. Caso contrário, a resolução do conjunto de ferramentas para o conjunto de ferramentas poderá escolher qualquer
plataforma de execução e não será necessariamente a mesma que a do pai. Isso
permite que qualquer dependência exec
do conjunto de ferramentas também seja executável para as
ações de build do pai. Qualquer uma das dependências do conjunto de ferramentas que usem cfg =
"target"
(ou que não especifiquem cfg
, já que "destino" é o padrão) são criadas para a mesma plataforma de destino que o pai. Isso permite que as regras do conjunto de ferramentas
contribuam com as bibliotecas (o atributo system_lib
acima) e as ferramentas (o atributo
compiler
) nas regras de build que precisam delas. As bibliotecas do sistema
estão vinculadas ao artefato final e, portanto, precisam ser criadas para a mesma
plataforma, enquanto o compilador é uma ferramenta invocada durante a criação e precisa
ser executado na plataforma de execução.
Como registrar e criar com conjuntos de ferramentas
Neste ponto, todos os elementos básicos estão montados e você só precisa disponibilizar os conjuntos de ferramentas para o procedimento de resolução do Bazel. Isso é feito
registrando o conjunto de ferramentas em um arquivo WORKSPACE
usando
register_toolchains()
ou transmitindo os rótulos deles na linha de
comando usando a sinalização --extra_toolchains
.
register_toolchains(
"//bar_tools:barc_linux_toolchain",
"//bar_tools:barc_windows_toolchain",
# Target patterns are also permitted, so you could have also written:
# "//bar_tools:all",
# or even
# "//bar_tools/...",
)
Ao usar padrões de destino para registrar conjuntos de ferramentas, a ordem em que eles são registrados é determinada pelas regras abaixo:
- Os conjuntos de ferramentas definidos em um subpacote de um pacote são registrados antes daqueles definidos no próprio pacote.
- Dentro de um pacote, os conjuntos de ferramentas são registrados na ordem lexicográfica dos respectivos nomes.
Agora, quando você criar um destino que depende de um tipo de conjunto de ferramentas, um conjunto apropriado será selecionado com base nas plataformas de destino e de execução.
# my_pkg/BUILD
platform(
name = "my_target_platform",
constraint_values = [
"@platforms//os:linux",
],
)
bar_binary(
name = "my_bar_binary",
...
)
bazel build //my_pkg:my_bar_binary --platforms=//my_pkg:my_target_platform
O Bazel vai ver que //my_pkg:my_bar_binary
está sendo criado com uma plataforma que
tem @platforms//os:linux
e, portanto, resolve a
referência //bar_tools:toolchain_type
para //bar_tools:barc_linux_toolchain
.
Isso vai criar //bar_tools:barc_linux
, mas não
//bar_tools:barc_windows
.
Resolução do conjunto de ferramentas
Para cada destino que usa conjuntos de ferramentas, o procedimento de resolução do conjunto de ferramentas do Bazel determina as dependências concretas do conjunto de ferramentas do destino. O procedimento usa como entrada um conjunto de tipos de conjuntos de ferramentas necessários, a plataforma de destino e as listas de plataformas de execução e de conjuntos de ferramentas disponíveis. As saídas são um conjunto de ferramentas selecionado para cada tipo, bem como uma plataforma de execução selecionada para o destino atual.
As plataformas de execução e os conjuntos de ferramentas disponíveis são reunidos do
arquivo WORKSPACE
via
register_execution_platforms
e
register_toolchains
.
Plataformas de execução e conjuntos de ferramentas adicionais também podem ser especificados na
linha de comando usando
--extra_execution_platforms
e
--extra_toolchains
.
A plataforma host é incluída automaticamente como uma plataforma de execução disponível.
As plataformas e os conjuntos de ferramentas disponíveis são rastreados como listas ordenadas para determinismo, com preferência para itens anteriores na lista.
O conjunto de conjuntos de ferramentas disponíveis, em ordem de prioridade, é criado a partir de
--extra_toolchains
e register_toolchains
:
- Os conjuntos de ferramentas registrados usando
--extra_toolchains
são adicionados primeiro.- Dentro deles, o último conjunto de ferramentas tem a prioridade mais alta.
- Conjuntos de ferramentas registrados usando
register_toolchains
- Dentro deles, o primeiro conjunto de ferramentas mencionado tem a maior prioridade.
OBSERVAÇÃO:pseudodestinos, como :all
, :*
e /...
, são ordenados pelo mecanismo de carregamento de pacotes do Bazel, que usa uma ordenação lexicográfica.
Estas são as etapas de resolução.
Uma cláusula
target_compatible_with
ouexec_compatible_with
corresponde a uma plataforma se, para cadaconstraint_value
na lista, a plataforma também tiverconstraint_value
, seja explicitamente ou como padrão.Se a plataforma tiver
constraint_value
s deconstraint_setting
s não referenciados pela cláusula, eles não afetarão a correspondência.Se o destino que está sendo criado especificar o atributo
exec_compatible_with
(ou a definição da regra dele especificar o argumentoexec_compatible_with
), a lista de plataformas de execução disponíveis será filtrada para remover quaisquer que não correspondam às restrições de execução.A lista de conjuntos de ferramentas disponíveis é filtrada para remover aqueles que especificam
target_settings
que não correspondem à configuração atual.Para cada plataforma de execução disponível, associe cada tipo de conjunto de ferramentas ao primeiro disponível, se houver, que seja compatível com essa plataforma de execução e de destino.
Qualquer plataforma de execução que não conseguiu encontrar um conjunto de ferramentas obrigatório compatível para um dos tipos de conjunto de ferramentas é descartada. Das demais plataformas, a primeira se torna a plataforma de execução do destino atual, e os conjuntos de ferramentas associados (se houver) se tornam dependências do destino.
A plataforma de execução escolhida é usada para executar todas as ações geradas pelo destino.
Nos casos em que o mesmo destino pode ser criado em várias configurações (por exemplo, para CPUs diferentes) no mesmo build, o procedimento de resolução é aplicado de forma independente a cada versão do destino.
Se a regra usar grupos de execução, cada grupo vai realizar a resolução do conjunto de ferramentas separadamente, e cada um terá a própria plataforma e conjuntos de ferramentas.
Depurar conjuntos de ferramentas
Se você estiver adicionando suporte ao conjunto de ferramentas a uma regra atual, use a sinalização --toolchain_resolution_debug=regex
. Durante a resolução do conjunto de ferramentas, a sinalização
fornece uma saída detalhada para os tipos de conjuntos de ferramentas ou nomes de destino que correspondem à variável regex. É
possível usar .*
para gerar todas as informações. Ele mostra os nomes dos conjuntos de ferramentas que ele
verifica e ignora durante o processo de resolução.
Se você quiser ver quais dependências do cquery
são da resolução do
conjunto de ferramentas, use a sinalização --transitions
de cquery
:
# Find all direct dependencies of //cc:my_cc_lib. This includes explicitly
# declared dependencies, implicit dependencies, and toolchain dependencies.
$ bazel cquery 'deps(//cc:my_cc_lib, 1)'
//cc:my_cc_lib (96d6638)
@bazel_tools//tools/cpp:toolchain (96d6638)
@bazel_tools//tools/def_parser:def_parser (HOST)
//cc:my_cc_dep (96d6638)
@local_config_platform//:host (96d6638)
@bazel_tools//tools/cpp:toolchain_type (96d6638)
//:default_host_platform (96d6638)
@local_config_cc//:cc-compiler-k8 (HOST)
//cc:my_cc_lib.cc (null)
@bazel_tools//tools/cpp:grep-includes (HOST)
# Which of these are from toolchain resolution?
$ bazel cquery 'deps(//cc:my_cc_lib, 1)' --transitions=lite | grep "toolchain dependency"
[toolchain dependency]#@local_config_cc//:cc-compiler-k8#HostTransition -> b6df211