Cadenas de herramientas

Informar un problema Ver fuente

En esta página, se describe el framework de la cadena de herramientas, que es una forma en que los autores de reglas pueden separar su lógica de reglas de la selección de herramientas basadas en la plataforma. Te recomendamos que leas las páginas de reglas y plataformas antes de continuar. En esta página, se explica por qué son necesarias las cadenas de herramientas, cómo definirlas y usarlas, y cómo Bazel selecciona una cadena de herramientas adecuada según las restricciones de la plataforma.

Motivación

Veamos primero los problemas que deben resolverse las cadenas de herramientas. Supongamos que escribes reglas para admitir el lenguaje de programación "bar". Tu regla bar_binary compilaría archivos *.bar con el compilador barc, una herramienta que se compila como otro destino en tu lugar de trabajo. Dado que los usuarios que escriben objetivos bar_binary no deberían tener que especificar una dependencia en el compilador, puedes convertirla en una dependencia implícita agregándola a la definición de la regla como un 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],
        ),
    },
)

Ahora, //bar_tools:barc_linux es una dependencia de cada destino bar_binary, por lo que se compilará antes que cualquier destino bar_binary. La función de implementación de la regla puede acceder a él como cualquier otro 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),
    )
    ...

El problema aquí es que la etiqueta del compilador está codificada en bar_binary, pero los distintos destinos pueden necesitar distintos compiladores según la plataforma para la que se compilan y la plataforma en la que se compilan, lo que se denomina plataforma de destino y plataforma de ejecución, respectivamente. Además, el autor de la regla no necesariamente conoce todas las herramientas y plataformas disponibles, por lo que no es factible codificarlas en la definición de la regla.

Una solución poco ideal sería trasladar la carga a los usuarios haciendo que el atributo _compiler no sea privado. Luego, los destinos individuales podrían codificarse para compilarse en una plataforma o en otra.

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

Si deseas mejorar esta solución, usa select para elegir las compiler según la 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",
    }),
)

Sin embargo, esto es tedioso y es un poco complicado pedirle a cada usuario de bar_binary. Si este estilo no se usa de manera coherente en todo el lugar de trabajo, genera compilaciones que funcionan bien en una sola plataforma, pero fallan cuando se extienden a situaciones multiplataforma. Tampoco soluciona el problema de agregar compatibilidad con plataformas y compiladores nuevos sin modificar las reglas o los objetivos existentes.

El framework de la cadena de herramientas resuelve este problema agregando un nivel adicional de indirección. En esencia, declaras que tu regla tiene una dependencia abstracta de algún miembro de una familia de destinos (un tipo de cadena de herramientas), y Bazel lo resuelve automáticamente para un objetivo en particular (una cadena de herramientas) en función de las restricciones de plataforma aplicables. Ni el autor de la regla ni el autor de destino necesitan conocer el conjunto completo de plataformas y cadenas de herramientas disponibles.

Escribir reglas que usen cadenas de herramientas

En el framework de la cadena de herramientas, en lugar de que las reglas dependan directamente de las herramientas, dependen de tipos de cadenas de herramientas. Un tipo de cadena de herramientas es un objetivo simple que representa una clase de herramientas que cumplen la misma función en diferentes plataformas. Por ejemplo, puedes declarar un tipo que represente el 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")

Se modificó la definición de la regla de la sección anterior para que, en lugar de tomar el compilador como un atributo, declare que consume una cadena de herramientas de //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"],
)

La función de implementación ahora accede a esta dependencia en ctx.toolchains en lugar de ctx.attr, y usa el tipo de cadena de herramientas como clave.

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"] muestra el proveedor ToolchainInfo del destino al que Bazel resolvió la dependencia de la cadena de herramientas. Los campos del objeto ToolchainInfo se establecen mediante la regla de la herramienta subyacente. En la siguiente sección, esta regla se define de modo que haya un campo barcinfo que une un objeto BarcInfo.

El procedimiento de Bazel para resolver cadenas de herramientas en objetivos se describe a continuación. Solo el destino de la cadena de herramientas resuelto es en realidad una dependencia del objetivo bar_binary, no todo el espacio de cadenas de herramientas candidatas.

