Starlark es un lenguaje de configuración similar a Python desarrollado originalmente para su uso en Bazel y desde que lo adoptaron otras herramientas. Los archivos BUILD
y .bzl
de Bazel están escritos en un dialecto de Starlark, bien conocido como el "lenguaje de compilación", aunque a menudo se lo conoce como "Starlark", especialmente cuando se hace hincapié en que una función se expresa en el lenguaje de compilación en lugar de ser una parte integrada o "nativa" de Bazel. Bazel aumenta el lenguaje principal con varias funciones relacionadas con la compilación, como glob
, genrule
, java_binary
, etcétera.
Consulta la documentación de Bazel y Starlark para obtener más detalles y la plantilla de SIG de reglas como punto de partida para nuevos conjuntos de reglas.
La regla vacía
Para crear tu primera regla, crea el archivo foo.bzl
:
def _foo_binary_impl(ctx):
pass
foo_binary = rule(
implementation = _foo_binary_impl,
)
Cuando llames a la función rule
, debes definir una función de devolución de llamada. La lógica irá allí, pero puedes dejar la función vacía por ahora. El argumento ctx
proporciona información sobre el objetivo.
Puedes cargar la regla y usarla desde un archivo BUILD
.
Crea un archivo BUILD
en el mismo directorio:
load(":foo.bzl", "foo_binary")
foo_binary(name = "bin")
Ahora, se puede compilar el destino:
$ bazel build bin
INFO: Analyzed target //:bin (2 packages loaded, 17 targets configured).
INFO: Found 1 target...
Target //:bin up-to-date (nothing to build)
Si bien la regla no hace nada, ya se comporta como otras reglas: tiene un nombre obligatorio y admite atributos comunes como visibility
, testonly
y tags
.
Modelo de evaluación
Antes de continuar, es importante comprender cómo se evalúa el código.
Actualiza foo.bzl
con algunas instrucciones de impresión:
def _foo_binary_impl(ctx):
print("analyzing", ctx.label)
foo_binary = rule(
implementation = _foo_binary_impl,
)
print("bzl file evaluation")
y BUILD:
load(":foo.bzl", "foo_binary")
print("BUILD file")
foo_binary(name = "bin1")
foo_binary(name = "bin2")
ctx.label
corresponde a la etiqueta del objetivo que se analiza. El objeto ctx
tiene muchos campos y métodos útiles. Puedes encontrar una lista completa en la referencia de la API.
Consulta el código:
$ bazel query :all
DEBUG: /usr/home/bazel-codelab/foo.bzl:8:1: bzl file evaluation
DEBUG: /usr/home/bazel-codelab/BUILD:2:1: BUILD file
//:bin2
//:bin1
Haz algunas observaciones:
- Primero, se imprime "bzl file evaluation". Antes de evaluar el archivo
BUILD
, Bazel evalúa todos los archivos que carga. Si se cargan varios archivosBUILD
foo.bzl, solo verás una ocurrencia de “evaluación de archivos bzl” porque Bazel almacena en caché el resultado de la evaluación. - No se llama a la función de devolución de llamada
_foo_binary_impl
. La consulta de Bazel carga archivosBUILD
, pero no analiza los destinos.
Para analizar los objetivos, usa cquery
("búsqueda configurada") o el comando build
:
$ bazel build :all
DEBUG: /usr/home/bazel-codelab/foo.bzl:8:1: bzl file evaluation
DEBUG: /usr/home/bazel-codelab/BUILD:2:1: BUILD file
DEBUG: /usr/home/bazel-codelab/foo.bzl:2:5: analyzing //:bin1
DEBUG: /usr/home/bazel-codelab/foo.bzl:2:5: analyzing //:bin2
INFO: Analyzed 2 targets (0 packages loaded, 0 targets configured).
INFO: Found 2 targets...
Como puedes ver, ahora se llama a _foo_binary_impl
dos veces, una para cada objetivo.
Algunos lectores notarán que se vuelve a imprimir "evaluación de archivo bzl", aunque la evaluación de foo.bzl se almacena en caché después de la llamada a bazel query
. Bazel
no vuelve a evaluar el código, solo vuelve a reproducir los eventos de impresión. Sin importar el estado de la caché, obtienes el mismo resultado.
Cómo crear un archivo
Para que tu regla sea más útil, actualízala para que genere un archivo. Primero, declara el archivo y asígnale un nombre. En este ejemplo, crea un archivo con el mismo nombre que el objetivo:
ctx.actions.declare_file(ctx.label.name)
Si ejecutas bazel build :all
ahora, verás un error:
The following files have no generating action:
bin2
Cada vez que declaras un archivo, debes decirle a Bazel cómo generarlo. Para ello, debes crear una acción. Usa ctx.actions.write
para crear un archivo con el contenido determinado.
def _foo_binary_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name)
ctx.actions.write(
output = out,
content = "Hello\n",
)
El código es válido, pero no hará nada:
$ bazel build bin1
Target //:bin1 up-to-date (nothing to build)
La función ctx.actions.write
registró una acción, lo que le enseñó a Bazel
a generar el archivo. Sin embargo, Bazel no creará el archivo hasta que se solicite. Por lo tanto, lo último que debes hacer es decirle a Bazel que el archivo
es un resultado de la regla y no un archivo temporal que se usa dentro de la implementación
de la regla.
def _foo_binary_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name)
ctx.actions.write(
output = out,
content = "Hello!\n",
)
return [DefaultInfo(files = depset([out]))]
Más adelante, observa las funciones DefaultInfo
y depset
. Por ahora, asumamos que la última línea es la forma de elegir los resultados de una regla.
Ahora, ejecuta Bazel:
$ bazel build bin1
INFO: Found 1 target...
Target //:bin1 up-to-date:
bazel-bin/bin1
$ cat bazel-bin/bin1
Hello!
Generaste un archivo correctamente.
Atributos
Para que la regla sea más útil, agrega atributos nuevos con el módulo attr
y actualiza la definición de la regla.
Agrega un atributo de cadena llamado username
:
foo_binary = rule(
implementation = _foo_binary_impl,
attrs = {
"username": attr.string(),
},
)
Luego, configúrala en el archivo BUILD
:
foo_binary(
name = "bin",
username = "Alice",
)
Para acceder al valor en la función de devolución de llamada, usa ctx.attr.username
. Por ejemplo:
def _foo_binary_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name)
ctx.actions.write(
output = out,
content = "Hello {}!\n".format(ctx.attr.username),
)
return [DefaultInfo(files = depset([out]))]
Ten en cuenta que puedes hacer que el atributo sea obligatorio o establecer un valor predeterminado. Consulta la documentación de attr.string
.
También puedes usar otros tipos de atributos, como booleano o lista de números enteros.
Dependencias
Los atributos de dependencia, como attr.label
y attr.label_list
, declaran una dependencia del objetivo al que pertenece el atributo al objetivo cuya etiqueta aparece en el valor del atributo. Este tipo de atributo conforma
la base del gráfico de destino.
En el archivo BUILD
, la etiqueta de destino aparece como un objeto de cadena, como //pkg:name
. En la función de implementación, se podrá acceder al objetivo como un objeto Target
. Por ejemplo, usa Target.files
para ver los archivos que muestra el destino.
Varios archivos
De forma predeterminada, solo los destinos creados por reglas pueden aparecer como dependencias (como un destino foo_library()
). Si deseas que el atributo acepte destinos que sean
archivos de entrada (como archivos de origen en el repositorio), puedes hacerlo con
allow_files
y especificar la lista de extensiones de archivo aceptadas (o True
para permitir cualquier extensión de archivo):
"srcs": attr.label_list(allow_files = [".java"]),
Se puede acceder a la lista de archivos con ctx.files.<attribute name>
. Por
ejemplo, se puede acceder a la lista de archivos del atributo srcs
a través de
ctx.files.srcs
Archivo único
Si solo necesitas un archivo, usa allow_single_file
:
"src": attr.label(allow_single_file = [".java"])
Luego, puedes acceder a este archivo en ctx.file.<attribute name>
:
ctx.file.src
Crea un archivo con una plantilla
Puedes crear una regla que genere un archivo .cc basado en una plantilla. Además, puedes
usar ctx.actions.write
para generar una cadena construida en la función
de implementación de reglas, pero esto tiene dos problemas. En primer lugar, a medida que la plantilla se hace más grande, es más eficiente en términos de memoria colocarla en un archivo separado y evitar la construcción de cadenas grandes durante la fase de análisis. En segundo lugar, usar un archivo separado es más conveniente para el usuario. En su lugar, usa
ctx.actions.expand_template
,
que realiza sustituciones en un archivo de plantilla.
Crea un atributo template
para declarar una dependencia en el archivo de plantilla:
def _hello_world_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name + ".cc")
ctx.actions.expand_template(
output = out,
template = ctx.file.template,
substitutions = {"{NAME}": ctx.attr.username},
)
return [DefaultInfo(files = depset([out]))]
hello_world = rule(
implementation = _hello_world_impl,
attrs = {
"username": attr.string(default = "unknown person"),
"template": attr.label(
allow_single_file = [".cc.tpl"],
mandatory = True,
),
},
)
Los usuarios pueden usar la regla de la siguiente manera:
hello_world(
name = "hello",
username = "Alice",
template = "file.cc.tpl",
)
cc_binary(
name = "hello_bin",
srcs = [":hello"],
)
Si no quieres exponer la plantilla al usuario final y siempre usar la misma, puedes establecer un valor predeterminado y hacer que el atributo sea privado:
"_template": attr.label(
allow_single_file = True,
default = "file.cc.tpl",
),
Los atributos que comienzan con un guion bajo son privados y no se pueden configurar en un archivo BUILD
. La plantilla ahora es una dependencia implícita: cada objetivo hello_world
tiene una dependencia en este archivo. No olvides hacer que este archivo sea visible para otros paquetes. Para ello, actualiza el archivo BUILD
y usa exports_files
:
exports_files(["file.cc.tpl"])
Más información
- Consulta la documentación de referencia de las reglas.
- Familiarízate con los depsets.
- Consulta el repositorio de ejemplos, que incluye ejemplos adicionales de reglas.