Instructivo de reglas

Informar un problema Ver fuente

Starlark es un lenguaje de configuración similar a Python que originalmente se desarrolló para su uso en Bazel y que lo adoptaron otras herramientas desde entonces. Los archivos BUILD y .bzl de Bazel están escritos en un dialecto de Starlark conocido como "lenguaje de compilación", aunque a menudo se denomina simplemente "Starlark", especialmente cuando se enfatiza que una función se expresa en el lenguaje de compilación, en lugar de ser una parte integrada o "nativa". 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, deberás 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 destino.

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)

Aunque la regla no realiza ninguna acción, 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 sentencias de impresión:

def _foo_binary_impl(ctx):
    print("analyzing", ctx.label)

foo_binary = rule(
    implementation = _foo_binary_impl,
)

print("bzl file evaluation")

y DESARROLLO:

load(":foo.bzl", "foo_binary")

print("BUILD file")
foo_binary(name = "bin1")
foo_binary(name = "bin2")

ctx.label corresponde a la etiqueta del destino 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 muestra "bzl file Assessment". Antes de evaluar el archivo BUILD, Bazel evalúa todos los archivos que carga. Si varios archivos BUILD cargan foo.bzl, solo verás un caso de “evaluación de archivo 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 archivos BUILD, pero no analiza los destinos.

Para analizar los destinos, usa el comando cquery ("consulta configurada") o build:

$ bazel build :all
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 destino.

Ten en cuenta que no se vuelven a mostrar los campos "bzl file Assessment" ni "BUILD file", ya que la evaluación de foo.bzl se almacena en caché después de la llamada a bazel query. Bazel solo emite sentencias print cuando se ejecutan.

Cómo crear un archivo

Si quieres que tu regla sea más útil, actualízala para generar un archivo. Primero, declara el archivo y asígnale un nombre. En este ejemplo, crea un archivo con el mismo nombre que el destino:

ctx.actions.declare_file(ctx.label.name)

Si ejecutas bazel build :all ahora, recibirás un error:

The following files have no generating action:
bin2

Cada vez que declares un archivo, debes indicarle a Bazel cómo generarlo mediante la creación de 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, que le enseñó a Bazel cómo generar el archivo. Sin embargo, Bazel no creará el archivo hasta que se solicite. Por lo tanto, lo último que debes hacer es indicarle 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 reglas.

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]))]

Observa las funciones DefaultInfo y depset más adelante. Por ahora, supongamos 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(),
    },
)

A continuación, configúralo 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 boolean o lista de números enteros.

Dependencias

Los atributos de dependencia, como attr.label y attr.label_list, declaran una dependencia del destino que posee el atributo al destino cuya etiqueta aparece en el valor del atributo. Este tipo de atributo constituye la base del gráfico objetivo.

En el archivo BUILD, la etiqueta de destino aparece como un objeto de cadena, por ejemplo, //pkg:name. En la función de implementación, se podrá acceder al destino como un objeto Target. Por ejemplo, puedes ver los archivos que muestra el destino con Target.files.

Varios archivos

De forma predeterminada, solo los destinos creados por reglas pueden aparecer como dependencias (por ejemplo, un objetivo foo_library()). Si deseas que el atributo acepte objetivos que son 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 en el atributo srcs mediante

ctx.files.srcs

Archivo único

Si solo necesitas un archivo, usa allow_single_file:

"src": attr.label(allow_single_file = [".java"])

Luego, se puede 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 string 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, se vuelve más eficiente en cuanto a la memoria ponerla 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 deseas exponer la plantilla al usuario final y usar siempre la misma, puedes configurar 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. Ahora, la plantilla es una dependencia implícita: cada destino hello_world tiene una dependencia de este archivo. No olvides hacer que este archivo sea visible para otros paquetes actualizando el archivo BUILD y usando exports_files:

exports_files(["file.cc.tpl"])

Un paso más allá