Reglas

Informar un problema Ver fuente Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Una regla define una serie de acciones que Bazel realiza en las entradas para producir un conjunto de resultados, a los que se hace referencia en los proveedores que muestra la función de implementación de la regla. Por ejemplo, un código C++ regla binaria podría tener las siguientes características:

  1. Toma un conjunto de archivos de origen (entradas) de .cpp.
  2. Ejecuta g++ en los archivos fuente (acción).
  3. Muestra el proveedor DefaultInfo con el resultado ejecutable y otros archivos para que estén disponibles en el tiempo de ejecución.
  4. 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 tener en cuenta 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 los tres principales de una compilación (carga, análisis y ejecución). También es útil aprender sobre las macros para comprender la diferencia entre las reglas y las macros. Para comenzar, revisa el instructivo de 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 principal para ciertos idiomas. Si defines tus propias reglas, puedes agregar compatibilidad similar para idiomas y herramientas que Bazel no admite de forma nativa.

Bazel proporciona un modelo de extensibilidad para escribir reglas con el lenguaje 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 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 según sea necesario.

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 resultado ejecutable (con executable=True) o, específicamente, un ejecutable de prueba (con test=True). Si es lo último, la regla 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 denomina creación de una instancia de la regla. Con esto se especifica un nombre para el una orientación y valores nuevos para los atributos del objetivo.

También se puede llamar a las reglas desde funciones de Starlark y cargarlas en archivos .bzl. Las funciones de Starlark que llaman a reglas se llaman macros de Starlark. En última instancia, se debe llamar a las macros de Starlark desde los archivos BUILD y solo se puede llamar a estas durante la fase de carga, cuando se evalúan los archivos BUILD 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 de nombres de atributos a esquemas (creados con el módulo attr) al parámetro attrs de rule. 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 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 que procesan las acciones de un destino. A menudo, el esquema de atributos especifica qué extensiones de archivo se esperan para el tipo de archivo fuente que procesa la regla. Las reglas para idiomas con archivos de encabezados generalmente especifican un atributo hdrs independiente para los encabezados que procesa un destino y sus consumidores.
  • deps especifica las dependencias de código para un destino. El esquema de atributos debe especificar qué proveedores deben proporcionar esas dependencias. (Por ejemplo, cc_library proporciona CcInfo).
  • data especifica los archivos que se pondrán a disposición en el tiempo de ejecución para cualquier ejecutable que dependa de un destino. Eso debería permitir que se especifiquen archivos arbitrarios.
example_library = rule(
    implementation = _example_library_impl,
    attrs = {
        "srcs": attr.label_list(allow_files = [".example"]),
        "hdrs": attr.label_list(allow_files = [".header"]),
        "deps": attr.label_list(providers = [ExampleInfo]),
        "data": attr.label_list(allow_files = True),
        ...
    },
)

Estos son ejemplos de atributos de dependencia. Cualquier atributo que especifique una etiqueta de entrada (aquellos definidos con attr.label_list, attr.label o attr.label_keyed_string_dict) especifica dependencias de un tipo determinado entre un objetivo y los objetivos cuyas etiquetas (o los objetos Label correspondientes) se enumeran en ese atributo cuando se define el objetivo. 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, primero se analiza other_target. 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 manera que otras dependencias.

Si deseas proporcionar una dependencia implícita sin permitir que el usuario supere ese valor, puedes hacer que el atributo sea privado. Para ello, dale 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 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 en el 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. Dado que _compiler es un atributo privado, se deduce que ctx.attr._compiler siempre apuntará a //tools:example_compiler en todos los destinos de este tipo de regla. Como alternativa, puedes nombrar el atributo compiler sin la virgulilla y conservar el valor predeterminado. Esto permite a los usuarios sustituir un compilador diferente si es necesario, pero no requiere conocimiento de la etiqueta del compilador.

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 de 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 salidas predeclaradas, de las que se puede depender de forma específica o solicitarlas en la línea de comandos.

Función de implementación

Cada regla requiere una función implementation. Estas funciones se ejecutan de forma estricta en la fase de análisis y transforman el grafo de objetivos generado en la fase de carga en un grafo 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 nombran con una virgulilla al comienzo). De manera convencional, se les asigna el mismo nombre que a su regla, pero con el sufijo _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

Las dependencias se representan en el momento del análisis 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, esta es una lista de Targets. Para los atributos label, es un solo 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 índice ([]), con el tipo de proveedor como 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 a través de un atributo hdrs y los proporciona a las acciones de compilación del destino y sus consumidores, podría recopilarlos de la siguiente manera:

def _example_library_impl(ctx):
    ...
    transitive_headers = [hdr[ExampleInfo].headers for hdr in ctx.attr.hdrs]

Para el estilo heredado en el que se muestra un struct desde la función de implementación de un objetivo en lugar de una lista de objetos de proveedor, haz lo siguiente:

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 fuente 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 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 atributos de dependencia cuyas especificaciones establecen allow_single_file=True. ctx.executable se comporta de la misma manera que ctx.file, pero solo contiene campos para atributos de dependencia cuyas especificaciones establecen executable=True.

