Tutorial de regras

Informar um problema Acessar a origem

O Starlark é uma linguagem de configuração semelhante ao Python, originalmente desenvolvida para uso no Bazel e adotada por outras ferramentas. Os arquivos BUILD e .bzl do Bazel são escritos em um dialeto de Starlark, também conhecido como "linguagem do build", embora ele seja frequentemente chamado de "Starlark", especialmente ao enfatizar que um recurso é expresso na linguagem de build em vez de ser uma parte integrada ou "nativa" do Bazel. O Bazel amplia a linguagem principal com várias funções relacionadas ao build, como glob, genrule, java_binary e assim por diante.

Consulte a documentação do Bazel e do Starlark para mais detalhes, e o modelo de SIG de regras como ponto de partida para novos conjuntos de regras.

A regra vazia

Para criar sua primeira regra, crie o arquivo foo.bzl:

def _foo_binary_impl(ctx):
    pass

foo_binary = rule(
    implementation = _foo_binary_impl,
)

Ao chamar a função rule, você precisa definir uma função de callback. A lógica será incluída, mas você pode deixar a função vazia por enquanto. O argumento ctx fornece informações sobre o destino.

É possível carregar a regra e usá-la de um arquivo BUILD.

Crie um arquivo BUILD no mesmo diretório:

load(":foo.bzl", "foo_binary")

foo_binary(name = "bin")

Agora, o destino pode ser criado:

$ bazel build bin
INFO: Analyzed target //:bin (2 packages loaded, 17 targets configured).
INFO: Found 1 target...
Target //:bin up-to-date (nothing to build)

Mesmo que a regra não faça nada, ela já se comporta como outras: tem um nome obrigatório e oferece suporte a atributos comuns, como visibility, testonly e tags.

Modelo de avaliação

Antes de continuar, é importante entender como o código é avaliado.

Atualize foo.bzl com algumas instruções de exibição:

def _foo_binary_impl(ctx):
    print("analyzing", ctx.label)

foo_binary = rule(
    implementation = _foo_binary_impl,
)

print("bzl file evaluation")

e CRIAR:

load(":foo.bzl", "foo_binary")

print("BUILD file")
foo_binary(name = "bin1")
foo_binary(name = "bin2")

ctx.label corresponde ao rótulo do destino que está sendo analisado. O objeto ctx tem muitos campos e métodos úteis. Veja uma lista completa na referência da API.

Consulte o código:

$ bazel query :all
DEBUG: /usr/home/bazel-codelab/foo.bzl:8:1: bzl file evaluation
DEBUG: /usr/home/bazel-codelab/BUILD:2:1: BUILD file
//:bin2
//:bin1

Faça algumas observações:

  • "bzl file Assessment" é mostrado primeiro. Antes de avaliar o arquivo BUILD, o Bazel avalia todos os arquivos carregados. Se vários arquivos BUILD estiverem carregando foo.bzl, você verá apenas uma ocorrência de "bzl file Assessment", porque o Bazel armazena o resultado da avaliação em cache.
  • A função de callback _foo_binary_impl não é chamada. A consulta do Bazel carrega arquivos BUILD, mas não analisa destinos.

Para analisar os destinos, use cquery ("consulta configurada") ou o comando build:

$ bazel build :all
DEBUG: /usr/home/bazel-codelab/foo.bzl:2:5: analyzing //:bin1
DEBUG: /usr/home/bazel-codelab/foo.bzl:2:5: analyzing //:bin2
INFO: Analyzed 2 targets (0 packages loaded, 0 targets configured).
INFO: Found 2 targets...

Como você pode notar, _foo_binary_impl agora é chamado duas vezes: uma para cada destino.

Observe que nem "avaliação de arquivo bzl" nem "arquivo BUILD" são mostrados novamente, porque a avaliação de foo.bzl é armazenada em cache após a chamada para bazel query. Ele só emite instruções print quando elas são realmente executadas.

Criação de um arquivo

Para tornar sua regra mais útil, atualize-a para gerar um arquivo. Primeiro, declare o arquivo e dê um nome a ele. Neste exemplo, crie um arquivo com o mesmo nome do destino:

ctx.actions.declare_file(ctx.label.name)

Se você executar bazel build :all agora, receberá um erro:

The following files have no generating action:
bin2

Sempre que você declarar um arquivo, precisará informar ao Bazel como ele será gerado criando uma ação. Use ctx.actions.write para criar um arquivo com o conteúdo especificado.

def _foo_binary_impl(ctx):
    out = ctx.actions.declare_file(ctx.label.name)
    ctx.actions.write(
        output = out,
        content = "Hello\n",
    )

O código é válido, mas não faz nada:

$ bazel build bin1
Target //:bin1 up-to-date (nothing to build)

A função ctx.actions.write registrou uma ação, que ensinou o Bazel a gerar o arquivo. Mas o Bazel não vai criar o arquivo até que ele seja solicitado. A última coisa a fazer é informar ao Bazel que o arquivo é uma saída da regra, e não um arquivo temporário usado na implementação da regra.

