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ólicas.
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 eles não podem ser criados nem usados 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
Consulte Visibilidade para uma discussão detalhada sobre visibilidade no Bazel.
Segmentação por visibilidade
Por padrão, os destinos criados por uma macro simbólica são visíveis apenas no pacote que contém o arquivo .bzl que define a macro. Especificamente, elas não são visíveis para o autor da chamada da macro simbólica, a menos que o autor esteja no mesmo pacote do arquivo .bzl da macro.
Para tornar um destino visível para o autor da chamada da macro simbólica, transmita
visibility = visibility
para a regra ou a macro interna. Você também pode tornar o
alvo visível em outros pacotes, a ele uma visibilidade mais ampla (ou até pública).
A visibilidade padrão de um pacote (conforme declarado em package()
) é transmitida por padrão
para o parâmetro visibility
da macro mais externa, mas cabe à macro transmitir (ou não!) esse visibility
para os destinos que ela instancia.
Visibilidade da dependência
Os destinos mencionados na implementação de uma macro precisam estar visíveis para a definição dela. A visibilidade pode ser dada de uma das seguintes maneiras:
- Os destinos ficam visíveis para uma macro se forem transmitidos a ela por rótulos, listas de rótulos ou atributos de dicionário com chave ou valor de rótulo, de forma explícita:
# pkg/BUILD
my_macro(... deps = ["//other_package:my_tool"] )
- ... ou como valores padrão de atributos:
# my_macro:macro.bzl
my_macro = macro(
attrs = {"deps" : attr.label_list(default = ["//other_package:my_tool"])},
...
)
- Os destinos também são visíveis para uma macro se forem declarados como visíveis para o pacote que contém o arquivo .bzl que define a 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__",
)
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 assuma 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 nome será ignorada.