Cadenas de herramientas obligatorias y opcionales

De forma predeterminada, cuando una regla expresa una dependencia del tipo de cadena de herramientas mediante una etiqueta desnuda (como se muestra más arriba), el tipo de cadena de herramientas se considera obligatorio. Si Bazel no puede encontrar una cadena de herramientas que coincida (consulta la Resolución de cadenas de herramientas más abajo) para un tipo de cadena de herramientas obligatorio, esto es un error y el análisis se detiene.

En su lugar, es posible declarar una dependencia opcional del tipo de cadena de herramientas, de la siguiente manera:

bar_binary = rule(
    ...
    toolchains = [
        config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False),
    ],
)

Cuando no se puede resolver un tipo de cadena de herramientas opcional, el análisis continúa y el resultado de ctx.toolchains["//bar_tools:toolchain_type"] es None.

La función config_common.toolchain_type es obligatoria de forma predeterminada.

Se pueden utilizar los siguientes formularios:

  • Tipos de cadenas de herramientas obligatorios:
    • 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 opcionales de cadenas de herramientas:
    • 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),
    ],
)

También puedes mezclar y combinar formularios en la misma regla. Sin embargo, si el mismo tipo de cadena de herramientas aparece en la lista varias veces, tomará la versión más estricta, en la que lo obligatorio es más estricto que opcional.

Escribir aspectos que usen cadenas de herramientas

Los aspectos tienen acceso a la misma API de la cadena de herramientas que las reglas: puedes definir los tipos de cadenas de herramientas necesarios, acceder a las cadenas de herramientas a través del contexto y usarlas para generar acciones nuevas con la cadena de herramientas.

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 []

Cómo definir las cadenas de herramientas

A fin de definir algunas cadenas de herramientas para un tipo determinado, necesitas tres elementos:

  1. Una regla específica de lenguaje que representa el tipo de herramienta o paquete de herramientas. Por convención, el nombre de esta regla tiene el sufijo “_toolchain”.

    1. Nota: La regla \_toolchain no puede crear ninguna acción de compilación. En cambio, recopila artefactos de otras reglas y los reenvía a la regla que usa la cadena de herramientas. Esa regla es responsable de crear todas las acciones de compilación.
  2. Varios objetivos de este tipo de regla, que representan versiones de la herramienta o paquete de herramientas para diferentes plataformas

  3. Para cada uno de esos objetivos, un destino asociado de la regla genérica toolchain, a fin de proporcionar los metadatos que usa el framework de la cadena de herramientas. El destino toolchain también hace referencia al toolchain_type asociado con esta cadena de herramientas. Esto significa que una regla _toolchain determinada podría asociarse con cualquier toolchain_type y solo en una instancia toolchain que usa esta regla _toolchain que la regla está asociada con un toolchain_type.

Para nuestro ejemplo en ejecución, esta es una definición de una regla bar_toolchain. Nuestro ejemplo solo tiene un compilador, pero también se pueden agrupar otras herramientas, como un vinculador.

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(),
    },
)

La regla debe mostrar un proveedor de ToolchainInfo, que se convierte en el objeto que la regla de consumo recupera mediante ctx.toolchains y la etiqueta del tipo de cadena de herramientas. ToolchainInfo, como struct, puede contener pares arbitrarios de valores de campo. La especificación de exactamente qué campos se agregan a ToolchainInfo debe documentarse claramente en el tipo de cadena de herramientas. En este ejemplo, los valores se muestran unidos en un objeto BarcInfo para reutilizar el esquema definido con anterioridad. Este estilo puede ser útil para la validación y la reutilización del código.

Ahora puedes definir objetivos 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 último, crea las definiciones de toolchain para los dos destinos bar_toolchain. Estas definiciones vinculan los destinos específicos del lenguaje al tipo de cadena de herramientas y proporcionan información de restricciones que le indica a Bazel cuándo la cadena de herramientas es adecuada para una plataforma determinada.

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

