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 do
Starlark conhecido como "Linguagem de build", embora seja frequentemente
chamado apenas 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 aumenta 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 do 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, é necessário definir uma função de callback. A lógica vai para lá, mas você
pode deixar a função vazia por enquanto. O ctx argumento
fornece informações sobre o destino.
É possível carregar a regra e usá-la em 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 regras: 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 impressão:
def _foo_binary_impl(ctx):
print("analyzing", ctx.label)
foo_binary = rule(
implementation = _foo_binary_impl,
)
print("bzl file evaluation")
e CRIE:
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. Você pode encontrar uma lista exaustiva 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 evaluation" é impresso primeiro. Antes de avaliar o arquivo
BUILD, o Bazel avalia todos os arquivos que ele carrega. Se váriosBUILDarquivos estiverem carregando foo.bzl, você verá apenas uma ocorrência de "bzl file evaluation" porque o Bazel armazena em cache o resultado da avaliação. - A função de callback
_foo_binary_implnão é chamada. A consulta do Bazel carregaBUILDarquivos, mas não analisa destinos.
Para analisar os destinos, use o cquery ("configurada
consulta") ou o build comando:
$ 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 ver, _foo_binary_impl agora é chamado duas vezes, uma para cada destino.
Observe que "bzl file evaluation" e "BUILD file" não são impressos novamente,
porque a avaliação de foo.bzl é armazenada em cache após a chamada para bazel query.
O Bazel só emite instruções print quando elas são realmente executadas.
Como criar 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, vai receber um erro:
The following files have no generating action:
bin2
Sempre que você declara um arquivo, é necessário informar ao Bazel como gerá-lo
criando uma ação. Use ctx.actions.write,
para criar um arquivo com o conteúdo fornecido.
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 fará nada:
$ bazel build bin1
Target //:bin1 up-to-date (nothing to build)
A função ctx.actions.write registrou uma ação que ensinou ao Bazel
como gerar o arquivo. No entanto, o Bazel não vai criar o arquivo até que ele seja
solicitado. Portanto, 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]))]
Consulte as funções DefaultInfo e depset mais tarde. Por enquanto,
suponha que a última linha seja a maneira de escolher as saídas 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.
Atributos
Para tornar a regra mais útil, adicione novos atributos usando
o attr módulo e atualize a definição da regra.
Adicione um atributo de string chamado username:
foo_binary = rule(
implementation = _foo_binary_impl,
attrs = {
"username": attr.string(),
},
)
Em seguida, defina-o no arquivo BUILD:
foo_binary(
name = "bin",
username = "Alice",
)
Para acessar o valor na função de callback, use ctx.attr.username. Por
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.
Você também pode usar outros tipos de atributos, como booleano
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, como
//pkg:name. Na função de implementação, o destino será acessível como um
Target objeto. Por exemplo, visualize os arquivos retornados
pelo destino usando Target.files.
Vários arquivos
Por padrão, apenas os destinos criados por regras podem aparecer como dependências (como um
foo_library() destino). Se você quiser que o atributo aceite destinos que sejam
arquivos de entrada (como arquivos de origem no repositório), faça isso com
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"]),
A lista de arquivos pode ser acessada com ctx.files.<attribute name>. Por
exemplo, a lista de arquivos no atributo srcs pode ser acessada por
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
É possível criar uma regra que gera um arquivo .cc com base em um modelo. Além disso, você
pode usar ctx.actions.write para gerar uma string construída na função de implementação de regra, mas isso tem dois problemas. Primeiro, à medida que o modelo aumenta, torna-se mais eficiente em termos de memória colocá-lo em um arquivo separado e evitar
a construção de strings grandes durante a fase de análise. Em segundo lugar, o uso de um arquivo
separado é mais conveniente para o usuário. Em vez disso, use
ctx.actions.expand_template,
que realiza 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 desta forma:
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 ao 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",
),
Os atributos que começam com um sublinhado são particulares e não podem ser definidos em um
BUILD arquivo. O modelo agora é uma dependência implícita: cada hello_world
destino 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"])
Indo mais longe
- Consulte a documentação de referência para regras.
- Conheça os depsets.
- Confira o repositório de exemplos que inclui outros exemplos de regras.