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á duas opções de macros: macros simbólicas, descritas nesta página, e macros legadas. Sempre que possível, recomendamos o uso de macros simbólicas para maior clareza do código.

As macros simbólicas oferecem argumentos com tipo (string para conversão de 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. 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.

Uso

As macros são definidas em arquivos .bzl chamando a função macro() com dois parâmetros: attrs e implementation.

Atributos

attrs aceita um dicionário de nome de atributo para tipos de atributos, que representa os argumentos para a macro. Dois atributos comuns, nome e visibilidade, são adicionados implicitamente a todas as macros e não são incluídos no dicionário transmitido para atributos.

# 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 nas seleções.

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, elas são nomeadas com o mesmo nome da macro, mas têm o prefixo _ e _impl como sufixo.

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

Declaração

As macros são declaradas carregando e chamando sua definição 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.

Detalhes

convenções de nomenclatura para segmentações criadas

Os nomes dos 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 com o nome foo ou prefixado por 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 argumento visibility
  • precisa ter uma função implementation
  • não pode retornar valores
  • não pode mudar o args
  • não podem chamar native.existing_rules(), a menos que sejam macros finalizer especiais
  • pode não ligar para native.package()
  • pode não 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 para mais detalhes).

Visibilidade

O que fazer: expandir esta seção

Visibilidade desejada

Por padrão, os destinos criados por macros simbólicas são visíveis para o pacote em que são criados. Elas também aceitam um atributo visibility, que pode expandir essa visibilidade para o autor da chamada da macro (transmitindo o atributo visibility diretamente da chamada da macro para o destino criado) e para outros pacotes (especificando-os explicitamente na visibilidade do destino).

Visibilidade da dependência

As macros devem ter visibilidade dos arquivos e destinos aos quais se referem. Isso pode ser feito das seguintes maneiras:

  • Transmitido explicitamente como um valor attr para a macro.

# pkg/BUILD
my_macro(... deps = ["//other_package:my_tool"] )
  • Padrão implícito de um valor attr
# my_macro:macro.bzl
my_macro = macro(
  attrs = {"deps" : attr.label_list(default = ["//other_package:my_tool"])} )
  • Já está visível para a definição da macro
# other_package/BUILD
cc_binary(
    name = "my_tool",
    visibility = "//my_macro:\\__pkg__",
)

Seleciona

Se um atributo for configurable, a função de implementação de macro sempre vai encontrar o valor do atributo como 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"], _my_macro_impl será invocado com o parâmetro deps definido como select({"//conditions:default": ["//a"]}).

Os destinos de regra revertem essa transformação e armazenam selects triviais como valores condicionais. Neste exemplo, se _my_macro_impl declarar um destino de regra my_rule(..., deps = deps), o deps desse destino será armazenado como ["//a"].

Finalizadores

Um finalizador de regra é uma macro simbólica especial que, independentemente 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 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 do que nas macros legadas: ele retorna apenas o conjunto de destinos de regras nã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 em processo de implementação de expansão e avaliação lentas de macros. 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 o desempenho de destinos em pacotes que também têm macros não relacionadas 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 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 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 nomenclatura será ignorada silenciosamente.