Regras

Informar um problema Mostrar fonte Por noite · 7,4 do Google. 7.3 · 7.2 · 7.1 · 7.0 · 6.5

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 em provedores retornados pela função de implementação da regra. Por exemplo, uma regra binária C++ pode:

  1. Pegue um conjunto de arquivos de origem .cpp (entradas).
  2. Execute g++ nos arquivos de origem (ação).
  3. Retorna o provedor DefaultInfo com a saída executável e outros arquivos para disponibilizar no momento da execução.
  4. Retorna o provedor CcInfo com informações específicas do 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 autor 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, confira 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, revise o Tutorial sobre regras. Em seguida, 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 para determinados idiomas. Ao definir suas próprias regras, você pode adicionar suporte semelhante a linguagens e ferramentas que o Bazel não oferece de forma nativa.

O Bazel fornece um modelo de extensibilidade para escrever regras usando a linguagem Starlark. Essas regras são gravadas 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 dela 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 uma nova regra e armazenar 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 executable (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 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 novos valores e segmentações para os atributos da segmentação.

As regras também podem ser chamadas de funções do Starlark e carregadas em arquivos .bzl. As funções Starlark que chamam regras são chamadas de macros Starlark. Os macros do Starlark precisam ser chamados de arquivos BUILD e só podem ser chamados 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 um implementação do destino ou podem se referir a outras de destino, criando um gráfico de dependências.

Atributos específicos da regra, como srcs ou deps, são definidos transmitindo um mapeamento de nomes de atributos para esquemas (criados usando o módulo attr) para o parâmetro attrs de rule. Os atributos comuns, como name e visibility, são adicionados implicitamente a todas as regras. Outros atributos são adicionados implicitamente a regras executáveis e de teste. Os 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 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 atributo hdrs 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 fornece CcInfo.
  • data especifica que os arquivos serão disponibilizados no momento da execução para qualquer executável que dependa de um destino. 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) especifique 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 são resolvidos em relação ao destino definido.

example_library(
    name = "my_target",
    deps = [":other_target"],
)

example_library(
    name = "other_target",
    ...
)

Neste exemplo, other_target é uma dependência de my_target e, portanto, other_target é analisado primeiro. É um erro se houver um ciclo no gráfico de dependências de destinos.

Atributos particulares e dependências implícitas

Um atributo de dependência com um valor padrão cria uma dependência implícita. Ele é implícito porque faz parte do gráfico de destino que o usuário não especifica em um arquivo BUILD. As dependências implícitas são úteis para codificar uma relação entre uma regra e uma ferramenta (uma dependência no momento de build, como um compilador), já que a maioria das vezes o usuário não tem interesse em especificar qual ferramenta a regra usa. Dentro da função de implementação da regra, isso é tratado da mesma forma que outras dependências.

Se você quiser fornecer uma dependência implícita sem permitir que o usuário 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 valores. Geralmente, só faz sentido usar atributos privados para dependências implícitas.

example_library = rule(
    implementation = _example_library_impl,
    attrs = {
        ...
        "_compiler": attr.label(
            default = Label("//tools:example_compiler"),
            allow_single_file = True,
            executable = True,
            cfg = "exec",
        ),
    },
)

Neste exemplo, cada destino do tipo example_library tem 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, ctx.attr._compiler sempre vai apontar para //tools:example_compiler em todos os destinos desse tipo de regra. 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 que a 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 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. Por isso, 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 têm o mesmo nome da regra, mas com o sufixo _impl.

As funções de implementação usam exatamente um parâmetro: um contexto de regra, convencionalmente chamado de ctx. Eles retornam uma lista de provedores.

Destinos

As dependências são representadas no momento da análise como objetos Target. Esses objetos contêm os provedores gerados quando o função de implementação de target 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 meio desse 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 usar arquivos de cabeçalho por um atributo hdrs e os fornecer às ações de compilação do destino e dos consumidores, ela poderá 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 uma função de implementação de destino em vez de uma lista de objetos do 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 realiza E/S de arquivos durante a fase de análise, esses objetos não podem ser usados para ler ou gravar o conteúdo do arquivo diretamente. Em vez disso, eles são transmitidos para funções que emitem ações (consulte ctx.actions) para construir partes do gráfico de ações.

Um File pode ser um arquivo de origem ou um arquivo gerado. Cada arquivo gerado precisa ser 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 todos 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 contém apenas campos para atributos de dependência com especificações que definem executable=True.

Como declarar saídas

Durante a fase de análise, a função de implementação de uma regra pode criar saídas. Como todos os rótulos precisam ser conhecidos durante a fase de carregamento, 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 predeclaradas, como as criadas para atributos de saída, os objetos File podem ser recuperados dos campos correspondentes de ctx.outputs.

