En esta página, se describe el framework de la cadena de herramientas, que es una forma para que los autores de las reglas separen su lógica de reglas de la selección de herramientas basada en la plataforma. Se recomienda leer las páginas sobre 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 cadena de herramientas adecuada según las restricciones de la plataforma.
Motivación
Primero, veamos los problemas que la cadena de herramientas está diseñada para resolver. Supongamos que escribes reglas para admitir el lenguaje de programación "bar". Tu regla bar_binary
compilará 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. Conviertela en una dependencia implícita si la agregas 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 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
; sin embargo, es posible que distintos objetivos necesiten diferentes compiladores según la plataforma para la que se compilan y la plataforma en la que se compilan. Esto 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 posible codificarlas en la definición de la regla.
Una solución no ideal sería convertir la carga en usuarios y hacer que el atributo _compiler
no sea privado. Luego, los objetivos 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",
)
Puedes mejorar esta solución si usas 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",
}),
)
Pero esto es tedioso y un poco difícil de preguntar 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 nuevas plataformas y compiladores sin modificar reglas ni objetivos existentes.
El framework de la cadena de herramientas resuelve este problema agregando un nivel adicional de indirección. En esencia, debes declarar 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 las restricciones de la 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.
Escribe reglas que usen cadenas de herramientas
En el marco de trabajo de la cadena de herramientas, en lugar de tener reglas que dependan directamente de las herramientas, dependen de los tipos de la cadena 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")
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, 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
, mediante 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
de cualquier destino en el que Bazel haya resuelto la dependencia de la cadena de herramientas. La regla subyacente de la herramienta establece los campos del objeto ToolchainInfo
. En la siguiente sección, esta regla se define de tal manera que hay un campo barcinfo
que une un objeto BarcInfo
.
El procedimiento de Bazel para resolver las cadenas de herramientas se describe
a continuación. Solo el objetivo de la cadena de herramientas resuelto
realmente depende del destino 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 simple (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 obtener un tipo de cadena de herramientas obligatorio, se trata de un error y se detiene el análisis.
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 opcional de cadena de herramientas, 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.
Puedes usar 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 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 puede combinar formularios en la misma regla. Sin embargo, si el mismo tipo de cadena de herramientas se enumera varias veces, tomará la versión más estricta, en la que el obligatorio es más estricto que el opcional.
Escribir aspectos 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 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 []
Define las cadenas de herramientas
A fin de 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 paquete 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 su lugar, 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 objetivos de este tipo de regla, que representan versiones de la herramienta o del paquete de herramientas para diferentes plataformas.
Para cada objetivo, un destino asociado de la regla
toolchain
genérica, a fin de proporcionar metadatos que usa el framework de la cadena de herramientas Este objetivotoolchain
también hace referencia altoolchain_type
asociado a esta cadena de herramientas. Esto significa que una regla_toolchain
determinada podría asociarse con cualquiertoolchain_type
y que solo en una instanciatoolchain
que usa esta regla_toolchain
, la regla está asociada con untoolchain_type
.
Para nuestro ejemplo en ejecución, esta es una definición de una regla bar_toolchain
. Nuestro ejemplo solo tiene un compilador, pero otras herramientas, como un vinculador, también se podrían agrupar debajo.
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 recupera con ctx.toolchains
y la etiqueta del tipo de cadena de herramientas. ToolchainInfo
, como struct
, puede contener pares de campo y valor arbitrarios. La especificación de los campos exactos que se agregan a ToolchainInfo
debe estar claramente documentada en el tipo de cadena de herramientas. En este ejemplo, los valores se muestran unidos en un objeto BarcInfo
a fin de reutilizar el esquema definido con anterioridad. Este estilo puede ser útil para la validación y la reutilización de 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 definiciones de toolchain
para los dos destinos bar_toolchain
.
Estas definiciones vinculan los destinos específicos de lenguaje con el tipo de la cadena de herramientas y
proporcionan información sobre la restricción que le indica a Bazel cuándo la cadena de herramientas
es 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 relativa anterior sugiere que todas estas definiciones se encuentran en el mismo paquete, pero no hay motivo para que el tipo de cadena de herramientas, las orientaciones de la cadena de herramientas específicas del lenguaje y las orientaciones de definición toolchain
no puedan estar en paquetes separados.
Consulta go_toolchain
para ver un ejemplo real.
Cadenas de herramientas y configuraciones
Una pregunta importante para los autores de reglas es que, 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 string, pero ¿qué pasarí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 objetivo (denominado “superior”) a una cadena de herramientas mediante la resolución de la cadena de herramientas utiliza una transición de configuración especial denominada “transición de la cadena de herramientas”. La transición de la cadena de herramientas mantiene la misma configuración, excepto que obliga a 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 de 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 la configuración predeterminada) se
compilan para la misma plataforma de destino que la superior. Esto permite que las reglas de la cadena de herramientas contribuyan 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 están vinculadas al artefacto final, por lo que 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.
Registro y compilación con cadenas de herramientas
En este punto, todos los componentes están armados, 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, ya sea en un archivo WORKSPACE
con
register_toolchains()
o pasando 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",
)
Ahora, cuando compiles un destino que dependa de un tipo de cadena de herramientas, se seleccionará un conjunto de herramientas apropiado 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 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 la cadena de herramientas de Bazel determina las dependencias concretas de la cadena de herramientas. 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, 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
.
Las plataformas y cadenas de herramientas adicionales de ejecución también se pueden especificar 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 cadenas de herramientas disponibles se rastrean como listas ordenadas para la determinación, con preferencia a los elementos anteriores de la lista.
Los pasos de resolución son los siguientes.
Una cláusula
target_compatible_with
oexec_compatible_with
coincide con una plataforma si, para cadaconstraint_value
de su lista, la plataforma también tiene esaconstraint_value
(ya sea de forma explícita o predeterminada).Si la plataforma tiene
constraint_value
deconstraint_setting
a las que la cláusula no hace referencia, estas no afectan la coincidencia.Si el destino que se compila 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 disponible, si la hubiera, 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 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 destino.
En los casos en que el mismo destino se puede compilar en varias configuraciones (por ejemplo, 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 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.
Conjuntos de herramientas de depuración
Si agregas 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 la cadena de herramientas o los nombres de destino que coinciden con la variable de regex. Puedes usar .*
para generar toda la información. Bazel mostrará los nombres de las cadenas de herramientas que
verifica y omite durante el proceso de resolución.
Si quieres ver qué dependencias de cquery
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