Macros

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 digitados (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 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 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.) 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 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 de 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
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 geralmente são privadas (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 da qual 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 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 chamados 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 name argumento e um visibility argumento
  • 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 macros finalizer especiais
  • não podem chamar native.package()
  • não podem chamar glob()
  • não podem chamar native.environment_group()
  • precisam criar destinos cujos nomes sigam o 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:

  1. 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.
  2. 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 BUILD se não estiver em uma macro).

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 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 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 a 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 ver o valor do atributo como encapsulado em um select. Isso facilita para o autor da macro detectar bugs em que ele não antecipou 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: ele pode 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.

  • 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 de lista de rótulos:

# BUILD file
my_macro(
    ...,
    deps = glob(...),
)
  • A macro legada tem um parâmetro que não é um tipo attr do 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.