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

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

Consulte Visibilidade para uma discussão detalhada sobre visibilidade no Bazel.

Segmentação por visibilidade

Por padrão, os destinos criados por uma macro simbólica são visíveis apenas no pacote que contém o arquivo .bzl que define a macro. Especificamente, elas não são visíveis para o autor da chamada da macro simbólica, a menos que o autor esteja no mesmo pacote do arquivo .bzl da macro.

Para tornar um destino visível para o autor da chamada da macro simbólica, transmita visibility = visibility para a regra ou a macro interna. Você também pode tornar o alvo visível em outros pacotes, a ele uma visibilidade mais ampla (ou até pública).

A visibilidade padrão de um pacote (conforme declarado em package()) é transmitida por padrão para o parâmetro visibility da macro mais externa, mas cabe à macro transmitir (ou não!) esse visibility para os destinos que ela instancia.

Visibilidade da dependência

Os destinos mencionados na implementação de uma macro precisam estar visíveis para a definição dela. A visibilidade pode ser dada de uma das seguintes maneiras:

  • Os destinos ficam visíveis para uma macro se forem transmitidos a ela por rótulos, listas de rótulos ou atributos de dicionário com chave ou valor de rótulo, de forma explícita:

# pkg/BUILD
my_macro(... deps = ["//other_package:my_tool"] )
  • ... ou como valores padrão de atributos:
# my_macro:macro.bzl
my_macro = macro(
  attrs = {"deps" : attr.label_list(default = ["//other_package:my_tool"])},
  ...
)
  • Os destinos também são visíveis para uma macro se forem declarados como visíveis para o pacote que contém o arquivo .bzl que define a macro:
# other_package/BUILD
# Any macro defined in a .bzl file in //my_macro package can use this tool.
cc_binary(
    name = "my_tool",
    visibility = "//my_macro:\\__pkg__",
)

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 assuma 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 nome será ignorada.