Ações

Uma ação descreve como gerar um conjunto de saídas a partir de um conjunto de entradas, por exemplo, "run gcc on hello.c and get 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:

O ctx.actions.args pode ser usado para e acumule os argumentos para ações. Ele evita a planificação de depsets até o tempo de execução:

def _example_library_impl(ctx):
    ...

    transitive_headers = [dep[ExampleInfo].headers for dep in ctx.attr.deps]
    headers = depset(ctx.files.hdrs, transitive=transitive_headers)
    srcs = ctx.files.srcs
    inputs = depset(srcs, transitive=[headers])
    output_file = ctx.actions.declare_file(ctx.label.name + ".output")

    args = ctx.actions.args()
    args.add_joined("-h", headers, join_with=",")
    args.add_joined("-s", srcs, join_with=",")
    args.add("-o", output_file)

    ctx.actions.run(
        mnemonic = "ExampleCompile",
        executable = ctx.executable._compiler,
        arguments = [args],
        inputs = inputs,
        outputs = [output_file],
    )
    ...

As ações recebem 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 deve ser conhecido durante o fase de análise. Ele pode depender do valor dos atributos, incluindo provedores de dependências, mas não pode depender do 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). Ações que criam um número variável de arquivos internamente podem ser agrupadas em um único arquivo (como ZIP, TAR ou outro formato de arquivamento).

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 precisam ser gravadas por alguma ação.

As ações são comparáveis a funções puras: elas dependem apenas das entradas fornecidas e evitam acessar informações do computador, nome de usuário, relógio, rede ou dispositivos de E/S, exceto para ler entradas e gravar 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 executadas. Um ciclo no gráfico de dependências gera 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

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 a função de implementação retorna um struct em vez de uma lista de objetos do provedor. Esse estilo é desencorajado e as regras precisam ser migradas para fora dele.

Saídas padrão

As saídas padrão de um destino são as que são solicitadas por padrão quando o destino é solicitado para build na linha de comando. Por exemplo, um //pkg:foo de destino java_library tem foo.jar como saída padrão, que será criado pelo comando bazel build //pkg:foo.

As saídas padrão são especificadas pelo parâmetro files de DefaultInfo:

def _example_library_impl(ctx):
    ...
    return [
        DefaultInfo(files = depset([output_file]), ...),
        ...
    ]

Se DefaultInfo não for retornado por uma implementação de regra ou se o parâmetro files não for especificado, DefaultInfo.files vai assumir o valor padrão de todas saídas predeclaradas (geralmente, as criadas por atributos de saída).

As regras que executam ações precisam fornecer saídas padrão, mesmo que não sejam usadas 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.

Runfiles

Os arquivos de execução são um conjunto de arquivos usados por um destino no momento da 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 prepara o ambiente para o binário, para que ele possa acessar os arquivos de execução durante a 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 à parâmetro 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 de data, cujas saídas são adicionadas aos runfiles de um destino. Os runfiles também precisam ser mesclados de data, bem como de qualquer atributo que possa fornecer código para execução futura, 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 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 uma lógica de pré-processamento e validação personalizada. Isso pode ser usado para garantir que todas as instâncias do provedor obedeçam a determinadas invariantes ou para oferecer aos usuários uma API mais limpa para extrair uma instância.

Para isso, transmita um callback init ao provider. Se esse callback for fornecido, o tipo de retorno de provider() vai mudar para uma tupla de dois valores: o símbolo do provedor, que é o valor de retorno normal quando init não é usado, e um " construtor bruto".

Nesse caso, quando o símbolo do provedor é chamado, em vez de retornar diretamente uma nova instância, ele encaminha os argumentos para o callback init. O valor de retorno do callback precisa ser um dicionário que mapeia nomes de campos (strings) para valores. Ele é 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, 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 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 de fábrica públicas alternativas que não passam pela lógica init. Por exemplo, em exampleinfo.bzl, podemos definir:

def make_barebones_exampleinfo(headers):
    """Returns an ExampleInfo with no files_to_link and only the specified headers."""
    return _new_exampleinfo(files_to_link = depset(), headers = all_headers)

Normalmente, o construtor bruto é vinculado a uma variável cujo nome começa com um sublinhado (_new_exampleinfo acima), para que o código do usuário não possa carregá-lo e gerar instâncias arbitrárias do provedor.

Outro uso 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 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, cujos destinos também podem ser invocados por um comando bazel test. As regras executáveis e de teste são criadas definindo o argumento executable ou 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 terminam em _test. Os nomes de target de teste também costumam terminar em _test por convenção, mas isso não é obrigatório. As regras que não são de teste não podem ter esse sufixo.

