Uma regra define uma série de ações que o Bazel executa em entradas para produzir um conjunto de saídas, que são referenciadas providers retornados pelo função de implementação. Por exemplo, uma linguagem C++ regra binária pode:
- Escolha 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 disponibilizar no ambiente de execução. - Retorne o provedor
CcInfo
com informações específicas de C++ coletadas do destino e as dependências dele.
Do ponto de vista do Bazel, g++
e as bibliotecas C++ padrão também são entradas.
a esta regra. Como elaborador de regras, você deve considerar não apenas as regras
entradas de 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 regras fases de build. É importante entender os três de um build (carregamento, análise e execução). Também é útil aprender sobre macros para compreender a diferença entre regras e . Para começar, leia o tutorial sobre regras. Depois, use esta página como referência.
Algumas regras são incorporadas ao Bazel. Essas regras nativas, como
cc_library
e java_binary
oferecem suporte básico a determinados idiomas.
Ao definir suas próprias regras, você pode adicionar suporte semelhante para idiomas e ferramentas
que o Bazel não tem suporte nativo.
O Bazel fornece um modelo de extensibilidade para escrever regras usando o
Idioma 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 ele gera resultados.
A função implementation
da regra define o comportamento exato durante a
fase de análise. Essa função não executa nenhum
comandos externos. Em vez disso, ele registra ações que são usadas
durante a fase de execução para criar as saídas da regra, se elas forem
necessários.
Criação de regras
Em um arquivo .bzl
, use a função rule para definir um novo
e armazene o resultado em uma variável global. A chamada para rule
especifica
atributos e um
função de implementação:
example_library = rule(
implementation = _example_library_impl,
attrs = {
"deps": attr.label_list(),
...
},
)
Isso define um tipo de regra chamada 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 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 para uma regra de criação não retorna nenhum valor, mas tem o efeito colateral de definir um alvo. Isso é chamado de instanciação da regra. Ela especifica um nome para o nova segmentação e valores para os atributos dela.
As regras também podem ser chamadas com base em funções do 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 a partir de arquivos BUILD
e só podem ser
chamado durante a fase de carregamento, quando BUILD
são avaliados para instanciar destinos.
Atributos
Um atributo é um argumento de regra. Os atributos podem fornecer valores específicos para um implementação do destino ou podem se referir a outras de destino, criando um gráfico de dependências.
Os atributos específicos da regra, como srcs
ou deps
, são definidos transmitindo um mapa
desde nomes de atributos até esquemas (criados usando a classe attr
módulo) para o parâmetro attrs
de rule
.
Atributos comuns, como
name
e visibility
são adicionadas implicitamente a todas as regras. Adicional
são implicitamente adicionados
executáveis e de teste especificamente. Atributos que
forem implicitamente adicionados a uma regra não podem ser incluídos no dicionário passado ao
attrs
:
Atributos de dependência
As regras que processam o código-fonte geralmente definem os seguintes atributos a serem processados 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 do arquivo de origem que a regra processa. Regras para idiomas com arquivos principais Em geral, especifique um atributohdrs
separado para cabeçalhos processados por uma e seus consumidores.deps
especifica 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
.- O
data
especifica os arquivos a serem disponibilizados no momento da execução para qualquer executável. que depende de uma meta. Isso deve permitir que arquivos arbitrários sejam especificado.
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 o rótulo
Label
) são listados nesse atributo quando o alvo
é definida. O repositório e possivelmente o caminho para esses rótulos foi resolvido
em relação à meta definida.
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 dos destinos.
Atributos particulares e dependências implícitas
Um atributo de dependência com um valor padrão cria uma dependência implícita. Ela
é implícito porque faz parte do gráfico de destino que o usuário não
especificar em um arquivo BUILD
. As dependências implícitas são úteis para codificar um
relação entre uma regra e uma ferramenta (uma dependência de tempo de build, como uma
), já que na maioria das vezes um usuário não está interessado em especificar o que
ferramenta que 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
substituir esse valor, é possível tornar o atributo privado atribuindo um nome a ele
que comece com um sublinhado (_
). Os atributos particulares devem ter valores padrão
e a distribuição dos valores dos dados. Geralmente, só faz sentido usar atributos privados para
dependências.
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 um valor implícito
dependência do compilador //tools:example_compiler
. Isso permite
Função de implementação de example_library
para gerar ações que invocam o
compilador, mesmo que o usuário não tenha passado seu rótulo como uma entrada. Como
_compiler
é um atributo particular, então ctx.attr._compiler
sempre apontará para //tools:example_compiler
em todos os destinos desta regra
não é válido. Como alternativa, é possível nomear o atributo compiler
sem o
underline e manter o valor padrão. Isso permite que os usuários substituam um
um compilador diferente, se necessário, mas não exige conhecimento da
rótulo.
As dependências implícitas geralmente são usadas para ferramentas que residem no mesmo repositório como implementação da regra. Se a ferramenta vier do plataforma de execução ou um repositório diferente, o precisa receber essa ferramenta de um conjunto de ferramentas.
Atributos de saída
Atributos de saída, como attr.output
e
attr.output_list
, declare 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 referirem aos destinos definidos em outro lugar.
- Os destinos do arquivo de saída dependem do destino da regra instanciada, e não vice-versa.
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 do destino. Se uma regra tiver
um atributo de saída, ele geralmente é denominado 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 solicitado na linha de comando.
Função de implementação
Todas as regras exigem uma função implementation
. Essas funções são executadas,
estritamente na fase de análise e transformam
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 nem gravar arquivos.
As funções de implementação de regras geralmente são particulares (com o nome
sublinhado). Convencionalmente, eles recebem o mesmo nome da regra, mas com sufixo
com _impl
.
As funções de implementação levam exatamente um parâmetro: um
contexto da regra, comumente chamado de ctx
. Eles retornam uma lista de
provedores de serviço.
Destinos
As dependências são representadas no momento da análise como Target
objetos. Esses objetos contêm os provedores gerados quando o
função de implementação de target foi executada.
ctx.attr
tem campos que correspondem aos nomes de cada
atributo de dependência, contendo objetos Target
representando cada
por meio desse atributo. Para os atributos label_list
, esta é 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. Podem ser provedores personalizados definidos no Starlark ou
provedores de regras nativas disponíveis como Starlark
variáveis globais.
Por exemplo, se uma regra recebe arquivos de cabeçalho por meio de um atributo hdrs
e fornece
às ações de compilação do público-alvo e dos consumidores,
coletá-los 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 de um
função de implementação de target em vez de uma lista de objetos de provedor:
return struct(example_info = struct(headers = depset(...)))
Os provedores podem ser recuperados do 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 devem ser migraram dela.
Arquivos
Os arquivos são representados por objetos File
. Como o Bazel não
realizar E/S de arquivo durante a fase de análise, esses objetos não podem ser usados para
leem ou gravam diretamente o conteúdo do arquivo. Em vez disso, eles são passados para um agente
(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 uma saída de exatamente uma ação. Os arquivos de origem não podem ser a saída
qualquer ação.
Para cada atributo de dependência, o campo correspondente
ctx.files
contém uma lista das saídas padrão de todas
dependências por meio desse 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 que definem allow_single_file=True
.
ctx.executable
se comporta da mesma forma que ctx.file
, mas apenas
contém campos para atributos de dependência com especificações definidas como executable=True
.
Declaração de 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,
e as saídas 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 alvo,
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, objetos File
podem ser recuperados
dos campos correspondentes de ctx.outputs
.
Ações
Uma ação descreve como gerar um conjunto de saídas com base em um conjunto de entradas, exemplo: "executar gcc em hello.c e obter hello.o". Quando uma ação é criada, o Bazel não executa o comando imediatamente. Ele o registra em um gráfico de dependências, porque uma ação pode depender do resultado 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 shell. kubectl.ctx.actions.write
, para gravar uma string em um arquivo.ctx.actions.expand_template
para gerar um arquivo a partir de um modelo.
O ctx.actions.args
pode ser usado para
e acumule os argumentos para ações. Evita achatamento de dependências até
ambiente 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 tomam uma lista ou remoção 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 deve ser conhecido durante o fase de análise. Isso pode depender do valor atributos, incluindo provedores de dependências, mas não pode depender dos resultado da execução. Por exemplo, se sua ação executar o comando unzip, você deve especificar 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 uni-los 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 é permitidos, mas ineficientes.
As ações precisam criar todas as saídas. Eles podem gravar outros arquivos, Aquilo que não estiver nas saídas não ficará disponível para os consumidores. Todas as saídas declaradas precisa ser escrita por alguma ação.
As ações são comparáveis a funções puras: elas devem depender apenas do fornecidas entradas, e evite 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 são executada. Caso haja um ciclo no gráfico de dependências, isso constituirá um erro. Criando uma ação não garante que ela será executada, isso depende de se as saídas dele são necessárias para o build.
Provedores
Provedores são informações que uma regra expõe a outras regras que dependerem disso. 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 alvo precisam saber
Como a função de implementação de uma regra só pode ler provedores do
dependências imediatas do destino instanciado, as regras precisam encaminhar
informações das dependências de um destino que precisam ser conhecidas pelo
consumidores, geralmente acumulando-os em um depset
.
Os provedores de um destino são especificados por uma lista de objetos Provider
retornados por
a função de implementação.
As funções de implementação antigas também podem ser escritas em um estilo legado, em que o
função de implementação retorna uma struct
em vez de uma lista de
objetos de provedor. Esse estilo não é recomendado, e as regras devem ser
migraram dela.
Saídas padrão
As saídas padrão de um destino são aquelas solicitadas por padrão ao
o destino é solicitado para build na linha de comando. Por exemplo,
O destino java_library
do //pkg:foo
tem foo.jar
como saída padrão, de modo que
será criado pelo comando bazel build //pkg:foo
.
As saídas padrão são especificadas pelo parâmetro files
da
DefaultInfo
:
def _example_library_impl(ctx):
...
return [
DefaultInfo(files = depset([output_file]), ...),
...
]
Se DefaultInfo
não for retornado pela implementação de uma regra ou pelo files
parâmetro não for especificado, DefaultInfo.files
será definido como padrão
resultados pré-declarados (geralmente, aqueles criados por saída
).
as regras que executam ações precisam fornecer saídas padrão, mesmo que elas não devem ser usados diretamente. Ações que não estão no gráfico do e as 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 de forma isolada. Isso dificulta a depuração, porque recriar apenas o destino com falha reproduzir a falha.
Arquivos de execução
Os arquivos de execução são um conjunto de arquivos usados por um destino no ambiente de execução (em vez de criar tempo de resposta). 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 organiza para o binário para que ele possa acessar os arquivos de execução durante o tempo 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 à
runfiles
em DefaultInfo
. A saída executável do
regras executáveis são adicionadas implicitamente aos arquivos de execução.
Algumas regras especificam atributos, geralmente chamados
data
, cujas saídas são adicionadas a
dos destinos arquivos de execução. Os arquivos de execução também precisam ser mesclados de data
, bem como
de quaisquer atributos que possam fornecer código para execução final, 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 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 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 lógica de pré-processamento e validação. Isso pode ser usado para garantir que todos as instâncias do provedor obedecem a certas invariantes ou fornecem aos usuários uma API mais limpa para para conseguir uma instância.
Para isso, transmita um callback init
ao
provider
. Se esse retorno de chamada for fornecido, o
O tipo de retorno de provider()
muda para uma tupla de dois valores: o provedor.
que é o valor de retorno comum quando init
não é usado, e uma expressão
construtor".
Nesse caso, quando o símbolo de provedor é chamado, em vez de retornar diretamente
uma nova instância, ele encaminhará os argumentos para o callback init
. A
o valor de retorno do callback precisa ser um dict que mapeia nomes de campos (strings) para valores.
usado para inicializar os campos da nova instância. Observe que o
callback poderá ter qualquer assinatura e, se os argumentos não corresponderem à assinatura,
um erro é relatado como se a chamada de retorno tivesse sido invocada diretamente.
O construtor bruto, por outro lado, ignora 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 pode instanciar o provedor da seguinte 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,
poderia 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 cujo nome começa com uma
sublinhado (_new_exampleinfo
acima), de modo que o código do usuário não possa carregá-lo, e
instâncias de provedor arbitrários.
Outro uso do init
é simplesmente impedir que o usuário chame o provedor
e forçá-los 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 regras de teste
As regras executáveis definem destinos que podem ser invocados por um comando bazel run
.
Regras de teste são um tipo especial de regra executável cujos destinos também podem ser
invocado por um comando bazel test
. Regras executáveis e de teste são criadas
definindo os respectivos executable
ou
Argumento test
para 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
. Teste nomes de destinos também
terminam em _test
por convenção, mas isso não é obrigatório. As regras que não são de teste não podem
têm 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 dizer
ao Bazel quais saídas de uma regra serão usadas como executável, transmita-a como
Argumento executable
de um DefaultInfo
retornado.
de nuvem. Esse executable
é adicionado às saídas padrão da regra (para que você
não é necessário transmitir isso para executable
e files
. 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 deve definir o bit executável no arquivo. Para
um ctx.actions.run
ou
ctx.actions.run_shell
ação necessária
pela ferramenta invocada pela ação. Para um
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 serve como
executável padrão se você não especificar um usando DefaultInfo
; não pode ser
usado de outra forma. Este mecanismo de saída foi descontinuado porque não oferece suporte
personalizando o nome do arquivo executável no momento da análise.
Veja exemplos de regra executável e um regra de teste.
Regras executáveis e regras de teste têm mais atributos definidos implicitamente, além daqueles adicionados para todas as regras. Os padrões de atributos adicionados implicitamente não podem ser alterados, embora isso possa ser contornado ao envolver uma regra privada em uma macro Starlark, que altera a 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
runfiles é adjacente ao executável. Os caminhos se relacionam 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
runfiles
. No entanto, os binários chamados pelos arquivos de execução não podem
a mesma suposição. Para atenuar isso, cada binário deve fornecer uma maneira de
aceitam a raiz dos arquivos de execução como um parâmetro usando um ambiente ou uma linha de comando.
argumento/flag. Isso permite que os binários passem a raiz correta dos arquivos de execução canônicos
aos binários chamados. Se isso não estiver definido, um binário pode adivinhar que foi
Primeiro binário chamado e procure um diretório adjacente dos arquivos de execução.
Temas avançados
Como solicitar arquivos de saída
Um único destino pode ter vários arquivos de saída. Quando um comando bazel build
é
em execução, algumas das saídas dos destinos fornecidos ao comando são consideradas
ser solicitadas. O Bazel só compila esses arquivos solicitados
dependem direta ou indiretamente. No gráfico de ações, somente o Bazel
executa as ações que são acessíveis como dependências transitivas da
arquivos solicitados.
Além das saídas padrão, qualquer saída pré-declarada pode
explicitamente solicitada na linha de comando. As regras podem especificar valores
saídas por atributos de saída. Nesse caso, o usuário
escolhe explicitamente rótulos para saídas quando eles instanciam a regra. Para receber
objetos File
para atributos de saída, use o
de ctx.outputs
. As regras podem
definir implicitamente saídas pré-declaradas com base
no nome do destino também, mas o uso desse 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
Para
exemplo, se um //pkg:mytarget
de destino for de um tipo de regra que tem um debug_files
grupo de saída, 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 uma
grupo.
Os grupos de saída podem ser especificados com o
OutputGroupInfo
. Ao contrário de muitos
provedores integrados, o 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 uma
aspect e a meta de regra à qual esse aspecto é aplicado, como
desde que não definam os mesmos grupos de saída. Nesse caso, o resultado
são mesclados.
Observe que OutputGroupInfo
geralmente não deve ser usado para transmitir classificações específicas.
de arquivos de um destino para as ações de seus consumidores. Definição
provedores específicos de regras para isso.
Configurações
Imagine que você quer criar um binário C++ para uma arquitetura diferente. A pode ser complexo e envolver várias etapas. Alguns dos canais intermediários binários, como compiladores e geradores de código, precisam ser executados a plataforma de execução (que pode ser seu host, ou um executor remoto). Alguns binários, como a saída final, devem ser criados para o arquitetura-alvo.
Por esse motivo, o Bazel tem um conceito de "configurações" e transições. A os principais alvos (aqueles solicitados na linha de comando) são construídos no “objetivo” configuração, enquanto as ferramentas que devem ser executadas na plataforma de execução são criados em um ambiente de execução configuração do Terraform. As regras podem gerar ações diferentes com base na configuração, por exemplo, para alterar a arquitetura de CPU que é passada ao compilador. Em alguns casos, a mesma biblioteca pode ser necessária para diferentes personalizadas. Se isso acontecer, ele será analisado e possivelmente construído várias vezes.
Por padrão, o Bazel cria as dependências de um destino na mesma configuração que o destino em si, ou seja, sem transições. Quando uma dependência é ferramenta necessária para ajudar a criar o destino, o atributo correspondente deve especificar uma transição para uma configuração "exec". Isso faz com que a ferramenta e todos os dependências a serem criadas para a plataforma de execução.
Em cada atributo de dependência, é possível usar cfg
para decidir se as dependências
deve compilar na mesma configuração ou fazer uma transição para uma configuração "exec".
Se um atributo de dependência tiver a flag executable=True
, será necessário definir cfg
.
explicitamente. Isso serve para evitar a criação acidental de uma ferramenta para o
configuração do Terraform.
Confira um exemplo
Em geral, fontes, bibliotecas dependentes e executáveis que serão necessários em ambiente de execução podem usar a mesma configuração.
Ferramentas que são executadas como parte do build (como compiladores ou geradores de código)
precisa ser criado para uma configuração "exec". Nesse caso, especifique cfg="exec"
no
o atributo.
Caso contrário, os executáveis usados no momento da execução (como parte de um teste) devem
ser criado para a configuração de destino. Nesse caso, especifique cfg="target"
no
o atributo.
A cfg="target"
não faz nada: é simplesmente um valor de conveniência
ajudam os designers de regras a deixar
as intenções explícitas. Quando executable=False
,
o que significa que cfg
é opcional, defina essa opção apenas quando ela realmente ajudar na legibilidade.
Também é possível usar cfg=my_transition
para usar
transições definidas pelo usuário, que permitem
aos autores de regras uma grande flexibilidade na alteração de configurações, com a
desvantagem de
deixar o gráfico de build maior e menos compreensível.
Observação: no passado, o Bazel não tinha o conceito de plataformas de execução, em vez disso, todas as ações de build eram executadas na máquina host. Júlio anteriores à 6.0 criavam um "host" distinto para representar isso. Se houver referências a "host" no código ou em documentação antiga, se refere. Recomendamos usar o Bazel 6.0 ou mais recente para evitar sobrecarga.
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 em
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
...
)
Links simbólicos dos arquivos de execução
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 árvore de saída gerada. Se essas
precisam ser diferentes por algum motivo, é possível especificar root_symlinks
ou
symlinks
. O root_symlinks
é um dicionário que mapeia caminhos para
, em que os caminhos são relativos à raiz do diretório runfiles. A
O dicionário symlinks
é o mesmo, mas os caminhos são implicitamente prefixados com o
do espaço de trabalho principal (não o nome do repositório que contém as
a meta 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
forem usados, tenha cuidado para não mapear dois tipos
para o mesmo caminho na árvore de arquivos de execução. Isso fará com que o build falhe
com um erro que descreve o conflito. Para corrigir isso, você precisará modificar seu
Argumentos ctx.runfiles
para remover a colisão. Essa verificação será feita para
todos os destinos que usam sua regra, bem como os destinos de qualquer tipo que dependem dessas
de destino. Isso é especialmente arriscado se é provável que sua ferramenta seja usada de forma transitiva
por outra ferramenta. os nomes dos links simbólicos devem ser únicos nos arquivos de execução de uma ferramenta e
todas as dependências.
Cobertura de código
Quando o comando coverage
é executado,
o build pode precisar adicionar instrumentação de cobertura para certos destinos. A
build também coleta a lista de arquivos de origem que estão instrumentados. O subconjunto de
que são considerados controlados pela sinalização
--instrumentation_filter
Os destinos de teste são excluídos, a menos
--instrument_test_targets
é especificado.
Se a implementação de uma regra adicionar instrumentação de cobertura no momento da criação, ela precisará levar isso em consideração na função de implementação. ctx.coverage_instrumented retorna "true" em o modo de cobertura se for necessário instrumentar as fontes de uma meta:
# Are this rule's sources instrumented?
if ctx.coverage_instrumented():
# Do something to turn on coverage for this compile action
Lógica que precisa estar sempre ativada no modo de cobertura (se as fontes de um alvo especificamente são instrumentados ou não) podem ser condicionados ctx.configuration.coverage_enabled.
Se a regra incluir diretamente fontes das dependências antes da compilação (como arquivos de cabeçalho), ele também pode precisar ativar a instrumentação em tempo de compilação se das dependências. devem ser instrumentados:
# 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
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 do ambiente 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 as
atributos de arquivos de origem da regra caso a instrumentação de cobertura possa 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
um atributo de dependência não relacionado a uma ferramenta que não seja definido
cfg
como "host"
ou "exec"
no esquema do 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 no
cadeia de dependências.
Ações de validação
Às vezes você precisa validar algo sobre a versão, e a as informações necessárias para fazer essa validação estão disponíveis somente em artefatos (arquivos de origem ou arquivos 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 . Em vez disso, as ações precisam fazer essa validação no ambiente de execução. Quando a validação falhar, a ação e o build também.
Exemplos de validações que podem ser executadas são análises estáticas, inspeção, verificações de dependência, consistência e de estilo.
As ações de validação também podem melhorar o desempenho do build ao mover partes. que não são necessárias para criar artefatos em ações separadas. Por exemplo, se uma única ação que realiza compilação e inspeção puder ser separadas em uma ação de compilação e uma ação de inspeção, pode ser executada como uma ação de validação e em paralelo com outras ações.
Essas "ações de validação" muitas vezes não produzem nada que seja usado em outro lugar no build, já que eles só precisam declarar coisas sobre as entradas. Isso apresenta um problema: se uma ação de validação não produzir nada for usada 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 e adiciona artificialmente essa saída às entradas de alguns outros arquivos na versão:
Isso funciona porque o Bazel sempre executa a ação de validação quando a compilação a ação é executada, mas isso tem desvantagens significativas:
A ação de validação está no caminho crítico do build. Como o Bazel achar que a saída vazia é necessária para executar a ação de compilação, ele executará de validação primeiro, mesmo que a ação de compilação ignore a entrada. Isso reduz o paralelismo e deixa os builds mais lentos.
Se outras ações no build puderem ser executadas em vez ação de compilação, as saídas vazias das ações de validação precisarão ser adicionadas ao essas ações também (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 é adicionado mais tarde, e a saída de validação vazia é 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 é um grupo de saída projetado para manter o saídas não utilizadas de ações de validação, para que não precisem ser artificialmente adicionados às entradas de outras ações.
Este grupo é especial porque suas saídas são sempre solicitadas, independentemente
o valor da sinalização --output_groups
e, independentemente de como o destino
dependem (por exemplo, na linha de comando, como uma dependência ou por meio de
saídas implícitas do destino). O armazenamento em cache normal e a incrementabilidade
ainda são aplicáveis: se as entradas da ação de validação não tiverem sido alteradas e o
a ação de validação já foi bem-sucedida, a ação não será
correr.
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 encapsulamento de algumas ferramentas que normalmente não criar saídas para criar um arquivo.
As ações de validação de um destino não são executadas em três casos:
- Quando o alvo depende do alvo como ferramenta
- Quando o destino depende como uma dependência implícita (por exemplo, um atributo que começa com "_")
- Quando o destino é criado na configuração do host ou do exec.
Presume-se que esses alvos tenham versões 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 _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 foi adicionado ao DefaultInfo
ou ao
entradas para qualquer outra ação. A ação de validação para um destino deste tipo de regra
ainda será executado se o rótulo depender do rótulo ou de qualquer
as saídas implícitas dependem direta ou indiretamente.
Geralmente, é importante que as saídas das ações de validação entrem apenas no e não são adicionados às entradas de outras ações, conforme isso poderia reduzir os ganhos de paralelismo. No entanto, atualmente o Bazel não alguma verificação especial para impor isso. Portanto, você deve testar que as saídas da ação de validação não sejam adicionadas às entradas das ações no e testes para as 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)
Sinalização de ações de validação
A execução de ações de validação é controlada pela linha de comando --run_validations
que tem como padrão verdadeiro.
Recursos descontinuados
Saídas pré-declaradas descontinuadas
Há duas maneiras descontinuadas de usar saídas pré-declaradas:
O parâmetro
outputs
dorule
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 a adição explícita de saídas paraDefaultInfo.files
. Use o método rótulo como entrada para regras que consomem a saída em vez de um valor no rótulo da saída.Para regras executáveis,
ctx.outputs.executable
se refere para 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 suas permissões para permitir a execução. explicitamente Transmita a saída executável ao parâmetroexecutable
deDefaultInfo
.
Recursos dos arquivos de execução a serem evitados
ctx.runfiles
e runfiles
tipo têm um conjunto complexo de atributos, muitos dos quais são mantidos por motivos legados.
As recomendações a seguir ajudam a reduzir a complexidade:
Evite o uso dos modos
collect_data
ecollect_default
dectx.runfiles
Esses modos coletam implicitamente arquivos de execução em determinadas bordas de dependência fixadas no código de maneiras confusas. Em vez disso, adicione arquivos usando os parâmetrosfiles
outransitive_files
doctx.runfiles
ou pela mesclagem de arquivos de execução de dependências comrunfiles = runfiles.merge(dep[DefaultInfo].default_runfiles)
.Evite usar
data_runfiles
edefault_runfiles
dos construtorDefaultInfo
. EspecifiqueDefaultInfo(runfiles = ...)
. A distinção entre "padrão" e "dados" os runfiles são mantidos para motivos legados. Por exemplo, algumas regras colocam suas saídas padrão emdata_runfiles
, mas nãodefault_runfiles
. Em vez de usardata_runfiles
, as regras precisam ambas incluir saídas padrão e se mesclardefault_runfiles
dos atributos que fornecem arquivos de execução (geralmentedata
).Ao recuperar
runfiles
deDefaultInfo
, geralmente apenas para mesclagem runfiles entre a regra atual e suas dependências), useDefaultInfo.default_runfiles
, nãoDefaultInfo.data_runfiles
.
Como migrar de provedores legados
Antes, os provedores do Bazel eram campos simples no objeto Target
. Eles
foram acessados usando o operador ponto e foram criados ao colocar o campo
em uma estrutura retornada pela função de implementação da regra.
O estilo foi descontinuado e não deve ser usado em novos códigos. Confira abaixo. informações que podem ajudar na migração. O novo mecanismo do provedor evita nomes entre em conflito. Ele também oferece suporte à ocultação de dados, exigindo que qualquer código provedor para recuperá-la usando o símbolo do provedor.
No momento, os provedores legados ainda são compatíveis. 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, o
provedores e seus conteúdos podem 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 que tenham um significado especial (e, portanto, não criam um modelo de
provedor):
Os campos
files
,runfiles
,data_runfiles
,default_runfiles
eexecutable
correspondem aos campos de mesmo nome deDefaultInfo
. Não é permitido especificar esses campos, além de retornar um provedorDefaultInfo
.O campo
output_groups
usa um valor de struct e corresponde a umOutputGroupInfo
.
Em declarações de regras provides
e em
providers
declarações de dependência
atributos, os provedores legados são transmitidos à medida que as strings e os provedores modernos são
passados pelo símbolo *Info
. Não se esqueça de mudar de strings para símbolos.
durante a migração. Para conjuntos de regras grandes ou complexos em que é difícil atualizar
todas as regras atomicamente, será mais fácil se você seguir essa sequência de
etapas:
Modifique as regras que produzem o provedor legado para produzir o legado e provedores modernos, usando a sintaxe acima. No caso de regras que declaram retornar o provedor legado, atualize essa declaração para incluir os valores de provedores legados e modernos.
Modifique as regras que consomem o provedor legado para consumir o um provedor moderno. Se alguma declaração de atributo exigir o provedor legado, atualize-os para exigir o provedor moderno. Também é possível intercalar esse trabalho com a etapa 1, fazendo com que os consumidores aceitem/exijam provedor: testa a presença do provedor legado usando
hasattr(target, 'foo')
ou o novo provedor usandoFooInfo in target
.Remova totalmente o provedor legado de todas as regras.