Cómo declarar resultados

Durante la fase de análisis, la función de implementación de una regla puede crear resultados. Dado que todas las etiquetas deben conocerse durante la fase de carga, estos resultados adicionales 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 los resultados se basan en el nombre del destino, ctx.label.name:

def _example_library_impl(ctx):
  ...
  output_file = ctx.actions.declare_file(ctx.label.name + ".output")
  ...

En el caso de los resultados declarados previamente, como los creados para los atributos de salida, los objetos File se pueden recuperar de los campos correspondientes de ctx.outputs.

Acciones

Una acción describe cómo generar un conjunto de salidas a partir de un conjunto de entradas, para Por ejemplo, “ejecuta gcc en hello.c y obtén 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.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 debe conocerse durante la fase de análisis. Puede depender del valor de los atributos, incluidos los proveedores de dependencias, pero no puede depender del resultado de la ejecución. Por ejemplo, si tu acción ejecuta el comando unzip, debes especificar qué archivos esperas que se expandan (antes de ejecutar unzip). 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 todo lo que no esté en los resultados no estará disponible para los consumidores. Todas las salidas declaradas deben escribirse con 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. Se produce un error si hay un ciclo en el gráfico de dependencias. Crear una acción no garantiza que se ejecutará, eso 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 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 estilo heredado en el que 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 migrar las reglas.

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

Las reglas que realizan acciones deben proporcionar resultados predeterminados, incluso si no se espera que esos resultados 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. Esto dificulta la depuración, ya que volver a compilar solo el destino con errores no reproducirá el error.

Runfiles

