Conjuntos de ferramentas

Informar um problema Ver código-fonte Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Esta página descreve o framework do conjunto de ferramentas, que é uma maneira de os autores de regras separarem a lógica delas da seleção de ferramentas baseada em 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

Primeiro, vamos analisar os problemas que os conjuntos de ferramentas foram projetados para resolver. Suponha que você esteja escrevendo regras para oferecer suporte à linguagem de programação "bar". A 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 no compilador, você pode torná-la uma dependência implícita adicionando-a à definição da regra como um atributo privado.

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 todos os destinos bar_binary. Portanto, 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 é codificado em bar_binary, mas diferentes destinos podem precisar de compiladores diferentes, dependendo da plataforma para a qual eles estão sendo criados e em qual plataforma eles 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 não ideal seria transferir a carga 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 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 maneira consistente em todo o espaço de trabalho, ele resultará em 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 as regras ou metas atuais.

O framework da cadeia de ferramentas resolve esse problema adicionando um nível extra de indirecionamento. 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 de destino precisam conhecer o conjunto completo de plataformas e conjuntos de ferramentas disponíveis.

Como escrever regras que usam cadeias de ferramentas

No framework de conjunto de ferramentas, em vez de ter regras que dependem diretamente de ferramentas, elas dependem de tipos de conjunto de ferramentas. Um tipo de toolchain é um destino simples que representa uma classe de ferramentas que desempenham a mesma função para diferentes plataformas. 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 conjuntos de ferramentas para destinos é descrito abaixo. Somente o destino do conjunto de ferramentas resolvido se torna 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 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.

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),
    ],
)

Você também pode combinar formulários na mesma regra. No entanto, se o mesmo tipo de toolchain for listado várias vezes, ele vai usar a versão mais rígida, em que o obrigatório é mais rigoroso do que o 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, você precisa de três coisas:

  1. Uma regra específica da linguagem que representa o tipo de ferramenta ou conjunto de ferramentas. Por convenção, o nome dessa regra é sufixado com "_conjunto de ferramentas".

    1. 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 o conjunto de ferramentas. Essa regra é responsável por criar todas as ações de build.
  2. Vários destinos desse tipo de regra, representando versões do conjunto de ferramentas ou ferramentas para diferentes plataformas.

  3. Para cada destino, um destino associado da regra genérica toolchain para fornecer metadados usados pelo framework do conjunto de ferramentas. Esse destino toolchain também se refere ao toolchain_type associado a essa cadeia de ferramentas. Isso significa que uma regra _toolchain pode ser associada a qualquer toolchain_type, e que apenas em uma instância toolchain que usa essa regra _toolchain é que a regra é associada a um toolchain_type.

Para nosso exemplo em execução, aqui está a definição de 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, assim como struct, pode conter pares arbitrários de valores de campo. 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 da linguagem ao tipo de conjunto de ferramentas e fornecem as informações de restrição que informam ao Bazel quando o conjunto é 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 conferir 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 encontra e quais transições devem 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 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 conjunto de ferramentas que usam cfg = "target" (ou que não especificam cfg, já que "destino" é o padrão) são criadas para a mesma plataforma 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 toolchains

Nesse ponto, todos os blocos de construção estão montados, e você só precisa disponibilizar as cadeias de ferramentas para o procedimento de resolução do Bazel. Isso é feito registrando o conjunto de ferramentas em um arquivo MODULE.bazel 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 toolchains, a ordem em que as toolchains individuais são registradas é determinada pelas seguintes regras:

  • As cadeias de ferramentas definidas em um subpacote de um pacote são registradas antes das definidas no próprio pacote.
  • Em um pacote, as cadeias de ferramentas são registradas na ordem alfabética dos nomes.

Agora, quando você cria um destino que depende de um tipo de toolchain, uma toolchain adequada é 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 detectar que //my_pkg:my_bar_binary está sendo criado com uma plataforma que tem @platforms//os:linux e, portanto, 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 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. Suas 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 as cadeias de ferramentas disponíveis são coletadas do gráfico de dependência externa usando register_execution_platforms e register_toolchains chamadas em MODULE.bazelfiles. Additional execution platforms and toolchains may also be specified on the command line via [--extra_execution_platforms](/versions/7.4.0/reference/command-line-reference#flag--extra_execution_platforms) and [--extra_toolchains`](/versions/7.4.0/reference/command-line-reference#flag--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.

O conjunto de toolchains disponíveis, em ordem de prioridade, é criado a partir de --extra_toolchains e register_toolchains:

  1. Os conjuntos de ferramentas registrados usando --extra_toolchains são adicionados primeiro. Entre elas, a cadeia de ferramentas última tem a maior prioridade.
  2. Chains de ferramentas registradas usando register_toolchains no gráfico de dependência externa transitiva, na seguinte ordem: (entre elas, a cadeia de ferramentas primeira mencionada tem a maior prioridade.)
    1. Conjuntos de ferramentas registrados pelo módulo raiz, como o MODULE.bazel na raiz do espaço de trabalho.
    2. Chains de ferramentas registradas no arquivo WORKSPACE do usuário, incluindo macros invocadas a partir dele;
    3. Chains de ferramentas registradas por módulos que não são raiz (como dependências especificadas pelo módulo raiz e suas dependências, e assim por diante);
    4. Toolchains registradas no "sufixo WORKSPACE";, que é usado apenas por certas regras nativas empacotadas com a instalação do Bazel.

Observação: pseudodestinos, como :all, :* e /..., são ordenados pelo mecanismo de carregamento de pacotes do Bazel, que usa uma ordem alfabética.

As etapas de resolução são as seguintes.

  1. Uma cláusula target_compatible_with ou exec_compatible_with corresponde a uma plataforma se, para cada constraint_value na lista, a plataforma também tiver essa constraint_value (explicitamente ou como padrão).

    Se a plataforma tiver constraint_values de constraint_settings não referenciados pela cláusula, eles não afetarão a correspondência.

  2. Se o destino que está sendo criado especificar o atributo exec_compatible_with (ou se a definição da regra especificar o argumento exec_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.

  3. Para cada plataforma de execução disponível, você associa cada tipo de toolchain à primeira toolchain disponível, se houver, que seja compatível com essa plataforma de execução e a plataforma de destino.

  4. 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ê está adicionando suporte para o conjunto de ferramentas a uma regra atual, use a sinalização --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.

Caso você queira ver quais dependências de 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