En esta página, se describe el framework de la cadena de herramientas, que permite a los autores de reglas separar su lógica de reglas de la selección de herramientas basada en la plataforma. Te recomendamos que leas las páginas reglas y plataformas antes de continuar. En esta página, se explica por qué se necesitan las cadenas de herramientas, cómo definirlas y usarlas, y cómo Bazel selecciona una apropiada según las restricciones de la plataforma.
Motivación
Primero, veamos los problemas que las cadenas de herramientas están diseñadas para resolver. Supongamos que estás escribiendo 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 espacio de trabajo. Dado que los usuarios que escriben objetivos bar_binary
no deben especificar una dependencia en el compilador, la conviertes 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 objetivo bar_binary
. Se puede acceder a través de la función de implementación de la regla 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 es posible que diferentes destinos necesiten compiladores diferentes según la plataforma para la que se compilan y en la que se compilan, llamadas 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 menos ideal sería trasladar la carga a los usuarios, haciendo que el atributo _compiler
no sea privado. Luego, los destinos individuales pueden codificarse para compilarse para una plataforma o para 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 con select
para elegir el 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 un poco excesivo para pedirle a cada usuario de bar_binary
.
Si este estilo no se usa de manera coherente en todo el espacio de trabajo, se generan compilaciones que funcionan bien en una sola plataforma, pero fallan cuando se extienden a situaciones multiplataforma. Tampoco aborda el problema de agregar compatibilidad con plataformas y compiladores nuevos sin modificar las reglas o los destinos 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 resuelve automáticamente esto en un destino específico (una cadena de herramientas) según las restricciones aplicables de la plataforma. Ni el autor de la regla ni el autor de destino necesitan 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, dependen de los tipos de cadenas de herramientas. Un tipo de cadena de herramientas es un destino 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 modifica la definición de la regla de la sección anterior para que, en lugar de tomar el compilador como atributo, declare que consume una cadena de herramientas //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 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. La regla de la herramienta subyacente establece los campos del objeto ToolchainInfo
. En la siguiente sección, esta regla se define de modo que haya un campo barcinfo
que une un objeto BarcInfo
.
A continuación, se describe el procedimiento de Bazel para resolver
las cadenas de herramientas en los destinos. Solo el destino de la cadena de herramientas resuelto se convierte en una dependencia del objetivo bar_binary
, no todo el espacio de las cadenas de herramientas candidatas.
Cadenas de herramientas obligatorias y opcionales
De forma predeterminada, cuando una regla expresa una dependencia de tipo de cadena de herramientas con una etiqueta sin formato (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 un tipo de cadena de herramientas obligatorio, se produce un error y se detiene el análisis.
En su lugar, es posible declarar una dependencia de tipo de cadena de herramientas opcional, 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
.
El valor predeterminado de la función config_common.toolchain_type
es obligatorio.
Se pueden usar los siguientes formularios:
- Tipos de cadena 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 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 tipo de cadena de herramientas aparece varias veces, se tomará la versión más estricta, en la que lo obligatorio es más estricto que lo 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 necesarios, acceder a las cadenas de herramientas a través del contexto y usarlas para generar nuevas acciones 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 determinado, necesitas tres elementos:
Es una regla específica del lenguaje que representa el tipo de herramienta o conjunto de herramientas. Por convención, el nombre de esta regla tiene el sufijo "_toolchain".
- 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.
- Nota: La regla
Varios destinos de este tipo de reglas, que representan versiones de la herramienta o el paquete de herramientas para diferentes plataformas
Para cada uno de esos destinos, se incluye un destino asociado de la regla genérica
toolchain
para proporcionar los metadatos que usa el framework de la cadena de herramientas. Este destinotoolchain
también hace referencia altoolchain_type
asociado con esta cadena de herramientas. Esto significa que una regla_toolchain
determinada se podría asociar con cualquiertoolchain_type
, y que solo en una instancia detoolchain
que use esta regla_toolchain
se la asociará con untoolchain_type
.
En nuestro ejemplo en ejecución, esta es una definición de una regla bar_toolchain
. En nuestro ejemplo, solo hay un compilador, pero también se podrían 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 de campo-valor arbitrarios. 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 previamente. Este estilo puede ser útil para la validación y la reutilización del código.
Ahora puedes 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 último, crearás definiciones de toolchain
para los dos objetivos de bar_toolchain
.
Estas definiciones vinculan los destinos específicos del lenguaje al tipo de cadena de herramientas y proporcionan la información de restricción 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 todas estas definiciones están en el mismo paquete, pero no hay razón para que el tipo de cadena de herramientas, los destinos de cadena de herramientas específicos del lenguaje y los destinos de definición de toolchain
no puedan estar en paquetes separados.
Consulta go_toolchain
para ver un ejemplo del mundo real.
Cadenas 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 deben usarse para las dependencias? En el ejemplo anterior, se usan atributos de cadena, pero ¿qué sucedería con una cadena de herramientas más complicada que dependa 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 ligeramente diferente.
La dependencia de un destino (llamado "superior") a una cadena de herramientas a través de la resolución de cadenas de herramientas usa una transición de configuración especial llamada "transición de cadena de herramientas". La transición de la cadena de herramientas mantiene la configuración igual, con la excepción de que obliga a la plataforma de ejecución a ser la misma para la cadena de herramientas que para el elemento 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 sea ejecutable para las acciones de compilación del elemento superior. Cualquiera de las dependencias de la cadena de herramientas que use cfg =
"target"
(o que no especifique cfg
, ya que "target" es la opción predeterminada) se compila para la misma plataforma de destino que el elemento superior. De esta manera, las reglas de la cadena de herramientas pueden contribuir con las bibliotecas (el atributo system_lib
anterior) y las herramientas (el atributo compiler
) a las reglas de compilación que las necesitan. Las bibliotecas del sistema se vinculan al artefacto final y, por lo tanto, deben compilarse para la misma plataforma, mientras que el compilador es una herramienta que se invoca durante la compilación y debe poder ejecutarse en la plataforma de ejecución.
Realiza el registro y la compilación 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 WORKSPACE
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 el que se registran las cadenas de herramientas individuales se determina según las siguientes reglas:
- Las cadenas de herramientas definidas en un subpaquete de un paquete se registran antes que las cadenas de herramientas definidas en el paquete en sí.
- Dentro de un paquete, las cadenas de herramientas se registran en el orden alfabético 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, resuelve la
referencia de //bar_tools:toolchain_type
a //bar_tools:barc_linux_toolchain
.
Esto compilará //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 la cadena de herramientas de Bazel determina las dependencias concretas de la cadena de herramientas del destino. El procedimiento toma como entrada un conjunto de tipos de cadenas de herramientas obligatorios, 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 de herramientas, así como una plataforma de ejecución seleccionada para el destino actual.
Las plataformas de ejecución y las cadenas de herramientas disponibles se recopilan del
archivo WORKSPACE
a través de
register_execution_platforms
y
register_toolchains
.
También se pueden especificar cadenas de herramientas y plataformas de ejecución adicionales en la línea de comandos a través de --extra_execution_platforms
y --extra_toolchains
.
La plataforma host se incluye automáticamente como una plataforma de ejecución disponible.
Se realiza un seguimiento de las plataformas y las cadenas de herramientas disponibles como listas ordenadas para el determinismo, con preferencia para 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
:
- Primero se agregan las cadenas de herramientas registradas con
--extra_toolchains
.- Dentro de estos, la cadena de herramientas más reciente tiene la prioridad más alta.
- Cadenas de herramientas registradas con
register_toolchains
- Dentro de ellos, la cadena de herramientas primera mencionada tiene la prioridad más alta.
NOTA: Los pseudoobjetivos, como :all
, :*
y /...
, se ordenan según el mecanismo de carga de paquetes de Bazel, que usa un orden lexicográfico.
Los pasos para la resolución son los siguientes.
Una cláusula
target_compatible_with
oexec_compatible_with
coincide con una plataforma si, para cadaconstraint_value
en su lista, la plataforma también tiene eseconstraint_value
(ya sea de forma explícita o como valor predeterminado).Si la plataforma tiene
constraint_value
deconstraint_setting
a los que la cláusula no hace referencia, esto no afecta la coincidencia.Si el destino que se está compilando especifica el atributo
exec_compatible_with
(o su definición de regla especifica el argumentoexec_compatible_with
), se filtra la lista de plataformas de ejecución disponibles para quitar aquellas que no coincidan con las restricciones de ejecución.Para cada plataforma de ejecución disponible, debes asociar cada tipo de cadena de herramientas con la primera cadena de herramientas disponible, si la hay, que sea compatible con esta plataforma de ejecución y la plataforma de destino.
Se descarta cualquier plataforma de ejecución que no haya encontrado una cadena de herramientas obligatoria compatible para uno de sus tipos de cadena de herramientas. De las plataformas restantes, la primera se convierte en la plataforma de ejecución del objetivo 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 que el mismo destino se puede compilar en varias configuraciones (como para CPUs diferentes) 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 realiza la resolución de la cadena de herramientas por separado, y cada uno tiene su propia plataforma de 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 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 los nombres de destino que coinciden con la variable de regex. Puedes usar .*
para mostrar toda la información. Bazel mostrará los nombres de las cadenas de herramientas que
verifica y omite durante el proceso de resolución.
Si deseas ver qué dependencias cquery
son de 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