Macros

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.

Las macros tienen dos variantes: las simbólicas, que se describen en esta página, y las heredadas. Siempre que sea posible, recomendamos usar macros simbólicas para mejorar la claridad del código.

Las macros simbólicas ofrecen argumentos escritos (conversión de cadena a etiqueta, en relación con el lugar al que se llamó a la macro) y la capacidad de restringir y especificar la visibilidad de los destinos creados. Están diseñados para admitir la evaluación diferida (que se agregará en una versión futura de Bazel). Las macros simbólicas están disponibles de forma predeterminada en Bazel 8. Cuando en este documento se menciona macros, se hace referencia a las macros simbólicas.

Uso

Para definir macros en archivos .bzl, llama a la función macro() con dos parámetros: attrs y implementation.

Atributos

attrs acepta un diccionario de nombres de atributos a tipos de atributos, que representa los argumentos de la macro. Dos atributos comunes, name y visibility, se agregan de forma implícita a todas las macros y no se incluyen en el diccionario que se pasa a attrs.

# macro/macro.bzl
my_macro = macro(
    attrs = {
        "deps": attr.label_list(mandatory = True, doc = "The dependencies passed to the inner cc_binary and cc_test targets"),
        "create_test": attr.bool(default = False, configurable = False, doc = "If true, creates a test target"),
    },
    implementation = _my_macro_impl,
)

Las declaraciones de tipo de atributo aceptan los parámetros, mandatory, default y doc. La mayoría de los tipos de atributos también aceptan el parámetro configurable, que determina si el atributo acepta select. Si un atributo es configurable, analizará los valores que no sean select como un select no configurable. "foo" se convertirá en select({"//conditions:default": "foo"}). Obtén más información en selección.

Implementación

implementation acepta una función que contiene la lógica de la macro. Las funciones de implementación suelen crear destinos llamando a una o más reglas y, por lo general, son privadas (se nombran con un guion inicial). Convencionalmente, se nombran igual que su macro, pero tienen el prefijo _ y el sufijo _impl.

A diferencia de las funciones de implementación de reglas, que toman un solo argumento (ctx) que contiene una referencia a los atributos, las funciones de implementación de macros aceptan un parámetro para cada argumento.

# macro/macro.bzl
def _my_macro_impl(name, deps, create_test):
    cc_library(
        name = name + "_cc_lib",
        deps = deps,
    )

    if create_test:
        cc_test(
            name = name + "_test",
            srcs = ["my_test.cc"],
            deps = deps,
        )

Declaración

Para declarar las macros, se carga y se llama a su definición en un archivo BUILD.


# pkg/BUILD

my_macro(
    name = "macro_instance",
    deps = ["src.cc"] + select(
        {
            "//config_setting:special": ["special_source.cc"],
            "//conditions:default": [],
        },
    ),
    create_tests = True,
)

Esto crearía los objetivos //pkg:macro_instance_cc_lib y//pkg:macro_instance_test.

Detalles

convenciones de nombres para los objetivos creados

Los nombres de los destinos o submacros creados por una macro simbólica deben coincidir con el parámetro name de la macro o deben tener el prefijo name seguido de _ (preferido), . o -. Por ejemplo, my_macro(name = "foo") solo puede crear archivos o destinos llamados foo, o con el prefijo foo_, foo- o foo., por ejemplo, foo_bar.

Se pueden declarar objetivos o archivos que infrinjan la convención de nombres de macros, pero no se pueden compilar ni usar como dependencias.

Los archivos y los destinos que no sean de macro dentro del mismo paquete que una instancia de macro no deben tener nombres que entren en conflicto con posibles nombres de destino de macro, aunque esta exclusividad no se aplica. Estamos implementando la evaluación diferida como una mejora de rendimiento para las macros simbólicas, que se verá afectada en los paquetes que infrinjan el esquema de nombres.

restricciones

Las macros simbólicas tienen algunas restricciones adicionales en comparación con las macros heredadas.

Macros simbólicas

  • debe tomar un argumento name y un argumento visibility
  • Debe tener una función implementation.
  • es posible que no devuelva valores.
  • no puede mutar su args
  • no pueden llamar a native.existing_rules(), a menos que sean macros finalizer especiales.
  • Puede que no llames a native.package().
  • Puede que no llames a glob().
  • Puede que no llames a native.environment_group().
  • deben crear destinos cuyos nombres se adhieran al esquema de nombres
  • No se puede hacer referencia a archivos de entrada que no se declararon ni se pasaron como argumento (consulta visibilidad para obtener más detalles).

