Cadenas de herramientas

Informar un problema Ver fuente Por la noche · 7.2 · 7.1 · 7.0 · 6.5 · 6.4

En esta página, se describe el framework de la cadena de herramientas, que permite a los autores de reglas separan su lógica de reglas de la selección de herramientas basada en la plataforma. Sí se recomienda leer las reglas y las plataformas páginas antes de continuar. En esta página, se explica por qué se necesitan las cadenas de herramientas, cómo definirlos y usarlos, y la forma en que Bazel selecciona una cadena de herramientas adecuada según de la plataforma.

Motivación

Primero, veamos los problemas que las cadenas de herramientas están diseñadas para resolver. Supongamos que es escribir reglas para admitir la "barra" de programación centrado en los datos. Tu bar_binary compilaría archivos *.bar usando el compilador barc, una herramienta que se compila como otro destino en tu lugar de trabajo. Dado que los usuarios que escriben bar_binary los destinos no deberían especificar una dependencia en el compilador, lo conviertes en 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. Se puede acceder a ella mediante el de implementación de la misma manera que 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 diferentes objetivos pueden necesitar diferentes compiladores según la plataforma en la que y en qué plataforma se están creando, denominada la plataforma de destino y plataforma de ejecución, respectivamente. Además, la regla El autor no necesariamente conoce todas las herramientas y plataformas disponibles, así que no es posible codificarlos en la definición de la regla.

Una solución poco ideal sería trasladar la carga a los usuarios haciendo el atributo _compiler no privado Entonces, los objetivos individuales podrían codificadas para compilar para una plataforma u 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",
)

Puedes mejorar esta solución usando select para elegir la 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 algo demasiado exigente para todos los usuarios de bar_binary. Si este estilo no se usa de manera coherente en todo el espacio de trabajo, genera que funcionan bien en una sola plataforma, pero fallan cuando se extienden y multiplataforma. Tampoco aborda el problema de agregar asistencia para nuevas plataformas y compiladores sin modificar las reglas ni los objetivos existentes.

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

Reglas de escritura que usan cadenas de herramientas

En el framework de la cadena de herramientas, en lugar de que las reglas dependan directamente de las herramientas, en su lugar, 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 para diferentes y plataformas de Google Cloud. Por ejemplo, puedes declarar un tipo que represente la 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")

La definición de la regla de la sección anterior se modifica para que, en lugar de recibe el compilador como un atributo, declara que consume un 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, usando el tipo de la 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"] devuelve el Proveedor de ToolchainInfo de cualquier destino al que Bazel haya resuelto la dependencia de la cadena de herramientas. Los campos del Los objetos ToolchainInfo se establecen mediante la regla de la herramienta subyacente. en los próximos esta regla se define de modo que haya un campo barcinfo que une Un objeto BarcInfo

Se describe el procedimiento de Bazel para resolver cadenas de herramientas en los destinos. a continuación. Solo el destino resuelto de la cadena de herramientas es Se convirtió en una dependencia del objetivo bar_binary, no todo el espacio de candidate. cadenas de herramientas.

Conjuntos de herramientas obligatorios y opcionales

De forma predeterminada, cuando una regla expresa una dependencia de un tipo de cadena de herramientas con 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 Resolución de la cadena de herramientas a continuación) para una cadena de herramientas obligatoria. este es un error y se detiene el análisis.

En cambio, es posible declarar una dependencia opcional de un tipo de cadena de herramientas, como sigue:

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

La config_common.toolchain_type de forma predeterminada es obligatorio.

Se pueden usar los siguientes formularios:

  • Tipos de cadenas de herramientas obligatorias:
    • 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 cadenas de herramientas opcionales:
    • 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 combinar formas en la misma regla. Sin embargo, si el mismo toolchain type aparece varias veces, tomará la versión más estricta en la que “obligatorio” es más estricto que “opcional”.

Aspectos de escritura que usan 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, accede a cadenas de herramientas a través del contexto y úsalas para generar nuevos 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

Para definir algunas cadenas de herramientas para un tipo de cadena de herramientas determinado, necesitas tres cosas:

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

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

  3. Para cada uno de estos destinos, se establece un objetivo asociado de la ruta toolchain para proporcionar metadatos utilizados por el framework de la cadena de herramientas. Este/a toolchain target también hace referencia al toolchain_type asociado con esta cadena de herramientas. Esto significa que una regla _toolchain determinada podría asociarse a cualquier toolchain_type, y eso solo en una instancia de toolchain que use esta regla de _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 tiene solo un compilador, pero otras herramientas, como un vinculador, también pueden agrupados debajo de él.

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 ToolchainInfo, que se convierte en el objeto que la regla de consumo se recupera con ctx.toolchains y la etiqueta del tipo de cadena de herramientas. ToolchainInfo, como struct, puede contener un valor de campo arbitrario. pares. La especificación de qué campos se agregan exactamente a ToolchainInfo deben estar claramente documentados en el tipo de cadena de herramientas. En este ejemplo, los valores Devuelve unidos en un objeto BarcInfo para volver a usar el esquema definido antes. este 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, crearás definiciones de toolchain para los dos objetivos de bar_toolchain. Estas definiciones vinculan los objetivos específicos del lenguaje al tipo de cadena de herramientas y la información sobre la restricción que le indica a Bazel cuándo se debe apropiada 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 todas estas definiciones están en el mismo paquete, pero no hay motivo para el tipo de cadena de herramientas, ya sea los destinos de la cadena de herramientas y los objetivos de definición de toolchain no pueden estar todos en paquetes.

