Conjuntos de ferramentas

Informar um problema Mostrar fonte Por noite · 7,3 · 7,2 · 7,1 · 7,0 · 6,5

Nesta página, descrevemos a estrutura do conjunto de ferramentas, que é uma forma dos autores de regras dissociar a lógica de regra da seleção de ferramentas baseada na plataforma. É recomendamos ler as regras e as plataformas páginas antes de continuar. Nesta página, explicamos por que os conjuntos de ferramentas são necessários, como defini-los e usá-los, além de como o Bazel seleciona um conjunto de ferramentas apropriado com base restrições da plataforma.

Motivação

Primeiro, vamos analisar os problemas que os conjuntos de ferramentas foram projetados para resolver. Suponha que você estão escrevendo regras para apoiar a "barra" de programação centrada no usuário. Seu bar_binary regra compilaria arquivos *.bar usando o compilador barc, uma ferramenta que por si só é criado como outro destino no seu espaço de trabalho. Como os usuários que escrevem bar_binary os destinos não precisam especificar uma dependência no compilador, você o 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. Ela pode ser acessada pelo usuário função de implementação, 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 alvos diferentes podem precisar de diferentes compiladores, dependendo da plataforma em que estão sendo desenvolvidas e em qual plataforma elas estão sendo desenvolvidas, chamada de plataforma de destino e plataforma de execução, respectivamente. Além disso, a regra nem necessariamente conhece todas as ferramentas e plataformas disponíveis, então 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. Então, os alvos 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 um pouco difícil de fazer para 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 falham quando estendidos para em vários cenários multiplataforma. Ela também não aborda o problema de adicionar suporte para novas plataformas e compiladores sem modificar as regras ou os destinos atuais.

A estrutura do conjunto de ferramentas resolve esse problema adicionando um nível extra de ou 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 conjunto de ferramentas), e o Bazel resolve isso automaticamente para um destino específico (um conjunto de ferramentas) com base no restrições de plataforma aplicáveis. Nem o autor da regra nem o autor de 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 as regras dependerem diretamente das ferramentas, em vez disso, eles dependem de tipos de conjunto de ferramentas. Um tipo de conjunto de ferramentas é um destino simples que representa uma classe de ferramentas que têm a mesma função para diferentes plataformas. Por exemplo, é possível declarar um tipo que representa a barra compilador:

# 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 absorvendo o compilador como um atributo, ele declara 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 ToolchainInfo provedor de qualquer destino em que o Bazel resolveu a dependência do conjunto de ferramentas. Os campos do elemento ToolchainInfo são definidos pela regra da ferramenta subjacente. no próximo essa regra é definida de forma que haja um campo barcinfo que une 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 de todo o espaço do candidato de última geração.

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 consegue encontrar um conjunto de ferramentas correspondente (consulte Resolução do conjunto de ferramentas abaixo) para um conjunto de ferramentas obrigatório. em tipo, isso é um erro, e a análise é interrompida.

