Esta página descreve a estrutura do conjunto de ferramentas, que é uma maneira de os autores de regras desvincularem a lógica da regra da seleção de ferramentas com base na plataforma. Recomendamos ler as páginas de regras e plataformas antes de continuar. Esta página aborda 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 da plataforma.
Motivação
Vamos primeiro analisar o problema que os conjuntos de ferramentas foram projetados para resolver. Suponha que você
esteja escrevendo regras para oferecer suporte à linguagem de programação "bar". Sua bar_binary
regra compilaria arquivos *.bar usando o compilador barc, uma ferramenta que
é criada como outro destino no seu espaço de trabalho. Como os usuários que escrevem bar_binary
destinos 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 cada destino bar_binary. Portanto, ele será criado antes de qualquer destino bar_binary. É possível acessar a 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 é codificado em bar_binary, mas
destinos diferentes podem precisar de compiladores diferentes, dependendo da plataforma para a qual estão sendo criados e da plataforma em que estão sendo criados, chamados 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. Portanto, não é possí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 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",
)
É 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 bar_binary.
Se esse estilo não for usado de maneira consistente em todo o espaço de trabalho, ele vai gerar
builds que funcionam bem em uma única plataforma, mas falham quando estendidos a
cenários multiplataforma. Ele também não resolve o problema de adicionar suporte
a novas plataformas e compiladores sem modificar regras ou destinos atuais.
A estrutura do conjunto 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 conjunto de ferramentas), e 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 destino precisam conhecer o conjunto completo de plataformas e conjuntos de ferramentas disponíveis.
Como escrever regras que usam conjuntos de ferramentas
Na estrutura 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 conjunto de ferramentas é um destino simples que representa uma classe de ferramentas que servem a mesma função para plataformas diferentes. Por exemplo, é possível 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 é modificada para que, em vez de
receber o compilador como um atributo, ela declare que consome um
//bar_tools:toolchain_type conjunto de ferramentas.
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
ToolchainInfo provedor
de qualquer destino que 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 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 é realmente
uma dependência do destino bar_binary, não todo o espaço de 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 simples (como mostrado acima), o tipo de conjunto 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 conjunto de ferramentas opcional , da seguinte 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 config_common.toolchain_type
função é definida como obrigatória por padrão.
As seguintes formas podem ser usadas:
- 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, ele vai usar a versão mais rigorosa, em que o obrigatório é mais rigoroso do que o opcional.
Como escrever aspectos que usam conjuntos de ferramentas
Os aspectos têm acesso à mesma API de conjunto de ferramentas que as regras: é possível definir tipos de conjunto de ferramentas necessários , acessar conjuntos de ferramentas pelo contexto e usá-los para gerar novas ações usando o conjunto de ferramentas.
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 de conjunto de ferramentas, você precisa de três coisas:
Uma regra específica da linguagem que representa o tipo de ferramenta ou conjunto de ferramentas. Por convenção, o nome dessa regra tem o sufixo "_toolchain".
- Observação: a regra
\_toolchainnão pode criar ações de build. Em vez disso, ela 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 da ferramenta ou do conjunto de ferramentas para plataformas diferentes.
Para cada destino, um destino associado da regra genérica
toolchain, para fornecer metadados usados pela estrutura do conjunto de ferramentas. Essetoolchaindestino também se refere aotoolchain_typeassociado a esse conjunto de ferramentas. Isso significa que uma determinada regra_toolchainpode ser associada a qualquertoolchain_typee que somente em uma instânciatoolchainque usa essa regra_toolchaina regra é associada a umtoolchain_type.
Para nosso exemplo em execução, confira 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 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 BarcInfo objeto 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 toolchain definições para os dois bar_toolchain destinos.
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 é
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 no
mesmo pacote, mas não há motivo para que 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 possam estar em pacotes separados.
Consulte go_toolchain
para um exemplo real.
Conjuntos de ferramentas e configurações
Uma pergunta importante para os 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 de uma regra padrão,
mas o significado do parâmetro cfg é um pouco diferente.
A dependência de um destino (chamado de "principal") em um conjunto de ferramentas pela resolução do conjunto de ferramentas 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 configuração a mesma, exceto
que ela força a plataforma de execução a ser a mesma para o conjunto de ferramentas e para
o principal. Caso contrário, a resolução do conjunto de ferramentas para o conjunto de ferramentas poderia escolher qualquer
plataforma de execução e não seria necessariamente a mesma do principal. Isso
permite que qualquer dependência exec do conjunto de ferramentas também seja executável para as
ações de build do principal. Qualquer dependência do conjunto de ferramentas que use cfg =
"target" (ou que não especifique cfg, já que "target" é o padrão) são
criadas para a mesma plataforma de destino que o principal. Isso permite que as regras do conjunto de ferramentas
contribuam com bibliotecas (o atributo system_lib acima) e ferramentas (o
compiler atributo) 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, enquanto o compilador é uma ferramenta invocada durante o build e precisa
ser executada na plataforma de execução.
Como registrar e criar com conjuntos de ferramentas
Nesse momento, todos os blocos de construção sã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 dos conjuntos 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 conjuntos de ferramentas, a ordem em que os conjuntos de ferramentas individuais são registrados é determinada pelas seguintes regras:
- Os conjuntos de ferramentas definidos em um subpacote de um pacote são registrados antes dos conjuntos de ferramentas definidos no próprio pacote.
- Em um pacote, os conjuntos de ferramentas são registrados na ordem lexicográfica de seus nomes.
Agora, quando você cria um destino que depende de um tipo de conjunto de ferramentas, um conjunto de ferramentas adequado será selecionado com base nos destinos e nas plataformas 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, resolverá a
//bar_tools:toolchain_type referência para //bar_tools:barc_linux_toolchain.
Isso vai acabar criando //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 recebe como entrada um conjunto de tipos de conjunto de ferramentas necessários, a plataforma de destino, a lista de plataformas de execução disponíveis e a lista de conjuntos de ferramentas disponíveis. As saídas são um conjunto de ferramentas selecionado para cada tipo de conjunto de ferramentas, 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 coletados do
WORKSPACE arquivo via
register_execution_platforms
e
register_toolchains.
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 do 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 em
--extra_toolchains e register_toolchains:
- Os conjuntos de ferramentas registrados usando
--extra_toolchainssão adicionados primeiro.- Entre eles, o último conjunto de ferramentas tem a maior prioridade.
- Conjuntos de ferramentas registrados usando
register_toolchains- Entre eles, 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.
As etapas de resolução são as seguintes.
Uma cláusula
target_compatible_withouexec_compatible_withcorresponde a uma plataforma se, para cadaconstraint_valuena lista, a plataforma também tiver esseconstraint_value(explicitamente ou como padrão).Se a plataforma tiver
constraint_values deconstraint_settings não referenciados pela cláusula, isso não afetará a correspondência.Se o destino que está sendo criado especificar o
exec_compatible_withatributo (ou a definição da regra especificar oexec_compatible_withargumento), 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, associe cada tipo de conjunto de ferramentas ao primeiro conjunto de ferramentas disponível, se houver, que seja compatível com essa execução plataforma e a plataforma de destino.
Qualquer plataforma de execução que não tenha encontrado um conjunto de ferramentas obrigatório compatível para um dos tipos de conjunto de ferramentas será descartada. Das plataformas restantes, 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 (como 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 do conjunto de ferramentas separadamente, e cada um terá a própria plataforma de execução e conjuntos de ferramentas.
Como depurar conjuntos de ferramentas
Se você estiver adicionando suporte a conjuntos de ferramentas a uma regra atual, use a
--toolchain_resolution_debug=regex flag. 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 correspondam à variável regex. É possível usar .* para gerar todas as informações. O Bazel vai gerar nomes de conjuntos de ferramentas que ele
verifica e ignora durante o processo de resolução.
Se você quiser saber quais cquery dependências são da resolução do conjunto 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