Uma regra define uma série de ações que o Bazel realiza em entradas para produzir um conjunto de saídas, que são referenciadas em provedores retornados pela função de implementação da regra. Por exemplo, uma regra binária em C++ pode:
- Pegue um conjunto de arquivos de origem
.cpp
(entradas). - Execute
g++
nos arquivos de origem (ação). - Retorne o provedor
DefaultInfo
com a saída executável e outros arquivos para disponibilizar no ambiente de execução. - Retorna o provedor
CcInfo
com informações específicas do C++ coletadas do destino e das dependências dele.
Da perspectiva do Bazel, g++
e as bibliotecas padrão do C++ também são entradas para essa regra. Como criador de regras, você precisa considerar não apenas as entradas fornecidas pelo usuário, mas também todas as ferramentas e bibliotecas necessárias para executar as ações.
Antes de criar ou modificar qualquer regra, familiarize-se com as fases de build do Bazel. É importante entender as três fases de um build (carregamento, análise e execução). Também é útil aprender sobre macros para entender a diferença entre regras e macros. Para começar, leia o tutorial sobre regras. Depois, use esta página como referência.
Algumas regras são incorporadas ao próprio Bazel. Essas regras nativas, como
genrule
e filegroup
, oferecem suporte básico.
Ao definir suas próprias regras, você pode adicionar suporte a linguagens e ferramentas
que o Bazel não oferece de forma nativa.
O Bazel oferece um modelo de extensibilidade para escrever regras usando a linguagem
Starlark. Essas regras são escritas em arquivos .bzl
, que
podem ser carregados diretamente de arquivos BUILD
.
Ao definir sua própria regra, você decide quais atributos ela aceita e como gera as saídas.
A função implementation
da regra define o comportamento exato dela durante a
fase de análise. Essa função não executa comandos externos. Em vez disso, ele registra ações que serão usadas
mais tarde, durante a fase de execução, para criar as saídas da regra, se forem
necessárias.
Criação da regra
Em um arquivo .bzl
, use a função rule para definir uma nova
regra e armazene o resultado em uma variável global. A chamada para rule
especifica atributos e uma função de implementação:
example_library = rule(
implementation = _example_library_impl,
attrs = {
"deps": attr.label_list(),
...
},
)
Isso define um tipo de regra chamado example_library
.
A chamada para rule
também precisa especificar se a regra cria uma saída executável (com executable = True
) ou especificamente um executável de teste (com test = True
). Se for o último caso, a regra será uma regra de teste, e o nome dela precisará terminar em _test
.
Instanciação de destino
As regras podem ser carregadas e chamadas em arquivos BUILD
:
load('//some/pkg:rules.bzl', 'example_library')
example_library(
name = "example_target",
deps = [":another_target"],
...
)
Cada chamada a uma regra de build não retorna um valor, mas tem o efeito colateral de definir uma meta. Isso é chamado de instanciação da regra. Isso especifica um nome para o novo destino e valores para os atributos dele.
As regras também podem ser chamadas de funções Starlark e carregadas em arquivos .bzl
.
As funções do Starlark que chamam regras são chamadas de macros do Starlark.
As macros do Starlark precisam ser chamadas de arquivos BUILD
e só podem ser
chamadas durante a fase de carregamento, quando os arquivos BUILD
são avaliados para instanciar destinos.
Atributos
Um atributo é um argumento de regra. Os atributos podem fornecer valores específicos para a implementação de um destino ou se referir a outros destinos, criando um gráfico de dependências.
Atributos específicos da regra, como srcs
ou deps
, são definidos transmitindo um mapa de nomes de atributos para esquemas (criados usando o módulo attr
) ao parâmetro attrs
de rule
.
Atributos comuns, como name
e visibility
, são adicionados implicitamente a todas as regras. Outros atributos são adicionados implicitamente a regras executáveis e de teste especificamente. Atributos adicionados implicitamente a uma regra não podem ser incluídos no dicionário transmitido para attrs
.
Atributos de dependência
As regras que processam o código-fonte geralmente definem os seguintes atributos para lidar com vários tipos de dependências:
srcs
especifica os arquivos de origem processados pelas ações de uma meta. Muitas vezes, o esquema de atributo especifica quais extensões de arquivo são esperadas para o tipo de arquivo de origem que a regra processa. As regras para linguagens com arquivos de cabeçalho geralmente especificam um atributohdrs
separado para cabeçalhos processados por um destino e seus consumidores.deps
especifica as dependências de código para um destino. O esquema de atributo precisa especificar quais provedores essas dependências precisam fornecer. Por exemplo,cc_library
forneceCcInfo
.data
especifica os arquivos que serão disponibilizados no tempo de execução para qualquer executável que dependa de um destino. Isso permite que arquivos arbitrários sejam especificados.
example_library = rule(
implementation = _example_library_impl,
attrs = {
"srcs": attr.label_list(allow_files = [".example"]),
"hdrs": attr.label_list(allow_files = [".header"]),
"deps": attr.label_list(providers = [ExampleInfo]),
"data": attr.label_list(allow_files = True),
...
},
)
Esses são exemplos de atributos de dependência. Qualquer atributo que especifique
um rótulo de entrada (definido com
attr.label_list
,
attr.label
ou
attr.label_keyed_string_dict
)
especifica dependências de um determinado tipo
entre um destino e os destinos cujos rótulos (ou os objetos
Label
correspondentes) estão listados nesse atributo quando o destino
é definido. O repositório e, possivelmente, o caminho desses rótulos são resolvidos em relação ao destino definido.
example_library(
name = "my_target",
deps = [":other_target"],
)
example_library(
name = "other_target",
...
)
Neste exemplo, other_target
é uma dependência de my_target
e, portanto, other_target
é analisado primeiro. É um erro se houver um ciclo no gráfico de dependência de destinos.
Atributos particulares e dependências implícitas
Um atributo de dependência com um valor padrão cria uma dependência implícita. Ele é implícito porque faz parte do gráfico de destino que o usuário não especifica em um arquivo BUILD
. As dependências implícitas são úteis para codificar uma
relação entre uma regra e uma ferramenta (uma dependência de tempo de build, como um
compilador), já que na maioria das vezes um usuário não tem interesse em especificar qual
ferramenta a regra usa. Dentro da função de implementação da regra, isso é tratado da mesma forma que outras dependências.
Se você quiser fornecer uma dependência implícita sem permitir que o usuário
substitua esse valor, defina o atributo como privado a ele um nome
que comece com um sublinhado (_
). Os atributos privados precisam ter valores
padrão. Em geral, só faz sentido usar atributos privados para dependências implícitas.
example_library = rule(
implementation = _example_library_impl,
attrs = {
...
"_compiler": attr.label(
default = Label("//tools:example_compiler"),
allow_single_file = True,
executable = True,
cfg = "exec",
),
},
)
Neste exemplo, cada destino do tipo example_library
tem uma dependência implícita do compilador //tools:example_compiler
. Isso permite que a função de implementação do
example_library
gere ações que invocam o
compilador, mesmo que o usuário não tenha transmitido o rótulo como entrada. Como _compiler
é um atributo particular, ctx.attr._compiler
sempre vai apontar para //tools:example_compiler
em todos os destinos desse tipo de regra. Se preferir, você pode nomear o atributo compiler
sem o
sublinhado e manter o valor padrão. Isso permite que os usuários substituam um
compilador diferente, se necessário, sem precisar saber o rótulo
do compilador.
As dependências implícitas geralmente são usadas para ferramentas que residem no mesmo repositório que a implementação da regra. Se a ferramenta vier da plataforma de execução ou de um repositório diferente, a regra vai precisar extrair essa ferramenta de um conjunto de ferramentas.
Atributos de saída
Atributos de saída, como attr.output
e attr.output_list
, declaram um arquivo de saída que o destino gera. Eles diferem dos atributos de dependência de duas maneiras:
- Eles definem destinos de arquivos de saída em vez de se referir a destinos definidos em outro lugar.
- Os destinos do arquivo de saída dependem do destino da regra instanciada, e não o contrário.
Normalmente, os atributos de saída só são usados quando uma regra precisa criar saídas
com nomes definidos pelo usuário que não podem ser baseados no nome de destino. Se uma regra tiver um atributo de saída, ela geralmente será chamada de out
ou outs
.
Os atributos de saída são a maneira preferida de criar saídas pré-declaradas, que podem ser especificamente dependentes ou solicitadas na linha de comando.
Função de implementação
Cada regra requer uma função implementation
. Essas funções são executadas estritamente na fase de análise e transformam o gráfico de metas gerado na fase de carregamento em um gráfico de ações a serem realizadas durante a fase de execução. Por isso, as funções de implementação não podem ler ou gravar arquivos.
As funções de implementação de regras geralmente são particulares (nomeadas com um sublinhado inicial). Por convenção, eles têm o mesmo nome da regra, mas com o sufixo _impl
.
As funções de implementação usam exatamente um parâmetro: um contexto de regra, convencionalmente chamado de ctx
. Eles retornam uma lista de provedores.
Destinos
As dependências são representadas no momento da análise como objetos Target
. Esses objetos contêm os provedores gerados quando a
função de implementação do destino foi executada.
ctx.attr
tem campos correspondentes aos nomes de cada atributo de dependência, contendo objetos Target
que representam cada dependência direta usando esse atributo. Para atributos label_list
, essa é uma lista de Targets
. Para atributos label
, é um único Target
ou None
.
Uma lista de objetos de provedor é retornada por uma função de implementação de destino:
return [ExampleInfo(headers = depset(...))]
É possível acessar esses dados usando a notação de índice ([]
), com o tipo de provedor como uma chave. Eles podem ser provedores personalizados definidos em Starlark ou provedores para regras nativas disponíveis como variáveis globais do Starlark.
Por exemplo, se uma regra usar arquivos de cabeçalho com um atributo hdrs
e os fornecer às ações de compilação do destino e dos consumidores dele, ela poderá coletá-los assim:
def _example_library_impl(ctx):
...
transitive_headers = [hdr[ExampleInfo].headers for hdr in ctx.attr.hdrs]
Há um estilo de struct legado, que é fortemente desencorajado e as regras devem ser migradas dele.
Arquivos
Os arquivos são representados por objetos File
. Como o Bazel não
realiza E/S de arquivo durante a fase de análise, esses objetos não podem ser usados para
ler ou gravar diretamente o conteúdo do arquivo. Em vez disso, eles são transmitidos para funções
de emissão de ações (consulte ctx.actions
) para construir partes do
gráfico de ações.
Um File
pode ser um arquivo de origem ou um arquivo gerado. Cada arquivo gerado
precisa ser a saída de exatamente uma ação. Os arquivos de origem não podem ser a saída de
nenhuma ação.
Para cada atributo de dependência, o campo correspondente de
ctx.files
contém uma lista das saídas padrão de todas as
dependências que usam esse atributo:
def _example_library_impl(ctx):
...
headers = depset(ctx.files.hdrs, transitive = transitive_headers)
srcs = ctx.files.srcs
...
ctx.file
contém um único File
ou None
para
atributos de dependência cujas especificações definem allow_single_file = True
.
ctx.executable
se comporta da mesma forma que ctx.file
, mas contém apenas campos para atributos de dependência cujas especificações definem executable = True
.
Como declarar saídas
Durante a fase de análise, a função de implementação de uma regra pode criar saídas.
Como todos os rótulos precisam ser conhecidos durante a fase de carregamento, essas saídas adicionais não têm rótulos. Os objetos File
para saídas podem ser criados usando
ctx.actions.declare_file
e
ctx.actions.declare_directory
.
Muitas vezes, os nomes das saídas são baseados no nome do destino,
ctx.label.name
:
def _example_library_impl(ctx):
...
output_file = ctx.actions.declare_file(ctx.label.name + ".output")
...
Para saídas pré-declaradas, como as criadas para
atributos de saída, os objetos File
podem ser recuperados
dos campos correspondentes de ctx.outputs
.
Ações
Uma ação descreve como gerar um conjunto de saídas de um conjunto de entradas, por exemplo, "execute gcc em hello.c e receba hello.o". Quando uma ação é criada, o Bazel não executa o comando imediatamente. Ele registra isso em um gráfico de dependências, porque uma ação pode depender da saída de outra. Por exemplo, em C, o vinculador precisa ser chamado depois do compilador.
As funções de uso geral que criam ações são definidas em
ctx.actions
:
ctx.actions.run
, para executar um executável.ctx.actions.run_shell
, para executar um comando do shell.ctx.actions.write
, para gravar uma string em um arquivo.ctx.actions.expand_template
, para gerar um arquivo de um modelo.
O ctx.actions.args
pode ser usado para acumular argumentos de ações de maneira eficiente. Isso evita o achatamento de depsets até o tempo de execução:
def _example_library_impl(ctx):
...
transitive_headers = [dep[ExampleInfo].headers for dep in ctx.attr.deps]
headers = depset(ctx.files.hdrs, transitive = transitive_headers)
srcs = ctx.files.srcs
inputs = depset(srcs, transitive = [headers])
output_file = ctx.actions.declare_file(ctx.label.name + ".output")
args = ctx.actions.args()
args.add_joined("-h", headers, join_with = ",")
args.add_joined("-s", srcs, join_with = ",")
args.add("-o", output_file)
ctx.actions.run(
mnemonic = "ExampleCompile",
executable = ctx.executable._compiler,
arguments = [args],
inputs = inputs,
outputs = [output_file],
)
...
As ações usam uma lista ou um conjunto de dependências de arquivos de entrada e geram uma lista (não vazia) de arquivos de saída. O conjunto de arquivos de entrada e saída precisa ser conhecido durante a fase de análise. Ele pode depender do valor de atributos, incluindo provedores de dependências, mas não pode depender do resultado da execução. Por exemplo, se a ação executar o comando "unzip", você precisará especificar quais arquivos serão descompactados (antes de executar "unzip"). As ações que criam um número variável de arquivos internamente podem ser agrupadas em um único arquivo (como zip, tar ou outro formato de arquivo).
As ações precisam listar todas as entradas. Listar entradas que não são usadas é permitido, mas ineficiente.
As ações precisam criar todas as saídas. Eles podem gravar outros arquivos, mas tudo o que não estiver em "outputs" não estará disponível para os consumidores. Todas as saídas declaradas precisam ser gravadas por alguma ação.
As ações são comparáveis a funções puras: elas dependem apenas das entradas fornecidas e evitam acessar informações do computador, nome de usuário, relógio, rede ou dispositivos de E/S (exceto para leitura de entradas e gravação de saídas). Isso é importante porque a saída será armazenada em cache e reutilizada.
As dependências são resolvidas pelo Bazel, que decide quais ações executar. É um erro se houver um ciclo no gráfico de dependências. A criação de uma ação não garante que ela será executada. Isso depende de se as saídas dela são necessárias para o build.
Provedores
Os provedores são informações que uma regra expõe a outras regras que dependem dela. Esses dados podem incluir arquivos de saída, bibliotecas, parâmetros para transmitir na linha de comando de uma ferramenta ou qualquer outra coisa que os consumidores de um destino precisem saber.
Como uma função de implementação de regra só pode ler provedores das dependências imediatas do destino instanciado, as regras precisam encaminhar qualquer informação das dependências de um destino que precise ser conhecida pelos consumidores dele, geralmente acumulando isso em um depset
.
Os provedores de um destino são especificados por uma lista de objetos de provedor retornados pela função de implementação.
As funções de implementação antigas também podem ser escritas em um estilo legado em que a função de implementação retorna um struct
em vez de uma lista de objetos de provedor. Esse estilo é fortemente desencorajado, e as regras precisam ser
migradas para longe dele.
Saídas padrão
As saídas padrão de um destino são as saídas solicitadas por padrão quando
o destino é solicitado para build na linha de comando. Por exemplo, um java_library
de destino //pkg:foo
tem foo.jar
como saída padrão, que será criada pelo comando bazel build //pkg:foo
.
As saídas padrão são especificadas pelo parâmetro files
de
DefaultInfo
:
def _example_library_impl(ctx):
...
return [
DefaultInfo(files = depset([output_file]), ...),
...
]
Se DefaultInfo
não for retornado por uma implementação de regra ou se o parâmetro files
não for especificado, DefaultInfo.files
vai usar como padrão todas as saídas pré-declaradas (geralmente, aquelas criadas por atributos de saída).
As regras que realizam ações precisam fornecer saídas padrão, mesmo que não sejam usadas diretamente. As ações que não estão no gráfico das saídas solicitadas são removidas. Se uma saída for usada apenas pelos consumidores de um destino, essas ações não serão realizadas quando o destino for criado isoladamente. Isso dificulta a depuração, porque a recriação apenas da meta com falha não reproduz a falha.
Runfiles
Runfiles são um conjunto de arquivos usados por uma meta no ambiente de execução (em vez de no momento do build). Durante a fase de execução, o Bazel cria uma árvore de diretórios que contém symlinks apontando para os arquivos de execução. Isso prepara o ambiente para o binário, permitindo que ele acesse os arquivos de execução durante o tempo de execução.
Os runfiles podem ser adicionados manualmente durante a criação da regra.
Os objetos runfiles
podem ser criados pelo método runfiles
no contexto da regra, ctx.runfiles
, e transmitidos ao
parâmetro runfiles
em DefaultInfo
. A saída executável das regras executáveis é adicionada implicitamente aos runfiles.
Algumas regras especificam atributos, geralmente chamados
data
, cujas saídas são adicionadas aos
runfiles de um destino. Os runfiles também precisam ser mesclados de data
, bem como de quaisquer atributos que possam fornecer código para execução eventual, geralmente srcs
(que pode conter destinos filegroup
com data
associados) e deps
.
def _example_library_impl(ctx):
...
runfiles = ctx.runfiles(files = ctx.files.data)
transitive_runfiles = []
for runfiles_attr in (
ctx.attr.srcs,
ctx.attr.hdrs,
ctx.attr.deps,
ctx.attr.data,
):
for target in runfiles_attr:
transitive_runfiles.append(target[DefaultInfo].default_runfiles)
runfiles = runfiles.merge_all(transitive_runfiles)
return [
DefaultInfo(..., runfiles = runfiles),
...
]
Provedores personalizados
Os provedores podem ser definidos usando a função provider
para transmitir informações específicas da regra:
ExampleInfo = provider(
"Info needed to compile/link Example code.",
fields = {
"headers": "depset of header Files from transitive dependencies.",
"files_to_link": "depset of Files from compilation.",
},
)
As funções de implementação de regras podem construir e retornar instâncias de provedor:
def _example_library_impl(ctx):
...
return [
...
ExampleInfo(
headers = headers,
files_to_link = depset(
[output_file],
transitive = [
dep[ExampleInfo].files_to_link for dep in ctx.attr.deps
],
),
)
]
Inicialização personalizada de provedores
É possível proteger a instanciação de um provedor com lógica de pré-processamento e validação personalizada. Isso pode ser usado para garantir que todas as instâncias do provedor atendam a determinados invariantes ou para oferecer aos usuários uma API mais limpa para obter uma instância.
Para isso, transmita um callback init
para a função
provider
. Se esse callback for fornecido, o
tipo de retorno de provider()
mudará para uma tupla de dois valores: o símbolo
do provedor, que é o valor de retorno comum quando init
não é usado, e um "construtor
bruto".
Nesse caso, quando o símbolo do provedor é chamado, em vez de retornar diretamente uma nova instância, ele encaminha os argumentos para o callback init
. O valor de retorno do callback precisa ser um dict que mapeia nomes de campo (strings) para valores. Ele é usado para inicializar os campos da nova instância. O callback pode ter qualquer assinatura, e se os argumentos não corresponderem à assinatura, um erro será informado como se o callback fosse invocado diretamente.
Em contraste, o construtor bruto vai ignorar o callback init
.
O exemplo a seguir usa init
para pré-processar e validar os argumentos:
# //pkg:exampleinfo.bzl
_core_headers = [...] # private constant representing standard library files
# Keyword-only arguments are preferred.
def _exampleinfo_init(*, files_to_link, headers = None, allow_empty_files_to_link = False):
if not files_to_link and not allow_empty_files_to_link:
fail("files_to_link may not be empty")
all_headers = depset(_core_headers, transitive = headers)
return {"files_to_link": files_to_link, "headers": all_headers}
ExampleInfo, _new_exampleinfo = provider(
fields = ["files_to_link", "headers"],
init = _exampleinfo_init,
)
Uma implementação de regra pode instanciar o provedor da seguinte maneira:
ExampleInfo(
files_to_link = my_files_to_link, # may not be empty
headers = my_headers, # will automatically include the core headers
)
O construtor bruto pode ser usado para definir funções de fábrica públicas alternativas
que não passam pela lógica init
. Por exemplo, exampleinfo.bzl
pode definir:
def make_barebones_exampleinfo(headers):
"""Returns an ExampleInfo with no files_to_link and only the specified headers."""
return _new_exampleinfo(files_to_link = depset(), headers = all_headers)
Normalmente, o construtor bruto é vinculado a uma variável cujo nome começa com um sublinhado (_new_exampleinfo
acima), para que o código do usuário não possa carregá-lo e gerar instâncias arbitrárias do provedor.
Outro uso para init
é impedir que o usuário chame o símbolo do provedor
e forçar o uso de uma função de fábrica:
def _exampleinfo_init_banned(*args, **kwargs):
fail("Do not call ExampleInfo(). Use make_exampleinfo() instead.")
ExampleInfo, _new_exampleinfo = provider(
...
init = _exampleinfo_init_banned)
def make_exampleinfo(...):
...
return _new_exampleinfo(...)
Regras executáveis e de teste
As regras executáveis definem destinos que podem ser invocados por um comando bazel run
.
As regras de teste são um tipo especial de regra executável cujas metas também podem ser
invocadas por um comando bazel test
. As regras executáveis e de teste são criadas
definindo o argumento executable
ou
test
respectivo como True
na chamada para rule
:
example_binary = rule(
implementation = _example_binary_impl,
executable = True,
...
)
example_test = rule(
implementation = _example_binary_impl,
test = True,
...
)
As regras de teste precisam ter nomes que terminem em _test
. Os nomes de destino de teste também costumam terminar em _test
por convenção, mas isso não é obrigatório. As regras que não são de teste não podem ter esse sufixo.
Os dois tipos de regras precisam produzir um arquivo executável de saída (que pode ou não ser pré-declarado) que será invocado pelos comandos run
ou test
. Para informar ao Bazel qual das saídas de uma regra usar como esse executável, transmita-o como o argumento executable
de um provedor DefaultInfo
retornado. Esse executable
é adicionado às saídas padrão da regra. Assim, você não precisa transmitir isso para executable
e files
. Ele também é adicionado implicitamente aos runfiles:
def _example_binary_impl(ctx):
executable = ctx.actions.declare_file(ctx.label.name)
...
return [
DefaultInfo(executable = executable, ...),
...
]
A ação que gera esse arquivo precisa definir o bit executável nele. Para uma ação ctx.actions.run
ou ctx.actions.run_shell
, isso precisa ser feito pela ferramenta subjacente invocada pela ação. Para uma ação de
ctx.actions.write
, transmita is_executable = True
.
Como um comportamento legad, as regras executáveis têm uma
saída especial ctx.outputs.executable
pré-declarada. Esse arquivo serve como o
executável padrão se você não especificar um usando DefaultInfo
. Ele não deve ser
usado de outra forma. Esse mecanismo de saída está descontinuado porque não permite personalizar o nome do arquivo executável durante a análise.
Confira exemplos de uma regra executável e uma regra de teste.
As regras executáveis e as regras de teste têm outros atributos definidos implicitamente, além daqueles adicionados para todas as regras. Os padrões de atributos adicionados implicitamente não podem ser mudados, mas isso pode ser evitado envolvendo uma regra particular em uma macro do Starlark que altera o padrão:
def example_test(size = "small", **kwargs):
_example_test(size = size, **kwargs)
_example_test = rule(
...
)
Local dos runfiles
Quando um destino executável é executado com bazel run
(ou test
), a raiz do
diretório de runfiles fica ao lado do executável. Os caminhos se relacionam da seguinte forma:
# Given launcher_path and runfile_file:
runfiles_root = launcher_path.path + ".runfiles"
workspace_name = ctx.workspace_name
runfile_path = runfile_file.short_path
execution_root_relative_path = "%s/%s/%s" % (
runfiles_root, workspace_name, runfile_path)
O caminho para um File
no diretório runfiles corresponde a
File.short_path
.
O binário executado diretamente por bazel
fica ao lado da raiz do diretório runfiles
. No entanto, os binários chamados de os runfiles não podem fazer a mesma suposição. Para evitar isso, cada binário precisa fornecer uma maneira de aceitar a raiz dos runfiles como um parâmetro usando um ambiente, um argumento ou uma flag de linha de comando. Isso permite que os binários transmitam a raiz canônica correta de runfiles
para os binários que ele chama. Se não estiver definido, um binário poderá adivinhar que foi o primeiro binário chamado e procurar um diretório de runfiles adjacente.
Temas avançados
Solicitar arquivos de saída
Um único destino pode ter vários arquivos de saída. Quando um comando bazel build
é executado, algumas das saídas dos destinos fornecidos ao comando são consideradas solicitadas. O Bazel só cria os arquivos solicitados e aqueles de que eles dependem direta ou indiretamente. Em termos do gráfico de ações, o Bazel só executa as ações que podem ser alcançadas como dependências transitivas dos arquivos solicitados.
Além das saídas padrão, qualquer saída pré-declarada pode ser solicitada explicitamente na linha de comando. As regras podem especificar saídas
pré-declaradas usando atributos de saída. Nesse caso, o usuário escolhe explicitamente os rótulos para as saídas ao instanciar a regra. Para receber objetos
File
para atributos de saída, use o atributo
correspondente de ctx.outputs
. As regras podem definir implicitamente saídas pré-declaradas com base no nome de destino também, mas esse recurso foi descontinuado.
Além das saídas padrão, há grupos de saída, que são coleções de arquivos de saída que podem ser solicitados juntos. Eles podem ser solicitados com
--output_groups
. Por exemplo, se um //pkg:mytarget
de destino for de um tipo de regra que tenha um grupo de saída debug_files
, esses arquivos poderão ser criados executando bazel build //pkg:mytarget
--output_groups=debug_files
. Como as saídas não pré-declaradas não têm rótulos, elas só podem ser solicitadas se aparecerem nas saídas padrão ou em um grupo de saídas.
Os grupos de saída podem ser especificados com o provedor OutputGroupInfo
. Ao contrário de muitos provedores
integrados, OutputGroupInfo
pode receber parâmetros com nomes arbitrários
para definir grupos de saída com esse nome:
def _example_library_impl(ctx):
...
debug_file = ctx.actions.declare_file(name + ".pdb")
...
return [
DefaultInfo(files = depset([output_file]), ...),
OutputGroupInfo(
debug_files = depset([debug_file]),
all_files = depset([output_file, debug_file]),
),
...
]
Além disso, ao contrário da maioria dos provedores, OutputGroupInfo
pode ser retornado por um aspecto e pelo destino da regra a que esse aspecto é aplicado, desde que eles não definam os mesmos grupos de saída. Nesse caso, os provedores resultantes são mesclados.
OutputGroupInfo
geralmente não deve ser usado para transmitir tipos específicos de arquivos de um destino para as ações dos consumidores. Em vez disso, defina provedores específicos da regra.
Configurações
Imagine que você queira criar um binário C++ para uma arquitetura diferente. O build pode ser complexo e envolver várias etapas. Alguns dos binários intermediários, como compiladores e geradores de código, precisam ser executados na plataforma de execução (que pode ser seu host ou um executor remoto). Alguns binários, como a saída final, precisam ser criados para a arquitetura de destino.
Por isso, o Bazel tem um conceito de "configurações" e transições. Os destinos principais (os solicitados na linha de comando) são integrados à configuração "target", enquanto as ferramentas que precisam ser executadas na plataforma de execução são integradas a uma configuração "exec". As regras podem gerar ações diferentes com base na configuração, por exemplo, para mudar a arquitetura da CPU transmitida ao compilador. Em alguns casos, a mesma biblioteca pode ser necessária para diferentes configurações. Se isso acontecer, ele será analisado e potencialmente criado várias vezes.
Por padrão, o Bazel cria as dependências de um destino na mesma configuração do destino em si, ou seja, sem transições. Quando uma dependência é uma ferramenta necessária para ajudar a criar o destino, o atributo correspondente precisa especificar uma transição para uma configuração de execução. Isso faz com que a ferramenta e todas as dependências dela sejam criadas para a plataforma de execução.
Para cada atributo de dependência, use cfg
para decidir se as dependências
precisam ser criadas na mesma configuração ou fazer a transição para uma configuração de execução.
Se um atributo de dependência tiver a flag executable = True
, cfg
precisará ser definido
explicitamente. Isso evita a criação acidental de uma ferramenta para a configuração errada.
Confira um exemplo
Em geral, fontes, bibliotecas dependentes e executáveis necessários em tempo de execução podem usar a mesma configuração.
As ferramentas executadas como parte do build (como compiladores ou geradores de código)
precisam ser criadas para uma configuração de execução. Nesse caso, especifique cfg = "exec"
no atributo.
Caso contrário, os executáveis usados no tempo de execução (como parte de um teste) precisam
ser criados para a configuração de destino. Nesse caso, especifique cfg = "target"
no atributo.
cfg = "target"
não faz nada: é apenas um valor de conveniência para ajudar os designers de regras a serem explícitos sobre as intenções deles. Quando executable = False
, o que significa que cfg
é opcional, só defina isso quando realmente ajudar na legibilidade.
Você também pode usar cfg = my_transition
para usar
transições definidas pelo usuário, que permitem
aos autores de regras muita flexibilidade na mudança de configurações, com a
desvantagem de
tornar o gráfico de build maior e menos compreensível.
Observação: historicamente, o Bazel não tinha o conceito de plataformas de execução. Em vez disso, todas as ações de build eram consideradas executadas na máquina host. As versões do Bazel anteriores à 6.0 criavam uma configuração "host" distinta para representar isso. Se você encontrar referências a "host" em código ou documentação antiga, é a isso que elas se referem. Recomendamos usar o Bazel 6.0 ou mais recente para evitar essa sobrecarga conceitual extra.
Fragmentos de configuração
As regras podem acessar fragmentos de configuração, como cpp
e java
. No entanto, todos os fragmentos obrigatórios precisam ser declarados para evitar erros de acesso:
def _impl(ctx):
# Using ctx.fragments.cpp leads to an error since it was not declared.
x = ctx.fragments.java
...
my_rule = rule(
implementation = _impl,
fragments = ["java"], # Required fragments of the target configuration
...
)
Links simbólicos de runfiles
Normalmente, o caminho relativo de um arquivo na árvore de arquivos de execução é o mesmo que o caminho relativo desse arquivo na árvore de origem ou na árvore de saída gerada. Se eles precisarem ser diferentes por algum motivo, especifique os argumentos root_symlinks
ou symlinks
. O root_symlinks
é um dicionário que mapeia caminhos para arquivos, em que os caminhos são relativos à raiz do diretório de runfiles. O dicionário symlinks
é o mesmo, mas os caminhos são prefixados implicitamente com o nome do espaço de trabalho principal (não o nome do repositório que contém o destino atual).
...
runfiles = ctx.runfiles(
root_symlinks = {"some/path/here.foo": ctx.file.some_data_file2}
symlinks = {"some/path/here.bar": ctx.file.some_data_file3}
)
# Creates something like:
# sometarget.runfiles/
# some/
# path/
# here.foo -> some_data_file2
# <workspace_name>/
# some/
# path/
# here.bar -> some_data_file3
Se symlinks
ou root_symlinks
for usado, tome cuidado para não mapear dois arquivos diferentes para o mesmo caminho na árvore de runfiles. Isso vai fazer com que o build falhe
com um erro descrevendo o conflito. Para corrigir, modifique os argumentos ctx.runfiles
para remover a colisão. Essa verificação será feita para todas as metas que usam sua regra, bem como para metas de qualquer tipo que dependam delas. Isso é especialmente arriscado se sua ferramenta for usada de forma transitiva
por outra ferramenta. Os nomes de symlink precisam ser exclusivos em todos os runfiles de uma ferramenta e
em todas as dependências dela.
Cobertura de código
Quando o comando coverage
é executado,
o build pode precisar adicionar instrumentação de cobertura para determinados destinos. O
build também coleta a lista de arquivos de origem instrumentados. O subconjunto de
metas consideradas é controlado pela flag
--instrumentation_filter
.
Os destinos de teste são excluídos, a menos que --instrument_test_targets
seja especificado.
Se uma implementação de regra adicionar instrumentação de cobertura no momento da build, ela precisará considerar isso na função de implementação. ctx.coverage_instrumented retorna True
no modo de cobertura se as fontes de um destino precisarem ser instrumentadas:
# Are this rule's sources instrumented?
if ctx.coverage_instrumented():
# Do something to turn on coverage for this compile action
A lógica que sempre precisa estar ativada no modo de cobertura (se as fontes de um destino são instrumentadas especificamente ou não) pode ser condicionada a ctx.configuration.coverage_enabled.
Se a regra incluir diretamente fontes das dependências antes da compilação (como arquivos de cabeçalho), talvez seja necessário ativar a instrumentação no momento da compilação se as fontes das dependências precisarem ser instrumentadas:
# Are this rule's sources or any of the sources for its direct dependencies
# in deps instrumented?
if ctx.coverage_instrumented() or any([ctx.coverage_instrumented(dep) for dep in ctx.attr.deps]):
# Do something to turn on coverage for this compile action
As regras também precisam fornecer informações sobre quais atributos são relevantes para a cobertura com o provedor InstrumentedFilesInfo
, construído usando coverage_common.instrumented_files_info
.
O parâmetro dependency_attributes
de instrumented_files_info
precisa listar
todos os atributos de dependência de tempo de execução, incluindo dependências de código como deps
e
dependências de dados como data
. O parâmetro source_attributes
precisa listar os atributos dos arquivos de origem da regra se a instrumentação de cobertura puder ser adicionada:
def _example_library_impl(ctx):
...
return [
...
coverage_common.instrumented_files_info(
ctx,
dependency_attributes = ["deps", "data"],
# Omitted if coverage is not supported for this rule:
source_attributes = ["srcs", "hdrs"],
)
...
]
Se InstrumentedFilesInfo
não for retornado, um padrão será criado com cada atributo de dependência que não seja de ferramenta e que não defina cfg
como "exec"
no esquema de atributo em dependency_attributes
. Esse não é o comportamento ideal, já que coloca atributos como srcs
em dependency_attributes
em vez de source_attributes
, mas evita a necessidade de configuração explícita de cobertura para todas as regras na cadeia de dependências.
Regras de teste
As regras de teste exigem configuração adicional para gerar relatórios de cobertura. A regra precisa adicionar os seguintes atributos implícitos:
my_test = rule(
...,
attrs = {
...,
# Implicit dependencies used by Bazel to generate coverage reports.
"_lcov_merger": attr.label(
default = configuration_field(fragment = "coverage", name = "output_generator"),
executable = True,
cfg = config.exec(exec_group = "test"),
),
"_collect_cc_coverage": attr.label(
default = "@bazel_tools//tools/test:collect_cc_coverage",
executable = True,
cfg = config.exec(exec_group = "test"),
)
},
test = True,
)
Ao usar configuration_field
, é possível evitar a dependência da ferramenta de fusão LCOV do Java, desde que a cobertura não seja solicitada.
Quando o teste é executado, ele emite informações de cobertura na forma de um ou mais arquivos LCOV com nomes exclusivos no diretório especificado pela variável de ambiente COVERAGE_DIR
. Em seguida, o Bazel vai mesclar esses arquivos em um único arquivo LCOV usando a ferramenta
_lcov_merger
. Se estiver presente, ele também vai coletar a cobertura de C/C++ usando a ferramenta
_collect_cc_coverage
.
Cobertura de valor de referência
Como a cobertura só é coletada para o código que acaba na árvore de dependências de um teste, os relatórios de cobertura podem ser enganosos, já que não necessariamente cobrem todo o código correspondente à flag --instrumentation_filter
.
Por isso, o Bazel permite que as regras especifiquem arquivos de cobertura de linha de base usando o atributo baseline_coverage_files
de ctx.instrumented_files_info
. Esses arquivos precisam ser gerados no formato LCOV por uma ação definida pelo usuário e devem listar todas as linhas, ramificações, funções e/ou blocos nos arquivos de origem do destino (de acordo com os parâmetros sources_attributes
e extensions
). Para
arquivos de origem em destinos instrumentados para cobertura, o Bazel mescla a cobertura
de linha de base no relatório de cobertura combinada gerado com
--combined_report
e garante que os arquivos não testados ainda apareçam como
não cobertos.
Se uma regra não fornecer arquivos de cobertura de linha de base, o Bazel vai gerar informações de cobertura sintéticas que mencionam apenas os caminhos dos arquivos de origem, mas não contêm informações sobre o conteúdo deles.
Ações de validação
Às vezes, é necessário validar algo sobre o build, e as informações necessárias para fazer isso estão disponíveis apenas em artefatos (arquivos de origem ou gerados). Como essas informações estão em artefatos, as regras não podem fazer essa validação no momento da análise porque não conseguem ler arquivos. Em vez disso, as ações precisam fazer essa validação no momento da execução. Quando a validação falha, a ação e o build também falham.
Exemplos de validações que podem ser executadas são análise estática, linting, verificações de dependência e consistência e verificações de estilo.
As ações de validação também podem ajudar a melhorar a performance do build movendo partes de ações que não são necessárias para criar artefatos em ações separadas. Por exemplo, se uma única ação que faz compilação e linting puder ser separada em uma ação de compilação e uma ação de linting, a ação de linting poderá ser executada como uma ação de validação e em paralelo com outras ações.
Essas "ações de validação" geralmente não produzem nada que seja usado em outro lugar na build, já que só precisam declarar coisas sobre as entradas. No entanto, isso apresenta um problema: se uma ação de validação não produzir nada que seja usado em outro lugar no build, como uma regra faz a ação ser executada? Historicamente, a abordagem era fazer com que a ação de validação gerasse um arquivo vazio e adicionasse artificialmente essa saída às entradas de outra ação importante no build:
Isso funciona porque o Bazel sempre executa a ação de validação quando a ação de compilação é executada, mas isso tem desvantagens significativas:
A ação de validação está no caminho crítico da build. Como o Bazel entende que a saída vazia é necessária para executar a ação de compilação, ele executa a ação de validação primeiro, mesmo que a ação de compilação ignore a entrada. Isso reduz o paralelismo e diminui a velocidade dos builds.
Se outras ações no build puderem ser executadas em vez da ação de compilação, as saídas vazias das ações de validação também precisarão ser adicionadas a essas ações (a saída do jar de origem de
java_library
, por exemplo). Isso também é um problema se novas ações que podem ser executadas em vez da ação de compilação forem adicionadas mais tarde e a saída de validação vazia for deixada de lado por engano.
A solução para esses problemas é usar o grupo de saída de validações.
Grupo de saída de validações
O grupo de saída de validações foi projetado para armazenar as saídas não utilizadas de ações de validação. Assim, elas não precisam ser adicionadas artificialmente às entradas de outras ações.
Esse grupo é especial porque as saídas dele são sempre solicitadas, independente do valor da flag --output_groups
e de como o destino é usado (por exemplo, na linha de comando, como uma dependência ou por saídas implícitas do destino). O cache normal e a incrementalidade ainda se aplicam: se as entradas da ação de validação não mudaram e a ação de validação foi concluída com êxito anteriormente, ela não será executada.
Usar esse grupo de saída ainda exige que as ações de validação gerem algum arquivo, mesmo que vazio. Isso pode exigir o encapsulamento de algumas ferramentas que normalmente não criam saídas para que um arquivo seja criado.
As ações de validação de uma meta não são executadas em três casos:
- Quando o destino é usado como uma ferramenta
- Quando o destino é usado como uma dependência implícita (por exemplo, um atributo que começa com "_")
- Quando o destino é criado na configuração de execução.
Esses destinos têm builds e testes separados que descobrem falhas de validação.
Como usar o grupo de saída de validações
O grupo de saída de validações é chamado de _validation
e é usado como qualquer outro grupo de saída:
def _rule_with_validation_impl(ctx):
ctx.actions.write(ctx.outputs.main, "main output\n")
ctx.actions.write(ctx.outputs.implicit, "implicit output\n")
validation_output = ctx.actions.declare_file(ctx.attr.name + ".validation")
ctx.actions.run(
outputs = [validation_output],
executable = ctx.executable._validation_tool,
arguments = [validation_output.path],
)
return [
DefaultInfo(files = depset([ctx.outputs.main])),
OutputGroupInfo(_validation = depset([validation_output])),
]
rule_with_validation = rule(
implementation = _rule_with_validation_impl,
outputs = {
"main": "%{name}.main",
"implicit": "%{name}.implicit",
},
attrs = {
"_validation_tool": attr.label(
default = Label("//validation_actions:validation_tool"),
executable = True,
cfg = "exec"
),
}
)
O arquivo de saída da validação não é adicionado ao DefaultInfo
nem às entradas de qualquer outra ação. A ação de validação para um destino desse tipo de regra ainda será executada se o destino depender de um rótulo ou se qualquer uma das saídas implícitas do destino depender direta ou indiretamente.
Normalmente, é importante que as saídas das ações de validação sejam incluídas apenas no grupo de saída de validação e não sejam adicionadas às entradas de outras ações, já que isso pode prejudicar os ganhos de paralelismo. No entanto, o Bazel não tem nenhuma verificação especial para aplicar isso. Portanto, teste se as saídas da ação de validação não são adicionadas às entradas de nenhuma ação nos testes para regras do Starlark. Exemplo:
load("@bazel_skylib//lib:unittest.bzl", "analysistest")
def _validation_outputs_test_impl(ctx):
env = analysistest.begin(ctx)
actions = analysistest.target_actions(env)
target = analysistest.target_under_test(env)
validation_outputs = target.output_groups._validation.to_list()
for action in actions:
for validation_output in validation_outputs:
if validation_output in action.inputs.to_list():
analysistest.fail(env,
"%s is a validation action output, but is an input to action %s" % (
validation_output, action))
return analysistest.end(env)
validation_outputs_test = analysistest.make(_validation_outputs_test_impl)
Flag de ações de validação
A execução de ações de validação é controlada pela flag de linha de comando --run_validations
, que tem o valor "true" por padrão.
Recursos descontinuados
Saídas pré-declaradas descontinuadas
Há duas maneiras descontinuadas de usar saídas pré-declaradas:
O parâmetro
outputs
derule
especifica um mapeamento entre nomes de atributos de saída e modelos de string para gerar rótulos de saída pré-declarados. Prefira usar saídas não pré-declaradas e adicionar explicitamente saídas aDefaultInfo.files
. Use o rótulo do destino da regra como entrada para regras que consomem a saída em vez de um rótulo de saída pré-declarado.Para regras executáveis,
ctx.outputs.executable
se refere a uma saída executável pré-declarada com o mesmo nome do destino da regra. Prefira declarar a saída explicitamente, por exemplo, comctx.actions.declare_file(ctx.label.name)
, e verifique se o comando que gera o executável define as permissões para permitir a execução. Transmita explicitamente a saída executável ao parâmetroexecutable
deDefaultInfo
.
Recursos de runfiles a serem evitados
O ctx.runfiles
e o tipo runfiles
têm um conjunto complexo de recursos, muitos dos quais são mantidos por motivos legados.
As recomendações a seguir ajudam a reduzir a complexidade:
Evite usar os modos
collect_data
ecollect_default
dectx.runfiles
. Esses modos coletam implicitamente runfiles em determinadas arestas de dependência codificadas de maneira confusa. Em vez disso, adicione arquivos usando os parâmetrosfiles
outransitive_files
dectx.runfiles
ou mesclando runfiles de dependências comrunfiles = runfiles.merge(dep[DefaultInfo].default_runfiles)
.Evite usar
data_runfiles
edefault_runfiles
do construtorDefaultInfo
. EspecifiqueDefaultInfo(runfiles = ...)
. A distinção entre arquivos de execução "padrão" e "de dados" é mantida por motivos legados. Por exemplo, algumas regras colocam as saídas padrão emdata_runfiles
, mas não emdefault_runfiles
. Em vez de usardata_runfiles
, as regras precisam incluir saídas padrão e mesclardefault_runfiles
de atributos que fornecem runfiles (geralmentedata
).Ao recuperar
runfiles
deDefaultInfo
(geralmente apenas para mesclar runfiles entre a regra atual e as dependências dela), useDefaultInfo.default_runfiles
, nãoDefaultInfo.data_runfiles
.
Como migrar de provedores legados
Historicamente, os provedores do Bazel eram campos simples no objeto Target
. Eles foram acessados usando o operador de ponto e criados colocando o campo em um struct
retornado pela função de implementação da regra, em vez de uma lista de objetos do provedor:
return struct(example_info = struct(headers = depset(...)))
Esses provedores podem ser recuperados do campo correspondente do objeto Target
:
transitive_headers = [hdr.example_info.headers for hdr in ctx.attr.hdrs]
Esse estilo está descontinuado e não deve ser usado em novos códigos. Consulte a seguir para ver informações que podem ajudar na migração. O novo mecanismo de provedor evita conflitos de nomes. Ele também oferece suporte à ocultação de dados, exigindo que qualquer código que acesse uma instância de provedor a recupere usando o símbolo do provedor.
Por enquanto, os provedores legados ainda são aceitos. Uma regra pode retornar provedores legados e modernos da seguinte forma:
def _old_rule_impl(ctx):
...
legacy_data = struct(x = "foo", ...)
modern_data = MyInfo(y = "bar", ...)
# When any legacy providers are returned, the top-level returned value is a
# struct.
return struct(
# One key = value entry for each legacy provider.
legacy_info = legacy_data,
...
# Additional modern providers:
providers = [modern_data, ...])
Se dep
for o objeto Target
resultante de uma instância dessa regra, os
provedores e o conteúdo deles poderão ser recuperados como dep.legacy_info.x
e
dep[MyInfo].y
.
Além de providers
, a struct retornada também pode usar vários outros campos com significado especial e, portanto, não cria um provedor legado correspondente:
Os campos
files
,runfiles
,data_runfiles
,default_runfiles
eexecutable
correspondem aos campos de mesmo nome deDefaultInfo
. Não é permitido especificar nenhum desses campos ao retornar um provedorDefaultInfo
.O campo
output_groups
usa um valor de struct e corresponde a umOutputGroupInfo
.
Nas declarações provides
de regras e nas declarações providers
de atributos de dependência, os provedores legados são transmitidos como strings, e os modernos são transmitidos pelo símbolo Info
. Não se esqueça de mudar de strings para símbolos ao migrar. Para conjuntos de regras complexos ou grandes em que é difícil atualizar
todas as regras de forma atômica, siga esta sequência de
etapas:
Modifique as regras que produzem o provedor legado para gerar os provedores legado e moderno, usando a sintaxe anterior. Para regras que declaram retornar o provedor legado, atualize essa declaração para incluir os provedores legados e modernos.
Modifique as regras que consomem o provedor legado para consumir o provedor moderno. Se alguma declaração de atributo exigir o provedor legado, atualize-as para exigir o provedor moderno. Como opção, você pode intercalar esse trabalho com a etapa 1 fazendo com que os consumidores aceitem ou exijam o provedor: teste a presença do provedor legado usando
hasattr(target, 'foo')
ou o novo provedor usandoFooInfo in target
.Remova completamente o provedor legado de todas as regras.