Em vez disso, é possível declarar uma dependência de tipo de conjunto de ferramentas opcional, conforme da seguinte forma:

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 o resultado de ctx.toolchains[""//bar_tools:toolchain_type"] é None.

O config_common.toolchain_type o padrão é obrigatório.

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

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 usará a versão mais estrita, em que o campo obrigatório é mais restrito do que 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 as regras de tipos de conjunto de ferramentas, acessar os conjuntos por meio do 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, três coisas são necessárias:

  1. Uma regra específica da linguagem que representa o tipo de ferramenta ou conjunto de ferramentas. De 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 o que usa o conjunto de ferramentas. Essa regra é responsável por criar todos ações de build.
  2. Vários destinos desse tipo de regra, representando as versões da ferramenta ou ferramenta para diferentes plataformas.

  3. Para cada uma dessas segmentações, uma segmentação associada do toolchain para fornecer metadados usados pela estrutura do conjunto de ferramentas. Este toolchain destino também se refere ao toolchain_type associado a este conjunto de ferramentas. Isso significa que determinada regra _toolchain pode ser associada a qualquer toolchain_type e isso apenas em uma instância de toolchain que usa a regra _toolchain que a regra está associada a um toolchain_type.

Para nosso exemplo em execução, aqui está a definição de uma regra bar_toolchain. Nossos 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 valores de campo arbitrários pares. A especificação de exatamente quais campos são adicionados ao ToolchainInfo precisam ser claramente documentados no tipo de conjunto de ferramentas. Neste exemplo, os valores retornar encapsulados em um objeto BarcInfo para reutilizar o esquema definido acima; este pode ser útil para validação e reutilização de 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, 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 de ferramentas está apropriadas 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 o tipo de conjunto de ferramentas, configurações destinos do conjunto de ferramentas, e os destinos de definição toolchain não podem estar todos em separados pacotes.

Consulte o go_toolchain para um exemplo do mundo real.

Conjuntos de ferramentas e configurações

Uma pergunta importante para autores de regras é: quando um destino bar_toolchain é analisados, qual configuração ela vê e quais transições precisa ser usado 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 alvos 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 um conjunto de ferramentas por meio do conjunto de ferramentas. usa uma transição de configuração especial chamada de "conjunto de ferramentas de transição". A transição do conjunto de ferramentas mantém a configuração a mesma, exceto isso força a plataforma de execução a ser a mesma para o conjunto de ferramentas e para as o pai. Caso contrário, a resolução do conjunto de ferramentas poderia escolher qualquer plataforma de execução, e não necessariamente o mesmo que para o pai). Isso permite que qualquer dependência exec do conjunto de ferramentas também seja executável para o das ações de build do pai. 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: criado para a mesma plataforma de destino que o pai. Isso permite que as regras do conjunto de ferramentas contribuem com bibliotecas (o atributo system_lib acima) e ferramentas (o atributo compiler) às regras de build que precisam deles. Bibliotecas do sistema são vinculadas ao artefato final e, portanto, precisam ser criadas para o mesmo plataforma, enquanto o compilador é uma ferramenta invocada durante o build e precisa possam ser executados 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 fazer os conjuntos de ferramentas disponíveis para o procedimento de resolução do Bazel. Isso é feito pela registrar o conjunto de ferramentas em um arquivo WORKSPACE usando register_toolchains() ou passando o URL os identificadores do 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",
)

Agora, quando você criar um destino que dependa de um tipo de conjunto de ferramentas, uma implementação conjunto de ferramentas serão selecionados 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 notar que o //my_pkg:my_bar_binary está sendo criado com uma plataforma que tem @platforms//os:linux e, portanto, resolver Referência de //bar_tools:toolchain_type a //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, é usado o procedimento de resolução de conjunto de ferramentas do Bazel. determina as dependências concretas do conjunto de ferramentas do destino. O procedimento toma como entrada uma de tipos de conjunto de ferramentas obrigatórios, a plataforma de destino, a lista de tipos plataformas de execução e a lista de conjuntos de ferramentas disponíveis. Suas saídas são uma conjunto de ferramentas selecionado para cada tipo de conjunto de ferramentas, bem como uma execução selecionada plataforma para o destino atual.

As plataformas de execução e os conjuntos de ferramentas disponíveis são reunidos nas Arquivo WORKSPACE por register_execution_platforms e register_toolchains Plataformas de execução e conjuntos de ferramentas adicionais também podem ser especificados no 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 os conjuntos de ferramentas disponíveis são rastreados como listas ordenadas para determinismo. com preferência para os itens anteriores da lista.

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, ela também tiver 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 afetam a correspondência.

  2. Se o destino que está sendo criado especificar Atributo exec_compatible_with (ou sua definição de regra especificar o argumento exec_compatible_with), a lista de plataformas de execução disponíveis é filtrada para remover de qualquer um que não corresponda às restrições de execução.

  3. Para cada plataforma de execução disponível, associe cada tipo de conjunto de ferramentas a o primeiro conjunto de ferramentas disponível, se houver, que seja compatível com essa execução entre a plataforma de destino e a de destino.

  4. 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 é descartado. Entre as plataformas restantes, a o primeiro se torna a plataforma de execução do alvo atual e a respectiva os conjuntos de ferramentas (se houver) tornam-se dependências do destino.

A plataforma de execução escolhida é usada para executar todas as ações gera.

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 maneira independente para cada versão do destino.

Se a regra usar grupos de execução, cada execução executa a resolução do conjunto de ferramentas separadamente, e cada um deles tem a própria execução do Google Cloud e dos conjuntos de ferramentas.

Depuração de conjuntos de ferramentas

Se você está adicionando suporte para o conjunto de ferramentas a uma regra existente, use o sinalização --toolchain_resolution_debug=regex. Durante a resolução do conjunto de ferramentas, a sinalização 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 os nomes dos conjuntos de ferramentas verificações e pulos durante o processo de resolução.

Caso você queira ver quais dependências de cquery são do conjunto de ferramentas resolução, 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