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: simbólicas, que são descritas nesta página, e legadas. Sempre que possível, recomendamos o uso de macros simbólicas para clareza do código.
As macros simbólicas oferecem argumentos tipados (conversão de string para rótulo, em relação ao
local em que 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 .bzl arquivos chamando a
macro() função 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 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
configurable parâmetro, que determina se o atributo aceita
selects. Se um atributo for configurable, ele vai analisar valores não select
como um select não configurável – "foo" 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 inalterados, 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 da regra ou
macro para o argumento macro()'s
inherit_attrs. 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.)
Apenas os 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 attrs
dicionário:
# 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 para
ser None, independentemente do valor padrão da definição do atributo original. 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, verifique se o caso None foi processado 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 à esquerda). Convencionalmente,
elas têm o mesmo nome da macro, mas com o prefixo _ e o sufixo
_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 precisa ter um
**kwargs parâmetro de palavra-chave residual, que pode ser encaminhado para a chamada que
invoca a regra ou submacro herdada. Isso ajuda a garantir que a 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.
```starlark
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 destinos criados
Os nomes de todos os destinos ou submacros criados por uma macro simbólica precisam
corresponder ao parâmetro name da macro ou ter o prefixo name seguido
por _ (preferencial), . ou -. Por exemplo, my_macro(name = "foo") só pode
criar arquivos ou destinos nomeados foo, ou prefixados por foo_, foo- ou foo.,
por exemplo, foo_bar.
Os destinos ou arquivos que violam a convenção de nomenclatura de macros podem ser declarados, mas não podem ser criados e não podem ser usados como dependências.
Arquivos e destinos não macro no mesmo pacote que uma instância de macro devem não 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
- precisam usar um
nameargumento e umvisibilityargumento - precisam ter uma função
implementation - não podem retornar valores
- não podem mudar os argumentos
- não podem chamar
native.existing_rules()a menos que sejam macrosfinalizerespeciais - não podem chamar
native.package() - não podem chamar
glob() - não podem chamar
native.environment_group() - precisam criar destinos cujos nomes aderem ao esquema de nomenclatura
- não podem 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 autores da chamada (consulte Visibilidade e macros para mais detalhes).
Visibilidade e macros
O sistema de visibilidade ajuda a proteger os detalhes de implementação de macros (simbólicas) e dos autores da chamada.
Por padrão, os destinos criados em uma macro simbólica são visíveis na própria macro, mas não necessariamente para o autor da chamada da macro. A macro pode "exportar" um
destino como uma API pública encaminhando o valor do próprio visibility
atributo, como em some_rule(..., visibility = visibility).
As principais ideias de 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, em regras e macros, incluem automaticamente o local em que a regra ou macro foi chamada.- Assim, um destino é incondicionalmente visível para outros destinos declarados na
mesma macro (ou no arquivo
BUILDse não estiver em uma macro).
- Assim, um destino é 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 seu
visibility, o destino é definido como interno para a macro. A visibilidade padrão do pacote
não
se aplica a 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 da 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",
]
)
Se my_macro fosse chamada com visibility = ["//other_pkg:__pkg__"], ou se
o pacote //pkg tivesse definido default_visibility como esse valor, então
//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 exportado).
É um antipadrão para uma macro declarar um destino com visibilidade
pública (visibility = ["//visibility:public"]). Isso ocorre porque torna
o destino incondicionalmente visível para todos os pacotes, mesmo que o autor da chamada
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 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 delas fosse qualquer arquivo BUILD ou macro simbólica de que foram chamadas.
Finalizadores e visibilidade
Os destinos declarados em um finalizador de regra, além de ver os destinos seguindo as regras de visibilidade de macro simbólica usuais, podem também ver todos os destinos que são visíveis para o pacote de destino do finalizador.
Isso significa que, se você migrar uma macro legada baseada em native.existing_rules() para
um finalizador, os destinos declarados pelo finalizador ainda poderão ver
as dependências antigas.
No entanto, observe que é possível declarar um destino em uma macro simbólica para que os destinos de um finalizador não possam vê-lo no sistema de visibilidade, mesmo que o finalizador possa introspecionar os atributos usando
native.existing_rules().
Selecionar
Se um atributo for configurable (o padrão) e o valor dele não for None,
a função de implementação da macro vai considerar o valor do atributo como encapsulado
em um select trivial. Isso facilita para o autor da macro detectar bugs
em que ele não previu que o valor do atributo poderia 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 invocada com deps = ["//a"], isso fará com que _my_macro_impl
seja invocada com o parâmetro deps definido como select({"//conditions:default":
["//a"]}). Se isso fizer com que a função de implementação falhe (por exemplo, porque o
código tentou indexar o valor como em deps[0], o que não é permitido para
selects), 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.
Os destinos de regra invertem essa transformação e armazenam selects triviais como seus
valores incondicionais. No exemplo acima, se _my_macro_impl declarar um destino de regra
my_rule(..., deps = deps), o deps desse destino de regra será armazenado como
["//a"]. Isso garante que o encapsulamento select não faça com que valores select
triviais sejam armazenados em todos os destinos instanciados por macros.
Se o valor de um atributo configurável for None, ele não será encapsulado 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 para o tipo attr.label() e para qualquer atributo herdado não obrigatório.
Finalizadores
Um finalizador de regra é uma macro simbólica especial que, independentemente da posição léxica
em um arquivo BUILD, é avaliada na fase final do carregamento de um pacote,
depois que todos os destinos não finalizadores foram definidos. 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 declarar o estado desse conjunto ou
definir novos destinos.
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 lentas. O 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 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, considerando um destino solicitado.
Solução de problemas de migração
Confira alguns problemas comuns de migração e como corrigi-los.
- A macro legada chama
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 de lista de rótulos:
# BUILD file
my_macro(
...,
deps = glob(...),
)
- A macro legada tem um parâmetro que não é um tipo
attrdo Starlark válido.
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.
- A macro legada chama uma regra que cria um destino que quebra o esquema de nomenclatura
Tudo bem, basta não depender do destino "ofensivo". A verificação de nomenclatura será ignorada.