Reglas

Informar un problema Ver fuente

Una regla define una serie de acciones que Bazel realiza en entradas para producir un conjunto de salidas, a las que se hace referencia en los proveedores que muestra la función de implementación de la regla. Por ejemplo, una regla binaria de C++ podría hacer lo siguiente:

  1. Toma un conjunto de archivos de origen .cpp (entradas).
  2. Ejecuta g++ en los archivos de origen (acción).
  3. Muestra el proveedor DefaultInfo con el resultado ejecutable y otros archivos para que estén disponibles en el entorno de ejecución.
  4. 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 de C++ estándar también son entradas a esta regla. Como escritor de reglas, debes considerar no solo las entradas proporcionadas por el usuario a una regla, sino también todas las herramientas y bibliotecas necesarias para ejecutar las acciones.

Antes de crear o modificar cualquier 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, primero revisa el Instructivo sobre reglas. Luego, usa esta página como referencia.

Bazel cuenta con algunas reglas integradas. Estas reglas nativas, como cc_library y java_binary, proporcionan compatibilidad principal con ciertos lenguajes. 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 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 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 necesarios.

Creación de reglas

En un archivo .bzl, usa la función rule para definir una regla 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 denominada 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 es el último, la regla es una regla de prueba y su nombre 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 destino. Esto se conoce como crear una instancia de la regla. Esto especifica un nombre para el nuevo objetivo y valores para los atributos del objetivo.

También se puede llamar a las reglas desde las funciones de Starlark y cargarse en archivos .bzl. Las funciones de Starlark que reglas de llamada se denominan macros de Starlark. En última instancia, se debe llamar a las macros de Starlark desde los archivos BUILD, y solo se puede llamar durante la fase de carga, cuando se evalúan los archivos BUILD para crear una instancia de objetivos.

Atributos

Un atributo es un argumento de una regla. Los atributos pueden proporcionar valores específicos a la implementación de un destino o pueden hacer referencia a otros destinos, lo que crea un gráfico de dependencias.

Los atributos específicos de la regla, como srcs o deps, se definen pasando un mapa de nombres de atributos a esquemas (creado 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 forma específica. Los atributos que se agregan de forma implícita a una regla no se pueden incluir en el diccionario que se pasa a attrs.

Atributos de dependencia

Por lo general, las reglas que procesan código fuente definen los siguientes atributos para controlar varios tipos de dependencias:

  • srcs especifica los archivos de origen que procesan las acciones de un destino. A menudo, el esquema del atributo especifica qué extensiones de archivo se esperan para el tipo de archivo de origen que procesa la regla. Por lo general, las reglas de idiomas con archivos de encabezado especifican un atributo hdrs independiente para los encabezados que procesan 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 proporciona CcInfo).
  • 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 especifica una etiqueta de entrada (los que se definen con attr.label_list, attr.label o attr.label_keyed_string_dict) especifica las dependencias de un cierto tipo entre un destino y los objetivos cuyas etiquetas (o los objetos Label correspondientes) se enumeran en ese atributo cuando se define el destino. El repositorio y posiblemente la ruta de acceso de estas etiquetas se resuelven 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 dependencias 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 una parte del gráfico 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 de tiempo de compilación, como un compilador), ya que la mayoría de las veces un usuario no está interesado en especificar qué herramienta usa la regla. Dentro de la función de implementación de la regla, esto se trata de la misma manera que otras dependencias.

Si deseas proporcionar una dependencia implícita sin permitir que el usuario anule ese valor, puedes hacer que el atributo sea private. Para ello, asígnale un nombre que comience con un guion bajo (_). Los atributos privados deben tener valores predeterminados. Por lo general, solo tiene sentido usar atributos privados para las 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 al compilador, aunque el usuario no haya pasado su etiqueta como entrada. Como _compiler es un atributo privado, se desprende que ctx.attr._compiler siempre apuntará a //tools:example_compiler en todos los destinos de este tipo de regla. Como alternativa, puedes asignar el nombre compiler al atributo 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 debe 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. Estas difieren de los atributos de dependencia de dos maneras:

  • Definen los objetivos del archivo de salida en lugar de hacer referencia a los destinos definidos en otra parte.
  • Los objetivos del archivo de salida dependen del objetivo de la regla de la que se haya creado 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 se pueden basar en el nombre del 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 resultados declarados con anterioridad, que se pueden solicitar en la línea de comandos o depender específicamente de ellos.

