Uma regra define uma série de ações que o Bazel executa nas entradas para produzir um conjunto de saídas, que são referenciadas nos provedores retornados pela função de implementação da regra. Por exemplo, uma regra binária do C++ pode:
- Pegue um conjunto de arquivos de origem (entradas)
.cpp
. - Executar
g++
nos arquivos de origem (ação). - Retorne o provedor
DefaultInfo
com a saída executável e outros arquivos que serão disponibilizados no momento da execução. - Retorne o provedor
CcInfo
com informações específicas de C++ coletadas do destino e das dependências dele.
Do ponto de vista do Bazel, g++
e as bibliotecas C++ padrão também são entradas
para essa regra. Como criador de regras, você precisa considerar não apenas as entradas fornecidas pelo usuário para uma regra, mas também todas as ferramentas e bibliotecas necessárias para executar as ações.
Antes de criar ou modificar qualquer regra, conheça 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 de regras. Depois, use esta página como referência.
Algumas regras são integradas ao próprio Bazel. Essas regras nativas, como
cc_library
e java_binary
, oferecem suporte básico a alguns idiomas.
Ao definir suas próprias regras, você pode adicionar compatibilidade semelhante para linguagens e ferramentas
sem suporte nativo do Bazel.
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 a quais atributos ela oferece suporte e como gera as saídas.
A função implementation
da regra define o comportamento exato durante a fase de análise. Essa função não executa comandos externos. Em vez disso, ela registra ações que serão usadas
posteriormente durante a fase de execução para criar as saídas da regra, se forem
necessárias.
Criação de regras
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
). No último caso, a regra é uma regra de teste,
e o nome da regra precisa terminar em _test
.
Instanciação do 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 para uma regra de build não retorna nenhum valor, mas tem o efeito colateral de definir um destino. Isso é chamado de instanciação da regra. Isso especifica um nome para o novo destino e valores para os atributos dela.
As regras também podem ser chamadas de funções Starlark e carregadas em arquivos .bzl
.
As funções Starlark que chamam regras são chamadas de macros Starlark.
As macros 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 implementation de um destino ou podem se referir a outros destinos, criando um gráfico de dependências.
Atributos específicos de regras, como srcs
ou deps
, são definidos transmitindo um mapa
de nomes de atributos para esquemas (criados usando o módulo
attr
) para o 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 em
regras executáveis e de teste especificamente. Atributos que
são 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 atributos abaixo para processar vários tipos de dependências:
srcs
especifica os arquivos de origem processados pelas ações de um destino. Muitas vezes, o esquema de atributos especifica quais extensões de arquivo são esperadas para a classificação de arquivo de origem que a regra processa. As regras para linguagens com arquivos principais 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 atributos precisa especificar quais provedores essas dependências precisam fornecer. Por exemplo,cc_library
forneceCcInfo
.data
especifica arquivos que serão disponibilizados durante a execução para qualquer executável que dependa de um destino. Isso permitirá 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),
...
},
)
Estes são exemplos de atributos de dependência. Qualquer atributo que especifique
um rótulo de entrada (aqueles definidos 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) são listados nesse atributo quando o destino
é definido. O repositório e, possivelmente, o caminho desses rótulos é resolvido
em relação ao destino definido.
example_library(
name = "my_target",
deps = [":other_target"],
)
example_library(
name = "other_target",
...
)
Nesse exemplo, other_target
é uma dependência de my_target
e, portanto,
other_target
é analisado primeiro. Se houver um ciclo no gráfico de dependência das metas, isso constituirá um erro.
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 está interessado 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, torne o atributo particular, fornecendo a ele um nome
que comece com um sublinhado (_
). Os atributos privados precisam ter valores
padrão. Em geral, só faz sentido usar atributos particulares 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 no compilador //tools:example_compiler
. Isso permite
que a função de implementação de example_library
gere ações que invocam o
compilador, mesmo que o usuário não tenha passado o rótulo como uma entrada. Como _compiler
é um atributo particular, ctx.attr._compiler
sempre aponta para //tools:example_compiler
em todos os destinos desse tipo de regra. Como alternativa, 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, mas não exige reconhecimento do 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 precisará buscar essa ferramenta em um conjunto de ferramentas.
Atributos de saída
Os atributos de saída, como attr.output
e
attr.output_list
, declaram um arquivo de saída gerado pelo
destino. 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 de arquivo de saída dependem do destino da regra instanciada, e não o contrário.
Normalmente, os atributos de saída são usados apenas 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 tem
um atributo de saída, ela normalmente é chamada de out
ou outs
.
Os atributos de saída são a maneira recomendada 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 destinos gerados na fase de carregamento em um gráfico de
ações a serem realizadas durante a fase de execução. Assim,
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 no início. Convencionalmente, eles são nomeados da mesma forma que a regra, mas com o sufixo _impl
.
As funções de implementação usam exatamente um parâmetro: um
contexto de regra, chamado convencionalmente 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 por 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 pela função de implementação de um destino:
return [ExampleInfo(headers = depset(...))]
Eles podem ser acessados usando a notação de índice ([]
), com o tipo de provedor como
uma chave. Eles podem ser provedores personalizados definidos no Starlark ou
provedores para regras nativas disponíveis como variáveis globais
do Starlark.
Por exemplo, se uma regra receber arquivos de cabeçalho por meio de um atributo hdrs
e os fornecer
às ações de compilação do destino e dos consumidores, ela poderá
coletar assim:
def _example_library_impl(ctx):
...
transitive_headers = [hdr[ExampleInfo].headers for hdr in ctx.attr.hdrs]
Para o estilo legado em que um struct
é retornado da
função de implementação de um destino em vez de uma lista de objetos do provedor:
return struct(example_info = struct(headers = depset(...)))
Os provedores podem ser recuperados no campo correspondente do objeto Target
:
transitive_headers = [hdr.example_info.headers for hdr in ctx.attr.hdrs]
Esse estilo não é recomendado, e as regras precisam ser migradas.
Arquivos
Os arquivos são representados por objetos File
. Como o Bazel não executa 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, elas são transmitidas para funções que emitem ações (consulte ctx.actions
) para criar partes do gráfico de ações.
Um File
pode ser um arquivo de origem ou 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 usando 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 com especificações definidas como allow_single_file=True
.
ctx.executable
se comporta da mesma forma que ctx.file
, mas contém apenas
campos para atributos de dependência com especificações definidas como 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. 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 aquelas criadas para
atributos de saída, os objetos File
podem ser recuperados
nos campos correspondentes de ctx.outputs
.
Ações
Uma ação descreve como gerar um conjunto de saídas a partir de um conjunto de entradas. Por exemplo, "executar gcc em hello.c e receber hello.o". Quando uma ação é criada, o Bazel não executa o comando imediatamente. Ela a registra 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 a partir de um modelo.
ctx.actions.args
pode ser usado para acumular com eficiência
os argumentos das ações. Isso evita o nivelamento de depsets até o momento da 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 pegam uma lista ou um conjunto 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. Isso pode depender do valor dos 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, especifique quais arquivos você espera que sejam inflados (antes de executar a descompactação). As ações que criam um número variável de arquivos internamente podem ser unidas em um único arquivo (como ZIP, tar ou outro formato).
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 delas. Eles podem gravar outros arquivos, mas o que não estiver nas saídas 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 às funções puras: elas precisam depender apenas das entradas fornecidas e evitar 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 decidirá quais ações serão executadas. Se houver um ciclo no gráfico de dependências, será considerado um erro. Criar uma ação não garante que ela será executada, isso depende 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 que dependem dela. Esses dados podem incluir arquivos de saída, bibliotecas, parâmetros para transmitir a linha de comando de uma ferramenta ou qualquer outra informação que os consumidores de um público-alvo precisem saber.
Como a função de implementação de uma 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
de um destino, geralmente acumulando isso em um depset
.
Os provedores de um destino são especificados por uma lista de objetos Provider
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 do provedor. Esse estilo não é recomendado, e as regras precisam ser migradas.
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 criação na linha de comando. Por exemplo, um //pkg:foo
de destino java_library
tem foo.jar
como saída padrão, então ela 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 o parâmetro files
não for especificado, DefaultInfo.files
será padronizado como todas as
saídas pré-declaradas, que geralmente são criadas por atributos
de saída.
As regras que executam ações precisam fornecer saídas padrão, mesmo que não se espere que elas 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 por consumidores de um destino, essas ações não serão executadas quando o destino for criado isoladamente. Isso dificulta a depuração, porque a recriação apenas do destino com falha não reproduz a falha.
Arquivos de execução
Os arquivos de execução são um conjunto de arquivos usados por um destino no momento da execução, e não durante o build. Durante a fase de execução, o Bazel cria uma árvore de diretórios contendo links simbólicos que apontam para os arquivos de execução. Isso prepara o ambiente do binário para que ele possa acessar os arquivos de execução durante o ambiente de execução.
Os arquivos de execução podem ser adicionados manualmente durante a criação da regra.
Objetos runfiles
podem ser criados pelo método runfiles
no contexto da regra, ctx.runfiles
, e transmitidos para o parâmetro runfiles
em DefaultInfo
. A saída executável das
regras executáveis é adicionada implicitamente aos arquivos de execução.
Algumas regras especificam atributos, geralmente chamados de
data
, com saídas adicionadas aos
arquivos de execução de um destino. Os arquivos de execução também precisam ser mesclados do data
, bem como
de todos os atributos que podem fornecer código para eventual execução, geralmente
srcs
(que pode conter destinos filegroup
com data
associado) 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 criar e retornar instâncias do 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 pré-processamento personalizado e lógica de validação. Isso pode ser usado para garantir que todas as instâncias de provedor obedeçam a determinadas variantes ou para fornecer aos usuários uma API mais limpa para receber uma instância.
Isso é feito transmitindo um callback init
para a
função provider
. Se esse callback for fornecido, o
tipo de retorno de provider()
mudará para ser 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 for chamado, em vez de retornar
diretamente uma nova instância, ele vai encaminhar os argumentos para o callback init
. O
valor de retorno do callback precisa ser um dict que mapeia nomes de campo (strings) para valores.
Isso é 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 tivesse sido invocado diretamente.
O construtor bruto, por outro lado, 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
# It's possible to define an init accepting positional arguments, but
# 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(
...
init = _exampleinfo_init)
export ExampleInfo
Uma implementação de regra poderá instanciar o provedor desta forma:
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 públicas alternativas de fábrica
que não passam pela lógica init
. Por exemplo, em exampleinfo.bzl,
poderíamos 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 está vinculado a uma variável com um nome que 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 de provedor.
Outro uso para init
é simplesmente impedir que o usuário chame o símbolo
de provedor e forçá-lo a usar 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 com destinos que também podem ser
invocados por um comando bazel test
. As regras executáveis e de teste são criadas
definindo o respectivo argumento executable
ou
test
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 geralmente
terminam em _test
por convenção, mas isso não é obrigatório. Regras que não são de teste não podem
ter esse sufixo.
Os dois tipos de regras precisam produzir um arquivo de saída executável (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 executável, transmita-a como o
argumento executable
de um provedor DefaultInfo
retornado. Esse executable
é adicionado às saídas padrão da regra (então você
não precisa passá-lo para executable
e files
). Ele também é implicitamente
adicionado 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 no arquivo. Para
uma ação ctx.actions.run
ou
ctx.actions.run_shell
, isso precisa ser feito
pela ferramenta subjacente que é invocada pela ação. Para uma
ação ctx.actions.write
, transmita is_executable=True
.
Como comportamento legado, as regras executáveis têm uma
saída pré-declarada de ctx.outputs.executable
especial. Esse arquivo servirá como
executável padrão se você não especificar um usando DefaultInfo
. Ele não pode ser
usado de outra forma. Esse mecanismo de saída foi descontinuado porque não é compatível com a personalização do nome do arquivo executável no momento da 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. Não é possível mudar os padrões de atributos implicitamente adicionados, mas isso pode ser resolvido ao unir uma regra privada 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 arquivos de execução
Quando um destino executável é executado com bazel run
(ou test
), a raiz do
diretório runfiles é adjacente ao executável. Os caminhos são relacionados da seguinte maneira:
# 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
é adjacente à raiz do
diretório runfiles
. No entanto, os binários chamados pelos arquivos de execução não podem fazer
a mesma suposição. Para atenuar isso, cada binário precisa fornecer uma maneira de
aceitar a raiz dos arquivos de execução como um parâmetro usando um argumento/sinalização de
ambiente ou linha de comando. Isso permite que os binários transmitam a raiz correta dos arquivos de execução canônicos
para os binários chamados por ele. Se isso não for definido, um binário poderá adivinhar que foi o
primeiro binário chamado e procurar um diretório de arquivos de execução adjacente.
Temas avançados
Como solicitar arquivos de saída
Um 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. Ele só cria esses arquivos solicitados e os arquivos de que eles dependem direta ou indiretamente. Em termos do gráfico de ações, o Bazel só
executa as ações acessíveis 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
com atributos de saída. Nesse caso, o usuário
escolhe explicitamente rótulos para saídas ao instanciar a regra. Para receber objetos
File
para atributos de saída, use o atributo
correspondente de ctx.outputs
. As regras também podem
definir implicitamente saídas pré-declaradas com base
no nome do destino, 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 em conjunto. Eles podem ser solicitados usando
--output_groups
. Por
exemplo, se um //pkg:mytarget
de destino for de um tipo de regra que tem um grupo de saída
debug_files
, esses arquivos podem 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 aparecendo nas saídas padrão ou em um grupo
de saída.
Os grupos de saída podem ser especificados com o
provedor OutputGroupInfo
. Observe que, ao contrário de muitos
provedores integrados, OutputGroupInfo
pode aceitar 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
não definam os mesmos grupos de saída. Nesse caso, os provedores resultantes são mesclados.
Observe que OutputGroupInfo
geralmente não pode 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ê quer 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 esse motivo, o Bazel tem um conceito de "configurações" e transições. Os destinos mais importantes (aqueles solicitados na linha de comando) são criados na configuração de "destino", enquanto as ferramentas que precisam ser executadas na plataforma de execução são criadas em uma configuração "exec". As regras podem gerar ações diferentes com base na configuração. Por exemplo, para alterar a arquitetura de CPU que é passada para o compilador. Em alguns casos, a mesma biblioteca pode ser necessária para configurações diferentes. Se isso acontecer, ele será analisado e possivelmente criado várias vezes.
Por padrão, o Bazel cria as dependências de um destino na mesma configuração que o próprio destino, 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 "exec". 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, você pode usar 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 sinalização executable=True
, será necessário definir cfg
explicitamente. Isso serve para evitar a criação acidental de uma ferramenta para a configuração
errada.
Conferir exemplo
Em geral, fontes, bibliotecas dependentes e executáveis que serão necessários no tempo de execução podem usar a mesma configuração.
As ferramentas que são executadas como parte do build, como compiladores ou geradores de código,
precisam ser criadas para uma configuração "exec". Nesse caso, especifique cfg="exec"
no
atributo.
Caso contrário, os executáveis usados no ambiente de execução (como parte de um teste) precisam ser criados para a configuração de destino. Nesse caso, especifique cfg="target"
no
atributo.
Na verdade, o cfg="target"
não faz nada. Ele é puramente um valor de conveniência para
ajudar os designers de regras a serem explícitos sobre as intenções deles. Quando executable=False
,
que significa que cfg
é opcional, só defina isso quando realmente ajudar a facilitar a leitura.
Também é possível usar cfg=my_transition
para usar transições definidas pelo usuário, que permitem aos autores de regras muita flexibilidade ao mudar configurações, com a desvantagem de tornar o gráfico de criação maior e menos compreensível.
Observação: anteriormente, o Bazel não tinha o conceito de plataformas de execução. Em vez disso, todas as ações de build eram consideradas para execução na máquina host. Por isso, há uma única configuração de "host" e uma transição de "host" que podem ser usadas para criar uma dependência na configuração do host. Muitas regras ainda usam a transição "host" para as ferramentas, mas ela está obsoleta e está sendo migrada para usar transições "exec" sempre que possível.
Existem várias diferenças entre as configurações "host" e "exec":
- "host" é terminal, "exec" não: quando uma dependência está na configuração "host", não são permitidas mais transições. É possível continuar fazendo outras transições de configuração quando estiver em uma configuração "exec".
- "host" é monolítico, "exec" não: há apenas uma configuração "host", mas pode haver uma configuração "exec" diferente para cada plataforma de execução.
- O "host" pressupõe que você executa ferramentas na mesma máquina que o Bazel ou em uma máquina significativamente semelhante. Isso não é mais verdade: você pode executar ações de compilação na sua máquina local ou em um executor remoto, e não há garantia de que o executor remoto seja a mesma CPU e SO que sua máquina local.
As configurações "exec" e "host" aplicam as mesmas mudanças de opção, por exemplo,
defina --compilation_mode
de --host_compilation_mode
, defina --cpu
de
--host_cpu
etc. A diferença é que a configuração "host" começa com
os valores default de todas as outras sinalizações, enquanto a configuração "exec"
começa com os valores atuais das sinalizações, com base na configuração de destino.
Fragmentos de configuração
As regras podem acessar
fragmentos de configuração, como
cpp
, java
e jvm
. No entanto, todos os fragmentos necessá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
host_fragments = ["java"], # Required fragments of the host configuration
...
)
ctx.fragments
fornece fragmentos de configuração apenas para a configuração
de destino. Para acessar fragmentos para a configuração do host, use
ctx.host_fragments
.
Links simbólicos de arquivos de execução
Normalmente, o caminho relativo de um arquivo na árvore de arquivos de execução é o mesmo que o
caminho relativo na árvore de origem ou na árvore de saída gerada. Se eles
precisam 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 dos arquivos de execução. O dicionário symlinks
é o mesmo, mas os caminhos são prefixados implicitamente com o nome do espaço de trabalho.
...
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, tenha cuidado para não mapear dois arquivos
diferentes para o mesmo caminho na árvore de arquivos de execução. Isso fará com que a compilação falhe
com um erro que descreve o conflito. Para corrigir, você precisará modificar os argumentos ctx.runfiles
para remover a colisão. Essa verificação será feita para todos os destinos que usam sua regra, bem como para qualquer tipo que dependa deles. Isso é especialmente arriscado caso sua ferramenta seja usada de forma transitiva
por outra ferramenta. Os nomes dos links simbólicos precisam ser únicos nos arquivos de execução 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
destinos considerados é controlado pela sinalização
--instrumentation_filter
.
Os destinos de teste serã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 criação, ela precisará considerar isso na função de implementação. ctx.coverage_instrumented retorna verdadeiro no modo de cobertura se as origens 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 (independentemente de as fontes de um destino serem especificamente instrumentadas ou não) pode ser condicionada em ctx.configuration.coverage_enabled.
Se a regra incluir diretamente origens das dependências antes da compilação (como arquivos principais), talvez seja necessário ativar a instrumentação no tempo de compilação caso as origens das dependências precisem ser instrumentadas:
# Are this rule's sources or any of the sources for its direct dependencies
# in deps instrumented?
if (ctx.configuration.coverage_enabled and
(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
cobertura com o provedor InstrumentedFilesInfo
, construídos usando
coverage_common.instrumented_files_info
.
O parâmetro dependency_attributes
de instrumented_files_info
precisa listar
todos os atributos de dependência de execução, incluindo dependências de código (como deps
) e
de dados (como data
). O parâmetro source_attributes
precisará 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 é de ferramenta e que não define
cfg
como "host"
ou "exec"
no esquema de atributos) em
dependency_attributes
. Esse não é o comportamento ideal, já que ele coloca atributos
como srcs
em dependency_attributes
em vez de source_attributes
, mas
evita a necessidade de configuração de cobertura explícita para todas as regras na
cadeia de dependência.
Ações de validação
Às vezes, você precisa validar algo sobre o build, e as informações necessárias para fazer essa validação 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 elas não podem 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 falha e, portanto, o build também.
Exemplos de validações que podem ser executadas são análises estáticas, inspeções, verificações de dependência e consistência e de estilo.
As ações de validação também podem ajudar a melhorar o desempenho 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 inspeção puder ser separada em uma de compilação e uma de lint, ela poderá ser executada como uma ação de validação e executada em paralelo com outras ações.
Essas "ações de validação" geralmente não produzem nada que seja usado em outro lugar no build, já que elas só precisam declarar coisas sobre as entradas. No entanto, isso apresenta um problema: se uma ação de validação não produz nada que é usado em outro lugar no build, como uma regra faz com que a ação seja 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 alguma 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 do build. Como o Bazel pensa que a saída vazia é necessária para executar a ação de compilação, ele executará a ação de validação primeiro, mesmo que a ação de compilação ignore a entrada. Isso reduz o paralelismo e desacelera os builds.
Se outras ações no build puderem ser executadas em vez da 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 acidentalmente interrompida.
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 é projetado para armazenar as saídas não usadas de ações de validação, para que elas não precisem ser adicionadas artificialmente às entradas de outras ações.
Esse grupo é especial porque as saídas são sempre solicitadas, independentemente do
valor da sinalização --output_groups
e de como o destino
depende (por exemplo, na linha de comando, como uma dependência ou por
saídas implícitas do destino). O armazenamento em cache normal e a incrementabilidade
ainda se aplicam: se as entradas na ação de validação não tiverem mudado e a
ação de validação foi bem-sucedida anteriormente, a ação de validação não será
executada.
O uso desse grupo de saída ainda exige que as ações de validação gerem algum arquivo, mesmo um vazio. Isso pode exigir o wrapper de algumas ferramentas que normalmente não criam saídas para que um arquivo seja criado.
As ações de validação de um destino não são executadas em três casos:
- Quando o alvo depende de como uma ferramenta
- Quando o destino depende de uma dependência implícita (por exemplo, um atributo que começa com "_")
- Quando o destino é incorporado na configuração do host ou do exec.
Presume-se que esses destinos têm os próprios builds e testes separados que revelariam 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"),
}
)
Observe que o arquivo de saída de validação não é adicionado ao DefaultInfo
ou às entradas a qualquer outra ação. A ação de validação para um destino desse tipo de regra ainda será executada se o rótulo depender do destino ou se qualquer uma das saídas implícitas do destino depender direta ou indiretamente.
Geralmente, é importante que as saídas das ações de validação entrem apenas no grupo de saída de validação e não sejam adicionadas às entradas de outras ações, porque isso pode anular ganhos de paralelismo. No entanto, no momento, o Bazel não tem nenhuma verificação especial para aplicar isso. Portanto, é necessário testar se as saídas da ação de validação não são adicionadas às entradas de nenhuma ação nos testes das regras 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)
Sinalização das ações de validação
A execução de ações de validação é controlada pela sinalização de linha de comando
--run_validations
, que tem como padrão "true".
Recursos descontinuados
Saídas pré-declaradas descontinuadas
Há duas maneiras descontinuadas de usar as 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 as saídas aDefaultInfo.files
. Use o rótulo do destino da regra como entrada para regras que consomem a saída em vez do rótulo de uma saída pré-declarada.Para regras executáveis,
ctx.outputs.executable
refere-se 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 para o parâmetroexecutable
deDefaultInfo
.
Recursos de runfiles a serem evitados
O ctx.runfiles
e o 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 arquivos runfiles em determinadas bordas de dependência fixadas no código de maneiras confusas. Em vez disso, adicione arquivos usando os parâmetrosfiles
outransitive_files
dectx.runfiles
ou mesclando arquivos de execução de dependências comrunfiles = runfiles.merge(dep[DefaultInfo].default_runfiles)
.Evite o uso de
data_runfiles
edefault_runfiles
do construtorDefaultInfo
. EspecifiqueDefaultInfo(runfiles = ...)
. A distinção entre arquivos de execução "padrão" e "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 devem incluir saídas padrão e mesclar emdefault_runfiles
a partir de atributos que fornecem arquivos de execução (geralmentedata
).Ao recuperar
runfiles
deDefaultInfo
, geralmente apenas para mesclar runfiles entre a regra atual e as dependências, 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 com o operador de ponto e criados ao colocar o campo em uma struct retornada pela função de implementação da regra.
Esse estilo foi descontinuado e não pode ser usado em novos códigos. Confira abaixo as informações que podem ajudar na migração. O novo mecanismo de provedor evita conflitos de nome. Ela também oferece suporte à ocultação de dados, exigindo que qualquer código que acesse a instância do provedor a recupere usando o símbolo do provedor.
No momento, os provedores legados ainda são aceitos. Uma regra pode retornar provedores legados e modernos da seguinte maneira:
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 para 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
, o struct retornado também pode usar vários outros
campos com significado especial e, portanto, não criam um provedor legado
correspondente:
Os campos
files
,runfiles
,data_runfiles
,default_runfiles
eexecutable
correspondem aos campos com o mesmo nome deDefaultInfo
. Não é permitido especificar nenhum desses campos ao mesmo tempo que retorna um provedorDefaultInfo
.O campo
output_groups
usa um valor de struct e corresponde a umaOutputGroupInfo
.
Nas declarações de regras provides
e
providers
de atributos
de dependência, os provedores legados são transmitidos como strings, e os provedores modernos são
transmitidos pelo símbolo *Info
. Mude de strings para símbolos
ao migrar. Para conjuntos de regras complexos ou grandes, em que é difícil atualizar todas as regras atomicamente, será mais fácil seguir esta sequência de etapas:
Modifique as regras que produzem o provedor legado para produzir os provedores legados e modernos, usando a sintaxe acima. Para as 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-a também para exigir o provedor moderno. Como opção, você pode intercalar esse trabalho com a etapa 1 fazendo com que os consumidores aceitem/exigim um dos provedores: teste a presença do provedor legado usando
hasattr(target, 'foo')
ou do novo provedor usandoFooInfo in target
.Remova totalmente o provedor legado de todas as regras.