Macros

Informar un problema Ver fuente Noche {/2/}}

En esta página, se abordan los conceptos básicos del uso de macros y se incluyen casos de uso, depuración y convenciones típicos.

Una macro es una función a la que se llama desde el archivo BUILD que puede crear una instancia de reglas. Las macros se usan principalmente para el encapsulamiento y la reutilización de código de reglas existentes y otras macros. Al final de la fase de carga, las macros ya no existen y Bazel solo ve el conjunto concreto de reglas con instancias creadas.

Uso

El caso de uso típico de una macro es cuando se desea reutilizar una regla.

Por ejemplo, genrule en un archivo BUILD genera un archivo mediante //:generator con un argumento some_arg codificado en el comando:

genrule(
    name = "file",
    outs = ["file.txt"],
    cmd = "$(location //:generator) some_arg > $@",
    tools = ["//:generator"],
)

Si deseas generar más archivos con diferentes argumentos, se recomienda extraer este código en una función de macro. Llamemos a la macro file_generator, que tiene los parámetros name y arg. Reemplaza la genrule por lo siguiente:

load("//path:generator.bzl", "file_generator")

file_generator(
    name = "file",
    arg = "some_arg",
)

file_generator(
    name = "file-two",
    arg = "some_arg_two",
)

file_generator(
    name = "file-three",
    arg = "some_arg_three",
)

Aquí, cargarás el símbolo file_generator desde un archivo .bzl ubicado en el paquete //path. Si colocas las definiciones de las funciones de macro en un archivo .bzl separado, mantienes los archivos BUILD limpios y declarativos. El archivo .bzl se puede cargar desde cualquier paquete en el lugar de trabajo.

Por último, en path/generator.bzl, escribe la definición de la macro para encapsular y parametrizar la definición original de genrule:

def file_generator(name, arg, visibility=None):
  native.genrule(
    name = name,
    outs = [name + ".txt"],
    cmd = "$(location //:generator) %s > $@" % arg,
    tools = ["//:generator"],
    visibility = visibility,
  )

También puedes usar macros para encadenar reglas. En este ejemplo, se muestran genrules encadenadas, en las que una genrule usa los resultados de una genrule anterior como entradas:

def chained_genrules(name, visibility=None):
  native.genrule(
    name = name + "-one",
    outs = [name + ".one"],
    cmd = "$(location :tool-one) $@",
    tools = [":tool-one"],
    visibility = ["//visibility:private"],
  )

  native.genrule(
    name = name + "-two",
    srcs = [name + ".one"],
    outs = [name + ".two"],
    cmd = "$(location :tool-two) $< $@",
    tools = [":tool-two"],
    visibility = visibility,
  )

En este ejemplo, solo se asigna un valor de visibilidad a la segunda genrule. Esto permite a los autores de macros ocultar los resultados de las reglas intermedias para que no dependan de otros objetivos en el lugar de trabajo.

Macros desplegables

Cuando desees investigar lo que hace una macro, usa el comando query con --output=build para ver la forma expandida:

$ bazel query --output=build :file
# /absolute/path/test/ext.bzl:42:3
genrule(
  name = "file",
  tools = ["//:generator"],
  outs = ["//test:file.txt"],
  cmd = "$(location //:generator) some_arg > $@",
)

Crea una instancia de reglas nativas

Se puede crear una instancia de las reglas nativas (reglas que no necesitan una declaración load()) desde el módulo native:

def my_macro(name, visibility=None):
  native.cc_library(
    name = name,
    srcs = ["main.cc"],
    visibility = visibility,
  )

Si necesitas conocer el nombre del paquete (por ejemplo, qué archivo BUILD llama a la macro), usa la función native.package_name(). Ten en cuenta que native solo se puede usar en archivos .bzl, y no en archivos WORKSPACE o BUILD.

Resolución de etiqueta en macros

Dado que las macros se evalúan en la fase de carga, las strings de etiquetas, como "//foo:bar", que aparecen en una macro se interpretan en relación con el archivo BUILD en el que se usa la macro, en lugar de en relación con el archivo .bzl en el que está definida. Por lo general, este comportamiento no es recomendable para las macros que están diseñadas para usarse en otros repositorios, por ejemplo, porque forman parte de un conjunto de reglas de Starlark publicado.

Para obtener el mismo comportamiento que con las reglas de Starlark, une las cadenas de etiquetas con el constructor Label:

# @my_ruleset//rules:defs.bzl
def my_cc_wrapper(name, deps = [], **kwargs):
  native.cc_library(
    name = name,
    deps = deps + select({
      # Due to the use of Label, this label is resolved within @my_ruleset,
      # regardless of its site of use.
      Label("//config:needs_foo"): [
        # Due to the use of Label, this label will resolve to the correct target
        # even if the canonical name of @dep_of_my_ruleset should be different
        # in the main workspace, such as due to repo mappings.
        Label("@dep_of_my_ruleset//tools:foo"),
      ],
      "//conditions:default": [],
    }),
    **kwargs,
  )

Depuración

  • bazel query --output=build //my/path:all te mostrará cómo se ve el archivo BUILD después de la evaluación. Se expanden todas las macros, globs y bucles. Limitación conocida: Por el momento, las expresiones select no se muestran en el resultado.

  • Puedes filtrar el resultado según generator_function (la función que generó las reglas) o generator_name (el atributo de nombre de la macro): bash $ bazel query --output=build 'attr(generator_function, my_macro, //my/path:all)'

  • Para saber dónde se genera exactamente la regla foo en un archivo BUILD, puedes probar el siguiente truco. Inserta esta línea cerca de la parte superior del archivo BUILD: cc_library(name = "foo"). Ejecuta Bazel. Recibirás una excepción cuando se cree la regla foo (debido a un conflicto de nombres), que mostrará el seguimiento de pila completa.

  • También puedes usar print para la depuración. Muestra el mensaje como una línea de registro DEBUG durante la fase de carga. Excepto en casos excepcionales, quita las llamadas a print o hazlas condicionales en un parámetro debugging cuyo valor predeterminado sea False antes de enviar el código al depósito.

Errores

Si quieres arrojar un error, usa la función fail. Explica claramente al usuario qué salió mal y cómo corregir su archivo BUILD. No es posible detectar un error.

def my_macro(name, deps, visibility=None):
  if len(deps) < 2:
    fail("Expected at least two values in deps")
  # ...

Convenciones

  • Todas las funciones públicas (funciones que no comienzan con guion bajo) que crean instancias de reglas deben tener un argumento name. Este argumento no debe ser opcional (no proporciones un valor predeterminado).

  • Las funciones públicas deben usar una docstring según las convenciones de Python.

  • En los archivos BUILD, el argumento name de las macros debe ser un argumento de palabra clave (no uno posicional).

  • El atributo name de las reglas generadas por una macro debe incluir el argumento name como prefijo. Por ejemplo, macro(name = "foo") puede generar un foo de cc_library y una foo_gen de regla general.

  • En la mayoría de los casos, los parámetros opcionales deben tener un valor predeterminado de None. None se puede pasar directamente a reglas nativas, que lo tratan de la misma manera que si no hubieses pasado ningún argumento. Por lo tanto, no es necesario reemplazarlo por 0, False o [] para este propósito. En cambio, la macro debe ceder a las reglas que crea, ya que sus valores predeterminados pueden ser complejos o cambiar con el tiempo. Además, un parámetro que se establece de forma explícita en su valor predeterminado es diferente de uno que nunca se establece (ni se establece en None) cuando se accede a través del lenguaje de consulta o los componentes internos del sistema de compilación.

  • Las macros deben tener un argumento visibility opcional.