Macros

Informar um problema Ver fonte Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

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

  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, 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).

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 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.

As metas de regra invertem essa transformação e armazenam selects 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.