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 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 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 selección.

Herencia de atributos

IMPORTANTE: La herencia de atributos es una función experimental habilitada por la marca --experimental_enable_macro_inherit_attrs. Es posible que algunos de los comportamientos descritos en esta sección cambien antes de que la función se habilite de forma predeterminada.

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

Consulta Visibilidad para obtener un análisis detallado de la visibilidad en Bazel.

Visibilidad del objetivo

De forma predeterminada, los destinos creados por una macro simbólica solo son visibles en el paquete que contiene el archivo .bzl que define la macro. En particular, no son visibles para el llamador de la macro simbólica, a menos que el llamador esté en el mismo paquete que el archivo .bzl de la macro.

Para que el llamador de la macro simbólica vea un destino, pasa visibility = visibility a la regla o a la macro interna. También puedes hacer que el objetivo sea visible en paquetes adicionales si le otorgas una visibilidad más amplia (o incluso pública).

La visibilidad predeterminada de un paquete (como se declara en package()) se pasa de forma predeterminada al parámetro visibility de la macro más externa, pero depende de la macro pasar (o no) ese visibility a los destinos de los que crea instancias.

Visibilidad de las dependencias

Los objetivos a los que se hace referencia en la implementación de una macro deben ser visibles para la definición de esa macro. La visibilidad se puede otorgar de una de las siguientes maneras:

  • Los destinos son visibles para una macro si se pasan a la macro a través de una etiqueta, una lista de etiquetas o atributos de diccionario con clave o valor de etiqueta, ya sea de forma explícita:

# pkg/BUILD
my_macro(... deps = ["//other_package:my_tool"] )
  • … o como valores predeterminados de atributos:
# my_macro:macro.bzl
my_macro = macro(
  attrs = {"deps" : attr.label_list(default = ["//other_package:my_tool"])},
  ...
)
  • Los destinos también son visibles para una macro si se declaran visibles para el paquete que contiene el archivo .bzl que define la macro:
# other_package/BUILD
# Any macro defined in a .bzl file in //my_macro package can use this tool.
cc_binary(
    name = "my_tool",
    visibility = "//my_macro:\\__pkg__",
)

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 la función de implementación falle (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ía 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 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 heredada 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.