Esta página aborda os conceitos básicos do uso de macros e inclui casos de uso típicos, depuração e convenções.
Uma macro é uma função chamada no arquivo BUILD
que pode instanciar regras.
As macros são usadas principalmente para encapsulamento e reutilização de código de regras e
outras macros.
Há dois tipos de macros: macros simbólicas, que são descritas nesta página, e macros legados. Sempre que possível, recomendamos o uso de macros simbólicas para clareza do código.
Macros simbólicas oferecem argumentos digitados (conversão de string para rótulo, em relação
aonde a macro foi chamada) e a capacidade de restringir e especificar a
visibilidade dos destinos criados. Eles foram projetados para serem suscetíveis a avaliação
preguiçosa (que será adicionada em uma versão futura do Bazel). Macros simbólicas estão
disponíveis por padrão no Bazel 8. Quando este documento menciona macros
, ele
se refere a macros simbólicos.
Um exemplo executável de macros simbólicas pode ser encontrado no repositório de exemplos.
Uso
As macros são definidas em arquivos .bzl
chamando a função
macro()
com
dois parâmetros obrigatórios: attrs
e implementation
.
Atributos
attrs
aceita um dicionário de nome de atributo para tipos
de atributo, que representa
os argumentos para a macro. Dois atributos comuns, name
e visibility
,
são adicionados implicitamente a todas as macros e não são incluídos no dicionário transmitido
para 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,
)
As declarações de tipo de atributo aceitam os
parâmetros
mandatory
, default
e doc
. A maioria dos tipos de atributo também aceita o
parâmetro configurable
, que determina se o atributo aceita
select
s. Se um atributo for configurable
, ele analisará valores que não são select
como um select
não configurável. "foo"
vai se tornar
select({"//conditions:default": "foo"})
. Saiba mais em seleções.
Herança de atributos
As macros geralmente são usadas para agrupar uma regra (ou outra macro), e o autor da macro
geralmente quer encaminhar a maior parte dos atributos do símbolo agrupado
inalterado, usando **kwargs
, para o destino principal da macro (ou a macro interna principal).
Para oferecer suporte a esse padrão, uma macro pode herdar atributos de uma regra ou de outra
macro transmitindo a regra ou o
símbolo da macro para o argumento inherit_attrs
de
macro()
. Também é possível usar a string especial "common"
em vez de uma regra ou um símbolo de macro para herdar os atributos comuns definidos para
todas as regras de build
do Starlark.
Somente os atributos públicos são herdados, e os atributos no dicionário attrs
da macro substituem os atributos herdados com o mesmo nome. Também é possível
remover atributos herdados usando None
como um valor no dicionário
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,
},
...
)
O valor padrão dos atributos herdados não obrigatórios é sempre substituído por
None
, independentemente do valor padrão da definição de atributo original. Se
você precisar examinar ou modificar um atributo não obrigatório herdado, por
exemplo, se quiser adicionar uma tag a um atributo tags
herdado, é necessário
processar o caso None
na função de implementação da 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,
)
...
Implementação
implementation
aceita uma função que contém a lógica da macro.
As funções de implementação geralmente criam destinos chamando uma ou mais regras e
geralmente são particulares (nomeadas com um sublinhado inicial). Convencionalmente,
eles têm o mesmo nome da macro, mas são prefixados com _
e sufixados com
_impl
.
Ao contrário das funções de implementação de regras, que usam um único argumento (ctx
) que
contém uma referência aos atributos, as funções de implementação de macros aceitam um
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,
)
Se uma macro herdar atributos, a função de implementação dela precisa ter um
parâmetro de palavra-chave residual **kwargs
, que pode ser encaminhado para a chamada que
invocou a regra ou submacro herdada. Isso ajuda a garantir que sua macro não será
interrompida se a regra ou macro de que você está herdando adicionar um novo
atributo.
Declaração
As macros são declaradas carregando e chamando a definição delas em um arquivo BUILD
.
# pkg/BUILD
my_macro(
name = "macro_instance",
deps = ["src.cc"] + select(
{
"//config_setting:special": ["special_source.cc"],
"//conditions:default": [],
},
),
create_tests = True,
)
Isso criaria destinos
//pkg:macro_instance_cc_lib
e//pkg:macro_instance_test
.
Assim como nas chamadas de regra, se um valor de atributo em uma chamada de macro for definido como None
,
esse atributo será tratado como se tivesse sido omitido pelo autor da chamada da macro. Por
exemplo, as duas chamadas de macro a seguir são equivalentes:
# pkg/BUILD
my_macro(name = "abc", srcs = ["src.cc"], deps = None)
my_macro(name = "abc", srcs = ["src.cc"])
Isso geralmente não é útil em arquivos BUILD
, mas é útil quando
uma macro é agrupada de forma programática dentro de outra.
Detalhes
Convenções de nomenclatura para segmentações criadas
Os nomes de qualquer destino ou submacro criado por uma macro simbólica precisam
corresponder ao parâmetro name
da macro ou ter o prefixo name
seguido
por _
(preferido), .
ou -
. Por exemplo, my_macro(name = "foo")
pode criar apenas
arquivos ou destinos com o nome foo
ou com prefixo foo_
, foo-
ou foo.
,
por exemplo, foo_bar
.
É possível declarar destinos ou arquivos que violam a convenção de nomenclatura de macros, mas não é possível criá-los nem usá-los como dependências.
Os arquivos e destinos que não são macros no mesmo pacote de uma instância de macro não podem ter nomes que conflitam com possíveis nomes de destino de macro, embora essa exclusividade não seja aplicada. Estamos em processo de implementação da avaliação lenta como uma melhoria de desempenho para macros simbólicas, que serão prejudicadas em pacotes que violam o esquema de nomenclatura.
Restrições
As macros simbólicas têm algumas restrições adicionais em comparação com as macros legadas.
Macros simbólicas
- precisa receber um argumento
name
e um argumentovisibility
- precisa ter uma função
implementation
- não pode retornar valores
- não pode mudar os argumentos
- não podem chamar
native.existing_rules()
, a menos que sejam macrosfinalizer
especiais. - não pode ligar para
native.package()
- não pode ligar para
glob()
- não pode ligar para
native.environment_group()
- precisa criar destinos cujos nomes aderem ao esquema de nomenclatura
- não pode se referir a arquivos de entrada que não foram declarados ou transmitidos como um argumento (consulte visibilidade e macros para mais detalhes).
Visibilidade e macros
O sistema de visibilidade ajuda a proteger os detalhes de implementação das macros (simbólicas) e dos autores de chamada.
Por padrão, os alvos criados em uma macro simbólica ficam visíveis na própria
macro, mas não necessariamente para o autor da chamada. A macro pode "exportar" um
destino como uma API pública, encaminhando o valor do próprio atributo visibility
,
como em some_rule(..., visibility = visibility)
.
As principais ideias da visibilidade de macro são:
A visibilidade é verificada com base na macro que declarou o destino, não no pacote que chamou a macro.
- Em outras palavras, estar no mesmo pacote não torna um destino visível para outro. Isso protege as metas internas da macro para que não se tornem dependências de outras macros ou metas de nível superior no pacote.
Todos os atributos
visibility
, em regras e macros, incluem automaticamente o local em que a regra ou macro foi chamada.- Assim, um destino fica incondicionalmente visível para outros destinos declarados na
mesma macro (ou no arquivo
BUILD
, se não estiver em uma macro).
- Assim, um destino fica incondicionalmente visível para outros destinos declarados na
mesma macro (ou no arquivo
Na prática, isso significa que, quando uma macro declara um destino sem definir o
visibility
, o destino padrão é interno à macro. A
visibilidade padrão do pacote não
se aplica em uma macro. Exportar o destino significa que ele fica visível
para o autor da chamada especificado no atributo visibility
da macro,
além do pacote do autor da chamada da macro e do próprio código da macro.
Outra maneira de pensar nisso é que a visibilidade de uma macro determina quem
(além da própria macro) pode acessar os destinos exportados dela.
# 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",
]
)
Se my_macro
fosse chamado com visibility = ["//other_pkg:__pkg__"]
ou se
o pacote //pkg
tivesse definido o default_visibility
como esse valor,
//pkg:foo_exported
também poderia ser usado em //other_pkg/BUILD
ou em uma
macro definida em //other_pkg:defs.bzl
, mas //pkg:foo_helper
permaneceria
protegido.
Uma macro pode declarar que um destino está visível para um pacote amigo transmitindo
visibility = ["//some_friend:__pkg__"]
(para um destino interno) ou
visibility = visibility + ["//some_friend:__pkg__"]
(para um destino exportado).
É um antipadrão para uma macro declarar um destino com visibilidade
pública (visibility = ["//visibility:public"]
). Isso porque ele torna
o destino incondicionalmente visível para todos os pacotes, mesmo que o autor da chamada
especifique uma visibilidade mais restrita.
Todas as verificações de visibilidade são feitas em relação à macro simbólica mais interna em execução. No entanto, há um mecanismo de delegação de visibilidade: se uma macro transmitir um rótulo como um valor de atributo para uma macro interna, todos os usos do rótulo na macro interna serão verificados em relação à macro externa. Consulte a página de visibilidade para mais detalhes.
Lembre-se de que as macros legadas são totalmente transparentes para o sistema de visibilidade e se comportam como se o local fosse o arquivo BUILD ou a macro simbólica de onde foram chamadas.
Seleciona
Se um atributo for configurable
(padrão) e o valor dele não for None
,
a função de implementação de macro vai considerar o valor do atributo como envolvido
em um select
trivial. Isso facilita a detecção de bugs pelo autor da macro,
que não previu que o valor do atributo poderia ser um select
.
Por exemplo, considere esta macro:
my_macro = macro(
attrs = {"deps": attr.label_list()}, # configurable unless specified otherwise
implementation = _my_macro_impl,
)
Se my_macro
for invocado com deps = ["//a"]
, _my_macro_impl
será invocado com o parâmetro deps
definido como select({"//conditions:default":
["//a"]})
. Se isso causar a falha da função de implementação (por exemplo, porque o
código tentou indexar o valor como em deps[0]
, o que não é permitido para
select
s), o autor da macro poderá fazer uma escolha: reescrever
a macro para usar apenas operações compatíveis com select
ou marcar
o atributo como não configurável (attr.label_list(configurable = False)
). O
último garante que os usuários não possam transmitir um valor select
.
Os destinos de regra revertem essa transformação e armazenam select
s triviais como
valores condicionais. No exemplo acima, se _my_macro_impl
declarar um destino
de regra my_rule(..., deps = deps)
, o deps
desse destino será armazenado como
["//a"]
. Isso garante que o agrupamento de select
não cause valores select
banais armazenados em todos os destinos instanciados por macros.
Se o valor de um atributo configurável for None
, ele não será envolvido em uma
select
. Isso garante que testes como my_attr == None
ainda funcionem e que,
quando o atributo é encaminhado para uma regra com um padrão computado, a regra
se comporta corretamente, ou seja, como se o atributo não tivesse sido transmitido. Nem sempre é possível que um atributo receba um valor None
, mas isso pode acontecer com o tipo attr.label()
e com qualquer atributo não obrigatório herdado.
Finalizadores
Um finalizador de regras é uma macro simbólica especial que, independente da posição
lexical em um arquivo BUILD, é avaliada na fase final do carregamento de um pacote,
depois que todas as metas não finalizadoras foram definidas. Ao contrário das macros simbólicas
comuns, um finalizador pode chamar native.existing_rules()
, em que ele se comporta
de maneira um pouco diferente das macros legadas: ele retorna apenas o conjunto de
destinos de regras que não são finalizadores. O finalizador pode declarar o estado desse conjunto ou
definir novos alvos.
Para declarar um finalizador, chame macro()
com 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,
)
Preguiça
IMPORTANTE: estamos implementando a expansão e avaliação de macros preguiçosas. Este recurso ainda não está disponível.
Atualmente, todas as macros são avaliadas assim que o arquivo BUILD é carregado, o que pode afetar negativamente a performance de destinos em pacotes que também têm macros não relacionadas e caras. No futuro, as macros simbólicas não de finalização só serão avaliadas se forem necessárias para o build. O esquema de nomenclatura de prefixo ajuda o Bazel a determinar qual macro será expandida com base em um destino solicitado.
Solução de problemas de migração
Confira algumas dores de cabeça comuns na migração e como corrigi-las.
- Chamadas de macro legada
glob()
Mova a chamada glob()
para o arquivo BUILD (ou para uma macro legada chamada no
arquivo BUILD) e transmita o valor glob()
para a macro simbólica usando um
atributo de lista de rótulos:
# BUILD file
my_macro(
...,
deps = glob(...),
)
- A macro legada tem um parâmetro que não é um tipo
attr
válido do Starlark.
Use o máximo de lógica possível em uma macro simbólica aninhada, mas mantenha a macro de nível superior como uma macro legada.
- A macro legada chama uma regra que cria um destino que viola o esquema de nomenclatura
Tudo bem, mas não dependa do alvo "ofensivo". A verificação de nomeação será ignorada.