Función de implementación

Cada regla requiere una función implementation. Estas funciones se ejecutan estrictamente en la fase de análisis y transforman el gráfico de objetivos generados en la fase de carga en uno de acciones que se realizarán durante la fase de ejecución. Por lo tanto, las funciones de implementación no pueden leer ni escribir archivos.

Las funciones de implementación de reglas suelen ser privadas (se les asigna un nombre con un guion bajo inicial). Convencionalmente, tienen el mismo nombre que su regla, pero tienen el sufijo _impl.

Las funciones de implementación toman exactamente un parámetro: un contexto de la regla, llamado ctx de forma convencional. Muestran una lista de proveedores.

Destinos

En el momento del análisis, las dependencias se representan como objetos Target. Estos objetos contienen los proveedores generados 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 contienen objetos Target que representan cada dependencia directa a través de ese atributo. Para los atributos label_list, 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 de proveedor:

return [ExampleInfo(headers = depset(...))]

Se puede acceder a ellos con la notación de índices ([]), con el tipo de proveedor como clave. Estos pueden ser proveedores personalizados definidos en Starlark o proveedores para reglas nativas disponibles como variables globales de Starlark.

Por ejemplo, si una regla toma archivos de encabezado mediante 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 destino en lugar de una lista de objetos de 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 las reglas deben migrarse de él.

Files

Los archivos se representan con objetos File. Dado que Bazel no realiza operaciones de E/S de archivos durante la fase de análisis, estos objetos no se pueden usar para leer ni escribir directamente el contenido del archivo. En cambio, se pasan a funciones que emiten acciones (consulta ctx.actions) para construir partes del gráfico 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 File o None para los 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.

Declara resultados

Durante la fase de análisis, la función de implementación de una regla puede crear resultados. Dado que se deben conocer todas las etiquetas durante la fase de carga, estos resultados adicionales no tienen etiquetas. Los objetos File para los 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 a partir de los campos correspondientes de ctx.outputs.

Acciones

Una acción describe cómo generar un conjunto de resultados 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, 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.args puede usarse para acumular de manera eficiente los argumentos para las acciones. Evita acoplar 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 depset de archivos de entrada y generan una lista (no vacía) de archivos de salida. El conjunto de archivos de entrada y salida debe conocerse durante la fase de análisis. Puede depender del valor de los atributos, incluidos los proveedores de las dependencias, pero no puede depender del resultado de la ejecución. Por ejemplo, si la 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 una cantidad variable de archivos internamente pueden unirlos en un solo archivo (como zip, tar o algún otro formato de archivo).

Las acciones deben enumerar todas sus entradas. Se permite enumerar las entradas que no se usan, pero es ineficiente.

Las acciones deben crear todos sus resultados. Pueden escribir otros archivos, pero lo que no esté en los resultados no estará disponible para los consumidores. Todos los resultados declarados deben escribirse con alguna acción.

Las acciones son comparables con las funciones puras: deben depender solo de las entradas proporcionadas y evitar acceder a información de la computadora, nombre de usuario, reloj, red o dispositivos de E/S (excepto para leer entradas y escribir salidas). Esto es importante porque el resultado se almacenará en caché y se volverá a usar.

Bazel resuelve las dependencias, y decidirá qué acciones se ejecutan. Se generará un error si hay un ciclo en el gráfico de dependencia. Crear una acción no garantiza que se ejecutará, ya que depende de si sus resultados son necesarios para la compilación.

Proveedores

Los proveedores son piezas de información que una regla expone a otras reglas que dependen de ella. Estos datos pueden incluir archivos de resultado, bibliotecas, parámetros para pasar la línea de comandos de una herramienta o cualquier otra información que los consumidores de destino deban saber.

Dado que la función de implementación de una regla solo puede leer proveedores de las dependencias inmediatas del destino con la que se creó la instancia, las reglas deben reenviar cualquier información de las dependencias de destino que los consumidores de un destino deben conocer, por lo general, acumulándola en un depset.

Los proveedores de un destino se especifican mediante 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 diseño heredado, en el que la función de implementación muestra un struct en lugar de una lista de objetos de proveedor. No se recomienda este estilo, y las reglas deben migrarse de él.

Resultados predeterminados

