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 do 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: as simbólicas, que são descritas nesta página, e as legadas. Sempre que possível, recomendamos usar macros simbólicas para clareza do código.
As macros simbólicas oferecem argumentos tipados (conversão de string para rótulo, relativa a
onde a macro foi chamada) e a capacidade de restringir e especificar a
visibilidade dos destinos criados. Elas são projetadas para serem adequadas à avaliação
lenta (que será adicionada em uma versão futura do Bazel). As macros simbólicas estão disponíveis por padrão no Bazel 8. Quando este documento menciona macros
, ele se refere a macros simbólicas.
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 da macro. Dois atributos comuns, name
e visibility
, são adicionados implicitamente a todas as macros e não estã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 vai analisar valores não select
como um select
não configurável. "foo"
vai se tornar select({"//conditions:default": "foo"})
. Saiba mais em selecionar.
Herança de atributos
As macros geralmente são destinadas a encapsular uma regra (ou outra macro), e o autor da macro geralmente quer encaminhar a maior parte dos atributos do símbolo encapsulado sem alterações, usando **kwargs
, para o destino principal da macro (ou macro interna principal).
Para oferecer suporte a esse padrão, uma macro pode herdar atributos de uma regra ou outra macro transmitindo o símbolo de regra ou macro ao argumento inherit_attrs
de macro()
. Também é possível usar a string especial "common"
em vez de um símbolo de regra ou macro para herdar os atributos comuns definidos para
todas as regras de build do
Starlark.
Somente atributos públicos são herdados, e os atributos no próprio 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
, independente do valor padrão da definição original do atributo. Se você precisar examinar ou modificar um atributo herdado não obrigatório, por exemplo, se quiser adicionar uma tag a um atributo tags
herdado, processe o caso None
na função de implementação da sua 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,
)
...
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
normalmente 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 deverá ter um
parâmetro de palavra-chave residual **kwargs
, que pode ser encaminhado para a chamada que
invoca a regra ou submacro herdada. Isso ajuda a garantir que sua macro não seja 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 os 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 ao encapsular programaticamente uma macro dentro de outra.
Detalhes
Convenções de nomenclatura para segmentações criadas
Os nomes de todos os destinos ou submácros criados por uma macro simbólica precisam
corresponder ao parâmetro name
da macro ou ser prefixados por name
seguido
por _
(preferencial), .
ou -
. Por exemplo, my_macro(name = "foo")
só pode
criar arquivos ou destinos chamados foo
ou com prefixo foo_
, foo-
ou foo.
,
por exemplo, foo_bar
.
Destinos ou arquivos que violam a convenção de nomenclatura de macros podem ser declarados, mas não podem ser criados nem usados como dependências.
Arquivos e destinos não macro no mesmo pacote que uma instância de macro não podem ter nomes que entrem em conflito com possíveis nomes de destino de macro, embora essa exclusividade não seja aplicada. Estamos implementando a avaliação lenta como uma melhoria de performance para macros simbólicas, que será prejudicada 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 usar um argumento
name
e um argumentovisibility
- precisa ter uma função
implementation
- não pode retornar valores
- não podem mudar os argumentos
- não pode chamar
native.existing_rules()
, a menos que sejam macros especiais definalizer
- não pode ligar para
native.package()
- não pode ligar para
glob()
- não pode ligar para
native.environment_group()
- precisam criar destinos cujos nomes sigam o esquema de nomenclatura
- não pode se referir a arquivos de entrada que não foram declarados ou transmitidos como um argumento
- não podem se referir a destinos particulares dos chamadores. 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 chamadores delas.
Por padrão, os destinos criados em uma macro simbólica ficam visíveis dentro da própria macro, mas não necessariamente para o chamador dela. 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 os destinos internos da macro de se tornarem dependências de outras macros ou destinos de nível superior no pacote.
Todos os atributos
visibility
, tanto em regras quanto em macros, incluem automaticamente o lugar em que a regra ou macro foi chamada.- Assim, um destino fica visível incondicionalmente para outros destinos declarados na mesma macro (ou no arquivo
BUILD
, se não estiver em uma macro).
- Assim, um destino fica visível incondicionalmente 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 é interno à macro por padrão. A visibilidade padrão do pacote não se aplica em uma macro. Exportar o destino significa que ele fica visível para o que o autor da chamada da macro especificou no atributo visibility
da macro, além do pacote do próprio autor da chamada da macro e do código da macro.
Outra maneira de pensar nisso é que a visibilidade de uma macro determina quem (além da própria macro) pode ver 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 default_visibility
com 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"]
), porque isso torna o destino incondicionalmente visível para todos os pacotes, mesmo que o caller tenha especificado uma visibilidade mais restrita.
Toda a verificação de visibilidade é feita em relação à macro simbólica mais interna em execução no momento. 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 a localização delas fosse qualquer arquivo BUILD ou macro simbólica de onde foram chamadas.
Finalizadores e visibilidade
Além de ver os destinos seguindo as regras normais de visibilidade de macro simbólica, os destinos declarados em um finalizador de regra também podem ver todos os destinos visíveis para o pacote do destino do finalizador.
Isso significa que, se você migrar uma macro legada baseada em native.existing_rules()
para
um finalizador, os destinos declarados por ele ainda poderão ver
as dependências antigas.
No entanto, é possível declarar uma meta em uma macro simbólica de forma que as metas de um finalizador não possam vê-la no sistema de visibilidade, mesmo que o finalizador possa introspecionar os atributos usando native.existing_rules()
.
Seleciona
Se um atributo for configurable
(o 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 identificação de bugs pelo autor da macro
quando ele não prevê que o valor do atributo pode ser um select
.
Por exemplo, considere a seguinte 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"]
, isso fará com que _my_macro_impl
seja invocado com o parâmetro deps
definido como select({"//conditions:default":
["//a"]})
. Se isso causar falha na 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)
). A
última opção garante que os usuários não possam transmitir um valor select
.
As metas de regra invertem essa transformação e armazenam select
s triviais como valores incondicionais. No exemplo acima, se _my_macro_impl
declarar uma meta de regra my_rule(..., deps = deps)
, o deps
dessa meta será armazenado como ["//a"]
. Isso garante que o encapsulamento de select
não faça com que valores triviais de select
sejam armazenados em todos os destinos instanciados por macros.
Se o valor de um atributo configurável for None
, ele não será envolvido em um
select
. Isso garante que testes como my_attr == None
ainda funcionem e que, quando o atributo for encaminhado para uma regra com um padrão calculado, a regra se comporte corretamente (ou seja, como se o atributo não tivesse sido transmitido). Nem sempre é possível que um atributo assuma um valor None
, mas isso pode acontecer com o tipo attr.label()
e com qualquer atributo herdado não obrigatório.
Finalizadores
Um finalizador de regra é uma macro simbólica especial que, independente da posição léxica em um arquivo BUILD, é avaliada na etapa final do carregamento de um pacote, depois que todas as metas não finalizadoras são 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 só retorna o conjunto de destinos de regra não finalizadores. O finalizador pode fazer asserções sobre o estado desse conjunto ou definir novas metas.
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 a avaliação de macros lentas. Este recurso ainda não está disponível.
No momento, 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 caras. No futuro, as macros simbólicas não finalizadoras só serão avaliadas se forem necessárias para o build. O esquema de nomenclatura de prefixo ajuda o Bazel a determinar qual macro expandir para um determinado destino solicitado.
Solução de problemas de migração
Confira alguns problemas comuns de migração e como resolvê-los.
- Chamadas de macro legadas
glob()
Mova a chamada glob()
para o arquivo BUILD (ou para uma macro legada chamada do arquivo BUILD) e transmita o valor glob()
para a macro simbólica usando um atributo label-list:
# BUILD file
my_macro(
...,
deps = glob(...),
)
- A macro legada tem um parâmetro que não é um tipo
attr
válido do Starlark.
Extraia o máximo de lógica possível para uma macro simbólica aninhada, mas mantenha a macro de nível superior como uma macro legada.
- Uma macro legada chama uma regra que cria um destino que viola o esquema de nomenclatura.
Não tem problema, só não dependa da meta "infratora". A verificação de nomenclatura será ignorada silenciosamente.