Visibilidad

TODO: Expandir esta sección

Visibilidad del objetivo

De forma predeterminada, los destinos creados por macros simbólicas son visibles para el paquete en el que se crean. También aceptan un atributo visibility, que puede expandir esa visibilidad al llamador de la macro (pasando el atributo visibility directamente desde la llamada a la macro al destino creado) y a otros paquetes (especificándolos de forma explícita en la visibilidad del objetivo).

Visibilidad de las dependencias

Las macros deben tener visibilidad de los archivos y los destinos a los que hacen referencia. Pueden hacerlo de una de las siguientes maneras:

  • Se pasa de forma explícita como un valor attr a la macro.

# pkg/BUILD
my_macro(... deps = ["//other_package:my_tool"] )
  • Configuración predeterminada implícita de un valor attr
# my_macro:macro.bzl
my_macro = macro(
  attrs = {"deps" : attr.label_list(default = ["//other_package:my_tool"])} )
  • Ya es visible para la definición de la macro
# other_package/BUILD
cc_binary(
    name = "my_tool",
    visibility = "//my_macro:\\__pkg__",
)

Selecciona

Si un atributo es configurable, la función de implementación de macro siempre verá el valor del atributo como select. Por ejemplo, considera la siguiente macro:

my_macro = macro(
    attrs = {"deps": attr.label_list()},  # configurable unless specified otherwise
    implementation = _my_macro_impl,
)

Si se invoca my_macro con deps = ["//a"], se invocará _my_macro_impl con su parámetro deps configurado como select({"//conditions:default": ["//a"]}).

Los destinos de reglas revierten esta transformación y almacenan select triviales como sus valores incondicionales. En este ejemplo, si _my_macro_impl declara un destino de regla my_rule(..., deps = deps), el deps de ese destino de regla se almacenará como ["//a"].

Finalizadores

Un finalizador de reglas es una macro simbólica especial que, independientemente de su posición léxica en un archivo BUILD, se evalúa en la etapa final de la carga de un paquete, después de que se hayan definido todos los destinos que no son de finalizador. A diferencia de las macros simbólicas normales, un finalizador puede llamar a native.existing_rules(), donde se comporta de manera ligeramente diferente que en las macros heredadas: solo muestra el conjunto de destinos de reglas que no son de finalizador. El finalizador puede confirmar el estado de ese conjunto o definir objetivos nuevos.

Para declarar un finalizador, llama a macro() con finalizer = True:

def _my_finalizer_impl(name, visibility, tags_filter):
    for r in native.existing_rules().values():
        for tag in r.get("tags", []):
            if tag in tags_filter:
                my_test(
                    name = name + "_" + r["name"] + "_finalizer_test",
                    deps = [r["name"]],
                    data = r["srcs"],
                    ...
                )
                continue

my_finalizer = macro(
    attrs = {"tags_filter": attr.string_list(configurable = False)},
    implementation = _impl,
    finalizer = True,
)

Pereza

IMPORTANTE: Estamos en proceso de implementar la expansión y evaluación de macros diferidas. Esta función aún no está disponible.

Actualmente, todas las macros se evalúan en cuanto se carga el archivo BUILD, lo que puede afectar negativamente el rendimiento de los destinos en paquetes que también tienen macros no relacionadas costosas. En el futuro, las macros simbólicas que no sean de finalizador solo se evaluarán si son necesarias para la compilación. El esquema de nombres de prefijos ayuda a Bazel a determinar qué macro expandir según un objetivo solicitado.

Solución de problemas de migración

A continuación, se incluyen algunos problemas comunes de migración y cómo solucionarlos.

  • Llamadas de macro heredadas a glob()

Mueve la llamada glob() a tu archivo BUILD (o a una macro heredada a la que se llama desde el archivo BUILD) y pasa el valor glob() a la macro simbólica con un atributo de lista de etiquetas:

# BUILD file
my_macro(
    ...,
    deps = glob(...),
)
  • La macro heredada tiene un parámetro que no es un tipo attr de Starlark válido.

Extrae la mayor cantidad de lógica posible en una macro simbólica anidada, pero mantén la macro de nivel superior como una heredada.

  • La macro heredada llama a una regla que crea un objetivo que rompe el esquema de nombres.

No hay problema, solo no dependas del objetivo "ofensivo". La verificación de nombres se ignorará en silencio.