El uso de la sintaxis de ruta de acceso relativa anterior sugiere que estas definiciones están todas en el mismo paquete, pero no hay ninguna razón por la cual el tipo de cadena de herramientas, los objetivos de la cadena de herramientas específica del lenguaje y los destinos de definición de toolchain no puedan estar todos en paquetes separados.

Consulta go_toolchain para ver un ejemplo real.

Conjuntos de herramientas y parámetros de configuración

Una pregunta importante para los autores de reglas es, cuando se analiza un destino bar_toolchain, ¿qué configuración ve y qué transiciones se deben usar para las dependencias? En el ejemplo anterior, se usan atributos de cadenas, pero ¿qué sucedería con una cadena de herramientas más complicada que depende de otros destinos en el repositorio de Bazel?

Veamos una versión más compleja 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(),
    },
)

El uso de attr.label es el mismo que para una regla estándar, pero el significado del parámetro cfg es un poco diferente.

La dependencia de un destino (llamado "superior") a una cadena de herramientas a través de la resolución de la cadena de herramientas usa una transición de configuración especial llamada "transición de la cadena de herramientas". La transición de la cadena de herramientas mantiene la configuración igual, excepto que fuerza la plataforma de ejecución a ser la misma para la cadena de herramientas que para la superior (de lo contrario, la resolución de la cadena de herramientas para la cadena de herramientas podría elegir cualquier plataforma de ejecución y no necesariamente sería la misma que para la superior). Esto permite que cualquier dependencia exec de la cadena de herramientas también se pueda ejecutar para las acciones de compilación del elemento superior. Todas las dependencias de la cadena de herramientas que usan cfg = "target" (o que no especifican cfg, ya que "target" es el valor predeterminado) se compilan para la misma plataforma de destino que la superior. Esto permite que las reglas de la cadena de herramientas contribuyan con bibliotecas (el atributo system_lib de arriba) y herramientas (el atributo compiler) a las reglas de compilación que las necesitan. Las bibliotecas del sistema están vinculadas al artefacto final y, por lo tanto, deben compilarse para la misma plataforma, mientras que el compilador es una herramienta invocada durante la compilación y debe poder ejecutarse en la plataforma de ejecución.

Cómo registrarse y compilar con cadenas de herramientas

En este punto, todos los componentes básicos están ensamblados y solo debes hacer que las cadenas de herramientas estén disponibles para el procedimiento de resolución de Bazel. Para ello, registra la cadena de herramientas, ya sea en un archivo MODULE.bazel con register_toolchains() o pasa las etiquetas de las cadenas de herramientas en la línea de comandos con la marca --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/...",
)

Cuando se usan patrones de destino para registrar cadenas de herramientas, el orden en que se registran cada una de ellas se determina según las siguientes reglas:

  • Las cadenas de herramientas definidas en un subpaquete de un paquete se registran antes que las definidas en el paquete.
  • Dentro de un paquete, las cadenas de herramientas se registran en el orden lexicográfico de sus nombres.

Ahora, cuando compiles un destino que dependa de un tipo de cadena de herramientas, se seleccionará una cadena de herramientas adecuada según las plataformas de destino y ejecución.

# 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

Bazel verá que //my_pkg:my_bar_binary se compila con una plataforma que tiene @platforms//os:linux y, por lo tanto, resolverá la referencia //bar_tools:toolchain_type a //bar_tools:barc_linux_toolchain. Esto terminará compilando //bar_tools:barc_linux, pero no //bar_tools:barc_windows.

Resolución de la cadena de herramientas

Para cada destino que usa cadenas de herramientas, el procedimiento de resolución de cadenas de herramientas de Bazel determina las dependencias concretas de las cadenas de herramientas del destino. El procedimiento toma como entrada un conjunto de tipos de cadenas de herramientas necesarios, la plataforma de destino, la lista de plataformas de ejecución disponibles y la lista de cadenas de herramientas disponibles. Sus resultados son una cadena de herramientas seleccionada para cada tipo de cadena, así como una plataforma de ejecución seleccionada para el objetivo actual.

