En esta página, se explican los conceptos básicos para usar 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
y que puede crear instancias de reglas.
Las macros se usan principalmente para la encapsulación y la reutilización de código de reglas existentes y otras macros.
Hay dos tipos de macros: las macros simbólicas, que se describen en esta página, y las macros heredadas. Siempre que sea posible, recomendamos usar 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 donde 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 este documento menciona macros
, se refiere a macros simbólicas.
En el repositorio de ejemplos, puedes encontrar un ejemplo ejecutable de macros simbólicas.
Uso
Las macros se definen en archivos .bzl
llamando a la función macro()
con dos parámetros obligatorios: attrs
y implementation
.
Atributos
attrs
acepta un diccionario de nombres de atributos para 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
s. 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 selects.
Herencia de atributos
A menudo, las macros están diseñadas para encapsular una regla (o bien otra macro), y el autor de la macro suele querer reenviar la mayor parte de los atributos del símbolo encapsulado sin cambios, usando **kwargs
, al destino principal de la macro (o a la macro interna principal).
Para admitir este patrón, una macro puede heredar atributos de una regla o de otra macro pasando el símbolo de regla o el símbolo de macro al argumento inherit_attrs
de macro()
. (También puedes usar la cadena especial "common"
en lugar de un símbolo de regla o 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
propio de la macro anulan los atributos heredados con el mismo nombre. También puedes quitar 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 y se establece en 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 deseas 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
def _my_macro_impl(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 suelen ser privadas (se nombran con un guion bajo inicial). Por convención, tienen el mismo nombre que 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 interrumpa si la regla o la macro de la que heredas agrega un atributo nuevo).
Declaración
Las macros se declaran cargando y llamando 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 destinos //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 una 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 macros 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 sí cuando se encapsula de forma programática una macro dentro de otra.
Detalles
Convenciones de nombres para los objetivos creados
Los nombres de los destinos o las submacros creados por una macro simbólica deben coincidir con el parámetro name
de la macro o 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 los destinos o archivos que incumplen la convención de nomenclatura de macros, pero no se pueden compilar ni usar como dependencias.
Los archivos y los destinos que no son macros dentro del mismo paquete que una instancia de macro no deben tener nombres que entren en conflicto con los posibles nombres de destino de la macro, aunque esta exclusividad no se aplica. Estamos en proceso de implementar la evaluación diferida como una mejora del rendimiento para las macros simbólicas, que se verá afectada en los paquetes que incumplan el esquema de nomenclatura.
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 argumentovisibility
- Debe tener una función
implementation
. - Es posible que no devuelva valores.
- no pueden mutar sus argumentos
- No se puede llamar a
native.existing_rules()
, a menos que sean macros especiales definalizer
. - No se puede llamar a
native.package()
- No se puede llamar a
glob()
- No se puede llamar a
native.environment_group()
- debe crear destinos cuyos nombres cumplan con el esquema de nombres
- No puede hacer referencia a archivos de entrada que no se declararon o pasaron como argumento.
- No pueden hacer referencia a destinos privados de sus llamadores (consulta visibilidad y macros para obtener más detalles).
Visibilidad y macros
El sistema de visibilidad ayuda a proteger los detalles de 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 destino 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 las macros son las siguientes:
La visibilidad se verifica en función de la macro que declaró el destino, no del paquete que llamó a la macro.
- En otras palabras, estar en el mismo paquete no hace que un destino sea visible para otro. Esto protege los destinos internos de la macro para que no se conviertan en dependencias de otras macros o destinos de nivel superior en el paquete.
Todos los atributos de
visibility
, tanto en las reglas como en las macros, incluyen automáticamente el lugar donde se llamó a la regla o macro.- Por lo tanto, un destino es visible de forma incondicional para otros destinos declarados en la misma macro (o en el archivo
BUILD
, si no está en una macro).
- Por lo tanto, un destino es visible de forma incondicional para otros destinos declarados en la misma macro (o en el archivo
En la práctica, esto significa que, cuando una macro declara un destino sin establecer su visibility
, el destino se establece de forma predeterminada como interno a la macro. (La visibilidad predeterminada del paquete no se aplica dentro de una macro). Exportar el destino significa que este es visible para lo que el llamador de la macro especificó en el atributo visibility
de la macro, además del paquete del llamador de la macro y 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 destinos 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, también se podría usar //pkg:foo_exported
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"]
), ya que esto 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 con respecto a la macro simbólica en ejecución más interna. 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 con respecto a 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 llamaron.
Finalizadores y visibilidad
Los destinos declarados en un finalizador de reglas, además de ver los destinos según las reglas de visibilidad de macros simbólicas habituales, también pueden ver todos los destinos que son visibles para el paquete del destino del finalizador.
Esto significa que, si migras una macro heredada basada en native.existing_rules()
a un finalizador, los destinos declarados por el finalizador aún podrán ver sus dependencias anteriores.
Sin embargo, ten en cuenta que es posible declarar un destino en una macro simbólica de modo que los destinos de un finalizador no puedan verlo en el sistema de visibilidad, aunque el finalizador pueda inspeccionar sus atributos con native.existing_rules()
.
Selecciones
Si un atributo es configurable
(el valor predeterminado) y su valor no es None
, la función de implementación de la macro verá el valor del atributo como si estuviera incluido en un select
trivial. Esto facilita que el autor de la macro detecte errores en los que no previó 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
establecido en 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 los select
), el autor de la macro puede tomar una decisión: puede volver a escribir su macro para que solo use operaciones compatibles con select
o puede marcar el atributo como no configurable (attr.label_list(configurable = False)
). Esta última opción garantiza que los usuarios no puedan pasar un valor de select
.
Los destinos de reglas invierten esta transformación y almacenan los 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 ajuste de select
no haga que se almacenen valores triviales de select
en todos los destinos que instancian las macros.
Si el valor de un atributo configurable es None
, no se incluye 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 hubiera pasado en absoluto). No siempre es posible que un atributo adopte un valor None
, pero puede suceder 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 finalizadores. A diferencia de las macros simbólicas comunes, un finalizador puede llamar a native.existing_rules()
, donde se comporta de manera ligeramente diferente que en las macros heredadas: solo devuelve el conjunto de destinos de reglas que no son finalizadores. El finalizador puede confirmar el estado de ese conjunto o definir nuevos objetivos.
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 diferida de macros. 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 costosas no relacionadas. En el futuro, las macros simbólicas no finalizadoras 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 dado un destino solicitado.
Solución de problemas de migración
A continuación, se incluyen algunos problemas comunes de la migración y cómo solucionarlos.
- Llamadas a macros heredadas
glob()
Mueve la llamada a glob()
a tu archivo BUILD (o a una macro heredada llamada desde el archivo BUILD) y pasa el valor de 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.
Incorpora la mayor cantidad de lógica posible 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 destino que incumple el esquema de nomenclatura.
No hay problema, solo no dependas del destino "infractor". La verificación de nombres se ignorará de forma silenciosa.