Los resultados predeterminados de un destino son aquellos que se solicitan de forma predeterminada cuando se solicita la compilación del destino en la línea de comandos. Por ejemplo, un //pkg:foo de destino java_library tiene foo.jar como resultado predeterminado, de modo que el comando bazel build //pkg:foo lo compilará.

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 no muestra DefaultInfo o no se especifica el parámetro files, DefaultInfo.files se establece de forma predeterminada en todas las salidas declaradas previamente (en general, los creados por atributos de salida).

Las reglas que realizan acciones deben proporcionar resultados predeterminados, incluso si no se espera que esos resultados se usen directamente. Se reducen las acciones que no están en el grafo de los resultados solicitados. Si solo los consumidores de un destino usan un resultado, esas acciones no se realizarán cuando el destino se compile de forma aislada. Esto dificulta la depuración, ya que volver a compilar solo el destino con errores no reproducirá la falla.

Archivos de ejecución

Los archivos de ejecución son un conjunto de archivos que usa un destino durante 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. Esto habilita a etapa el entorno 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 de forma manual durante la creación de reglas. 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 archivos de ejecución de un destino. Los archivos de ejecución también deben combinarse desde data, así como desde cualquier atributo que pueda proporcionar código para una ejecución eventual, generalmente srcs (que puede contener destinos filegroup con data asociado) 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 crear y mostrar instancias del 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 una lógica de validación y procesamiento previo personalizada. Esto se puede usar a fin de garantizar que todas las instancias del proveedor obedezcan determinadas variantes, o bien para proporcionar a los usuarios una API más limpia para obtener una instancia.

Para ello, se pasa una devolución de llamada init a la función provider. Si se realiza esta devolución de llamada, el tipo de datos que se muestra de provider() cambia a una tupla de dos valores: el símbolo del proveedor que es el valor común que se muestra 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 a la devolución de llamada init. El valor que se muestra de la devolución de llamada debe ser un dict que asigna nombres de campo (strings) a valores. Se usa para inicializar los campos de la nueva instancia. Ten en cuenta que la devolución de llamada puede tener alguna firma y, si los argumentos no coinciden con la firma, se informa un error como si la devolución de llamada se hubiera invocado directamente.

Por el contrario, el constructor sin procesar omitirá la devolución de llamada init.

En el siguiente ejemplo, se usa init para el procesamiento previo y la validación de 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

Luego, la implementación de una regla 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 pasan por la lógica 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 del usuario no puede cargarlo y generar instancias arbitrarias de proveedores.

Otro uso para init es simplemente evitar que el usuario llame al símbolo del proveedor por completo 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 destinos que se pueden invocar con un comando bazel run. Las reglas de prueba son un tipo especial de regla ejecutable cuyos destinos también se pueden invocar con un comando bazel test. Para crear reglas ejecutables y de prueba, configura el argumento executable o test respectivo 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 los destinos 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 estar predeclarado o no) que será invocado por los comandos run o test. Para indicarle a Bazel cuál de los resultados de una regla se usará como este ejecutable, pásalo como el argumento executable de un proveedor DefaultInfo que se muestra. Ese 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. En el caso de 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 ctx.outputs.executable especial declarado previamente. Este archivo sirve como el 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 agregados para todas las reglas. No se pueden cambiar los valores predeterminados de los atributos agregados implícitamente, pero se puede evitar uniendo una regla privada en una macro de Starlark que modifique el valor 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 del directorio de 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 de acceso a un File en el directorio runfiles corresponde a File.short_path.

