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 instancias 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 se presentan en dos versiones: macros simbólicas, que se describen en esta página, y macros heredadas. Siempre que sea posible, te recomendamos que uses macros simbólicas para que el código sea más claro.

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

Puedes encontrar un ejemplo ejecutable de macros simbólicas en el repositorio de ejemplos.

Uso

Para definir macros en archivos .bzl, llama a la función macro() con dos parámetros obligatorios: 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 Seleccionar.

Herencia de atributos

A menudo, las macros se usan para unir una regla (o otra macro), y el autor de la macro suele querer reenviar la mayor parte de los atributos del símbolo unido sin cambios, con **kwargs, al objetivo principal de la macro (o macro interna principal).

Para admitir este patrón, una macro puede heredar atributos de una regla o de otra macro pasando la regla o el símbolo de macro al argumento inherit_attrs de macro(). (También puedes usar la cadena especial "common" en lugar de una regla o un símbolo de macro para heredar los atributos comunes definidos para todas las reglas de compilación de Starlark). Solo se heredan los atributos públicos, y los atributos del diccionario attrs de la macro anula los atributos heredados con el mismo nombre. También puedes quitar los atributos heredados usando None como valor en el diccionario attrs:

# macro/macro.bzl
my_macro = macro(
    inherit_attrs = native.cc_library,
    attrs = {
        # override native.cc_library's `local_defines` attribute
        local_defines = attr.string_list(default = ["FOO"]),
        # do not inherit native.cc_library's `defines` attribute
        defines = None,
    },
    ...
)

El valor predeterminado de los atributos heredados no obligatorios siempre se anula para ser None, independientemente del valor predeterminado de la definición del atributo original. Si necesitas examinar o modificar un atributo heredado no obligatorio (por ejemplo, si quieres agregar una etiqueta a un atributo tags heredado), debes asegurarte de controlar el caso None en la función de implementación de tu macro:

# macro/macro.bzl
_my_macro_implementation(name, visibility, tags, **kwargs):
    # Append a tag; tags attr is an inherited non-mandatory attribute, and
    # therefore is None unless explicitly set by the caller of our macro.
    my_tags = (tags or []) + ["another_tag"]
    native.cc_library(
        ...
        tags = my_tags,
        **kwargs,
    )
    ...

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). De manera convencional, se les asigna el mismo nombre que a su macro, pero con 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, visibility, 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,
        )

Si una macro hereda atributos, su función de implementación debe tener un parámetro de palabra clave residual **kwargs, que se puede reenviar a la llamada que invoca la regla o submacro heredada. (Esto ayuda a garantizar que tu macro no se rompa si la regla o macro de la que heredas agrega un atributo nuevo).

Declaración

Para declarar macros, se carga y 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.

Al igual que en las llamadas a reglas, si el valor de un atributo en una llamada a macro se establece en None, ese atributo se trata como si el llamador de la macro lo hubiera omitido. Por ejemplo, las siguientes dos llamadas a macro son equivalentes:

# pkg/BUILD
my_macro(name = "abc", srcs = ["src.cc"], deps = None)
my_macro(name = "abc", srcs = ["src.cc"])

Por lo general, esto no es útil en los archivos BUILD, pero es útil cuando se une de forma programática una macro dentro de otra.

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 sus argumentos.
  • 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().
  • Debes crear objetivos cuyos nombres cumplan con el esquema de nombres.
  • no puede hacer referencia a archivos de entrada que no se declararon ni se pasaron como argumento (consulta visibilidad y macros para obtener más detalles).

Visibilidad y macros

El sistema de visibilidad ayuda a proteger los detalles de la implementación de las macros (simbólicas) y sus llamadores.

De forma predeterminada, los destinos creados en una macro simbólica son visibles dentro de la macro, pero no necesariamente para el llamador de la macro. La macro puede “exportar” un objetivo como una API pública reenviando el valor de su propio atributo visibility, como en some_rule(..., visibility = visibility).

Las ideas clave de la visibilidad de macro son las siguientes:

  1. La visibilidad se verifica en función de qué macro declaró el objetivo, no de qué paquete llamó a la macro.

    • En otras palabras, estar en el mismo paquete no hace que un objetivo sea visible para otro. Esto protege a los objetivos internos de la macro de convertirse en dependencias de otras macros o objetivos de nivel superior en el paquete.
  2. Todos los atributos visibility, tanto en las reglas como en las macros, incluyen automáticamente el lugar al que se llamó a la regla o macro.

    • Por lo tanto, un objetivo es visible de forma incondicional para otros objetivos declarados en la misma macro (o en el archivo BUILD, si no está en una macro).