Ambos os tipos de regras precisam produzir um arquivo de saída executável (que pode ou não ser predeclarado) 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 uma ação ctx.actions.run ou ctx.actions.run_shell, isso precisa ser feito pela ferramenta subjacente 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 predeclarada 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.

Confira exemplos de uma regra executável e uma regra de teste.

As regras executáveis e de teste têm outros atributos definidos implicitamente, além daqueles adicionados para todas as regras. Os padrões de atributos adicionados implicitamente não podem ser 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 de (link em inglês) os runfiles não podem fazer a mesma suposição. Para atenuar isso, cada binário deve fornecer uma maneira de aceitam a raiz dos arquivos de execução como parâmetro usando um ambiente ou uma linha de comando. argumento/flag. Isso permite que os binários transmitam a raiz de arquivos de execução canônicos correta para os binários que são chamados. Se ele não estiver 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 único destino pode ter vários arquivos de saída. Quando um comando bazel build é executado, algumas das saídas das metas fornecidas ao comando são consideradas 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 saídas predeclaradas usando 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 atributo correspondente 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. Por exemplo, se um //pkg:mytarget de destino for de um tipo de regra que tenha um grupo de saída debug_files, esses arquivos poderão ser criados executando bazel build //pkg:mytarget --output_groups=debug_files. Como as saídas não declaradas não têm rótulos, elas só podem ser solicitadas ao aparecer nas saídas padrão ou em um grupo de saídas.

Os grupos de saída podem ser especificados com o OutputGroupInfo. Ao contrário de muitos provedores integrados, OutputGroupInfo pode receber parâmetros com nomes arbitrários para definir grupos de saída com esse nome:

def _example_library_impl(ctx):
    ...
    debug_file = ctx.actions.declare_file(name + ".pdb")
    ...
    return [
        DefaultInfo(files = depset([output_file]), ...),
        OutputGroupInfo(
            debug_files = depset([debug_file]),
            all_files = depset([output_file, debug_file]),
        ),
        ...
    ]

Além disso, ao contrário da maioria dos provedores, OutputGroupInfo pode ser retornado por uma aspect e a meta de regra à qual esse aspecto é aplicado, conforme 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ê queira criar um binário C++ para uma arquitetura diferente. O build pode ser complexo e envolver várias etapas. Alguns dos 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, precisam ser criados para a arquitetura de destino.

Por esse motivo, o Bazel tem um conceito de "configurações" e transições. O os principais alvos (aqueles solicitados na linha de comando) são construídos no "target" 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 mudar a arquitetura da CPU que é transmitida ao compilador. Em alguns casos, a mesma biblioteca pode ser necessária para diferentes configurações. 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 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.

Para cada atributo de dependência, é possível 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 flag executable=True, será necessário definir cfg. explicitamente. Isso evita que uma ferramenta seja criada acidentalmente para a configuração errada. 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.

As ferramentas que são executadas como parte do build (como compiladores ou geradores de código) precisam ser criadas para uma configuração de execução. Nesse caso, especifique cfg="exec" no atributo.

Caso contrário, os executáveis usados no momento da execução (como parte de um teste) devem ser criado para a configuração de destino. Nesse caso, especifique cfg="target" no atributo.

cfg="target" não faz nada. É apenas um valor de conveniência para ajudar os designers de regras a serem explícitos sobre as intenções. 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 que os autores de regras tenham uma grande flexibilidade na alteração de configurações, com a desvantagem de tornar o gráfico de build maior e menos compreensível.

Observação: historicamente, o Bazel não tinha o conceito de plataformas de execução. Em vez disso, todas as ações de build eram consideradas executadas na máquina host. As versões do Bazel anteriores à 6.0 criavam uma configuração "host" distinta para representar isso. Se houver referências a "host" no código ou em documentação antiga, se refere. Recomendamos o uso do Bazel 6.0 ou mais recente para evitar esse overhead conceitual extra.

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
    ...
)

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 eles precisarem ser diferentes por algum motivo, especifique os argumentos root_symlinks ou symlinks. O root_symlinks é um dicionário que mapeia caminhos para arquivos, em que os caminhos são relativos à raiz do diretório de arquivos de execução. O 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 for usado, tenha cuidado para não mapear dois arquivos diferentes para o mesmo caminho na árvore de runfiles. Isso vai causar a falha do build 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 todas as metas que usam sua regra, bem como para metas de qualquer tipo que dependam dessas metas. 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 a instrumentação de cobertura para determinados destinos. O build também coleta a lista de arquivos de origem que estão instrumentados. O subconjunto de alvos considerados é controlado pela flag --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á considerar isso na função de implementação. ctx.coverage_instrumented retorna "true" em o modo de cobertura quando as fontes de uma meta precisam ser instrumentadas:

# Are this rule's sources instrumented?
if ctx.coverage_instrumented():
  # Do something to turn on coverage for this compile action

A lógica que sempre precisa estar ativada no modo de cobertura (se as origens de um destino forem especificamente instrumentadas ou não) pode ser condicionada a ctx.configuration.coverage_enabled.

Se a regra incluir diretamente fontes das dependências antes da compilação (como arquivos de cabeçalho), talvez seja necessário ativar a instrumentação no momento da compilação se as fontes das dependências precisarem ser instrumentadas:

# Are this rule's sources or any of the sources for its direct dependencies
# in deps instrumented?
if (ctx.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 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 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 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 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 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 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 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 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 que seja 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 produzisse um arquivo vazio e adicionar artificialmente essa saída às entradas de outra ação importante no build:

Isso funciona porque o Bazel sempre executa a ação de validação quando a compilação a ação é executada, mas isso tem desvantagens significativas:

  1. 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.

  2. Se outras ações no build puderem ser executadas em vez da ação de compilação, as saídas vazias das ações de validação também precisarão ser adicionadas a essas ações (por exemplo, a saída do jar de origem de java_library). 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 deixada de fora.

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 armazenar 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 incrementalidade ainda se aplicam: se as entradas para a ação de validação não mudarem e a ação de validação tiver sido bem-sucedida anteriormente, ela não será executada.

O uso desse grupo de saída ainda exige que as ações de validação gerem algum arquivo, mesmo que vazio. Isso pode exigir o agrupamento 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 destino é uma ferramenta dependente
  • Quando o destino é dependente 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.

Pressupõe-se que esses alvos tenham 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 _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 desse tipo de regra ainda será executada se o destino depender do rótulo ou se qualquer uma das saídas implícitas do destino for diretamente ou indiretamente dependente.

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 alguma verificação especial para aplicar 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 flag de linha de comando --run_validations, que é definida como "true" por padrão.

Recursos descontinuados

Saídas predeclaradas descontinuadas

Há duas maneiras descontinuadas de usar saídas pré-declaradas:

  • O parâmetro outputs do rule 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 declaradas previamente e adicionar explicitamente saídas a DefaultInfo.files. Use o rótulo do destino da regra como entrada para regras que consomem a saída em vez de um rótulo de saída predeclarado.

  • Para regras executáveis, ctx.outputs.executable se refere a uma saída executável predeclarada com o mesmo nome do destino da regra. De preferência, declare a saída explicitamente, por exemplo, com ctx.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âmetro executable do DefaultInfo.

Recursos de arquivos de execução a evitar

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 e collect_default de ctx.runfiles. Esses modos coletam implicitamente arquivos de execução em determinadas bordas de dependência codificadas de forma fixa de maneira confusa. Em vez disso, adicione arquivos usando os parâmetros files ou transitive_files do ctx.runfiles ou pela mesclagem de arquivos de execução de dependências com runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles).

  • Evite usar data_runfiles e default_runfiles dos construtor DefaultInfo. Especifique DefaultInfo(runfiles = ...). A distinção entre os runfiles "default" e "data" é mantida por razões legadas. Por exemplo, algumas regras colocam suas saídas padrão em data_runfiles, mas não default_runfiles. Em vez de usar data_runfiles, as regras precisam incluir saídas padrão e mesclar default_runfiles de atributos que fornecem arquivos de execução (geralmente data).

  • Ao extrair runfiles de DefaultInfo (geralmente apenas para mesclar arquivos de execução entre a regra atual e as dependências dela), use DefaultInfo.default_runfiles, não DefaultInfo.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.

Esse 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, a struct retornada também pode ter vários outros campos com significado especial (e, portanto, não cria um provedor compatível legado):

  • Os campos files, runfiles, data_runfiles, default_runfiles e executable correspondem aos campos com o mesmo nome de DefaultInfo. Não é permitido especificar qualquer um destes campos e retornar um provedor DefaultInfo.

  • O campo output_groups recebe um valor de struct e corresponde a um OutputGroupInfo.

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:

  1. Modifique as regras que produzem o provedor legado para produzir os provedores legados e modernos usando a sintaxe acima. Para regras que declaram que retornam o provedor legado, atualize essa declaração para incluir os provedores legados e modernos.

  2. 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-a 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 usando FooInfo in target.

  3. Remova completamente o provedor legada de todas as regras.