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 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 selects. 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 argumento visibility
  • 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 macros finalizer 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:

  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 as metas internas da macro para que não se tornem dependências de outras macros ou metas 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 fica 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 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 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)). O último garante que os usuários não possam transmitir um valor select.

Os destinos de regra revertem essa transformação e armazenam selects 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.