En esta página, se describe el framework de la cadena de herramientas, que es una forma para que los autores de reglas desacoplen su lógica de reglas de la selección de herramientas basada en la plataforma. Se recomienda leer las páginas de reglas y plataformas antes de continuar. En esta página, se explica por qué se necesitan cadenas de herramientas, cómo definirlas y usarlas, y cómo Bazel selecciona una cadena de herramientas adecuada en función de las restricciones de la plataforma.
Motivación
Primero, veamos el problema que las cadenas de herramientas están diseñadas para resolver. Supongamos que
escribes reglas para admitir el lenguaje de programación "bar". Tu bar_binary
regla 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 bar_binary
destinos no deberían tener que 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],
),
},
)
//bar_tools:barc_linux ahora 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 la función de implementación de la regla
, al igual 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 de forma rígida en bar_binary, pero
es posible que diferentes destinos necesiten compiladores diferentes según la plataforma para la que se compilan y la plataforma en la que se compilan, llamada
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 posible codificarlas de forma rígida en la definición de la regla.
Una solución menos que ideal sería trasladar la carga a los usuarios haciendo
que el _compiler atributo no sea privado. Luego, los destinos individuales se podrían codificar de forma rígida 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",
)
Puedes mejorar esta solución usando select para elegir el compiler
basado en 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",
}),
)
Pero esto es tedioso y un poco difícil de pedir 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 de varias plataformas. 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. Básicamente, declaras que tu regla tiene una dependencia abstracta en algún miembro de una familia de destinos (un tipo de cadena de herramientas) y Bazel lo resuelve automáticamente en un destino en particular (una cadena de herramientas) en función de las restricciones de plataforma aplicables. Ni el autor de la regla ni el autor del destino necesitan conocer el conjunto completo de plataformas y cadenas de herramientas disponibles.
Cómo escribir reglas 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 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 para 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")
La definición de la regla en la sección anterior se modifica de modo que, en lugar de
tomar el compilador como un atributo, declara que consume una
//bar_tools:toolchain_type cadena de herramientas.
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"] devuelve el
ToolchainInfo proveedor
de cualquier 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 ajuste
un objeto BarcInfo.
A continuación, se describe el procedimiento de Bazel para resolver cadenas de herramientas en destinos
a continuación. Solo el destino de la cadena de herramientas resuelta se convierte en una dependencia del destino bar_binary, no en todo el espacio de 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 simple (como se muestra arriba), el tipo de cadena de herramientas se considera obligatorio. Si Bazel no puede encontrar una cadena de herramientas coincidente (consulta Resolución de cadenas de herramientas a continuación) para un tipo de cadena de herramientas obligatoria, esto es 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.
La config_common.toolchain_type
función se establece de forma predeterminada como obligatoria.
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 formularios en la misma regla. Sin embargo, si el mismo tipo de cadena de herramientas aparece varias veces, tomará la versión más estricta, en la que la obligatoria es más estricta que la opcional.
Cómo escribir aspectos que usan cadenas de herramientas
Los aspectos tienen acceso a la misma API de cadena de herramientas que las reglas: puedes definir los tipos de cadenas de herramientas requeridos , 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 []
Definición de cadenas de herramientas
Para definir algunas cadenas de herramientas para un tipo de cadena de herramientas determinado, necesitas tres elementos:
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
\_toolchainno 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 regla, que representan versiones de la herramienta o el conjunto de herramientas para diferentes plataformas.
Para cada destino de este tipo, un destino asociado de la regla genérica
toolchain, para proporcionar metadatos que usa el framework de la cadena de herramientas. Estetoolchaindestino también hace referencia altoolchain_typeasociado con esta cadena de herramientas. Esto significa que una regla_toolchaindeterminada se puede asociar con cualquiertoolchain_type, y que solo en una instanciatoolchainque usa esta regla_toolchainla regla se asocia con untoolchain_type.
Para nuestro ejemplo en ejecución, aquí tienes una definición para una regla bar_toolchain. Nuestro
ejemplo solo tiene 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 devolver un proveedor ToolchainInfo, que se convierte en el objeto que
la regla de consumo recupera con ctx.toolchains y la etiqueta del
tipo de cadena de herramientas. ToolchainInfo, como struct, puede contener pares de valor de campo
arbitrarios. La especificación de los campos que se agregan a los ToolchainInfo
debe documentarse claramente en el tipo de cadena de herramientas. En este ejemplo, los valores
devuelven ajustados en un objeto BarcInfo para reutilizar el esquema definido anteriormente. Este
estilo puede ser útil para la validación y la reutilización de 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, crea toolchain definiciones para los dos bar_toolchain destinos.
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 es
adecuada la cadena de herramientas 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 ningún motivo por el que el tipo de cadena de herramientas, los destinos de cadena de herramientas específicos del lenguaje
y los destinos de definición toolchain no puedan estar en paquetes separados.
Consulta go_toolchain
para ver un ejemplo del mundo real.
Cadenas de herramientas y configuraciones
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 cadena, 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 ligeramente diferente.
La dependencia de un destino (llamado "elemento 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 cadena de herramientas". La transición de la cadena de herramientas mantiene la misma configuración, excepto
que obliga a que la plataforma de ejecución sea 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 el elemento 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. Cualquiera de 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 el elemento superior. Esto permite que las reglas de la cadena de herramientas
aporten bibliotecas (el atributo system_lib anterior) y herramientas (el
compiler atributo) a las reglas de compilación que las necesiten. Las bibliotecas del sistema
se vinculan al artefacto final, por lo que 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.
Registro y compilación con cadenas de herramientas
En este punto, se ensamblan todos los bloques de compilación, y solo necesitas que
las cadenas de herramientas estén disponibles para el procedimiento de resolución de Bazel. Para ello, registra la cadena de herramientas 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 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.
- Dentro de un paquete, las cadenas de herramientas se registran en el orden lexicográfico de sus nombres.
Ahora, cuando compilas un destino que depende de un tipo de cadena de herramientas, se seleccionará una cadena de herramientas adecuada en función de las plataformas de destino y de 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 está compilando con una plataforma que
tiene @platforms//os:linux y, por lo tanto, resolverá la
//bar_tools:toolchain_type referencia a //bar_tools:barc_linux_toolchain.
Esto terminará compilando //bar_tools:barc_linux, pero no
//bar_tools:barc_windows.
Resolución de cadenas 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 la cadena de herramientas del destino. El procedimiento toma como entrada un conjunto de tipos de cadenas de herramientas requeridos, 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
gráfico de dependencia externa a través de las
register_execution_platforms
y
register_toolchains llamadas en
MODULE.bazel archivos.
También se pueden especificar plataformas de ejecución y cadenas de herramientas 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.
Las plataformas y las cadenas de herramientas disponibles se registran como listas ordenadas para el determinismo,
con preferencia por 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 estas, la última cadena de herramientas tiene la prioridad más alta). - Cadenas de herramientas registradas con
register_toolchainsen el gráfico de dependencia externa transitiva, en el siguiente orden: (Dentro de estas, la primera cadena de herramientas mencionada tiene la prioridad más alta).- Cadenas de herramientas registradas por el módulo raíz (es decir, el
MODULE.bazelen la raíz del espacio de trabajo); - Cadenas de herramientas registradas en el archivo
WORKSPACEdel usuario, incluidas las macros invocadas desde allí; - Cadenas de herramientas registradas por módulos que no son raíz (es decir, dependencias especificadas por el módulo raíz y sus dependencias, etcétera);
- Cadenas de herramientas registradas en el "sufijo WORKSPACE"; esto solo lo usan ciertas reglas nativas incluidas en la instalación de Bazel.
- Cadenas de herramientas registradas por el módulo raíz (es decir, el
NOTA: Los pseudo-destinos como :all, :* y
/... se ordenan según el mecanismo de carga de paquetes
de Bazel, que usa un orden lexicográfico.
Los pasos de resolución son los siguientes.
Una cláusula
target_compatible_withoexec_compatible_withcoincide con una plataforma si, para cadaconstraint_valuede su lista, la plataforma también tiene eseconstraint_value(ya sea de forma explícita o como valor predeterminado).Si la plataforma tiene
constraint_values deconstraint_settings no referenciadas por la cláusula, estas no afectan la coincidencia.Si el destino que se está compilando especifica el
exec_compatible_withatributo (o su definición de regla especifica elexec_compatible_withargumento), se filtra la lista de plataformas de ejecución disponibles para quitar las que no coincidan con las restricciones de ejecución.Se filtra la lista de cadenas de herramientas disponibles para quitar las cadenas de herramientas que especifican
target_settingsque no coinciden con la configuración actual.Para cada plataforma de ejecución disponible, asocias cada tipo de cadena de herramientas con la primera cadena de herramientas disponible, si la hay, que sea compatible con esta ejecución plataforma y la plataforma de destino.
Se descarta cualquier plataforma de ejecución que no haya podido encontrar 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 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 el destino genera.
En los casos en los que el mismo destino se puede compilar en varias configuraciones (como para diferentes CPUs) 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 grupo de ejecución 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 cadenas de herramientas
Si agregas compatibilidad con cadenas de herramientas a una regla existente, usa la
--toolchain_resolution_debug=regex marca. 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 regex. Puedes usar .* para generar toda la información. Bazel generará los nombres de las cadenas de herramientas que
verifica y omite durante el proceso de resolución.
Si deseas ver qué cquery dependencias provienen 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