Macros legadas

As macros legadas são funções não estruturadas chamadas de arquivos BUILD que podem criar destinos. No final da fase de carregamento, as macros legadas não existem mais, e o Bazel só vê o conjunto concreto de regras instanciadas.

Por que não usar macros legadas (e usar macros simbólicas)

Sempre que possível, use macros simbólicas.

Macros simbólicas

  • Impedir ações à distância
  • Permitir ocultar detalhes de implementação com visibilidade granular
  • Aceitar atributos digitados, o que significa conversão automática de rótulos e seleção.
  • Ser mais legível
  • Em breve, terá avaliação lenta

Uso

O caso de uso típico de uma macro é quando você quer reutilizar uma regra.

Por exemplo, genrule em um arquivo BUILD gera um arquivo usando //:generator com um argumento some_arg codificado no comando:

genrule(
    name = "file",
    outs = ["file.txt"],
    cmd = "$(location //:generator) some_arg > $@",
    tools = ["//:generator"],
)

Se você quiser gerar mais arquivos com argumentos diferentes, extraia esse código para uma função de macro. Para criar uma macro chamada file_generator, que tem parâmetros name e arg, podemos substituir a genrule pelo seguinte:

load("//path:generator.bzl", "file_generator")

file_generator(
    name = "file",
    arg = "some_arg",
)

file_generator(
    name = "file-two",
    arg = "some_arg_two",
)

file_generator(
    name = "file-three",
    arg = "some_arg_three",
)

Aqui, você carrega o símbolo file_generator de um arquivo .bzl localizado no pacote //path. Ao colocar definições de função de macro em um arquivo .bzl separado, você mantém seus arquivos BUILD limpos e declarativos. O arquivo .bzl pode ser carregado de qualquer pacote no espaço de trabalho.

Por fim, em path/generator.bzl, escreva a definição da macro para encapsular e parametrizar a definição original da genrule:

def file_generator(name, arg, visibility=None):
  native.genrule(
    name = name,
    outs = [name + ".txt"],
    cmd = "$(location //:generator) %s > $@" % arg,
    tools = ["//:generator"],
    visibility = visibility,
  )

Também é possível usar macros para encadear regras. Este exemplo mostra genrules encadeadas, em que uma genrule usa as saídas de uma genrule anterior como entradas:

def chained_genrules(name, visibility=None):
  native.genrule(
    name = name + "-one",
    outs = [name + ".one"],
    cmd = "$(location :tool-one) $@",
    tools = [":tool-one"],
    visibility = ["//visibility:private"],
  )

  native.genrule(
    name = name + "-two",
    srcs = [name + ".one"],
    outs = [name + ".two"],
    cmd = "$(location :tool-two) $< $@",
    tools = [":tool-two"],
    visibility = visibility,
  )

O exemplo só atribui um valor de visibilidade à segunda genrule. Isso permite que os autores de macros ocultem as saídas de regras intermediárias para que não dependam de outros destinos no espaço de trabalho.

Como expandir macros

Quando quiser investigar o que uma macro faz, use o comando query com --output=build para conferir o formulário expandido:

$ bazel query --output=build :file
# /absolute/path/test/ext.bzl:42:3
genrule(
  name = "file",
  tools = ["//:generator"],
  outs = ["//test:file.txt"],
  cmd = "$(location //:generator) some_arg > $@",
)

Como instanciar regras nativas

As regras nativas (regras que não precisam de uma instrução load()) podem ser instanciadas no módulo nativo:

def my_macro(name, visibility=None):
  native.cc_library(
    name = name,
    srcs = ["main.cc"],
    visibility = visibility,
  )

Se você precisar saber o nome do pacote (por exemplo, qual arquivo BUILD está chamando a macro), use a função native.package_name(). Observe que native só pode ser usado em arquivos .bzl, e não em arquivos BUILD.

Resolução de rótulos em macros