def _foo_binary_impl(ctx):
    out = ctx.actions.declare_file(ctx.label.name)
    ctx.actions.write(
        output = out,
        content = "Hello!\n",
    )
    return [DefaultInfo(files = depset([out]))]

Confira as funções DefaultInfo e depset posteriormente. Por enquanto, suponha que a última linha seja a maneira de escolher os resultados de uma regra.

Agora, execute o Bazel:

$ bazel build bin1
INFO: Found 1 target...
Target //:bin1 up-to-date:
  bazel-bin/bin1

$ cat bazel-bin/bin1
Hello!

Você gerou um arquivo com sucesso!

Atributos

Para tornar a regra mais útil, adicione novos atributos usando o módulo attr e atualize a definição da regra.

Adicione um atributo de string com o nome username:

foo_binary = rule(
    implementation = _foo_binary_impl,
    attrs = {
        "username": attr.string(),
    },
)

Depois, defina-o no arquivo BUILD:

foo_binary(
    name = "bin",
    username = "Alice",
)

Para acessar o valor na função de callback, use ctx.attr.username. Exemplo:

def _foo_binary_impl(ctx):
    out = ctx.actions.declare_file(ctx.label.name)
    ctx.actions.write(
        output = out,
        content = "Hello {}!\n".format(ctx.attr.username),
    )
    return [DefaultInfo(files = depset([out]))]

É possível tornar o atributo obrigatório ou definir um valor padrão. Consulte a documentação de attr.string. Também é possível usar outros tipos de atributos, como boolean ou lista de números inteiros.

Dependências

Os atributos de dependência, como attr.label e attr.label_list, declaram uma dependência do destino que possui o atributo para o destino cujo rótulo aparece no valor do atributo. Esse tipo de atributo forma a base do gráfico de destino.

No arquivo BUILD, o rótulo de destino aparece como um objeto de string, por exemplo, //pkg:name. Na função de implementação, o destino estará acessível como um objeto Target. Por exemplo, veja os arquivos retornados pelo destino usando Target.files.

Vários arquivos

Por padrão, somente destinos criados por regras podem aparecer como dependências (como um destino foo_library()). Para que o atributo aceite destinos que sejam arquivos de entrada (por exemplo, arquivos de origem no repositório), use allow_files e especifique a lista de extensões de arquivo aceitas (ou True para permitir qualquer extensão de arquivo):

"srcs": attr.label_list(allow_files = [".java"]),

Acesse a lista de arquivos usando ctx.files.<attribute name>. Por exemplo, a lista de arquivos no atributo srcs pode ser acessada

ctx.files.srcs

Arquivo único

Se você precisar de apenas um arquivo, use allow_single_file:

"src": attr.label(allow_single_file = [".java"])

Esse arquivo pode ser acessado em ctx.file.<attribute name>:

ctx.file.src

Criar um arquivo com um modelo

Você pode criar uma regra que gere um arquivo .cc com base em um modelo. Além disso, é possível usar ctx.actions.write para gerar uma string criada na função de implementação de regra, mas isso tem dois problemas. Primeiro, à medida que o modelo aumenta, melhora a eficiência da memória ao colocá-lo em um arquivo separado, evitando a criação de strings grandes durante a fase de análise. Em segundo lugar, usar um arquivo separado é mais conveniente para o usuário. Em vez disso, use ctx.actions.expand_template, que executa substituições em um arquivo de modelo.

Crie um atributo template para declarar uma dependência no arquivo de modelo:

def _hello_world_impl(ctx):
    out = ctx.actions.declare_file(ctx.label.name + ".cc")
    ctx.actions.expand_template(
        output = out,
        template = ctx.file.template,
        substitutions = {"{NAME}": ctx.attr.username},
    )
    return [DefaultInfo(files = depset([out]))]

hello_world = rule(
    implementation = _hello_world_impl,
    attrs = {
        "username": attr.string(default = "unknown person"),
        "template": attr.label(
            allow_single_file = [".cc.tpl"],
            mandatory = True,
        ),
    },
)

Os usuários podem usar a regra assim:

hello_world(
    name = "hello",
    username = "Alice",
    template = "file.cc.tpl",
)

cc_binary(
    name = "hello_bin",
    srcs = [":hello"],
)

Se você não quiser expor o modelo para o usuário final e sempre usar o mesmo, defina um valor padrão e torne o atributo particular:

    "_template": attr.label(
        allow_single_file = True,
        default = "file.cc.tpl",
    ),

Atributos que começam com um sublinhado são particulares e não podem ser definidos em um arquivo BUILD. O modelo agora é uma dependência implícita: cada destino hello_world tem uma dependência nesse arquivo. Não se esqueça de tornar esse arquivo visível para outros pacotes atualizando o arquivo BUILD e usando exports_files:

exports_files(["file.cc.tpl"])

Mais informações