Nesta página, descrevemos o framework do conjunto de ferramentas, que é uma maneira dos autores de regras desacoplarem a lógica de regras da seleção de ferramentas baseada na plataforma. Recomendamos ler as páginas regras e plataformas antes de continuar. Esta página aborda por que os conjuntos de ferramentas são necessários, como definir e usar esses conjuntos e como o Bazel seleciona um conjunto de ferramentas adequado com base nas restrições da plataforma.
Motivação
Vamos primeiro analisar o problema que as cadeias de ferramentas foram projetadas para resolver. Suponha que você
esteja escrevendo regras para oferecer suporte à linguagem de programação "bar". A regra bar_binary
compila arquivos *.bar
usando o compilador barc
, uma ferramenta
criada como outro destino no espaço de trabalho. Como os usuários que criam 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],
),
},
)
//bar_tools:barc_linux
agora é uma dependência de todos os destinos bar_binary
, então
ele será criado antes de qualquer destino bar_binary
. Ele pode ser acessado 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 aqui é que o rótulo do compilador está fixado no código em bar_binary
, mas
destinos diferentes podem precisar de compiladores diferentes, dependendo da plataforma para que
estão sendo criados e em qual plataforma estão sendo criados, chamados de
plataforma de destino e plataforma de execução, respectivamente. Além disso, o autor da regra
não necessariamente conhece todas as ferramentas e plataformas disponíveis. Portanto,
não é viável codificá-las na definição da regra.
Uma solução menos ideal seria transferir a carga para os usuários, tornando
o atributo _compiler
não privado. Em seguida, os destinos individuais poderiam ser
codificados para criar 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",
)
Para melhorar essa solução, use 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",
}),
)
No entanto, isso é tedioso e um pouco demais para cada usuário do bar_binary
.
Se esse estilo não for usado de forma consistente em todo o espaço de trabalho, ele vai levar a
builds que funcionam bem em uma única plataforma, mas falham quando estendidas para
cenários multiplataforma. Ele também não resolve o problema de adicionar suporte
a novas plataformas e compiladores sem modificar as regras ou metas atuais.
A estrutura do conjunto de ferramentas resolve esse problema adicionando um nível extra de indireção. Basicamente, você declara que a regra tem uma dependência abstrata em algum membro de uma família de destinos (um tipo de toolchain), e o Bazel resolve isso automaticamente para um destino específico (uma toolchain) com base nas restrições da plataforma aplicável. Nem o autor da regra nem o autor de destino precisam conhecer o conjunto completo de plataformas e cadeias de ferramentas disponíveis.
Como escrever regras que usam cadeias de ferramentas
No framework de conjunto de ferramentas, em vez de as regras dependerem diretamente de 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 à mesma função em plataformas diferentes. Por exemplo, é possível declarar um tipo que represente 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 de regra na seção anterior foi modificada para que, em vez de
usar 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 toolchain 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 o qual o Bazel tenha resolvido 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 modo que haja um campo barcinfo
que envolva
um objeto BarcInfo
.
O procedimento do Bazel para resolver cadeias de ferramentas para destinos é descrito
abaixo. Apenas o destino da cadeia de ferramentas resolvido é
feito uma dependência do destino bar_binary
, não todo o espaço de cadeias de ferramentas
candidatas.
Cadeias de ferramentas obrigatórias e opcionais
Por padrão, quando uma regra expressa uma dependência de tipo de toolchain usando um rótulo simples (como mostrado acima), o tipo de toolchain é considerado obrigatório. Se o Bazel não conseguir encontrar um conjunto de ferramentas correspondente (consulte Resolução de conjunto de ferramentas abaixo) para um tipo de conjunto de ferramentas obrigatório, isso será um erro e a análise será interrompida.
É possível declarar uma dependência de tipo de cadeia de ferramentas opcional, conforme a seguir:
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.
É possível usar os seguintes formulários:
- 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),
],
)
Você também pode combinar formulários na mesma regra. No entanto, se o mesmo tipo de conjunto de ferramentas for listado várias vezes, ele usará a versão mais estrita, em que o obrigatório é mais restrito do que opcional.
Como escrever aspectos que usam cadeias de ferramentas
Os aspectos têm acesso à mesma API de toolchain que as regras: é possível definir tipos de toolchain necessários, acessar toolchains pelo contexto e usá-los para gerar novas ações usando a toolchain.
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 um determinado tipo, três coisas são necessárias:
Uma regra específica da linguagem que representa o tipo de ferramenta ou conjunto de ferramentas. Por convenção, o nome dessa regra é seguido por "_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 conjunto de ferramentas para diferentes plataformas.
Para cada destino, um destino associado da regra genérica
toolchain
para fornecer metadados usados pelo framework do conjunto de ferramentas. Este destinotoolchain
também se refere aotoolchain_type
associado a esse conjunto de ferramentas. Isso significa que determinada regra_toolchain
pode ser associada a qualquertoolchain_type
e que somente uma instânciatoolchain
que usa essa regra_toolchain
que a regra seja associada a umatoolchain_type
.
Para nosso exemplo em execução, aqui está 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 de exatamente quais campos são adicionados ao ToolchainInfo
precisa ser claramente documentada no tipo de conjunto de ferramentas. Neste exemplo, os valores
são retornados 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, você vai criar definições de toolchain
para os dois destinos bar_toolchain
.
Essas definições vinculam os destinos específicos do idioma ao tipo de conjunto de ferramentas e
fornecem as informações de restrição que informam ao Bazel quando o conjunto de ferramentas é
adequado 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 toolchain, os destinos de toolchain específicos
de idioma e os destinos de definição toolchain
não possam estar em pacotes
separados.
Consulte o go_toolchain
para um exemplo real.
Toolchains e configurações
Uma pergunta importante para os autores de regras é: quando um destino bar_toolchain
é
analisado, qual configuração ele encontra 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 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 que 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 uma cadeia de ferramentas pela resolução
de cadeia de ferramentas usa uma transição de configuração especial chamada de "transição de
cadeia de ferramentas". A transição da cadeia de ferramentas mantém a configuração, exceto
que ela força a plataforma de execução a ser a mesma para a cadeia de ferramentas e para
a mãe. Caso contrário, a resolução da cadeia de ferramentas para a cadeia de ferramentas poderia escolher qualquer
plataforma de execução e não seria necessariamente a mesma que a mãe. Isso
permite que todas as dependências exec
da cadeia de ferramentas também sejam executáveis para as
ações de build da mãe. Todas as dependências do toolchain 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 bibliotecas (o atributo system_lib
acima) e as ferramentas (o
atributo compiler
) para as 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. Já o compilador é uma ferramenta invocada durante o build e precisa
ser executado na plataforma de execução.
Como registrar e criar com conjuntos de ferramentas
Nesse 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, seja em um arquivo WORKSPACE
usando
register_toolchains()
ou transmitindo os rótulos dos conjuntos de ferramentas 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",
)
Agora, quando você criar um destino que dependa de um tipo de conjunto de ferramentas, um conjunto apropriado será selecionado com base nas plataformas de destino e 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, resolverá a
referência de //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 de conjuntos de ferramentas do Bazel determina as dependências concretas do conjunto de ferramentas do destino. O procedimento recebe como entrada um conjunto de tipos de toolchain necessários, a plataforma de destino, a lista de plataformas de execução disponíveis e a lista de toolchains 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 e os conjuntos de ferramentas de execução disponíveis são coletados do arquivo
WORKSPACE
por
register_execution_platforms
e
register_toolchains
.
Outras plataformas de execução e conjuntos de ferramentas também podem ser especificadas 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 as cadeias de ferramentas disponíveis são rastreadas como listas ordenadas para determinismo,
com preferência dada aos itens anteriores na lista.
As etapas de resolução são as seguintes.
Uma cláusula
target_compatible_with
ouexec_compatible_with
corresponde a uma plataforma se, para cadaconstraint_value
na lista, a plataforma também tiver essaconstraint_value
(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 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.Para cada plataforma de execução disponível, você associa cada tipo de conjunto de ferramentas ao primeiro conjunto disponível, se houver, que seja compatível com essa plataforma e com a plataforma de destino.
Qualquer plataforma de execução que não conseguiu encontrar uma cadeia de ferramentas obrigatória compatível para um dos tipos de cadeia de ferramentas é excluída. Das plataformas restantes, a primeira se torna a plataforma de execução do destino atual e os conjuntos de ferramentas associados (se houver) tornam-se dependências do destino.
A plataforma de execução escolhida é usada para executar todas as ações que o destino gera.
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 de execução vai realizar a resolução da cadeia de ferramentas separadamente, e cada um terá a própria plataforma de execução e cadeias de ferramentas.
Como depurar conjuntos de ferramentas
Se você estiver adicionando suporte ao conjunto de ferramentas a uma regra, use a
flag --toolchain_resolution_debug=regex
. Durante a resolução do conjunto de ferramentas, a flag
fornece uma saída detalhada para tipos de conjunto de ferramentas ou nomes de destino que correspondem à variável regex. Você
pode usar .*
para gerar todas as informações. O Bazel mostra nomes de conjuntos de ferramentas que
verifica e pula durante o processo de resolução.
Se você quiser saber quais dependências cquery
são da resolução
do conjunto de ferramentas, use a flag --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