Los archivos de ejecución son un conjunto de archivos que usa un destino en el tiempo de ejecución (a diferencia del tiempo de compilación). Durante la fase de ejecución, Bazel crea un árbol de directorios que contiene symlinks que apuntan a los archivos de ejecución. 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 de forma manual 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 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 final, por lo general, srcs (que puede contener destinos 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 con la función provider para transmitir información específica de la regla:

ExampleInfo = provider(
    "Info needed to compile/link Example code.",
    fields={
        "headers": "depset of header Files from transitive dependencies.",
        "files_to_link": "depset of Files from compilation.",
    })

Luego, las funciones de implementación de reglas pueden construir y mostrar instancias de 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 obedece ciertas invariantes, o para dar 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, el tipo devuelto de provider() cambia a una tupla de dos valores: el símbolo del proveedor que es el valor que se muestra de forma ordinaria 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, reenviará los argumentos a la devolución de llamada init. El valor que muestra la devolución de llamada debe ser un diccionario que asigne nombres de campos (cadenas) a valores. Esto se usa para inicializar los campos de la instancia nueva. Ten en cuenta que la devolución de llamada puede tener cualquier firma y, si los argumentos no coinciden con la firma, se informa un error como si se hubiera invocado la devolución de llamada directamente.

Por el contrario, el constructor sin procesar 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 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 se pueden invocar con 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 un resultado predeclarado ctx.outputs.executable especial. 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 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.

Reglas ejecutables y las reglas de prueba tienen restricciones atributos 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 de forma implícita, aunque se puede solucionar el problema uniendo una regla privada en una macro de Starlark que altere 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 archivos de ejecución está junto 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 directamente bazel está junto a la raíz del directorio runfiles. Sin embargo, los objetos binarios llamados desde los archivos de ejecución no pueden hacer 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 no se establece, un objeto binario puede adivinar que fue el primero al que se llamó y buscar un directorio de archivos de ejecución 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, se puede solicitar de forma explícita cualquier resultado declarado previamente 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 explícitamente las etiquetas para las salidas 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, existen grupos de resultados, que son colecciones de archivos de salida que se pueden solicitar juntos. Estas se pueden solicitar --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 ejecutando bazel build //pkg:mytarget --output_groups=debug_files. Dado que los resultados no declarados previamente no tienen etiquetas, solo se pueden solicitar si aparecen en los resultados predeterminados o en un grupo de resultados.

Los grupos de salida se pueden especificar 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. En su lugar, define proveedores específicos de la regla.

Configuraciones

Imagina que quieres 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 podría 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. Los objetivos más altos (los solicitados en la línea de comandos) se compilan en la configuración "target", mientras que las herramientas que deben ejecutarse en la plataforma de ejecución se compilan en una configuración "exec". Las reglas pueden generar diferentes acciones según 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 a fin de 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.

Las herramientas que se ejecutan como parte de la compilación (como los compiladores o los 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 tiempo de ejecución (como parte de una prueba) deben compilarse para la configuración de destino. En este caso, especifica cfg="target" en el atributo.

En realidad, cfg="target" no 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, solo configúralo cuando realmente ayude a 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 consideraba que todas las acciones de compilación se ejecutaban en la máquina host. Bazel versiones anteriores a la 6.0 creaban un "host" distinto actual para representarlo. Si ves referencias a "host" en el código o en la documentación anterior, a eso se refiere. Te recomendamos que uses Bazel 6.0 o una versión posterior para evitar esta sobrecarga conceptual adicional.

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

Por lo general, la ruta de acceso 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 salida 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 diccionario symlinks es el mismo, pero las rutas de acceso tienen un prefijo implícito con el nombre del espacio de trabajo principal (no el nombre del repositorio que contiene el destino actual).

    ...
    runfiles = ctx.runfiles(
        root_symlinks = {"some/path/here.foo": ctx.file.some_data_file2}
        symlinks = {"some/path/here.bar": ctx.file.some_data_file3}
    )
    # Creates something like:
    # sometarget.runfiles/
    #     some/
    #         path/
    #             here.foo -> some_data_file2
    #     <workspace_name>/
    #         some/
    #             path/
    #                 here.bar -> some_data_file3

Si se 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 otra herramienta use tu herramienta 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. 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 objetivos de prueba, a menos que se especifique --instrument_test_targets.

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 El parámetro dependency_attributes de instrumented_files_info debe enumerar todos los atributos de dependencia del entorno de ejecución, incluidas las dependencias de código, como deps, y las dependencias de datos, como data. El parámetro source_attributes debe enumerar 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 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 su lugar, las acciones deben realizar 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 linting, las verificaciones de dependencia y coherencia, y las verificaciones de estilo.

Las acciones de validación también pueden ayudar a mejorar el rendimiento de la compilación, ya que mueven partes de acciones que no son necesarias para compilar artefactos en acciones separadas. Por ejemplo, si una sola acción que realiza la compilación y la lint se puede separar en una acción de compilación y una acción de lint, la acción de lint se puede ejecutar como una acción de validación y en paralelo con otras acciones.

A menudo, estas "acciones de validación" no producen nada que se use en otro lugar de la compilación, ya que solo deben confirmar elementos 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 acción de compilación, pero esto tiene inconvenientes significativos:

  1. La acción de validación se encuentra en la ruta crítica de la compilación. Como Bazel piensa que el resultado vacío es necesario para ejecutar la acción de compilación, primero ejecutará la acción de validación, aunque la acción de compilación ignorará la entrada. Esto reduce el paralelismo y ralentiza las compilaciones.

  2. Si es posible que se ejecuten otras acciones en la compilación en lugar de la acción de compilación, también se deben agregar a esas acciones los resultados vacíos de las acciones de validación (por ejemplo, el resultado del 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 resultados de Validations.

Grupo de resultados 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 resultados siempre se solicitan, independientemente del valor de la marca --output_groups y de cómo se dependa del destino (por ejemplo, en la línea de comandos, como una dependencia o a través de resultados implícitos del destino). Ten en cuenta que 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 algún archivo, incluso uno vacío. 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 atributo que comienza con “_”)
  • Cuando el destino se compila en la configuración del host o de la 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, actualmente, Bazel no tiene ninguna verificación especial para aplicar esta medida. 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 de rule especifica una asignación entre los nombres de atributos de salida y las plantillas de cadenas para generar etiquetas de salida declaradas previamente. Prefiere usar resultados no declarados previamente y agregar explícitamente resultados a DefaultInfo.files Usa la etiqueta del objetivo de la regla como entrada para las reglas que consumen la salida en lugar de la etiqueta de una salida predeclarada.

  • 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. Prefiere declarar el resultado de forma 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. Explícitamente Pasa el resultado ejecutable al parámetro executable de DefaultInfo.

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 y collect_default de ctx.runfiles. Estos modos recopilan implícitamente archivos de ejecución en ciertos bordes de dependencia codificados de forma incomprensible. En su lugar, agrega archivos con los parámetros files o transitive_files de ctx.runfiles, o bien combina archivos de ejecución de dependencias con runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles).

  • Evita el uso de data_runfiles y default_runfiles del constructor DefaultInfo. Especifica DefaultInfo(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 en data_runfiles, pero no default_runfiles. En lugar de usar data_runfiles, las reglas ambas deberían incluir resultados predeterminados y combinarse default_runfiles de atributos que proporcionan archivos de ejecución (a menudo, data).

  • Cuando se recupera runfiles desde DefaultInfo (generalmente, solo para combinaciones) runfiles 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 pudo acceder a ellos con el operador de punto, y se crearon colocando el campo en un struct 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 a continuación la información que puede ayudarte a realizar la migración. El nuevo mecanismo de proveedor evita el uso conflictos. También admite el ocultamiento de datos, ya que requiere que cualquier código que acceda a una instancia del proveedor lo recupere con el símbolo del proveedor.

Por el momento, aún se admiten los 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, el proveedores y su contenido se puede 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 DefaultInfo.

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

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 cambiar de cadenas a símbolos durante la migración. En el caso de conjuntos de reglas complejos o grandes en los que es difícil actualizar todas las reglas de forma 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 el proveedor heredado y modernos, con la sintaxis anterior. En el caso de las reglas que declaran que muestran el proveedor heredado, actualiza esa declaración para incluir los proveedores heredados y modernos.

  2. Modifica las reglas que consumen el proveedor heredado para que consuman el proveedor moderno. 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 con FooInfo in target.

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