El objeto binario que ejecuta bazel es adyacente a la raíz del directorio runfiles. Sin embargo, los objetos binarios llamados desde los archivos de ejecución no pueden suponer lo mismo. Para mitigar este problema, cada objeto binario debe proporcionar una forma de aceptar la raíz de los archivos de ejecución como un parámetro mediante un entorno o un argumento o una marca de línea de comandos. Esto permite que los objetos binarios pasen la raíz de los archivos de ejecución canónicos correctos a los objetos binarios a los que llama. Si no se establece, un objeto binario puede adivinar que fue el primero al que se llamó y buscar un directorio de 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 destinos asignados al comando se solicitaron. Bazel solo compila estos archivos solicitados y los archivos de los que dependen directa o indirectamente. (En términos del gráfico de acciones, Bazel solo ejecuta las acciones que son accesibles 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 salidas declaradas previamente mediante los atributos de salida. En ese caso, el usuario elige de forma explícita etiquetas para los resultados cuando crea una instancia de la regla. Si deseas obtener objetos File para los atributos de salida, usa el atributo correspondiente de ctx.outputs. Las reglas también pueden definir implícitamente los resultados declarados con anterioridad en función del nombre del destino, pero esta función dejó de estar disponible.

Además de las salidas predeterminadas, existen grupos de salida, que son colecciones de archivos de salida que se pueden solicitar juntos. Se pueden 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, se pueden compilar estos archivos mediante la ejecución de bazel build //pkg:mytarget --output_groups=debug_files. Debido a que las salidas no declaradas previamente no tienen etiquetas, solo se pueden solicitar si aparecen en las salidas predeterminadas o en un grupo de salidas.

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 al que se aplica ese aspecto pueden mostrar OutputGroupInfo, siempre que no definan los mismos grupos de salida. En ese caso, se combinan los proveedores resultantes.

Ten en cuenta que, por lo general, OutputGroupInfo no debe usarse para transmitir tipos específicos de archivos de un objetivo a las acciones de sus consumidores. Define proveedores específicos de reglas para eso.

Parámetros de configuración

Imagina que deseas compilar un objeto binario de C++ para una arquitectura diferente. La compilación puede ser compleja y requerir varios pasos. Algunos de los objetos binarios intermedios, como los compiladores y los generadores de código, deben ejecutarse en 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 "configuración" y transiciones. Los objetivos principales (los solicitados en la línea de comandos) se compilan en la configuración de "destino", mientras que las herramientas que deben ejecutarse en la plataforma de ejecución se compilan en una configuración de "ejecución". Las reglas pueden generar diferentes acciones según la configuración, por ejemplo, para cambiar la arquitectura de la CPU que se pasa al compilador. En algunos casos, es posible que se necesite la misma biblioteca para distintas configuraciones. Si esto sucede, se analizará y, posiblemente, se compilará varias veces.

De forma predeterminada, Bazel compila las dependencias de un destino en la misma configuración que este, es decir, sin transiciones. Cuando una dependencia es una herramienta que se necesita para ayudar a 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 para decidir si las dependencias deben compilarse en la misma configuración o hacer una transición a una configuración de ejecución. Si un atributo de dependencia tiene la marca executable=True, cfg debe configurarse de forma explícita. Esto permite evitar la compilación accidental de una herramienta con 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.

cfg="target" en realidad no hace nada: solo es un valor conveniente para ayudar a los diseñadores de reglas a ser explícitos sobre sus intenciones. Cuando sea executable=False, que significa que cfg es opcional, solo configúralo cuando realmente ayude a mejorar la legibilidad.

También puedes usar cfg=my_transition para usar transiciones definidas por el usuario, que permiten a los autores de reglas una gran flexibilidad cuando cambian las 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, en su lugar, se consideraba que todas las acciones de compilación se ejecutaban en la máquina anfitrión. Las versiones de Bazel anteriores a la 6.0 crearon una configuración de "host" distinta para representar esto. Si ves referencias al "host" en el código o en la documentación antigua, a eso se refiere. Recomendamos usar Bazel 6.0 o versiones posteriores para evitar esta sobrecarga conceptual adicional.

Fragmentos de configuración

Las reglas pueden acceder a fragmentos de configuración, como cpp, java y jvm. Sin embargo, se deben declarar todos los fragmentos necesarios 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
    ...
)

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 generado. Si, por algún motivo, es necesario que sean diferentes, puedes especificar los argumentos root_symlinks o symlinks. root_symlinks es un diccionario que asigna rutas a archivos, en el que las rutas están relacionadas con la raíz del directorio de runfiles. El diccionario symlinks es el mismo, pero las rutas de acceso tienen un prefijo de forma implícita con 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 usas 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 y muestre un error que describe el conflicto. Para solucionarlo, deberás modificar los argumentos ctx.runfiles a fin de quitar la colisión. Esta verificación se realizará para todos los destinos que usen tu regla, además de los objetivos de cualquier tipo que dependan de ellos. Esto es especialmente riesgoso si es probable que otra herramienta la utilice de forma transitiva. Los nombres de symlinks deben ser únicos en los archivos de ejecución de una herramienta y 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 que se instrumentan. La marca --instrumentation_filter controla el subconjunto de destinos considerados. Se excluyen los objetivos de prueba, a menos que se especifique --instrument_test_targets.

Si la implementación de una regla agrega instrumentación de cobertura en el momento de la compilación, debe tenerla 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 (sin importar si las fuentes de un destino están instrumentadas específicamente o no) se puede condicionar en ctx.configuration.coverage_enabled.