Las plataformas de ejecución y las cadenas de herramientas disponibles se recopilan del gráfico de dependencias externas a través de llamadas a register_execution_platforms y register_toolchains en MODULE.bazelfiles. Additional execution platforms and toolchains may also be specified on the command line via [--extra_execution_platforms](/reference/command-line-reference#flag--extra_execution_platforms) and [--extra_toolchains`](/reference/command-line-reference#flag--extra_toolchains). La plataforma host se incluye automáticamente como una plataforma de ejecución disponible. Se hace un seguimiento de las plataformas y las cadenas de herramientas disponibles como listas ordenadas para determinar el determinismo, con preferencia en los elementos anteriores de la lista.

El conjunto de cadenas de herramientas disponibles, en orden de prioridad, se crea a partir de --extra_toolchains y register_toolchains:

  1. Las cadenas de herramientas registradas con --extra_toolchains se agregan primero. (Dentro de ellas, la cadena de herramientas last tiene la prioridad más alta).
  2. Cadenas de herramientas registradas con register_toolchains en el gráfico de dependencias externas transitivas, en el siguiente orden: (en estas, la primera cadena de herramientas mencionada tiene la prioridad más alta).
    1. Cadenas de herramientas registradas por el módulo raíz (como en MODULE.bazel en la raíz del lugar de trabajo)
    2. Cadenas de herramientas registradas en el archivo WORKSPACE del usuario, incluidas las macros que se invocan desde allí
    3. Cadenas de herramientas registradas por módulos no raíz (como en las dependencias especificadas por el módulo raíz y sus dependencias, etc.)
    4. Cadenas de herramientas registradas en el “sufijo WORKSPACE”; solo se usa en ciertas reglas nativas agrupadas con la instalación de Bazel.

NOTA: Los seudoobjetivos, como :all, :* y /..., se ordenan por el mecanismo de carga de paquetes de Bazel, que usa un orden lexicográfico.

Los pasos para la resolución son los siguientes:

  1. Una cláusula target_compatible_with o exec_compatible_with coincide con una plataforma si, para cada constraint_value de su lista, la plataforma también tiene ese constraint_value (ya sea de forma explícita o como predeterminada).

    Si la plataforma tiene constraint_value de constraint_setting a los que no hace referencia la cláusula, estos no afectan la coincidencia.

  2. Si el destino que se compila especifica el atributo exec_compatible_with (o su definición de la regla especifica el argumento exec_compatible_with), se filtra la lista de plataformas de ejecución disponibles para quitar las que no coincidan con las restricciones de ejecución.

  3. Se filtra la lista de cadenas de herramientas disponibles para quitar aquellas que especifiquen target_settings que no coincidan con la configuración actual.

  4. Para cada plataforma de ejecución disponible, debes asociar cada tipo de cadena de herramientas con la primera, si la hay, que sea compatible con esta plataforma de ejecución y la plataforma de destino.

  5. Se descartan todas las plataformas de ejecución que no pudieron encontrar una cadena de herramientas obligatoria compatible para uno de sus tipos de cadenas de herramientas. De las plataformas restantes, la primera se convierte en la plataforma de ejecución del destino actual y sus cadenas de herramientas asociadas (si las hay) se convierten en dependencias del destino.

La plataforma de ejecución elegida se usa para ejecutar todas las acciones que genera el objetivo.

En los casos en los que el mismo destino se puede compilar en varias configuraciones (como para diferentes CPU) dentro de la misma compilación, el procedimiento de resolución se aplica de forma independiente a cada versión del destino.

Si la regla usa grupos de ejecución, cada uno de ellos realiza la resolución de las cadenas de herramientas por separado y cada uno tiene su propia plataforma de ejecución y sus propias cadenas de herramientas.

Cadenas de herramientas de depuración

Si agregas compatibilidad con una cadena de herramientas a una regla existente, usa la marca --toolchain_resolution_debug=regex. Durante la resolución de la cadena de herramientas, la marca proporciona un resultado detallado para los tipos de cadenas de herramientas o nombres de destino que coinciden con la variable de regex. Puedes usar .* para generar toda la información. Bazel mostrará nombres de cadenas de herramientas que verifica y omite durante el proceso de resolución.

Si deseas ver qué dependencias de cquery corresponden a la resolución de la cadena de herramientas, usa la marca --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