Como as macros legadas são avaliadas na fase de carregamento, as strings de rótulo, como "//foo:bar", que ocorrem em uma macro legada são interpretadas em relação ao arquivo BUILD em que a macro é usada, e não em relação ao arquivo .bzl em que ela é definida. Esse comportamento geralmente é indesejável para macros que devem ser usadas em outros repositórios, como porque fazem parte de um conjunto de regras do Starlark publicado.

Para ter o mesmo comportamento das regras do Starlark, envolva as strings de rótulo com o Label construtor:

# @my_ruleset//rules:defs.bzl
def my_cc_wrapper(name, deps = [], **kwargs):
  native.cc_library(
    name = name,
    deps = deps + select({
      # Due to the use of Label, this label is resolved within @my_ruleset,
      # regardless of its site of use.
      Label("//config:needs_foo"): [
        # Due to the use of Label, this label will resolve to the correct target
        # even if the canonical name of @dep_of_my_ruleset should be different
        # in the main repo, such as due to repo mappings.
        Label("@dep_of_my_ruleset//tools:foo"),
      ],
      "//conditions:default": [],
    }),
    **kwargs,
  )

Com a flag --incompatible_resolve_select_keys_eagerly ativada, todas as chaves que são strings de rótulo serão resolvidas automaticamente para Label objetos em relação ao pacote do arquivo que contém a chamada select. Se essa opção não for escolhida, envolva a string de rótulo com native.package_relative_label().

Depuração

  • bazel query --output=build //my/path:all mostra como o arquivo BUILD fica após a avaliação. Todas as macros legadas, globs e loops são expandidos. Limitação conhecida: as expressões select não são mostradas na saída.

  • É possível filtrar a saída com base em generator_function (qual função gerou as regras) ou generator_name (o atributo de nome da macro): bash $ bazel query --output=build 'attr(generator_function, my_macro, //my/path:all)'

  • Para descobrir onde exatamente a regra foo é gerada em um arquivo BUILD, tente o truque a seguir. Insira esta linha perto da parte de cima do BUILD arquivo: cc_library(name = "foo"). Execute o Bazel. Você vai receber uma exceção quando a regra foo for criada (devido a um conflito de nomes), que mostrará o stack trace completo.

  • Também é possível usar a impressão para depuração. Ela mostra a mensagem como uma linha de registro DEBUG durante a fase de carregamento. Exceto em casos raros, remova as chamadas print ou torne-as condicionais em um parâmetro debugging que seja definido como False por padrão antes de enviar o código para o depósito.

Erros

Se você quiser gerar um erro, use a fail função. Explique claramente ao usuário o que deu errado e como corrigir o arquivo BUILD. Não é possível detectar um erro.

def my_macro(name, deps, visibility=None):
  if len(deps) < 2:
    fail("Expected at least two values in deps")
  # ...

Convenções

  • Todas as funções públicas (funções que não começam com sublinhado) que instanciam regras precisam ter um argumento name. Esse argumento não pode ser opcional (não forneça um valor padrão).

  • As funções públicas precisam usar uma docstring seguindo as convenções do Python.

  • Em arquivos BUILD, o argumento name das macros precisa ser um argumento de palavra-chave (não um argumento posicional).

  • O atributo name das regras geradas por uma macro precisa incluir o argumento de nome como um prefixo. Por exemplo, macro(name = "foo") pode gerar um cc_library foo e uma genrule foo_gen.

  • Na maioria dos casos, os parâmetros opcionais precisam ter um valor padrão de None. None pode ser transmitido diretamente para regras nativas, que o tratam da mesma forma que se você não tivesse transmitido nenhum argumento. Portanto, não é necessário substituí-lo por 0, False ou [] para essa finalidade. Em vez disso, a macro precisa ser adiada para as regras que ela cria, já que os padrões podem ser complexos ou mudar com o tempo. Além disso, um parâmetro definido explicitamente como o valor padrão parece diferente de um que nunca é definido (ou definido como None) quando acessado pelo idioma de consulta ou pelos internos do sistema de build.

  • As macros precisam ter um argumento visibility opcional.