Una regla define una serie de acciones en las que Bazel realiza para producir un conjunto de salidas, a las que se hace referencia en providers que muestra la regla función de implementación. Por ejemplo, un código C++ regla binaria podría tener las siguientes características:
- Toma un conjunto de archivos de origen
.cpp
(entradas). - Ejecuta
g++
en los archivos de origen (acción). - Muestra el proveedor
DefaultInfo
con el resultado ejecutable y otros archivos para que estén disponibles en el tiempo de ejecución. - Devuelve el proveedor
CcInfo
con información específica de C++ recopilada desde el objetivo y sus dependencias.
Desde la perspectiva de Bazel, g++
y las bibliotecas C++ estándar también son entradas.
a esta regla. Como escritor de reglas, debes considerar no solo los roles
las entradas de una regla, pero también todas las herramientas y bibliotecas necesarias para ejecutar
las acciones.
Antes de crear o modificar una regla, asegúrate de conocer los fases de compilación. Es importante comprender los tres principales de una compilación (carga, análisis y ejecución). También es útil aprenderás sobre las macros para entender la diferencia entre reglas y o usar las macros. Para comenzar, primero consulta 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 compatibilidad básica para ciertos idiomas.
Si defines tus propias reglas, puedes agregar una compatibilidad similar para lenguajes y herramientas
que Bazel no admite de forma nativa.
Bazel proporciona un modelo de extensibilidad para escribir reglas con el
Starlark. Estas reglas están escritas en archivos .bzl
, que
se puede cargar directamente desde archivos BUILD
.
Cuando define su propia regla, puede decidir qué atributos admite cómo genera sus resultados.
La función implementation
de la regla define su comportamiento exacto durante el
fase de análisis. Esta función no ejecuta ningún
comandos externos. En cambio, registra acciones que se usarán
más adelante durante la fase de ejecución para compilar los resultados de la regla, si son
según tus necesidades.
Creación de reglas
En un archivo .bzl
, usa la función rule para definir una nueva
y almacena 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 llamado example_library
.
La llamada a rule
también debe especificar si la regla crea un
Salida ejecutable (con executable=True
) o específicamente
Un ejecutable de prueba (con test=True
) Si la última es una regla de prueba,
y el nombre de la regla debe terminar en _test
.
Creación de instancias de destino
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 llama crear una instancia de la regla. Con esto se especifica un nombre para el una orientación y valores nuevos para los atributos del objetivo.
Las reglas también se pueden llamar desde las funciones de Starlark y se pueden cargar en archivos .bzl
.
Las funciones de Starlark que llaman a reglas se llaman macros de Starlark.
Las macros de Starlark deben llamarse en última instancia desde archivos BUILD
, y solo se pueden
se llama durante la fase de carga, cuando BUILD
y los archivos adjuntos para crear
instancias de destinos.
Atributos
Un atributo es un argumento de regla. Los atributos pueden proporcionar valores específicos a un implementación de un objetivo, o pueden referirse a otras objetivos, creando un gráfico de dependencias.
Los atributos específicos de la regla, como srcs
o deps
, se definen pasando un mapa.
desde nombres de atributos hasta esquemas (creados con el attr
módulo) al parámetro attrs
de rule
.
Atributos comunes, como
name
y visibility
se agregan de forma implícita a todas las reglas. Adicional
se agregan implícitamente a
reglas ejecutables y de prueba específicamente. Atributos que
se agregan implícitamente a una regla no pueden incluirse en el diccionario que se pasa a
attrs
Atributos de dependencia
Las reglas que procesan código fuente suelen definir 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 el esquema de atributos especifica las extensiones de archivo que se esperan para el orden del archivo fuente que procesa la regla. Reglas para idiomas con archivos de encabezado Por lo general, debes especificar un atributohdrs
independiente para los encabezados procesados por un objetivo 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. (Para Por ejemplo,cc_library
proporcionaCcInfo
).data
especifica los archivos que estarán disponibles durante el tiempo de ejecución para cualquier archivo ejecutable que depende de un objetivo. Eso debería permitir que los archivos arbitrarios especificada.
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 (aquellas que se definen con
attr.label_list
:
attr.label
o
attr.label_keyed_string_dict
)
especifica dependencias de un tipo determinado
entre un destino y los destinos cuyas etiquetas (o las
Label
) se enumeran en ese atributo cuando el destino
está definido. El repositorio y posiblemente la ruta de acceso de estas etiquetas se resuelven
en relación con el objetivo 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. Integra
está implícita porque es una parte del gráfico de destino que el usuario no
especificar en un archivo BUILD
. Las dependencias implícitas son útiles para codificar un
relación entre una regla y una herramienta (una dependencia de tiempo de compilación, como un
ya que, la mayor parte del tiempo, un usuario no está interesado en especificar
herramienta que usa la regla. Dentro de la función de implementación de la regla, esto se trata
de la misma forma que otras dependencias.
Si deseas proporcionar una dependencia implícita sin permitir que el usuario
anular ese valor, puede configurar el atributo como privado dándole un nombre
que comience con un guion bajo (_
). Los atributos privados deben tener parámetros
de salida. Por lo general, solo tiene sentido usar atributos privados para
dependencias.
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 un valor
en el compilador //tools:example_compiler
. Esto permite
La función de implementación de example_library
para generar acciones que invoquen la función
compilador, incluso si el usuario no pasó su etiqueta como entrada. Desde
_compiler
es un atributo privado, sigue que ctx.attr._compiler
Siempre apuntará a //tools:example_compiler
en todos los destinos de esta regla.
el tipo de letra. También puedes asignar el nombre compiler
al atributo sin el signo de interrogación de cierre
guion bajo y mantén el valor predeterminado. Esto permite a los usuarios sustituir un
un compilador diferente si es necesario, pero este no requiere conocer la
etiqueta.
Las dependencias implícitas generalmente se usan para herramientas que residen en la misma repositorio como la implementación de la regla. Si la herramienta proviene de ejecución o un repositorio diferente, el debes obtener esa herramienta de una cadena de herramientas.
Atributos de salida
Atributos de salida, como attr.output
y
attr.output_list
, declara un archivo de salida al que
genera el destino. Estos difieren de los atributos de dependencia de dos maneras:
- Definen los objetivos del archivo de salida en lugar de hacer referencia a destinos definidos. en otro lugar.
- Los destinos del archivo de salida dependen de la instancia de destino, 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 del destino. Si una regla tiene
Un atributo de salida, por lo general, se denomina out
o outs
.
Los atributos de salida son la forma preferida de crear resultados declarados previamente, que se puede depender o no de solicitada en la línea de comandos.
Función de implementación
Cada regla requiere una función implementation
. Estas funciones se ejecutan
estrictamente en la fase de análisis y transformar la
gráfico de objetivos generados en la fase de carga en un gráfico de
acciones que se deben realizar durante la fase de ejecución. Por lo tanto,
de implementación no pueden leer ni escribir archivos.
Las funciones de implementación de reglas suelen ser privadas (nombradas con un signo
guion bajo). Convencionalmente, se nombran igual que la regla, pero con sufijo
con _impl
.
Las funciones de implementación toman exactamente un parámetro: un
contexto de la regla, cuyo nombre convencional es ctx
. Muestran una lista de
proveedores.
Destinos
En el momento del análisis, las dependencias se representan como Target
objetos. Estos objetos contienen los providers generados cuando la
se ejecutó la función de implementación de destino.
ctx.attr
tiene campos que corresponden a los nombres de cada una
atributo de dependencia, que contiene objetos Target
que representan cada
dependencia a través de ese atributo. En el caso de los atributos label_list
, esta es una lista de
Targets
Para los atributos label
, es una sola Target
o None
.
La función de implementación de un destino muestra una lista de objetos de proveedor:
return [ExampleInfo(headers = depset(...))]
Se puede acceder a estos con la notación de índices ([]
), con el tipo de proveedor como
una clave. Pueden ser proveedores personalizados definidos en Starlark o
proveedores para reglas nativas disponibles como Starlark.
variables globales.
Por ejemplo, si una regla toma archivos de encabezado mediante un atributo hdrs
y proporciona
a las acciones de compilación del objetivo y sus consumidores, podría
recogerlos de la siguiente manera:
def _example_library_impl(ctx):
...
transitive_headers = [hdr[ExampleInfo].headers for hdr in ctx.attr.hdrs]
Para el diseño heredado en el que se muestra una struct
desde una
la función de implementación del destino 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]
No se recomienda este estilo y se deben aplicar reglas de migraron de allí.
Archivos
Los archivos se representan con objetos File
. Como Bazel no
realizar operaciones de E/S de archivos durante la fase de análisis, estos objetos no pueden
leer o escribir directamente el contenido del archivo. sino que pasan a acciones
(consulta ctx.actions
) para construir partes de la
gráfico de acciones.
Un File
puede ser un archivo de origen o un archivo 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 todos
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 File
o None
para
atributos de dependencia 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.
Como es necesario conocer todas las etiquetas durante la fase de carga, estas etiquetas adicionales
los resultados no tienen etiquetas. Los objetos File
para las salidas se pueden crear con
ctx.actions.declare_file
y
ctx.actions.declare_directory
A menudo,
los nombres de las salidas se basan en el nombre del objetivo,
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
atributos de salida; en su lugar, se pueden recuperar objetos File
.
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, para Por ejemplo, “ejecutar gcc en hello.c y obtener hello.o”. Cuando se crea una acción, Bazel no ejecuta el comando de inmediato. Lo registra en un gráfico de dependencias, porque 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 shell kubectl.ctx.actions.write
, para escribir una cadena en un archivo.ctx.actions.expand_template
, a genera un archivo a partir de una plantilla.
ctx.actions.args
se puede usar para aumentar
los argumentos para las acciones. Evita acoplar los depsets hasta que
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 baja 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. Podría depender del valor de 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 esperas que se aumenten (antes de ejecutar la descompresión). Las acciones que crean un número variable de archivos internamente pueden agruparlos en una único archivo (como ZIP, TAR o cualquier otro formato de archivo).
Las acciones deben enumerar todas sus entradas. Enumerar las entradas que no se usan permitido, pero ineficiente.
Las acciones deben crear todos sus resultados. Pueden escribir otros archivos, pero algo que no esté en los resultados no estará disponible para los consumidores. Todos los resultados declarados debe escribirse mediante alguna acción.
Las acciones son comparables con las funciones puras: deben depender únicamente del las entradas proporcionadas, y evitas el acceso a la información, el nombre de usuario, el reloj red o E/S (excepto las entradas de lectura y escritura de salida). Este es importante porque el resultado se almacenará en caché y se reutilizará.
Bazel resuelve las dependencias, que decidirá qué acciones ejecutado. Es un error si hay un ciclo en el gráfico de la dependencia. Creando una acción no garantiza que se ejecutará, depende de si sus resultados son necesarios para la compilación.
Proveedores
Los proveedores son información que una regla expone a otras reglas que dependen de él. Estos datos pueden incluir archivos de salida, bibliotecas, parámetros para pasar en la línea de comandos de una herramienta, o cualquier otra cosa que los consumidores sobre el tema.
Dado que la función de implementación de una regla solo puede leer proveedores desde el
las dependencias inmediatas del destino, las reglas deben reenviar
información de las dependencias del objetivo que debe conocer la dirección
a los consumidores, en general acumulándolos 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 antiguas también pueden escribirse en un estilo heredado, en el que el elemento
la función de implementación muestra un struct
en lugar de una lista de
objetos del proveedor. No se recomienda este estilo y se deben aplicar reglas
de migraron de allí.
Salidas predeterminadas
Los resultados predeterminados de un destino son aquellos que se solicitan de forma predeterminada cuando
se solicita el destino para la compilación en la línea de comandos. Por ejemplo, un
El //pkg:foo
de destino de java_library
tiene foo.jar
como resultado predeterminado, de modo que
se compilarán con el comando bazel build //pkg:foo
.
Los resultados predeterminados se especifican con el parámetro files
de
DefaultInfo
:
def _example_library_impl(ctx):
...
return [
DefaultInfo(files = depset([output_file]), ...),
...
]
Si una implementación de reglas o files
no devuelve DefaultInfo
no se especifica el parámetro, DefaultInfo.files
se establece de forma predeterminada en todo
resultados declarados previamente (por lo general, los creados por el resultado
atributos).
Las reglas que realizan acciones deben proporcionar salidas predeterminadas, incluso si esas salidas no se espera que se usen directamente. Las acciones que no están en el gráfico del se reducen las salidas solicitadas. Si solo los consumidores de un objetivo usan un resultado, y esas acciones no se realizarán cuando el destino se compile de forma aislada. Esta dificulta la depuración porque volver a compilar solo el objetivo con errores no hará y reproducir la falla.
Archivos de ejecución
Los archivos runfiles son un conjunto de archivos que un destino usa en el entorno de ejecución (en lugar de tiempo). Durante la fase de ejecución, Bazel crea un árbol de directorios que contiene symlinks que apuntan a los archivos de ejecución. Esto organiza la del objeto binario para que pueda acceder a los archivos de ejecución durante el tiempo de ejecución.
Los archivos de ejecución se pueden agregar manualmente durante la creación de reglas.
Los objetos runfiles
se pueden crear con el método runfiles
en el contexto de la regla, ctx.runfiles
, y se pasan al
Parámetro runfiles
en DefaultInfo
. El resultado ejecutable de
Las reglas ejecutables se agregan de manera implícita a los archivos de ejecución.
Algunas reglas especifican atributos, generalmente llamados
data
, cuyas salidas se agregan a
de objetivos runfiles. Los archivos de ejecución también deberían combinarse desde data
, así como
de cualquier atributo que pudiera proporcionar código para una ejecución eventual, por lo general,
srcs
(que puede contener objetivos filegroup
con data
asociados) 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 usando 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 proveedores:
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 de validación y procesamiento previo. Esto se puede usar para garantizar que todos de proveedores obedecen ciertos invariantes o para brindar a los usuarios una API más limpia para obtener una instancia.
Para ello, se debe pasar una devolución de llamada init
al
Función provider
. Si se proporciona esta devolución de llamada, la
El tipo de datos que se muestra de provider()
cambia para que sea una tupla de dos valores: el proveedor.
que es el valor común que se muestra cuando no se usa init
y un valor "sin procesar"
".
En este caso, cuando se llama al símbolo del proveedor, en lugar de mostrar directamente
una nueva instancia, reenviará los argumentos junto con la devolución de llamada init
. El
el valor de retorno de la devolución de llamada debe ser un dict que asigne nombres de campo (cadenas) a los valores;
se usa para inicializar los campos de la nueva instancia. Ten en cuenta que
la devolución de llamada puede tener cualquier firma y, si los argumentos no coinciden con la firma
un error se informa como si la devolución de llamada se hubiera invocado 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 puede entonces 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 pasan por la lógica de init
. Por ejemplo, en exampleinfo.bzl,
podría definir:
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
arriba), para que el código de usuario no pueda cargarlo y
generar instancias arbitrarias de proveedores.
Otro uso de init
es simplemente evitar que el usuario llame al proveedor.
y los obliga a usar una función de fábrica en su lugar:
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 destinos que pueden invocarse mediante un comando bazel run
.
Las reglas de prueba son un tipo especial de regla ejecutable cuyos destinos también pueden ser
invocada por un comando bazel test
Las reglas ejecutables y de prueba son creadas por
configura el executable
correspondiente
El argumento test
para 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
. (Prueba los nombres de destino también a menudo
terminar en _test
por convención, pero esto no es obligatorio) Las reglas que no son de prueba
tienen este sufijo.
Ambos tipos de reglas deben producir un archivo de salida ejecutable (que puede o no
declararse previamente) que se invocará mediante los comandos run
o test
. Para contar
a Bazel para saber cuál de los resultados de la regla usar como este ejecutable, pasarlo como el
Argumento executable
de un DefaultInfo
que se muestra
proveedor. Ese executable
se agrega a los resultados predeterminados de la regla (por lo que
no es necesario que pases eso a executable
y files
). También está implícitamente
agregado 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
un ctx.actions.run
o
ctx.actions.run_shell
acción: debe hacerse
por la herramienta subyacente
que invoca la acción. Para un
ctx.actions.write
, pasa is_executable=True
.
Como comportamiento heredado, las reglas ejecutables tienen una
resultado especial ctx.outputs.executable
declarado previamente. Este archivo sirve
ejecutable predeterminado si no especificas uno con DefaultInfo
; no debe ser
de otro modo. Este mecanismo de salida dejó de estar disponible porque no es compatible
personalizar el nombre del archivo ejecutable en el momento del análisis.
Consulta ejemplos de un regla ejecutable y un regla de prueba.
Reglas ejecutables y las reglas de prueba tienen restricciones atributos definidos de forma implícita, además de los agregados para todas las reglas. Los valores predeterminados de Los atributos agregados implícitamente no se pueden cambiar, aunque esto se puede evitar con una regla privada en una macro de Starlark que modifica la Predeterminado:
def example_test(size="small", **kwargs):
_example_test(size=size, **kwargs)
_example_test = rule(
...
)
Ubicación de los archivos de ejecución
Cuando se ejecuta un destino ejecutable con bazel run
(o test
), la raíz de la
runfiles se encuentra junto al ejecutable. Las rutas se relacionan de la siguiente manera:
# Given executable_file and runfile_file:
runfiles_root = executable_file.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 de la
runfiles
. Sin embargo, los objetos binarios llamados desde los archivos de ejecución no pueden crearse
la misma suposición. Para mitigar esto, cada objeto binario debe proporcionar una forma de
Aceptar su raíz de archivos de ejecución como parámetro mediante un entorno o línea de comandos
argumento/marca. Esto permite que los objetos binarios pasen la raíz de los archivos runfiles canónicos correctos
a los objetos binarios a los que llama. Si eso no está configurado, un objeto binario puede adivinar que fue el
primer binario llamado y busca un directorio runfiles adyacente.
Temas avanzados
Cómo solicitar archivos de salida
Un solo destino puede tener varios archivos de salida. Cuando se activa un comando bazel build
ejecutar, se considera que algunas de las salidas de los objetivos proporcionados al comando
se solicitarán. Bazel solo compila estos archivos solicitados y los archivos que
directa o indirectamente de las que dependen. (En términos del gráfico de acción, Bazel solo
ejecuta las acciones que son alcanzables como dependencias transitivas de la
archivos solicitados).
Además de los resultados predeterminados, cualquier resultado declarado previamente también puede
solicitarse explícitamente en la línea de comandos. Las reglas pueden especificar valores
salidas a través de atributos de salida. En ese caso, el usuario
elige de forma explícita etiquetas para los resultados cuando crea una instancia de la regla. Para obtener
File
para los atributos de salida, usa el valor correspondiente
atributo de ctx.outputs
. Las reglas pueden
definir de forma implícita los resultados declarados previamente
en el nombre del destino, pero esta función ya no está disponible.
Además de los resultados predeterminados, hay grupos de salida, que son colecciones
de archivos de salida que se pueden solicitar juntos. Estas se pueden solicitar
--output_groups
Para
Por ejemplo, si un //pkg:mytarget
de destino es de un tipo de regla que tiene una debug_files
grupo de salida, puedes compilar estos archivos ejecutando bazel build //pkg:mytarget
--output_groups=debug_files
. Como las salidas no declaradas previamente no tienen etiquetas,
solo se pueden solicitar si aparecen en los resultados predeterminados o en un
grupo.
Los grupos de salida se pueden especificar
OutputGroupInfo
. Ten en cuenta que, a diferencia de muchas
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, OutputGroupInfo
puede ser devuelto por un
aspect y el objetivo de la regla a la que se aplica ese aspecto, como
pero no definen los mismos grupos de salida. En ese caso, el resultado
proveedores de servicios en la nube.
Ten en cuenta que, por lo general, OutputGroupInfo
no debe usarse para transmitir ordenaciones específicas.
de archivos de un objetivo a las acciones de sus consumidores. Definir
proveedores específicos de reglas para eso en su lugar.
Configuraciones
Imagina que quieres compilar un objeto binario de C++ para una arquitectura diferente. El puede ser complejo y requerir varios pasos. Algunos de los binarios, como compiladores y generadores de código, deben ejecutarse la plataforma de ejecución (que puede ser tu host, o un ejecutor remoto). Algunos objetos binarios, como el resultado final, deben compilarse para la arquitectura de destino.
Por esta razón, Bazel tiene un concepto de “configuraciones”. y transiciones. El Los destinos superiores (los solicitados en la línea de comandos) se compilan en la "objetivo" configuración de la ejecución, mientras que las herramientas que deben ejecutarse se compilan en un comando “exec” configuración. Las reglas pueden generar diferentes acciones según en la configuración, por ejemplo, para cambiar la arquitectura de la CPU que se pasa al compilador. En algunos casos, se puede necesitar la misma biblioteca para diferentes parámetros de configuración. Si esto sucede, se analizarán y posiblemente se compilen. varias veces.
De forma predeterminada, Bazel compila las dependencias de un destino en la misma configuración que el mismo objetivo, es decir, sin transiciones. Cuando una dependencia es un necesaria para crear el destino, el atributo correspondiente especificar una transición a una configuración de ejecución. Esto provoca que la herramienta y todas sus dependencias que se compilarán para la plataforma de ejecución.
Para cada atributo de dependencia, puedes usar cfg
para decidir si las dependencias
debería compilar 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 establecer cfg
.
de forma explícita. De esta forma, evitas crear accidentalmente una herramienta
configuración.
Ver ejemplo
En general, las fuentes, las bibliotecas dependientes y los ejecutables que se necesitarán en entorno de ejecución pueden usar la misma configuración.
Herramientas que se ejecutan como parte de la compilación (como los compiladores o generadores de código)
debe 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 tiempo de ejecución (como parte de una prueba) deben
que debe compilarse
para la configuración de destino. En este caso, especifica cfg="target"
en
el atributo.
En realidad, cfg="target"
no realiza ninguna acción: solo es un valor de conveniencia
ayudan a los diseñadores de reglas a ser explícitos sobre sus intenciones. Cuando executable=False
,
lo que significa que cfg
es opcional, configúralo solo cuando realmente facilite la legibilidad.
También puedes usar cfg=my_transition
para usar
transiciones definidas por el usuario, que permiten
a los creadores de reglas de firewall una gran flexibilidad a la hora de cambiar la configuración, 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, en su lugar, se consideró que todas las acciones de compilación se ejecutaban en la máquina anfitrión. Debido a esto, hay un solo "host" configuración y un “host” transición que puede usarse para crear una dependencia en la configuración del host. Muchas reglas seguir usando el "host" de sus herramientas, pero actualmente esta es obsoleto y se está migrando para usar “exec” de transición cuando sea posible.
Existen numerosas diferencias entre los "hosts" y "exec" parámetros de configuración:
- "anfitrión" es terminal, “exec” No lo es: Una vez que una dependencia se encuentra en el "host". configuración, no se permiten más transiciones. Puedes seguir haciendo las transiciones de configuración una vez que esté en configuración.
- "anfitrión" es monolítico, “exec” no: solo hay un "organizador" configuración, pero puede haber un comando "exec" diferente configuración de cada ejecución plataforma.
- "anfitrión" supone que ejecutas herramientas en la misma máquina que Bazel o en un máquina significativamente similar. Esto ya no es así: puedes ejecutar compilaciones acciones en tu máquina local o en un ejecutor remoto, y no hay garantizar que el ejecutor remoto sea la misma CPU y el mismo SO que tu aplicación local máquina.
Tanto el comando "exec" y "organizar" de configuración aplican los mismos cambios de opciones (por ejemplo,
configurar --compilation_mode
desde --host_compilation_mode
, establecer --cpu
desde
--host_cpu
, etcétera). La diferencia es que el "anfitrión" de Terraform comienza con
los valores default de todas las demás marcas, mientras que “exec” configuración
comienza con los valores actuales de las marcas, según la configuración de destino.
Fragmentos de configuración
Las reglas pueden acceder
fragmentos de configuración como
cpp
, java
y jvm
. Sin embargo, todos los fragmentos requeridos se deben declarar en
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
...
)
ctx.fragments
solo proporciona fragmentos de configuración para el destino.
configuración. Si quieres acceder a fragmentos para la configuración del host, usa
ctx.host_fragments
en su lugar.
Vínculos simbólicos de archivos de ejecución
Normalmente, la ruta relativa de un archivo en el árbol de archivos de ejecución es la misma que
la ruta de acceso relativa de ese archivo en el árbol de fuentes o en el árbol de resultados generado. Si estos
deben ser diferentes por algún motivo, puedes especificar root_symlinks
o
Argumentos de symlinks
. root_symlinks
es una ruta de asignación de diccionarios a
de estado, en las que las rutas son relativas a la raíz del directorio runfiles. El
El diccionario symlinks
es el mismo, pero las rutas de acceso tienen implícitamente el prefijo del
del espacio de trabajo.
...
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 usan symlinks
o root_symlinks
, ten cuidado de no asignar dos
archivos en la misma ruta del árbol de archivos de ejecución. Esto hará que la compilación falle
con un error que describe el conflicto. Para solucionarlo, debes modificar tu
Argumentos ctx.runfiles
para quitar la colisión. Esta verificación se hará
todos los destinos que usan su regla, así como los objetivos de cualquier tipo que dependan de esos
objetivos. Esto es especialmente riesgoso si es probable que la herramienta se use de forma transitiva
por otra herramienta Los nombres de symlink deben ser únicos en todos los archivos de ejecución de una herramienta.
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 objetivos. El
build también reúne la lista de archivos de origen que se instrumentaron. El subconjunto de
objetivos que se consideran controlados por la marca
--instrumentation_filter
Se excluyen los destinos de prueba, a menos que
--instrument_test_targets
una regla de firewall.
Si una implementación de reglas agrega instrumentación de cobertura en el tiempo de compilación, necesita para tenerlo en cuenta en su función de implementación. ctx.coverage_instrumented devuelve 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
Lógica que siempre debe estar activada en el modo de cobertura (ya sean las fuentes de un objetivo si se instrumentan o no) se pueden condicionar ctx.configuration.coverage_enabled.
Si la regla incluye directamente fuentes de sus dependencias antes de la compilación (como archivos de encabezado), quizás también debas activar la instrumentación del tiempo de compilación si las dependencias se deben instrumentar las siguientes fuentes:
# 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 los atributos que son relevantes para
con el proveedor de InstrumentedFilesInfo
, construida con
coverage_common.instrumented_files_info
Se debe mostrar el parámetro dependency_attributes
de instrumented_files_info
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
atributos de los archivos de origen de la regla si se puede agregar 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 se establece
cfg
a "host"
o "exec"
en el esquema del atributo) en
dependency_attributes
(No es un comportamiento ideal, ya que coloca atributos
como srcs
en dependency_attributes
en lugar de source_attributes
, pero
evita la necesidad de una configuración de cobertura explícita para todas las reglas en la
la cadena de dependencia).
Acciones de validación
A veces, es necesario validar algo sobre la compilación, y la información necesaria para realizar la validación solo esté disponible en los artefactos (archivos de origen o generados). Debido a que esta información se encuentra en artefactos, las reglas no pueden realizar esta validación en el momento del análisis porque no pueden leerlas archivos. En cambio, las acciones deben hacer esta validación en el momento de la ejecución. Cuándo la validación falla, la acción fallará y, por lo tanto, también lo hará la compilación.
Algunos ejemplos de validaciones que se pueden ejecutar son el análisis estático, el análisis con lint, de dependencias y coherencia, y de estilo.
Las acciones de validación también pueden ayudar a mejorar el rendimiento de la compilación con partes móviles de acciones que no se requieren para compilar artefactos en acciones separadas. Por ejemplo, si una sola acción que realiza compilación y análisis con lint puede en una acción de compilación y una acción de análisis con lint, acción se puede ejecutar como una acción de validación y ejecutarse en paralelo con otras acciones.
Estas “acciones de validación” a menudo no producen nada que se use en otro lugar en la compilación, ya que solo necesitan realizar aserciones sobre sus entradas. Esta pero presenta un problema: si una acción de validación no produce nada que en otra parte de la compilación, ¿cómo hace una regla para que la acción se ejecute? Históricamente, el enfoque consistía en hacer que el resultado de la acción de validación fuera y agregar artificialmente ese resultado a las entradas de alguna otra acción en la compilación:
Esto funciona, ya que Bazel siempre ejecutará la acción de validación cuando se ejecute la compilación. una acción, pero esto tiene importantes desventajas:
La acción de validación se encuentra en la ruta crítica de la compilación. Porque Bazel piensa que se requiere un resultado vacío para ejecutar la acción de compilación, ejecutará el acción de validación primero, aunque la acción de compilación ignorará 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 acción de compilación, se deben agregar los resultados vacíos de las acciones de validación también esas acciones (por ejemplo, el resultado del archivo jar de origen de
java_library
). Este es Esto también es un problema si se ejecutan acciones nuevas en lugar de la acción de compilación agregar más tarde, y el resultado de validación vacío se deja accidentalmente.
La solución a estos problemas es usar el grupo de salida de validaciones.
Grupo de salida de validaciones
El grupo de salida de validaciones es un grupo de salida diseñado para contener el resultados sin usar de acciones de validación, de modo que no tengan que ser se agregan a las entradas de otras acciones.
Este grupo es especial porque sus salidas siempre se solicitan, independientemente de
el valor de la marca --output_groups
, sin importar cómo se encuentre
de las que se depende (por ejemplo, en la línea de comandos, como una dependencia o a través de
resultados implícitos del objetivo). Ten en cuenta que el almacenamiento en caché y la incrementalidad
aún se aplican: si las entradas a la acción de validación no cambiaron y el
acción de validación se realizó correctamente, entonces la acción de validación no se
cuando se ejecute.
El uso de este grupo de salida aún requiere que las acciones de validación generen un archivo, incluso si está vacía. Esto podría requerir envolver algunas herramientas que normalmente no crear resultados para que se cree un archivo.
Las acciones de validación de un destino no se ejecutan en tres casos:
- Cuando se depende del destino como herramienta
- Cuando se depende del objetivo como una dependencia implícita (por ejemplo, un que comienza con “_”)
- Cuando el destino se compila en la configuración de host o de ejecución.
Se supone que estos objetivos tienen sus compilaciones y pruebas independientes que descubrirían cualquier falla de validación.
Cómo usar el grupo de salida de validaciones
El grupo de salida de validaciones se llama _validation
y se usa como cualquier otro.
grupo de salida:
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 a DefaultInfo
ni al
entradas a cualquier otra acción. La acción de validación para un destino de este tipo de regla
se ejecutará si el destino depende de una etiqueta, o cualquiera de
de las salidas implícitas directa o indirectamente.
Por lo general, es importante que los resultados de las acciones de validación solo vayan al de salida de validación y no se agregan a las entradas de otras acciones, esto podría vencer las ganancias del paralelismo. Sin embargo, ten en cuenta que Bazel actualmente no realizar una 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 el y pruebas para 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 ejecución de acciones de validación se controla con la línea de comandos de --run_validations
su valor predeterminado es verdadero.
Funciones obsoletas
Los resultados declarados previamente no están disponibles
Existen dos formas obsoletas de usar resultados declarados previamente:
El parámetro
outputs
derule
especifica una asignación entre nombres de atributos de salida y plantillas de cadenas para generar etiquetas de salida declaradas previamente. Prefiere usar resultados no declarados previamente y agregar explícitamente resultados aDefaultInfo.files
Usa el parámetro de configuración del etiqueta como entrada para las reglas que consumen la salida en lugar de una declaración de salida.Para las reglas ejecutables,
ctx.outputs.executable
hace referencia a en un resultado ejecutable declarado previamente con el mismo nombre que el objetivo de la regla. Es preferible declarar la salida de forma explícita, por ejemplo, conctx.actions.declare_file(ctx.label.name)
y asegúrate de que el comando que genera el ejecutable, que configura sus permisos para permitir la ejecución. Explícitamente Pasa el resultado ejecutable al parámetroexecutable
deDefaultInfo
.
Funciones de Runfiles que se deben evitar
ctx.runfiles
y runfiles
tienen un conjunto complejo de atributos, muchos de los 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 implícitamente runfiles en ciertos bordes codificados de las dependencias de maneras confusas. En su lugar, agrega archivos con los parámetrosfiles
otransitive_files
dectx.runfiles
o combinando archivos de ejecución desde dependencias conrunfiles = runfiles.merge(dep[DefaultInfo].default_runfiles)
Evita el uso de
data_runfiles
ydefault_runfiles
de las Es el constructorDefaultInfo
. EspecificaDefaultInfo(runfiles = ...)
en su lugar. La distinción entre “predeterminado” y "datos" runfiles se mantienen por motivos heredados. Por ejemplo, algunas reglas ponen sus resultados predeterminados endata_runfiles
, pero nodefault_runfiles
. En lugar de usardata_runfiles
, las reglas ambas deberían incluir resultados predeterminados y combinarsedefault_runfiles
de atributos que proporcionan archivos de ejecución (a menudo,data
).Cuando se recupera
runfiles
desdeDefaultInfo
(generalmente, solo para combinaciones) runfiles entre la regla actual y sus dependencias), usaDefaultInfo.default_runfiles
, noDefaultInfo.data_runfiles
.
Migra desde proveedores heredados
Históricamente, los proveedores de Bazel eran campos simples en el objeto Target
. Ellas
se accedió con el operador de punto y se crearon colocando el campo
en un struct devuelto por la función de implementación de la regla.
Este estilo dejó de estar disponible y no se debería usar en código nuevo; consulta a continuación información que podría ayudarte a migrar. El nuevo mecanismo de proveedor evita el uso conflictos. También admite la ocultación de datos, ya que requiere que cualquier código que acceda a un proveedor para recuperarla con el símbolo de proveedor.
Por el momento, se siguen admitiendo los proveedores heredados. Una regla puede mostrar ambos 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, el
proveedores y su contenido se puede recuperar como dep.legacy_info.x
y
dep[MyInfo].y
Además de providers
, el struct que se muestra también puede tomar otras
campos que tienen un significado especial (y, por lo tanto, no crean una biblioteca
proveedor):
Los campos
files
,runfiles
,data_runfiles
,default_runfiles
yexecutable
corresponden a los campos con el mismo nombre 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 a unOutputGroupInfo
En las declaraciones de reglas provides
y en
Declaraciones de dependencia providers
atributos, los proveedores heredados se pasan como cadenas y los proveedores modernos
y se pasa por su símbolo *Info
. Asegúrate de pasar de cadenas a símbolos
durante la migración. Para conjuntos de reglas complejos o grandes en los que es difícil actualizar
reglas de manera atómica, tal vez te resulte más fácil si sigues esta secuencia de
pasos:
Modifica las reglas que producen el proveedor heredado para producir el proveedor heredado y proveedores modernos con la sintaxis anterior. Para las reglas que declaran devuelve el proveedor heredado, actualiza esa declaración para incluir el proveedores heredados y modernos.
Modifica las reglas que consumen el proveedor heredado de modo que, en su lugar, consuman el más reciente. Si alguna declaración de atributo requiere el proveedor heredado, actualizarlas para que requieran el proveedor moderno en su lugar. De manera opcional, puedes intercalar este trabajo con el paso 1 pidiéndoles a los consumidores que acepten o exijan proveedor: Prueba la presencia del proveedor heredado usando
hasattr(target, 'foo')
o el proveedor nuevo conFooInfo in target
.Quita por completo el proveedor heredado de todas las reglas.