Nesta página, descrevemos a estrutura do conjunto de ferramentas, que é uma maneira de os criadores de regras desacoplarem a lógica delas da seleção de ferramentas com base na plataforma. Recomendamos que você leia as páginas de regras e plataformas antes de continuar. Esta página explica por que os conjuntos de ferramentas são necessários, como defini-los e usá-los e como o Bazel seleciona um conjunto de ferramentas adequado com base nas restrições de plataforma.
Motivação
Primeiro, vamos analisar o problema que os toolchains foram criados para resolver. Suponha que você esteja escrevendo regras para oferecer suporte à linguagem de programação "bar". Sua regra bar_binary
compilaria arquivos *.bar
usando o compilador barc
, uma ferramenta criada
como outro destino no seu espaço de trabalho. Como os usuários que escrevem destinos bar_binary
não precisam especificar uma dependência do 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
. Ela pode ser acessada pela função de implementação da regra, assim 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 é que o rótulo do compilador é codificado em bar_binary
, mas
diferentes destinos podem precisar de compiladores diferentes, dependendo da plataforma em que
estão sendo criados e da plataforma em que estão sendo criados, chamadas de
plataforma de destino e plataforma de execução, respectivamente. Além disso, o autor da regra não precisa conhecer todas as ferramentas e plataformas disponíveis. Por isso, não é viável codificá-las na definição da regra.
Uma solução menos que ideal seria transferir o ônus para os usuários, tornando o atributo _compiler
não particular. Em seguida, os destinos individuais podem ser
codificados 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 o 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",
}),
)
Mas isso é tedioso e um pouco demais para pedir a cada usuário do bar_binary
.
Se esse estilo não for usado de maneira consistente em todo o espaço de trabalho, isso vai gerar
builds que funcionam bem em uma única plataforma, mas falham quando estendidos para
cenários multiplataforma. Ele também não resolve o problema de adicionar suporte
para novas plataformas e compiladores sem modificar regras ou destinos atuais.
O framework da cadeia de ferramentas resolve esse problema adicionando um nível extra de indireção. Basicamente, você declara que sua regra tem uma dependência abstrata em algum membro de uma família de destinos (um tipo de cadeia de ferramentas), e o Bazel resolve automaticamente isso para um destino específico (uma cadeia de ferramentas) com base nas restrições de plataforma aplicáveis. Nem o autor da regra nem o autor de destino precisam conhecer o conjunto completo de plataformas e toolchains disponíveis.
Escrever regras que usam toolchains
No framework do conjunto de ferramentas, em vez de ter regras que dependem diretamente de ferramentas, elas dependem de tipos de conjunto de ferramentas. Um tipo de cadeia de ferramentas é um destino simples que representa uma classe de ferramentas que têm a mesma função em diferentes plataformas. Por exemplo, é possível declarar um tipo que representa o compilador bar:
# 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 de regra na seção anterior é modificada para que, em vez de
usar o compilador como um atributo, ela declare que consome uma
cadeia 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 cadeia 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 para que o Bazel resolveu a dependência do conjunto de ferramentas. Os campos do objeto ToolchainInfo
são definidos pela regra da ferramenta subjacente. Na próxima seção, essa regra é definida de forma que haja um campo barcinfo
que encapsula um objeto BarcInfo
.
O procedimento do Bazel para resolver toolchains em destinos é descrito abaixo. Somente o destino da cadeia de ferramentas resolvido é realmente
feito uma dependência do destino bar_binary
, não todo o espaço de cadeias de ferramentas
candidatas.
Toolchains obrigatórias e opcionais
Por padrão, quando uma regra expressa uma dependência de tipo de cadeia de ferramentas usando um rótulo simples (como mostrado acima), o tipo de cadeia de ferramentas é considerado obrigatório. Se o Bazel não conseguir encontrar um conjunto de ferramentas correspondente (consulte Resolução do conjunto de ferramentas abaixo) para um tipo de conjunto de ferramentas obrigatório, isso será um erro e a análise será interrompida.
Em vez disso, é possível declarar uma dependência de tipo de cadeia de ferramentas opcional, da seguinte forma:
bar_binary = rule(
...
toolchains = [
config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False),
],
)
Quando um tipo de cadeia 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.
É possível usar os seguintes formulários:
- Tipos de cadeia 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 cadeia 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),
],
)
Você também pode combinar formas na mesma regra. No entanto, se o mesmo tipo de cadeia de ferramentas for listado várias vezes, será usada a versão mais rigorosa, em que "obrigatório" é mais rigoroso do que "opcional".
Aspectos de escrita que usam toolchains
Os aspectos têm acesso à mesma API de cadeia de ferramentas que as regras: é possível definir tipos de cadeia de ferramentas obrigatórios, acessar cadeias de ferramentas pelo contexto e usá-las 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 de um determinado tipo, você precisa de três coisas:
Uma regra específica de linguagem que representa o tipo de ferramenta ou pacote de ferramentas. Por convenção, o nome dessa regra tem o sufixo "_toolchain".
- Observação:a regra
\_toolchain
não pode criar ações de build. Em vez disso, ele coleta artefatos de outras regras e os encaminha para a regra que usa a cadeia de ferramentas. Essa regra é responsável por criar todas as ações de build.
- Observação:a regra
Várias metas desse tipo de regra, representando versões da ferramenta ou do pacote de ferramentas para diferentes plataformas.
Para cada destino, um destino associado da regra genérica
toolchain
para fornecer metadados usados pelo framework da cadeia de ferramentas. Essa metatoolchain
também se refere aotoolchain_type
associado a essa cadeia de ferramentas. Isso significa que uma determinada regra de_toolchain
pode ser associada a qualquertoolchain_type
, e que somente em uma instância detoolchain
que usa essa regra de_toolchain
é que a regra é associada a umtoolchain_type
.
Para nosso exemplo em execução, aqui está uma definição de 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 cadeia de ferramentas. ToolchainInfo
, assim como struct
, pode conter pares arbitrários de campo-valor. A especificação de quais campos são adicionados ao ToolchainInfo
precisa ser claramente documentada no tipo de conjunto de ferramentas. Neste exemplo, os valores retornam encapsulados em um objeto BarcInfo
para reutilizar o esquema definido acima. Esse estilo pode ser útil para validação e reutilização de código.
Agora é possível 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 cadeia de ferramentas e fornecem as informações de restrição que informam ao Bazel quando a cadeia de ferramentas é adequada 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 estão todas no mesmo pacote, mas não há motivo para que o tipo de cadeia de ferramentas, os destinos de cadeia de ferramentas específicos da linguagem e os destinos de definição toolchain
não possam estar em pacotes separados.
Consulte o go_toolchain
para ver um exemplo real.
Toolchains e configurações
Uma pergunta importante para os criadores de regras é: quando um destino bar_toolchain
é analisado, qual configuração ele vê e quais transições devem ser usadas para dependências? O exemplo acima usa atributos de string, mas
o que aconteceria com uma cadeia de ferramentas mais complicada que depende de outras metas
no repositório do Bazel?
Vamos ver 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 de uma regra padrão, mas o significado do parâmetro cfg
é um pouco diferente.
A dependência de um destino (chamado de "pai") para uma cadeia de ferramentas via resolução
de cadeia de ferramentas usa uma transição de configuração especial chamada "transição
de cadeia de ferramentas". A transição da cadeia de ferramentas mantém a configuração igual, exceto que força a plataforma de execução a ser a mesma para a cadeia de ferramentas e para o pai. Caso contrário, a resolução da cadeia de ferramentas poderia escolher qualquer plataforma de execução e não seria necessariamente a mesma do pai. Isso
permite que qualquer dependência exec
da cadeia de ferramentas também seja executável para as
ações de build do pai. Todas as dependências da cadeia de ferramentas que usam cfg =
"target"
(ou que não especificam cfg
, já que "target" é o padrão) são criadas para a mesma plataforma de destino que o pai. Isso permite que as regras da cadeia de ferramentas contribuam com as duas bibliotecas (o atributo system_lib
acima) e ferramentas (o atributo compiler
) para as regras de build que precisam delas. As bibliotecas do sistema são vinculadas ao artefato final e, portanto, precisam ser criadas para a mesma plataforma. Já o compilador é uma ferramenta invocada durante o build e precisa ser executado na plataforma de execução.
Registro e criação com conjuntos de ferramentas
Neste ponto, todos os blocos de construção estão montados, e você só precisa disponibilizar as toolchains para o procedimento de resolução do Bazel. Isso é feito registrando a cadeia de ferramentas em um arquivo MODULE.bazel
usando register_toolchains()
ou transmitindo os rótulos das cadeias de ferramentas na linha de comando usando a flag --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 toolchains, a ordem em que as toolchains individuais são registradas é determinada pelas seguintes regras:
- As toolchains definidas em um subpacote de um pacote são registradas antes das toolchains definidas no próprio pacote.
- Em um pacote, as toolchains são registradas na ordem lexicográfica dos nomes.
Agora, quando você cria uma meta que depende de um tipo de cadeia de ferramentas, uma cadeia de ferramentas apropriada é selecionada 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 perceber que //my_pkg:my_bar_binary
está sendo criado com uma plataforma que
tem @platforms//os:linux
e, portanto, vai resolver 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 da cadeia de ferramentas
Para cada destino que usa conjuntos de ferramentas, o procedimento de resolução do Bazel determina as dependências concretas do destino. O procedimento usa como entrada um conjunto de tipos de cadeia de ferramentas obrigatórios, a plataforma de destino, a lista de plataformas de execução disponíveis e a lista de cadeias de ferramentas disponíveis. As saídas são uma cadeia de ferramentas selecionada para cada tipo de cadeia de ferramentas e uma plataforma de execução selecionada para o destino atual.
As plataformas de execução e as toolchains disponíveis são coletadas do gráfico de dependência externa usando as chamadas register_execution_platforms
e register_toolchains
em arquivos MODULE.bazel
.
Outras plataformas de execução e conjuntos de ferramentas também podem ser especificados na linha de comando via --extra_execution_platforms
e --extra_toolchains
.
A plataforma host é incluída automaticamente como uma plataforma de execução disponível.
As plataformas e toolchains disponíveis são rastreadas como listas ordenadas para determinismo, com preferência para itens anteriores na lista.
O conjunto de toolchains disponíveis, em ordem de prioridade, é criado com base em
--extra_toolchains
e register_toolchains
:
- As toolchains registradas usando
--extra_toolchains
são adicionadas primeiro. Dentro delas, a última cadeia de ferramentas tem a maior prioridade. - Toolchains registradas usando
register_toolchains
no gráfico de dependência externa transitiva, na seguinte ordem: (entre elas, a primeira toolchain mencionada tem a maior prioridade).- Toolchains registradas pelo módulo raiz (ou seja, o
MODULE.bazel
na raiz do workspace); - Toolchains registradas no arquivo
WORKSPACE
do usuário, incluindo macros invocadas de lá; - Toolchains registradas por módulos não raiz (ou seja, dependências especificadas pelo módulo raiz e as dependências dele, e assim por diante);
- Toolchains registradas no "sufixo WORKSPACE". Isso só é usado por algumas regras nativas agrupadas com a instalação do Bazel.
- Toolchains registradas pelo módulo raiz (ou seja, o
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 para resolver o problema.
Uma cláusula
target_compatible_with
ouexec_compatible_with
corresponde a uma plataforma se, para cadaconstraint_value
na lista, a plataforma também tiver esseconstraint_value
(de forma explícita ou como padrão).Se a plataforma tiver
constraint_value
s deconstraint_setting
s não referenciados pela cláusula, eles não vão afetar a correspondência.Se o destino que está sendo criado especificar o
exec_compatible_with
atributo (ou se a definição da regra especificar o argumentoexec_compatible_with
), a lista de plataformas de execução disponíveis será filtrada para remover aquelas que não correspondem às restrições de execução.A lista de toolchains disponíveis é filtrada para remover aquelas que especificam
target_settings
e não correspondem à configuração atual.Para cada plataforma de execução disponível, associe cada tipo de cadeia de ferramentas à primeira cadeia de ferramentas disponível, se houver, que seja compatível com essa plataforma de execução e a plataforma de destino.
Qualquer plataforma de execução que não encontrou uma cadeia de ferramentas obrigatória compatível para um dos tipos de cadeia de ferramentas é descartada. Das plataformas restantes, a primeira se torna a plataforma de execução do destino atual, e as toolchains associadas (se houver) se tornam dependências do destino.
A plataforma de execução escolhida é usada para executar todas as ações geradas pela meta.
Nos casos em que o mesmo destino pode ser criado em várias configurações (como para diferentes CPUs) na mesma 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 da cadeia de ferramentas separadamente, e cada um terá a própria plataforma de execução e as próprias cadeias de ferramentas.
Como depurar conjuntos de ferramentas
Se você estiver adicionando suporte a um conjunto de ferramentas a uma regra atual, use a
flag --toolchain_resolution_debug=regex
. Durante a resolução da cadeia de ferramentas, a flag
fornece uma saída detalhada para tipos de cadeia de ferramentas ou nomes de destino que correspondem à variável regex. Você pode usar .*
para gerar todas as informações. O Bazel vai gerar os nomes das toolchains que ele verifica e ignora durante o processo de resolução.
Por exemplo, para depurar a seleção da cadeia de ferramentas de todas as ações criadas diretamente por
//my:target
:
$ bazel build //my:all --toolchain_resolution_debug=//my:target
Para depurar a seleção da cadeia de ferramentas em todas as ações e destinos de build:
$ bazel build //my:all --toolchain_resolution_debug=.*
Se você quiser saber quais dependências cquery
são da resolução
da cadeia de ferramentas, use a flag --transitions
do 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