Consulta la go_toolchain para ver un ejemplo del mundo real.

Conjuntos de herramientas y parámetros de configuración

Una pregunta importante para los autores de reglas es, cuando un objetivo bar_toolchain es analizada, qué configuración ve y qué transiciones debería usarse para las dependencias? En el ejemplo anterior, se usan atributos de cadena, pero lo que sucedería con una cadena de herramientas más complicada que depende de otros objetivos 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 el de una regla estándar, pero el significado del parámetro cfg es un poco diferente.

La dependencia de un objetivo (llamado "principal") a una cadena de herramientas a través de una cadena de herramientas. resolución usa una transición de configuración especial llamada "cadena de herramientas de transición". La transición de la cadena de herramientas mantiene la configuración igual, excepto que obliga a la plataforma de ejecución a ser la misma para la cadena de herramientas y para el elemento superior (de lo contrario, la resolución de la cadena de herramientas podría elegir cualquiera de ejecución de Compute Engine y no sería necesariamente la misma que la superior). Esta Permite que cualquier dependencia exec de la cadena de herramientas también se pueda ejecutar para el acciones de compilación superiores. Cualquiera de las dependencias de la cadena de herramientas que use cfg = "target" (o que no especifique cfg, ya que "target" es el valor predeterminado) se para la misma plataforma de destino que la plataforma superior. Esto permite que las reglas de la cadena de herramientas aportan bibliotecas (el atributo system_lib mencionado anteriormente) y herramientas (las compiler) a las reglas de compilación que las necesiten. Las bibliotecas del sistema se vinculan al artefacto final, por lo que deben construirse para el mismo de procesamiento, mientras que el compilador es una herramienta invocada durante la compilación y debe pueda ejecutarse en la plataforma de ejecución.

Realiza el registro y la compilación con cadenas de herramientas

En este punto, todos los componentes están ensamblados, y solo debes hacer las cadenas de herramientas disponibles para el procedimiento de resolución de Bazel. Esto lo hace registrando la cadena de herramientas, ya sea en un archivo WORKSPACE con register_toolchains() o si pasas los valores de etiquetas de recurso en el comando 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",
)

Cuando compilas un objetivo que depende de un tipo de cadena de herramientas, se debe considerar La cadena de herramientas se seleccionará 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, resuelve el Referencia de //bar_tools:toolchain_type a //bar_tools:barc_linux_toolchain Con esta acción, se compilará //bar_tools:barc_linux, pero no //bar_tools:barc_windows

Resolución de la cadena de herramientas

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

Las plataformas de ejecución y las cadenas de herramientas disponibles se recopilan de la WORKSPACE mediante register_execution_platforms y register_toolchains También se pueden especificar plataformas de ejecución y cadenas de herramientas adicionales en el la línea de comandos mediante --extra_execution_platforms y --extra_toolchains La plataforma host se incluye automáticamente como una plataforma de ejecución disponible. Se hace un seguimiento de las plataformas disponibles y las cadenas de herramientas como listas ordenadas de determinismo. con preferencia a los elementos anteriores de la lista.

Los pasos para la resolución son los siguientes.

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

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

  2. Si el destino que se está compilando especifica la Atributo exec_compatible_with (o la definición de su regla especifica la argumento de exec_compatible_with), la lista de plataformas de ejecución disponibles se filtra para quitar cualquiera que no coincida con las restricciones de ejecución.

  3. Para cada plataforma de ejecución disponible, se asocia cada tipo de cadena de herramientas con la primera cadena de herramientas disponible, si la hay, que sea compatible con esta ejecución como la plataforma de destino y la plataforma objetivo.

  4. Cualquier plataforma de ejecución que no pudo encontrar una cadena de herramientas obligatoria compatible para uno de sus tipos de cadenas de herramientas. De las plataformas restantes, la el primero se convierte en la plataforma de ejecución del objetivo actual, y su plataforma Las cadenas de herramientas (si las hay) se convierten en dependencias del objetivo.

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

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

Si la regla usa grupos de ejecución, cada ejecución El grupo realiza la resolución de la cadena de herramientas por separado, y cada uno tiene su propia ejecución. y cadenas de herramientas.

Depuración de las cadenas de herramientas

Si quieres agregar compatibilidad con la cadena de herramientas a una regla existente, usa el --toolchain_resolution_debug=regex. Durante la resolución del conjunto de herramientas, la marca Proporciona un resultado detallado para tipos de cadenas de herramientas o nombres de objetivos que coinciden con la variable de regex. Tú Puedes usar .* para obtener toda la información. Bazel mostrará los nombres de las cadenas de herramientas que contiene. comprobaciones y omisiones durante el proceso de resolución.

Si quieres ver qué dependencias cquery son 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