Si la regla incluye directamente fuentes de sus dependencias antes de la compilación (como archivos de encabezado), es posible que también deba activar la instrumentación de tiempo de compilación si se deben instrumentar los orígenes 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, construido con coverage_common.instrumented_files_info. El parámetro dependency_attributes de instrumented_files_info debe enumerar todos los atributos de dependencias 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 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 sea de herramienta y que no establezca cfg en "host" o "exec" en el esquema de atributos) en dependency_attributes. (Este comportamiento no es 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 de la cadena de dependencias).

Acciones de validación

A veces, necesitas validar algo sobre la compilación, y la información necesaria para realizar esa validación solo está disponible en artefactos (archivos de origen o archivos generados). Debido a que esta información está en artefactos, las reglas no pueden realizar esta validación en el momento del análisis porque no pueden leer archivos. En cambio, las acciones deben realizar esta validación en el momento de la ejecución. Cuando falla la validación, la acción fallará y, por lo tanto, lo mismo sucederá con la compilación.

Algunos ejemplos de validaciones que se pueden ejecutar 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, ya que mueven las partes de las acciones que no son necesarias para compilar artefactos en acciones separadas. Por ejemplo, si una sola acción que realiza la compilación y el análisis con lint se puede separar en una acción de compilación y una de análisis con lint, la acción de análisis con lint se puede ejecutar como una acción de validación y se puede ejecutar 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 confirmar elementos 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 hace una regla para que se ejecute la acción? Antes, el enfoque consistía en que la acción de validación generara un archivo vacío y agregar ese resultado de forma artificial a las entradas de alguna otra acción importante en la compilación:

Esto funciona, ya que Bazel siempre ejecutará la acción de validación cuando se ejecute la acción de compilación, pero esto tiene desventajas importantes:

  1. La acción de validación se encuentra en la ruta crítica de la compilación. Debido a 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 ignorará la entrada. Esto reduce el paralelismo y ralentiza las compilaciones.

  2. Si se pueden ejecutar otras acciones de la compilación en lugar de la acción de compilación, los resultados vacíos de las acciones de validación también deberán agregarse a esas acciones (por ejemplo, el resultado del jar de origen de java_library). Esto también es un problema si se agregan más adelante acciones nuevas que podrían ejecutarse en lugar de la acción de compilación y el resultado de validación vacío se deja accidentalmente.

La solución a estos problemas es usar el grupo de resultados de validaciones.

Grupo de resultados de validaciones

El grupo de resultados de validaciones es un grupo de salida diseñado para contener los resultados de acciones de validación que de otra manera no se usarían, de modo que no sea necesario agregarlos artificialmente 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 mediante resultados implícitas del destino). Ten en cuenta que la incrementalidad y el almacenamiento en caché normal también se aplican: si las entradas a la acción de validación no cambiaron y la acción de validación se realizó de forma correcta, 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, por lo general, no crean resultados para que se genere un archivo.

Las acciones de validación de un destino no se ejecutan en tres casos:

  • Cuando se depende del objetivo como una herramienta
  • Cuando se depende del objetivo como una dependencia implícita (por ejemplo, un atributo que comienza con “_”)
  • Cuando el destino se compila en la configuración del host o de ejecución.

Se supone que estos destinos tienen sus propias compilaciones y pruebas separadas que descubrirí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 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 las entradas a ninguna otra acción. La acción de validación para un objetivo de este tipo de regla se ejecutará si el objetivo se depende de una etiqueta, o si se depende de forma directa o indirecta de cualquiera de los resultados implícitos del destino.

Por lo general, es importante que los resultados de las acciones de validación solo vayan al grupo de salida de validación y no se agreguen a las entradas de otras acciones, ya que esto podría anular las ganancias de paralelismo. Sin embargo, ten en cuenta que, por el momento, Bazel no tiene ninguna verificación especial para aplicar esto. Por lo tanto, debes probar que los resultados de las acciones de validación no se agreguen a las entradas de ninguna acción en las 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 marca de la línea de comandos --run_validations controla la ejecución de acciones de validación, la cual se establece de forma predeterminada como verdadera.

Funciones obsoletas

Resultados declarados previamente y obsoletos

