Una regla define una serie de acciones que realiza Bazel en las entradas para producir un conjunto de salidas, a las que se hace referencia en proveedores que muestra la función de implementación de la regla. Por ejemplo, una regla binaria de C++ podría realizar las siguientes acciones:
- Toma un conjunto de
.cpp
archivos de origen (entradas). - Ejecuta
g++
en los archivos de origen (acción). - Muestra el proveedor
DefaultInfo
con el resultado ejecutable y otros archivos que estarán disponibles en el entorno de ejecución. - Muestra el proveedor
CcInfo
con información específica de C++ recopilada del destino y sus dependencias.
Desde la perspectiva de Bazel, g++
y las bibliotecas C++ estándar también son entradas
de esta regla. Como escritor de reglas, debes considerar no solo las entradas que proporciona el usuario a una regla, sino también todas las herramientas y bibliotecas necesarias para ejecutar las acciones.
Antes de crear o modificar una regla, asegúrate de estar familiarizado con las fases de compilación de Bazel. Es importante comprender las tres fases de una compilación (carga, análisis y ejecución). También es útil obtener información sobre las macros para comprender la diferencia entre reglas y macros. Para comenzar, revise el Instructivo sobre reglas. Luego, usa esta página como referencia.
Algunas reglas están integradas en Bazel. Estas reglas nativas, como cc_library
y java_binary
, proporcionan cierta compatibilidad principal con ciertos lenguajes.
Si defines tus propias reglas, puedes agregar compatibilidad similar para lenguajes y herramientas
que Bazel no admite de forma nativa.
Bazel proporciona un modelo de extensibilidad para escribir reglas con el lenguaje
Starlark. Estas reglas se escriben en archivos .bzl
, que se pueden cargar directamente desde archivos BUILD
.
Cuando defines tu propia regla, puedes decidir qué atributos admite y cómo genera sus resultados.
La función implementation
de la regla define su comportamiento exacto durante la fase de análisis. Esta función no ejecuta ningún comando externo. En su lugar, registra las acciones que se usarán más adelante durante la fase de ejecución para compilar los resultados de la regla, si es necesario.
Creación de reglas
En un archivo .bzl
, usa la función de regla para definir una regla nueva y almacenar el resultado en una variable global. La llamada a rule
especifica atributos y una función de implementación:
example_library = rule(
implementation = _example_library_impl,
attrs = {
"deps": attr.label_list(),
...
},
)
Esto define un tipo de regla llamada example_library
.
La llamada a rule
también debe especificar si la regla crea un resultado ejecutable (con executable=True
) o, en particular, un ejecutable de prueba (con test=True
). Si esta última es una regla de prueba, y el nombre de la regla debe terminar en _test
.
Creación de instancias objetivo
Las reglas se pueden cargar y llamar en archivos BUILD
:
load('//some/pkg:rules.bzl', 'example_library')
example_library(
name = "example_target",
deps = [":another_target"],
...
)
Cada llamada a una regla de compilación no muestra ningún valor, pero tiene el efecto secundario de definir un objetivo. Esto se denomina creación de una instancia de la regla. Esto especifica un nombre para el objetivo nuevo y valores para los atributos del objetivo.
También se puede llamar a las reglas desde las funciones de Starlark y cargarlas en archivos .bzl
.
Las funciones de Starlark que llaman reglas se llaman macros de Starlark.
En última instancia, las macros de Starlark deben llamarse desde archivos BUILD
y solo se pueden llamar durante la fase de carga, cuando los archivos BUILD
se evalúan para crear instancias de destinos.
Atributos
Un atributo es un argumento de la regla. Los atributos pueden proporcionar valores específicos a la implementación de un destino o pueden hacer referencia a otros destinos y crear un gráfico de dependencias.
Los atributos específicos de la regla, como srcs
o deps
, se definen cuando se pasa una asignación de nombres de atributos a esquemas (creados con el módulo attr
) al parámetro attrs
de rule
.
Los atributos comunes, como name
y visibility
, se agregan de forma implícita a todas las reglas. Los atributos adicionales se agregan de forma implícita a las reglas ejecutables y de prueba de manera específica. Los atributos que se agregan de manera implícita a una regla no se pueden incluir en el diccionario que se pasa a attrs
.
Atributos de dependencias
Por lo general, las reglas que procesan el código fuente definen los siguientes atributos para controlar varios tipos de dependencias:
srcs
especifica los archivos de origen procesados por las acciones de un destino. A menudo, el esquema de atributo especifica qué extensiones de archivo se esperan para el tipo de archivo fuente que procesa la regla. En general, las reglas para los idiomas con archivos de encabezado especifican un atributohdrs
independiente para los encabezados que procesa un destino y sus consumidores.deps
especifica las dependencias de código para un destino. El esquema de atributos debe especificar qué proveedores deben proporcionar esas dependencias. (Por ejemplo,cc_library
proporcionaCcInfo
).data
especifica los archivos que estarán disponibles en el entorno de ejecución para cualquier ejecutable que dependa de un destino. Eso debería permitir que se especifiquen archivos arbitrarios.
example_library = rule(
implementation = _example_library_impl,
attrs = {
"srcs": attr.label_list(allow_files = [".example"]),
"hdrs": attr.label_list(allow_files = [".header"]),
"deps": attr.label_list(providers = [ExampleInfo]),
"data": attr.label_list(allow_files = True),
...
},
)
Estos son ejemplos de atributos de dependencia. Cualquier atributo que especifique una etiqueta de entrada (aquellos definidos con attr.label_list
, attr.label
o attr.label_keyed_string_dict
) especifica dependencias de un tipo determinado entre un objetivo y los objetivos cuyas etiquetas (o los objetos Label
correspondientes) se incluyen en ese atributo cuando se define el objetivo. El repositorio, y posiblemente la ruta, para estas etiquetas se resuelve en relación con el destino definido.
example_library(
name = "my_target",
deps = [":other_target"],
)
example_library(
name = "other_target",
...
)
En este ejemplo, other_target
es una dependencia de my_target
y, por lo tanto, other_target
se analiza primero. Es un error si hay un ciclo en el gráfico de dependencia de los objetivos.
Atributos privados y dependencias implícitas
Un atributo de dependencia con un valor predeterminado crea una dependencia implícita. Está implícito porque es parte del grafo de destino que el usuario no especifica en un archivo BUILD
. Las dependencias implícitas son útiles para codificar una relación entre una regla y una herramienta (una dependencia en tiempo de compilación, como un compilador), ya que la mayor parte del tiempo un usuario no está interesado en especificar qué herramienta usa la regla. Dentro de la función de implementación de la regla, se trata de la misma manera que otras dependencias.
Si quieres proporcionar una dependencia implícita sin permitir que el usuario anule ese valor, puedes asignar un nombre que comience con un guion bajo (_
) al atributo private. Los atributos privados deben tener valores predeterminados. Por lo general, solo tiene sentido usar atributos privados para dependencias implícitas.
example_library = rule(
implementation = _example_library_impl,
attrs = {
...
"_compiler": attr.label(
default = Label("//tools:example_compiler"),
allow_single_file = True,
executable = True,
cfg = "exec",
),
},
)
En este ejemplo, cada destino de tipo example_library
tiene una dependencia implícita del compilador //tools:example_compiler
. Esto permite que la función de implementación de example_library
genere acciones que invoquen el compilador, aunque el usuario no haya pasado su etiqueta como entrada. Dado que _compiler
es un atributo privado, ctx.attr._compiler
siempre apuntará a //tools:example_compiler
en todos los destinos de este tipo de regla. Como alternativa, puedes asignar un nombre al atributo compiler
sin el guion bajo y mantener el valor predeterminado. Esto permite a los usuarios sustituir un compilador diferente si es necesario, pero no requiere conocer la etiqueta del compilador.
Por lo general, las dependencias implícitas se usan para herramientas que residen en el mismo repositorio que la implementación de reglas. Si la herramienta proviene de la plataforma de ejecución o de un repositorio diferente, la regla debería obtener esa herramienta de una cadena de herramientas.
Atributos de salida
Los atributos de salida, como attr.output
y attr.output_list
, declaran un archivo de salida que genera el destino. Estos difieren de los atributos de las dependencias de dos maneras:
- Definen los objetivos del archivo de salida en lugar de hacer referencia a destinos definidos en otro lugar.
- Los objetivos del archivo de salida dependen del objetivo de la regla que se crea una instancia, en lugar de al revés.
Por lo general, los atributos de salida solo se usan cuando una regla necesita crear resultados con nombres definidos por el usuario que no pueden basarse en el nombre de destino. Si una regla tiene un atributo de salida, por lo general, se llama out
o outs
.
Los atributos de salida son la forma preferida de crear salidas declaradas previamente, que se pueden depender específicamente de la línea de comandos o se pueden solicitar en ella.
Función de implementación
Cada regla requiere una función implementation
. Estas funciones se ejecutan de forma estricta en la fase de análisis y transforman el grafo de objetivos generados en la fase de carga en un grafo de acciones que se realizan durante la fase de ejecución. Por lo tanto, las funciones de implementación no pueden leer ni escribir archivos.
Por lo general, las funciones de implementación de reglas son privadas (se denominan con un guion bajo inicial). De manera convencional, tienen el mismo nombre que su regla, pero se les agrega el sufijo _impl
.
Las funciones de implementación usan solo un parámetro: un
contexto de reglas, que se denomina convencionalmente ctx
. Muestran una lista de proveedores.
Objetivos
En el momento del análisis, las dependencias se representan como objetos Target
. Estos objetos contienen los proveedores que se generaron cuando se ejecutó la función de implementación del destino.
ctx.attr
tiene campos que corresponden a los nombres de cada atributo de dependencia, que contiene objetos Target
que representan cada dependencia directa a través de ese atributo. Para los atributos label_list
, esta es una lista de Targets
. Para los atributos label
, es un único Target
o None
.
La función de implementación de un destino muestra una lista de objetos del proveedor:
return [ExampleInfo(headers = depset(...))]
Se puede acceder a ellas mediante la notación de índices ([]
), con el tipo de proveedor como clave. Pueden ser proveedores personalizados definidos en Starlark o
proveedores de reglas nativas disponibles como variables globales
de Starlark.
Por ejemplo, si una regla toma archivos de encabezado a través de un atributo hdrs
y los proporciona a las acciones de compilación del destino y sus consumidores, podría recopilarlos de la siguiente manera:
def _example_library_impl(ctx):
...
transitive_headers = [hdr[ExampleInfo].headers for hdr in ctx.attr.hdrs]
Para el estilo heredado en el que se muestra un struct
desde la función de implementación de un objetivo en lugar de una lista de objetos del proveedor:
return struct(example_info = struct(headers = depset(...)))
Los proveedores se pueden recuperar desde el campo correspondiente del objeto Target
:
transitive_headers = [hdr.example_info.headers for hdr in ctx.attr.hdrs]
Este estilo se desaconseja y se deben migrar las reglas.
Archivos
Los archivos se representan con objetos File
. Dado que Bazel no
realiza E/S de archivos durante la fase de análisis, estos objetos no se pueden usar para
leer ni escribir contenido de archivos directamente. En su lugar, se pasan a funciones de emisión de acciones (consulta ctx.actions
) para construir partes del grafo de acción.
Un File
puede ser un archivo de origen o uno generado. Cada archivo generado debe ser el resultado de exactamente una acción. Los archivos de origen no pueden ser el resultado de ninguna acción.
Para cada atributo de dependencia, el campo correspondiente de ctx.files
contiene una lista de los resultados predeterminados de todas las dependencias a través de ese atributo:
def _example_library_impl(ctx):
...
headers = depset(ctx.files.hdrs, transitive=transitive_headers)
srcs = ctx.files.srcs
...
ctx.file
contiene un solo objeto File
o None
para los atributos de dependencias cuyas especificaciones establecen allow_single_file=True
.
ctx.executable
se comporta igual que ctx.file
, pero solo contiene campos para atributos de dependencia cuyas especificaciones establecen executable=True
.
Cómo declarar resultados
Durante la fase de análisis, la función de implementación de una regla puede crear resultados.
Dado que todas las etiquetas se deben conocer durante la fase de carga, estos resultados adicionales no tienen etiquetas. Los objetos File
para resultados se pueden crear mediante ctx.actions.declare_file
y ctx.actions.declare_directory
. A menudo, los nombres de los resultados se basan en el nombre del destino, ctx.label.name
:
def _example_library_impl(ctx):
...
output_file = ctx.actions.declare_file(ctx.label.name + ".output")
...
En el caso de los resultados declarados previamente, como los creados para los atributos de salida, los objetos File
se pueden recuperar de los campos correspondientes de ctx.outputs
.
Acciones
Una acción describe cómo generar un conjunto de salidas a partir de un conjunto de entradas, por ejemplo, “ejecutar gcc en hello.c y obtener hello.o”. Cuando se crea una acción, Bazel no ejecuta el comando de inmediato. La registra en un gráfico de dependencias, ya que una acción puede depender del resultado de otra. Por ejemplo, en C, se debe llamar al vinculador después del compilador.
Las funciones de uso general que crean acciones se definen en ctx.actions
:
ctx.actions.run
, para ejecutar un ejecutablectx.actions.run_shell
, para ejecutar un comando de shellctx.actions.write
, para escribir una string en un archivo.ctx.actions.expand_template
, para generar un archivo a partir de una plantilla.
ctx.actions.args
se puede usar para acumular los argumentos de las acciones de manera eficiente. Evita acoplar los dependencias hasta el tiempo de ejecución:
def _example_library_impl(ctx):
...
transitive_headers = [dep[ExampleInfo].headers for dep in ctx.attr.deps]
headers = depset(ctx.files.hdrs, transitive=transitive_headers)
srcs = ctx.files.srcs
inputs = depset(srcs, transitive=[headers])
output_file = ctx.actions.declare_file(ctx.label.name + ".output")
args = ctx.actions.args()
args.add_joined("-h", headers, join_with=",")
args.add_joined("-s", srcs, join_with=",")
args.add("-o", output_file)
ctx.actions.run(
mnemonic = "ExampleCompile",
executable = ctx.executable._compiler,
arguments = [args],
inputs = inputs,
outputs = [output_file],
)
...
Las acciones toman una lista o un archivo de seguridad de archivos de entrada y generan una lista (no vacía) de archivos de salida. El conjunto de archivos de entrada y salida se debe conocer durante la fase de análisis. Puede depender del valor de los atributos, incluidos los proveedores de dependencias, pero no puede depender del resultado de la ejecución. Por ejemplo, si tu acción ejecuta el comando de descompresión, debes especificar qué archivos se espera que se aumenten (antes de ejecutar la descompresión). Las acciones que crean una cantidad variable de archivos de forma interna pueden unirlas en un solo archivo (como un archivo ZIP, TAR o algún otro formato de archivo).
Las acciones deben enumerar todas sus entradas. Se permiten las entradas que no se usan, pero son ineficientes.
Las acciones deben crear todos sus resultados. Pueden escribir otros archivos, pero nada que no esté en los resultados no estará disponible para los consumidores. Todos los resultados declarados deben escribirse mediante alguna acción.
Las acciones son comparables a las funciones puras: deben depender solo de las entradas proporcionadas y evitar el acceso a la información de la computadora, el nombre de usuario, el reloj, la red o los dispositivos de E/S (excepto las entradas de lectura y las salidas de escritura). Esto es importante porque el resultado se almacenará en caché y se volverá a usar.
Bazel resuelve las dependencias, que decidirán qué acciones se ejecutan. Es un error si hay un ciclo en el gráfico de dependencias. Crear una acción no garantiza que se ejecutará, ya que eso depende de si los resultados son necesarios para la compilación.
Proveedores
Los proveedores son información que una regla expone a otras reglas que dependen de ella. Estos datos pueden incluir archivos de salida, bibliotecas, parámetros que se deben pasar a la línea de comandos de una herramienta o cualquier otro elemento que los consumidores de un destino tengan que conocer.
Dado que la función de implementación de una regla solo puede leer proveedores de las dependencias inmediatas de un destino con instancia, las reglas deben reenviar cualquier información de las dependencias de un objetivo que los consumidores del destino deben conocer, en general, acumulándola en un depset
.
Los proveedores de un destino se especifican con una lista de objetos Provider
que muestra la función de implementación.
Las funciones de implementación anteriores también se pueden escribir en un estilo heredado en el que la función de implementación muestre un struct
en lugar de una lista de objetos del proveedor. Este estilo se desaconseja y se deben migrar las reglas.
Resultados predeterminados
Los resultados predeterminados de un destino son los que se solicitan de forma predeterminada cuando se solicita la compilación en la línea de comandos. Por ejemplo, un //pkg:foo
de destino java_library
tiene foo.jar
como resultado predeterminado, por lo que el comando bazel build //pkg:foo
lo compilará.
Los resultados predeterminados se especifican mediante el parámetro files
de DefaultInfo
:
def _example_library_impl(ctx):
...
return [
DefaultInfo(files = depset([output_file]), ...),
...
]
Si una implementación de reglas no muestra DefaultInfo
o no se especifica el parámetro files
, DefaultInfo.files
usa la configuración predeterminada de todos los resultados declarados previamente (por lo general, los creados por atributos de salida).
Las reglas que realizan acciones deben proporcionar resultados predeterminados, incluso si esos resultados no se esperan que se usen de forma directa. Se reducen las acciones que no están en el grafo de los resultados solicitados. Si solo los consumidores de un destino utilizan un resultado, esas acciones no se realizarán cuando el destino se cree de forma aislada. Esto hace que la depuración sea más difícil porque volver a compilar solo el destino con errores no reproducirá la falla.
Archivos en ejecución
Los runfiles son un conjunto de archivos que usa un destino en el tiempo de ejecución (a diferencia del tiempo de compilación). Durante la fase de ejecución, Bazel crea un árbol de directorios que contiene symlinks que apuntan a los archivos de ejecución. De esta manera, se habilita el entorno para el objeto binario a fin de que pueda acceder a los archivos de ejecución durante el tiempo de ejecución.
Los archivos runrun se pueden agregar manualmente durante la creación de la regla.
El método runfiles
puede crear objetos runfiles
en el contexto de la regla, ctx.runfiles
, y pasarlos al parámetro runfiles
en DefaultInfo
. El resultado ejecutable de las reglas ejecutables se agrega de forma implícita a los archivos de ejecución.
Algunas reglas especifican atributos, generalmente llamados data
, cuyos resultados se agregan a los runfiles de un destino. Los runfiles también deben combinarse desde data
, y desde cualquier atributo que pueda proporcionar código para una ejecución eventual, generalmente
srcs
(que puede contener destinos filegroup
asociados con data
) y
deps
.
def _example_library_impl(ctx):
...
runfiles = ctx.runfiles(files = ctx.files.data)
transitive_runfiles = []
for runfiles_attr in (
ctx.attr.srcs,
ctx.attr.hdrs,
ctx.attr.deps,
ctx.attr.data,
):
for target in runfiles_attr:
transitive_runfiles.append(target[DefaultInfo].default_runfiles)
runfiles = runfiles.merge_all(transitive_runfiles)
return [
DefaultInfo(..., runfiles = runfiles),
...
]
Proveedores personalizados
Los proveedores se pueden definir con la función provider
para transmitir información específica de la regla:
ExampleInfo = provider(
"Info needed to compile/link Example code.",
fields={
"headers": "depset of header Files from transitive dependencies.",
"files_to_link": "depset of Files from compilation.",
})
Luego, las funciones de implementación de reglas pueden construir y mostrar instancias de proveedor:
def _example_library_impl(ctx):
...
return [
...
ExampleInfo(
headers = headers,
files_to_link = depset(
[output_file],
transitive = [
dep[ExampleInfo].files_to_link for dep in ctx.attr.deps
],
),
)
]
Inicialización personalizada de proveedores
Es posible proteger la creación de instancias de un proveedor con procesamiento previo personalizado y lógica de validación. Esto se puede usar a fin de garantizar que todas las instancias del proveedor cumplan con ciertas variantes o para proporcionar a los usuarios una API más limpia a fin de que obtengan una instancia.
Para ello, se pasa una devolución de llamada init
a la función provider
. Si se proporciona esta devolución de llamada, el tipo de datos que se muestra de provider()
cambia para ser una tupla de dos valores: el símbolo del proveedor que es el valor de retorno común cuando no se usa init
y un "constructor sin procesar".
En este caso, cuando se llame al símbolo del proveedor, en lugar de mostrar directamente una instancia nueva, se reenviarán los argumentos junto con la devolución de llamada init
. El valor que se muestra de la devolución de llamada debe ser un diccionario de asignación de nombres (strings) a valores; se usa para inicializar los campos de la instancia nueva. Ten en cuenta que la devolución de llamada puede tener cualquier firma y, si los argumentos no coinciden con la firma, se informa un error como si la devolución de llamada se invocara directamente.
El constructor sin procesar, por el contrario, omitirá la devolución de llamada init
.
En el siguiente ejemplo, se usa init
para procesar previamente y validar sus argumentos:
# //pkg:exampleinfo.bzl
_core_headers = [...] # private constant representing standard library files
# It's possible to define an init accepting positional arguments, but
# keyword-only arguments are preferred.
def _exampleinfo_init(*, files_to_link, headers = None, allow_empty_files_to_link = False):
if not files_to_link and not allow_empty_files_to_link:
fail("files_to_link may not be empty")
all_headers = depset(_core_headers, transitive = headers)
return {'files_to_link': files_to_link, 'headers': all_headers}
ExampleInfo, _new_exampleinfo = provider(
...
init = _exampleinfo_init)
export ExampleInfo
Una implementación de reglas luego puede crear una instancia del proveedor de la siguiente manera:
ExampleInfo(
files_to_link=my_files_to_link, # may not be empty
headers = my_headers, # will automatically include the core headers
)
El constructor sin procesar se puede usar para definir funciones de fábrica públicas alternativas que no pasen por la lógica de init
. Por ejemplo, en exampleinfo.bzl, podríamos definir lo siguiente:
def make_barebones_exampleinfo(headers):
"""Returns an ExampleInfo with no files_to_link and only the specified headers."""
return _new_exampleinfo(files_to_link = depset(), headers = all_headers)
Por lo general, el constructor sin procesar está vinculado a una variable cuyo nombre comienza con un guion bajo (_new_exampleinfo
anterior), de modo que el código de usuario no puede cargarlo y generar instancias arbitrarias de proveedor.
Otro uso de init
consiste en evitar que el usuario llame al símbolo del proveedor y forzarlo a usar una función de fábrica:
def _exampleinfo_init_banned(*args, **kwargs):
fail("Do not call ExampleInfo(). Use make_exampleinfo() instead.")
ExampleInfo, _new_exampleinfo = provider(
...
init = _exampleinfo_init_banned)
def make_exampleinfo(...):
...
return _new_exampleinfo(...)
Reglas ejecutables y reglas de prueba
Las reglas ejecutables definen los destinos que puede invocar un comando bazel run
.
Las reglas de prueba son un tipo especial de regla ejecutable cuyos objetivos también pueden invocarse mediante un comando bazel test
. Para crear reglas ejecutables y de prueba, configura el argumento executable
o test
correspondiente en True
en la llamada a rule
:
example_binary = rule(
implementation = _example_binary_impl,
executable = True,
...
)
example_test = rule(
implementation = _example_binary_impl,
test = True,
...
)
Las reglas de prueba deben tener nombres que terminen en _test
. (Los nombres de target de prueba también suelen terminar en _test
por convención, pero esto no es obligatorio). Las reglas que no son de prueba no deben tener este sufijo.
Ambos tipos de reglas deben producir un archivo de salida ejecutable (que puede o no declararse de forma previa) que invocarán los comandos run
o test
. Para indicarle a Bazel cuál de los resultados de una regla debe usar como este ejecutable, pásalo como argumento executable
de un proveedor de DefaultInfo
que se muestra. El elemento executable
se agrega a los resultados predeterminados de la regla (por lo que no es necesario pasarlo a executable
y files
). También se agrega de forma implícita a los runfiles:
def _example_binary_impl(ctx):
executable = ctx.actions.declare_file(ctx.label.name)
...
return [
DefaultInfo(executable = executable, ...),
...
]
La acción que genera este archivo debe establecer el bit ejecutable en el archivo. Para una acción ctx.actions.run
o ctx.actions.run_shell
, la herramienta subyacente debe invocarla. Para una acción ctx.actions.write
, pasa is_executable=True
.
Como comportamiento heredado, las reglas ejecutables tienen un resultado especial ctx.outputs.executable
declarado previamente. Este archivo actúa como ejecutable predeterminado si no especificas uno con DefaultInfo
. No debe usarse de otra manera. Este mecanismo de salida dejó de estar disponible porque no admite la personalización del nombre del archivo ejecutable en el momento del análisis.
Consulta ejemplos de una regla ejecutable y una regla de prueba.
Las reglas ejecutables y las reglas de prueba tienen atributos adicionales definidos de forma implícita, además de los que se agregan para todas las reglas. Los valores predeterminados de los atributos agregados de forma implícita no se pueden cambiar, aunque se puede evitar si se ajusta una regla privada en una macro de Starlark que altera el valor predeterminado:
def example_test(size="small", **kwargs):
_example_test(size=size, **kwargs)
_example_test = rule(
...
)
Ubicación de los archivos RunRun
Cuando se ejecuta un destino ejecutable con bazel run
(o test
), la raíz del directorio runfiles es adyacente al ejecutable. Las rutas se relacionan de la siguiente manera:
# Given launcher_path and runfile_file:
runfiles_root = launcher_path.path + ".runfiles"
workspace_name = ctx.workspace_name
runfile_path = runfile_file.short_path
execution_root_relative_path = "%s/%s/%s" % (
runfiles_root, workspace_name, runfile_path)
La ruta a un File
en el directorio runfiles corresponde a File.short_path
.
El objeto binario que ejecuta directamente bazel
es adyacente a la raíz del directorio runfiles
. Sin embargo, los objetos binarios llamados from a los runfiles no pueden hacer la misma suposición. Para mitigar este problema, cada objeto binario debe proporcionar una manera de aceptar su raíz de runfiles como un parámetro mediante un entorno o una marca o un argumento de línea de comandos. Esto permite que los objetos binarios pasen la raíz canónica del archivo en ejecución a los objetos binarios a los que llama. Si no está configurado, un objeto binario puede adivinar que fue el primer objeto binario llamado y buscar un directorio runfiles adyacente.
Temas avanzados
Solicita archivos de salida
Un solo destino puede tener varios archivos de salida. Cuando se ejecuta un comando bazel build
, se considera que algunos de los resultados de los objetivos proporcionados al comando son solicitados. Bazel solo compila estos archivos solicitados y los archivos de los que
dependen de forma directa o indirecta. (En términos del gráfico de acción, Bazel solo
ejecuta las acciones a las que se puede acceder como dependencias transitivas de los
archivos solicitados).
Además de los resultados predeterminados, cualquier resultado declarado previamente se puede solicitar de forma explícita en la línea de comandos. Las reglas pueden especificar resultados declarados con anterioridad mediante los atributos de salida. En ese caso, el usuario elige de forma explícita las etiquetas para los resultados cuando crea una instancia de la regla. A fin de obtener objetos File
para atributos de salida, usa el atributo correspondiente de ctx.outputs
. Las reglas también pueden definir resultados predeclarados de forma implícita en función del nombre del destino, pero esta función dejó de estar disponible.
Además de los resultados predeterminados, existen grupos de salida, que son colecciones de archivos de salida que pueden solicitarse en conjunto. Esto se puede solicitar con --output_groups
. Por ejemplo, si un //pkg:mytarget
de destino es de un tipo de regla que tiene un grupo de salida debug_files
, estos archivos se pueden compilar mediante la ejecución de bazel build //pkg:mytarget
--output_groups=debug_files
. Dado que los resultados no declarados no tienen etiquetas, solo se pueden solicitar si aparecen en los resultados predeterminados o en un grupo de resultados.
Los grupos de salida se pueden especificar con el proveedor OutputGroupInfo
. Ten en cuenta que, a diferencia de muchos proveedores integrados, OutputGroupInfo
puede tomar parámetros con nombres arbitrarios para definir grupos de salida con ese nombre:
def _example_library_impl(ctx):
...
debug_file = ctx.actions.declare_file(name + ".pdb")
...
return [
DefaultInfo(files = depset([output_file]), ...),
OutputGroupInfo(
debug_files = depset([debug_file]),
all_files = depset([output_file, debug_file]),
),
...
]
Además, a diferencia de la mayoría de los proveedores, un aspecto y el objetivo de la regla a los que se aplica ese aspecto pueden mostrar OutputGroupInfo
, siempre que no definan los mismos grupos de salida. En ese caso, los proveedores resultantes se combinan.
Ten en cuenta que, por lo general, no se debe usar OutputGroupInfo
para transmitir tipos específicos de archivos de un objetivo a las acciones de sus consumidores. En su lugar, define proveedores específicos de reglas.
Parámetros de configuración
Imagina que quieres compilar un objeto binario de C++ para una arquitectura diferente. La compilación puede ser compleja y contener varios pasos. Algunos de los objetos binarios intermedios, como compiladores y generadores de código, deben ejecutarse en la plataforma de ejecución (que podría ser tu host, o un ejecutor remoto). Algunos objetos binarios, como el resultado final, deben compilarse para la arquitectura de destino.
Por este motivo, Bazel tiene un concepto de “configuración” y transiciones. Los destinos principales (los que se solicitan en la línea de comandos) se compilan en la configuración "objetivo", mientras que las herramientas que deben ejecutarse en la plataforma de ejecución se compilan en una configuración "exec". Las reglas pueden generar diferentes acciones según la configuración, por ejemplo, para cambiar la arquitectura de CPU que se pasa al compilador. En algunos casos, es posible que se necesite la misma biblioteca para diferentes configuraciones. Si esto sucede, se analizará y se compilará varias veces.
De forma predeterminada, Bazel compila las dependencias de un destino en la misma configuración que el destino, en otras palabras sin transiciones. Cuando una dependencia es una herramienta necesaria para compilar el destino, el atributo correspondiente debe especificar una transición a una configuración de ejecución. Esto hace que la herramienta y todas sus dependencias se compilen para la plataforma de ejecución.
Para cada atributo de dependencia, puedes usar cfg
a fin de decidir si las dependencias deben compilarse en la misma configuración o pasar a una configuración de ejecución.
Si un atributo de dependencia tiene la marca executable=True
, se debe configurar cfg
de manera explícita. Esto se realiza a fin de evitar la compilación accidental de una herramienta para la configuración incorrecta.
Ver ejemplo
En general, las fuentes, las bibliotecas dependientes y los ejecutables que se necesitarán en el entorno de ejecución pueden usar la misma configuración.
Las herramientas que se ejecutan como parte de la compilación (como compiladores o generadores de código) deben compilarse para una configuración de ejecución. En este caso, especifica cfg="exec"
en el atributo.
De lo contrario, los ejecutables que se usan en el entorno de ejecución (como parte de una prueba) deben compilarse para la configuración de destino. En este caso, especifica cfg="target"
en el atributo.
En realidad, cfg="target"
no hace nada: es puramente un valor de conveniencia para ayudar a los diseñadores de reglas a ser explícitos sobre sus intenciones. Cuando executable=False
, que significa que cfg
es opcional, configúralo solo cuando realmente ayude a facilitar la lectura.
También puedes usar cfg=my_transition
para usar transiciones definidas por el usuario, que permiten a los autores de reglas una gran flexibilidad en el cambio de configuraciones, con la desventaja de hacer que el gráfico de compilación sea más grande y menos comprensible.
Nota: Históricamente, Bazel no tenía el concepto de plataformas de ejecución y todas las acciones de compilación se consideraban ejecutarse en la máquina anfitrión. Las versiones de Bazel anteriores a 6.0 crearon una configuración “host” distinta para representar esto. Si ves referencias a "host" en el código o en la documentación antigua, se refiere a esto. Recomendamos usar Bazel 6.0 o una versión más reciente para evitar esta sobrecarga conceptual adicional.
Fragmentos de configuración
Las reglas pueden acceder a los fragmentos de configuración, como cpp
, java
y jvm
. Sin embargo, todos los fragmentos necesarios deben declararse para evitar errores de acceso:
def _impl(ctx):
# Using ctx.fragments.cpp leads to an error since it was not declared.
x = ctx.fragments.java
...
my_rule = rule(
implementation = _impl,
fragments = ["java"], # Required fragments of the target configuration
host_fragments = ["java"], # Required fragments of the host configuration
...
)
symlinks de runfiles
Por lo general, la ruta relativa de un archivo en el árbol de archivos de ejecución es la misma que la ruta relativa de ese archivo en el árbol de fuentes o en el árbol de resultados que se genera. Si por algún motivo deben ser diferentes, puedes especificar los argumentos root_symlinks
o symlinks
. root_symlinks
es un diccionario que asigna rutas a archivos, en los que las rutas se relacionan con la raíz del directorio runfiles. El diccionario symlinks
es el mismo, pero las rutas de acceso tienen como prefijo de manera implícita el nombre del lugar de trabajo principal (no el nombre del repositorio que contiene el destino actual).
...
runfiles = ctx.runfiles(
root_symlinks = {"some/path/here.foo": ctx.file.some_data_file2}
symlinks = {"some/path/here.bar": ctx.file.some_data_file3}
)
# Creates something like:
# sometarget.runfiles/
# some/
# path/
# here.foo -> some_data_file2
# <workspace_name>/
# some/
# path/
# here.bar -> some_data_file3
Si se usa symlinks
o root_symlinks
, ten cuidado de no asignar dos archivos diferentes a la misma ruta en el árbol de archivos de ejecución. Esto hará que la compilación falle con un error que describe el conflicto. Para solucionar el problema, deberás modificar los argumentos ctx.runfiles
a fin de quitar la colisión. Esta verificación se realizará con todos los destinos que usen tu regla y con cualquier tipo de destino que dependa de ellos. Esto es especialmente riesgoso si es probable que otra herramienta utilice de forma transitiva tu herramienta; los nombres de los symlinks deben ser únicos en todos los archivos de ejecución de una herramienta y en todas sus dependencias.
Cobertura de código
Cuando se ejecuta el comando coverage
, es posible que la compilación deba agregar instrumentación de cobertura para ciertos destinos. La compilación también recopila la lista de archivos de origen instrumentados. La marca --instrumentation_filter
controla el subconjunto de destinos que se considera.
Se excluyen los destinos de prueba, a menos que
se especifique --instrument_test_targets
.
Si una implementación de regla agrega instrumentación de cobertura en el momento de la compilación, debe tener esto en cuenta en su función de implementación. ctx.coverage_instrumented muestra el valor "true" en el modo de cobertura si se deben instrumentar las fuentes de un destino:
# Are this rule's sources instrumented?
if ctx.coverage_instrumented():
# Do something to turn on coverage for this compile action
La lógica que siempre debe estar activada en el modo de cobertura (ya sea que las fuentes de un destino estén instrumentadas específicamente o no) se puede condicionar a ctx.configuration.coverage_enabled.
Si la regla incluye fuentes de sus dependencias directamente antes de la compilación (como archivos de encabezado), es posible que también debas activar la instrumentación de tiempo de compilación si deben instrumentarse las fuentes de las dependencias:
# Are this rule's sources or any of the sources for its direct dependencies
# in deps instrumented?
if (ctx.configuration.coverage_enabled and
(ctx.coverage_instrumented() or
any([ctx.coverage_instrumented(dep) for dep in ctx.attr.deps]))):
# Do something to turn on coverage for this compile action
Las reglas también deben proporcionar información sobre qué atributos son relevantes para la cobertura con el proveedor InstrumentedFilesInfo
, construidos con coverage_common.instrumented_files_info
.
El parámetro dependency_attributes
de instrumented_files_info
debe enumerar todos los atributos de dependencia del entorno de ejecución, incluidas las dependencias de código como deps
y las dependencias de datos como data
. El parámetro source_attributes
debe enumerar los atributos de archivos fuente de la regla si se puede agregar la instrumentación de cobertura:
def _example_library_impl(ctx):
...
return [
...
coverage_common.instrumented_files_info(
ctx,
dependency_attributes = ["deps", "data"],
# Omitted if coverage is not supported for this rule:
source_attributes = ["srcs", "hdrs"],
)
...
]
Si no se muestra InstrumentedFilesInfo
, se crea uno predeterminado con cada atributo de dependencia que no es una herramienta y que no establece cfg
como "host"
o "exec"
en el esquema de atributos) en dependency_attributes
. (Este no es un comportamiento ideal, ya que coloca atributos como srcs
en dependency_attributes
en lugar de source_attributes
, pero evita la configuración de cobertura explícita para todas las reglas de la cadena de dependencias).
Acciones de validación
A veces, es necesario validar algo sobre la compilación, y la información necesaria para realizar esa validación solo está disponible en artefactos (archivos fuente o generados). Debido a que esta información se encuentra en artefactos, las reglas no pueden realizar esta validación en el momento en que se analizan porque las reglas no pueden leer archivos. En su lugar, las acciones deben realizar esta validación en el momento de la ejecución. Cuando la validación falla, la acción falla y, por lo tanto, la compilación también.
Algunos ejemplos de validaciones que pueden ejecutarse son el análisis estático, el análisis con lint, las verificaciones de dependencia y coherencia, y las verificaciones de diseño.
Las acciones de validación también pueden ayudar a mejorar el rendimiento de la compilación moviendo partes de acciones que no son necesarias para compilar artefactos en acciones separadas. Por ejemplo, si una sola acción que realiza una compilación con lint se puede separar en una acción de compilación y una acción de análisis con lint, esta acción se puede ejecutar como una acción de validación y en paralelo con otras acciones.
Estas "acciones de validación" a menudo no producen nada que se use en otra parte de la compilación, ya que solo necesitan afirmar cosas sobre sus entradas. Sin embargo, esto presenta un problema: si una acción de validación no produce nada que se use en otra parte de la compilación, ¿cómo obtiene una regla para ejecutarla? Históricamente, el enfoque era que la acción de validación mostrara un archivo vacío y agregar de forma artificial ese resultado a las entradas de alguna otra acción importante en la compilación:
Esto funciona porque Bazel siempre ejecutará la acción de validación cuando se ejecute la acción de compilación, pero tiene desventajas significativas:
La acción de validación se encuentra en la ruta crítica de la compilación. Dado que Bazel cree que el resultado vacío es necesario para ejecutar la acción de compilación, ejecutará la acción de validación primero, aunque la acción de compilación ignore la entrada. Esto reduce el paralelismo y ralentiza las compilaciones.
Si es posible que se ejecuten otras acciones en la compilación en lugar de la de compilación, también se deben agregar los resultados vacíos de las acciones de validación (por ejemplo, el resultado del archivo jar de origen de
java_library
). Este también es un problema si se agregan después acciones nuevas que podrían ejecutarse en lugar de la acción de compilación, y el resultado de la validación vacía se deja accidentalmente.
La solución a estos problemas es usar el grupo de resultados de validaciones.
Grupo de salida de validaciones
El grupo de salida de validaciones es un grupo de salida diseñado para contener los resultados de las acciones de validación que, de otro modo, no se usarían, de modo que no tengan que agregarse de forma artificial a las entradas de otras acciones.
Este grupo es especial porque sus resultados siempre se solicitan, sin importar el valor de la marca --output_groups
y de cómo se depende el destino (por ejemplo, en la línea de comandos, como una dependencia o a través de resultados implícitos del destino). Ten en cuenta que se seguirán aplicando el almacenamiento en caché y la incrementalidad normales: si las entradas de la acción de validación no cambiaron y la acción de validación se realizó con éxito, la acción de validación no se ejecutará.
El uso de este grupo de salida aún requiere que las acciones de validación generen algún archivo, incluso uno vacío. Esto puede requerir la unión de algunas herramientas que normalmente no crean resultados para que se cree un archivo.
Las acciones de validación de un destino no se ejecutan en tres casos:
- Cuando se depende el objetivo como herramienta
- Cuando se depende del objetivo como dependencia implícita (por ejemplo, un atributo que comienza con "_")
- Cuando el destino se compila en la configuración de host o ejecución.
Se supone que estos destinos tienen sus propias compilaciones y pruebas independientes que revelarían cualquier falla de validación.
Usa el grupo de resultados de validaciones
El grupo de salida de validaciones se llama _validation
y se usa como cualquier otro grupo de resultados:
def _rule_with_validation_impl(ctx):
ctx.actions.write(ctx.outputs.main, "main output\n")
ctx.actions.write(ctx.outputs.implicit, "implicit output\n")
validation_output = ctx.actions.declare_file(ctx.attr.name + ".validation")
ctx.actions.run(
outputs = [validation_output],
executable = ctx.executable._validation_tool,
arguments = [validation_output.path])
return [
DefaultInfo(files = depset([ctx.outputs.main])),
OutputGroupInfo(_validation = depset([validation_output])),
]
rule_with_validation = rule(
implementation = _rule_with_validation_impl,
outputs = {
"main": "%{name}.main",
"implicit": "%{name}.implicit",
},
attrs = {
"_validation_tool": attr.label(
default = Label("//validation_actions:validation_tool"),
executable = True,
cfg = "exec"),
}
)
Ten en cuenta que el archivo de salida de validación no se agrega al DefaultInfo
ni a las entradas de ninguna otra acción. La acción de validación para un objetivo de este tipo de regla se seguirá ejecutando si se depende del objetivo por etiqueta o si alguno de los resultados implícitos del destino se depende de forma directa o indirecta.
Por lo general, es importante que los resultados de las acciones de validación solo vayan al grupo de resultados de validación y no se agreguen a las entradas de otras acciones, ya que esto podría vencer las ganancias del paralelismo. Sin embargo, ten en cuenta que, en la actualidad, Bazel no tiene ninguna verificación especial para aplicar esto. Por lo tanto, debes probar que los resultados de la acción de validación no se agreguen a las entradas de ninguna acción en las pruebas de las reglas de Starlark. Por ejemplo:
load("@bazel_skylib//lib:unittest.bzl", "analysistest")
def _validation_outputs_test_impl(ctx):
env = analysistest.begin(ctx)
actions = analysistest.target_actions(env)
target = analysistest.target_under_test(env)
validation_outputs = target.output_groups._validation.to_list()
for action in actions:
for validation_output in validation_outputs:
if validation_output in action.inputs.to_list():
analysistest.fail(env,
"%s is a validation action output, but is an input to action %s" % (
validation_output, action))
return analysistest.end(env)
validation_outputs_test = analysistest.make(_validation_outputs_test_impl)
Marca de acciones de validación
La marca de línea de comandos --run_validations
controla la ejecución de acciones de validación, cuyo valor predeterminado es verdadero.
Funciones obsoletas
Salidas declaradas previamente obsoletas
Existen dos formas obsoletas de usar resultados declarados con anterioridad:
El parámetro
outputs
derule
especifica una asignación entre los nombres de atributos de salida y las plantillas de strings para generar etiquetas de salida declaradas previamente. Prefiere el uso de resultados no declarados y agregar resultados de forma explícita aDefaultInfo.files
. Usa la etiqueta del destino de la regla como entrada para las reglas que consumen la salida en lugar de una etiqueta de salida declarada previamente.Para las reglas ejecutables,
ctx.outputs.executable
hace referencia a un resultado ejecutable declarado con el mismo nombre que el objetivo de la regla. Prefiere declarar el resultado de manera explícita, por ejemplo, conctx.actions.declare_file(ctx.label.name)
, y asegúrate de que el comando que genera el ejecutable establece sus permisos para permitir la ejecución. Pasa de manera explícita el resultado ejecutable al parámetroexecutable
deDefaultInfo
.
Funciones de Runfiles que debes evitar
ctx.runfiles
y el tipo runfiles
tienen un conjunto complejo de funciones, muchas de las cuales se conservan por motivos heredados.
Las siguientes recomendaciones ayudan a reducir la complejidad:
Evita el uso de los modos
collect_data
ycollect_default
dectx.runfiles
. Estos modos recopilan de manera implícita los archivos runrun en ciertos bordes de dependencias codificados de formas confusas. En su lugar, agrega archivos con los parámetrosfiles
otransitive_files
dectx.runfiles
, o bien fusiona archivos run de dependencias conrunfiles = runfiles.merge(dep[DefaultInfo].default_runfiles)
.Evita el uso de
data_runfiles
ydefault_runfiles
del constructorDefaultInfo
. En su lugar, especificaDefaultInfo(runfiles = ...)
. La distinción entre los archivos run “default” y “data” se mantiene por razones heredadas. Por ejemplo, algunas reglas colocan sus resultados predeterminados endata_runfiles
, pero no endefault_runfiles
. En lugar de usardata_runfiles
, las reglas deben incluir ambos resultados predeterminados y combinarse endefault_runfiles
desde atributos que proporcionan archivos de ejecución (a menudo,data
).Cuando recuperes
runfiles
deDefaultInfo
(por lo general, solo para combinar archivos de ejecución entre la regla actual y sus dependencias), usaDefaultInfo.default_runfiles
, noDefaultInfo.data_runfiles
.
Migración desde proveedores heredados
Históricamente, los proveedores de Bazel eran campos simples en el objeto Target
. Se accedió a ellas mediante el operador de punto, y se crearon a partir de un campo en una estructura que mostró la función de implementación de la regla.
Este estilo está obsoleto y no se debe usar en un código nuevo. A continuación, encontrarás información que puede ayudarte a migrar. El nuevo mecanismo del proveedor evita conflictos de nombres. También admite la ocultación de datos, ya que requiere que cualquier código que acceda a una instancia del proveedor para recuperarla mediante el símbolo del proveedor.
Por el momento, los proveedores heredados aún son compatibles. Una regla puede mostrar proveedores heredados y modernos de la siguiente manera:
def _old_rule_impl(ctx):
...
legacy_data = struct(x="foo", ...)
modern_data = MyInfo(y="bar", ...)
# When any legacy providers are returned, the top-level returned value is a
# struct.
return struct(
# One key = value entry for each legacy provider.
legacy_info = legacy_data,
...
# Additional modern providers:
providers = [modern_data, ...])
Si dep
es el objeto Target
resultante para una instancia de esta regla, los proveedores y su contenido se pueden recuperar como dep.legacy_info.x
y dep[MyInfo].y
.
Además de providers
, el struct que se muestra también puede tomar otros campos que tienen un significado especial (y, por lo tanto, no crear un proveedor heredado correspondiente):
Los campos
files
,runfiles
,data_runfiles
,default_runfiles
yexecutable
corresponden a los mismos campos deDefaultInfo
. No se permite especificar ninguno de estos campos y, al mismo tiempo, mostrar un proveedorDefaultInfo
.El campo
output_groups
toma un valor de struct y corresponde aOutputGroupInfo
.
En las declaraciones de reglas provides
y en las declaraciones providers
de atributos de dependencia, los proveedores heredados se pasan a medida que las strings y los proveedores modernos se pasan con su símbolo *Info
. Asegúrate de cambiar de strings a símbolos cuando migres. En el caso de los conjuntos de reglas complejos o grandes en los que es difícil actualizar todas las reglas de forma atómica, es posible que tengas un tiempo más fácil si sigues esta secuencia de pasos:
Modifica las reglas que producen el proveedor heredado para producir los proveedores heredados y modernos mediante la sintaxis anterior. En el caso de las reglas que declaran que muestran el proveedor heredado, actualiza esa declaración para incluir los proveedores heredados y modernos.
Modifica las reglas que consumen el proveedor heredado para consumir el proveedor moderno. Si alguna declaración de atributo requiere el proveedor heredado, actualízala para que, en su lugar, requiera el proveedor moderno. De manera opcional, puedes intercalar este trabajo con el paso 1 haciendo que los consumidores acepten o exijan cualquiera de los proveedores: prueba la presencia del proveedor heredado con
hasattr(target, 'foo')
o usa el proveedor nuevo conFooInfo in target
.Quita por completo el proveedor heredado de todas las reglas.