En la práctica, esto significa que, cuando una macro declara un objetivo sin configurar su visibility, el objetivo se establece de forma predeterminada como interno en la macro. (La visibilidad predeterminada del paquete no se aplica dentro de una macro). Exportar el destino significa que este es visible para cualquier llamador de la macro especificado en el atributo visibility de la macro, además del paquete del llamador de la macro en sí, así como el código de la macro. Otra forma de pensarlo es que la visibilidad de una macro determina quién (además de la macro en sí) puede ver los objetivos exportados de la macro.

# tool/BUILD
...
some_rule(
    name = "some_tool",
    visibility = ["//macro:__pkg__"],
)
# macro/macro.bzl

def _impl(name, visibility):
    cc_library(
        name = name + "_helper",
        ...
        # No visibility passed in. Same as passing `visibility = None` or
        # `visibility = ["//visibility:private"]`. Visible to the //macro
        # package only.
    )
    cc_binary(
        name = name + "_exported",
        deps = [
            # Allowed because we're also in //macro. (Targets in any other
            # instance of this macro, or any other macro in //macro, can see it
            # too.)
            name + "_helper",
            # Allowed by some_tool's visibility, regardless of what BUILD file
            # we're called from.
            "//tool:some_tool",
        ],
        ...
        visibility = visibility,
    )

my_macro = macro(implementation = _impl, ...)
# pkg/BUILD
load("//macro:macro.bzl", "my_macro")
...

my_macro(
    name = "foo",
    ...
)

some_rule(
    ...
    deps = [
        # Allowed, its visibility is ["//pkg:__pkg__", "//macro:__pkg__"].
        ":foo_exported",
        # Disallowed, its visibility is ["//macro:__pkg__"] and
        # we are not in //macro.
        ":foo_helper",
    ]
)

Si se llamara a my_macro con visibility = ["//other_pkg:__pkg__"] o si el paquete //pkg hubiera establecido su default_visibility en ese valor, //pkg:foo_exported también se podría usar dentro de //other_pkg/BUILD o dentro de una macro definida en //other_pkg:defs.bzl, pero //pkg:foo_helper permanecería protegido.

Una macro puede declarar que un destino es visible para un paquete amigo pasando visibility = ["//some_friend:__pkg__"] (para un destino interno) o visibility = visibility + ["//some_friend:__pkg__"] (para uno exportado). Ten en cuenta que es un antipatrón que una macro declare un destino con visibilidad pública (visibility = ["//visibility:public"]). Esto se debe a que hace que el destino sea visible de forma incondicional para todos los paquetes, incluso si el llamador especificó una visibilidad más restringida.

Todas las verificaciones de visibilidad se realizan en función de la macro simbólica más interna que se está ejecutando. Sin embargo, existe un mecanismo de delegación de visibilidad: si una macro pasa una etiqueta como valor de atributo a una macro interna, cualquier uso de la etiqueta en la macro interna se verifica en relación con la macro externa. Consulta la página de visibilidad para obtener más detalles.

Recuerda que las macros heredadas son completamente transparentes para el sistema de visibilidad y se comportan como si su ubicación fuera cualquier archivo BUILD o macro simbólica desde la que se las llamó.

Selecciona

Si un atributo es configurable (el valor predeterminado) y su valor no es None, la función de implementación de macro verá el valor del atributo como unido en un select trivial. Esto facilita que el autor de la macro detecte errores en los que no anticipó que el valor del atributo podría ser un 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"]}). Si esto hace que falle la función de implementación (por ejemplo, porque el código intentó indexar el valor como en deps[0], lo que no se permite para select), el autor de la macro puede elegir entre reescribir su macro para usar solo operaciones compatibles con select o marcar el atributo como no configurable (attr.label_list(configurable = False)). Esto garantiza que los usuarios no puedan pasar un valor select.

Los destinos de reglas revierten esta transformación y almacenan select triviales como sus valores incondicionales. En el ejemplo anterior, si _my_macro_impl declara un destino de regla my_rule(..., deps = deps), el deps de ese destino de regla se almacenará como ["//a"]. Esto garantiza que el enlace de select no haga que se almacenen valores select triviales en todos los destinos a los que se les crea una instancia con macros.

Si el valor de un atributo configurable es None, no se une en un select. Esto garantiza que las pruebas como my_attr == None sigan funcionando y que, cuando el atributo se reenvíe a una regla con un valor predeterminado calculado, la regla se comporte correctamente (es decir, como si el atributo no se pasara en absoluto). No siempre es posible que un atributo tenga un valor None, pero puede ocurrir para el tipo attr.label() y para cualquier atributo heredado no obligatorio.

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 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 a 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 posible de lógica en una macro simbólica anidada, pero mantén la macro de nivel superior como una macro 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". Se ignorará la verificación de nombres.