Existen dos formas obsoletas de usar resultados declarados con anterioridad:

  • El parámetro outputs de rule especifica una asignación entre los nombres de los atributos de salida y las plantillas de strings para generar etiquetas de salida declaradas previamente. Es preferible usar salidas no declaradas previamente y agregar salidas de forma explícita a DefaultInfo.files. Usa la etiqueta del objetivo de la regla como entrada para las reglas que consumen el resultado en lugar de la etiqueta de una salida declarada previamente.

  • Para las reglas ejecutables, ctx.outputs.executable hace referencia a un resultado ejecutable declarado previamente con el mismo nombre que el destino de la regla. Es preferible declarar el resultado de manera explícita, por ejemplo, con ctx.actions.declare_file(ctx.label.name), y asegúrate de que el comando que genera el ejecutable establezca sus permisos para permitir la ejecución. Pasa el resultado ejecutable de forma explícita al parámetro executable de DefaultInfo.

Funciones de archivos de ejecución que se deben evitar

ctx.runfiles y el tipo runfiles tienen un conjunto complejo de funciones, muchas de las cuales se conservan por razones heredadas. Las siguientes recomendaciones ayudan a reducir la complejidad:

  • Evita el uso de los modos collect_data y collect_default de ctx.runfiles. Estos modos recopilan archivos de ejecución de forma implícita en ciertos perímetros de dependencia codificados de maneras confusas. En su lugar, agrega archivos con los parámetros files o transitive_files de ctx.runfiles, o bien combinando los archivos de ejecución de las dependencias con runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles).

  • Evita el uso de data_runfiles y default_runfiles del constructor DefaultInfo. En su lugar, especifica DefaultInfo(runfiles = ...). La distinción entre los archivos de ejecución “predeterminados” y “datos” se mantiene por motivos heredados. Por ejemplo, algunas reglas colocan sus resultados predeterminados en data_runfiles, pero no en default_runfiles. En lugar de usar data_runfiles, las reglas deben incluir ambos resultados predeterminados y combinar en default_runfiles a partir de atributos que proporcionan archivos de ejecución (a menudo, data).

  • Cuando recuperes runfiles de DefaultInfo (por lo general, solo para combinar archivos de ejecución entre la regla actual y sus dependencias), usa DefaultInfo.default_runfiles, no DefaultInfo.data_runfiles.

Migra desde proveedores heredados

Históricamente, los proveedores de Bazel eran campos simples en el objeto Target. Se accedió a ellos con el operador punto y se crearon colocando el campo en una estructura que muestra la función de implementación de la regla.

Este estilo dejó de estar disponible y no se debe usar en código nuevo. Consulta la siguiente información que puede ayudarte con la migración. El nuevo mecanismo del proveedor evita los conflictos de nombres. También admite la ocultación de datos, ya que requiere que cualquier código que acceda a una instancia de proveedor la recupere con el símbolo del proveedor.

Por el momento, se admiten proveedores heredados. 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, la estructura que se muestra también puede tomar varios otros campos que tienen un significado especial (y, por lo tanto, no crean un proveedor heredado correspondiente):

  • Los campos files, runfiles, data_runfiles, default_runfiles y executable corresponden a los campos con el mismo nombre de DefaultInfo. No se permite especificar ninguno de estos campos y, al mismo tiempo, mostrar un proveedor de DefaultInfo.

  • El campo output_groups toma un valor de struct y corresponde a un OutputGroupInfo.

En las declaraciones de reglas provides y en las declaraciones providers de atributos de dependencia, los proveedores heredados se pasan como 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 conjuntos de reglas grandes o complejos en los que es difícil actualizar todas las reglas de manera atómica, es posible que te resulte más fácil seguir esta secuencia de pasos:

  1. Modifica las reglas que producen el proveedor heredado para producir los proveedores heredados y los modernos con la sintaxis anterior. En el caso de las reglas que declaran que muestran el proveedor heredado, actualiza esa declaración para incluir el proveedor heredado y el moderno.

  2. Modifica las reglas que consumen el proveedor heredado para que, en su lugar, consuman el proveedor moderno. Si alguna declaración de atributo requiere el proveedor heredado, actualízala para que requiera el proveedor moderno. De manera opcional, puedes intercalar este trabajo con el paso 1 si los consumidores aceptan o solicitan cualquiera de los dos proveedores: prueba la presencia del proveedor heredado mediante hasattr(target, 'foo') o del proveedor nuevo con FooInfo in target.

  3. Quita por completo el proveedor heredado de todas las reglas.