Aspectos

Informar um problema Ver código-fonte Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Esta página explica os conceitos básicos e os benefícios do uso de aspectos e fornece exemplos simples e avançados.

Os aspectos permitem aumentar os gráficos de dependência de build com mais informações e ações. Alguns cenários típicos em que os aspectos podem ser úteis:

  • Os ambientes de desenvolvimento integrados ao Bazel podem usar aspectos para coletar informações sobre o projeto.
  • As ferramentas de geração de código podem aproveitar aspectos para executar as entradas de forma independente do destino. Por exemplo, os arquivos BUILD podem especificar uma hierarquia de definições da biblioteca protobuf, e as regras específicas da linguagem podem usar aspectos para anexar ações que geram código de suporte do protobuf para um idioma específico.

Noções básicas de aspecto

Os arquivos BUILD fornecem uma descrição do código-fonte de um projeto: quais arquivos de origem fazem parte do projeto, quais artefatos (alvos) devem ser criados a partir desses arquivos, quais são as dependências entre esses arquivos etc. O Bazel usa essas informações para realizar um build, ou seja, ele descobre o conjunto de ações necessárias para produzir os artefatos (como executar o compilador ou o vinculador) e executa essas ações. O Bazel faz isso construindo um gráfico de dependência entre os destinos e acessando esse gráfico para coletar essas ações.

Considere o seguinte arquivo BUILD:

java_library(name = 'W', ...)
java_library(name = 'Y', deps = [':W'], ...)
java_library(name = 'Z', deps = [':W'], ...)
java_library(name = 'Q', ...)
java_library(name = 'T', deps = [':Q'], ...)
java_library(name = 'X', deps = [':Y',':Z'], runtime_deps = [':T'], ...)

Esse arquivo BUILD define um gráfico de dependência mostrado na figura abaixo:

Criar gráfico

Figura 1. Gráfico de dependência de arquivo BUILD.

O Bazel analisa esse gráfico de dependência chamando uma função de implementação da regra correspondente (neste caso, "java_library") para cada destino no exemplo acima. As funções de implementação de regras geram ações que criam artefatos, como arquivos .jar, e transmitem informações, como locais e nomes desses artefatos, para as dependências reversas dessas metas em provedores.

Os aspectos são semelhantes às regras porque têm uma função de implementação que gera ações e retorna provedores. No entanto, o poder deles vem da maneira como o gráfico de dependência é criado para eles. Um aspecto tem uma implementação e uma lista de todos os atributos que ele propaga. Considere um aspecto A que se propaga ao longo de atributos chamados "deps". Esse aspecto pode ser aplicado a um destino X, gerando um nó de aplicação de aspecto A(X). Durante a aplicação, o aspecto A é aplicado recursivamente a todas as metas a que X se refere no atributo "deps" (todos os atributos na lista de propagação de A).

Assim, a aplicação do aspecto A a um destino X gera um "gráfico sombra" do gráfico de dependências original dos destinos mostrado na figura abaixo:

Criar gráfico com aspecto

Figura 2. Crie um gráfico com aspectos.

As únicas arestas sombreadas são as arestas ao longo dos atributos no conjunto de propagação. Portanto, a aresta runtime_deps não é sombreada neste exemplo. Uma função de implementação de aspecto é invocada em todos os nós no gráfico de sombra de maneira semelhante a como as implementações de regras são invocadas nos nós do gráfico original.

Exemplo simples

Este exemplo demonstra como imprimir recursivamente os arquivos de origem de uma regra e todas as dependências dela que têm um atributo deps. Ele mostra uma implementação de aspecto, uma definição de aspecto e como invocar o aspecto na linha de comando do Bazel.

def _print_aspect_impl(target, ctx):
    # Make sure the rule has a srcs attribute.
    if hasattr(ctx.rule.attr, 'srcs'):
        # Iterate through the files that make up the sources and
        # print their paths.
        for src in ctx.rule.attr.srcs:
            for f in src.files.to_list():
                print(f.path)
    return []

print_aspect = aspect(
    implementation = _print_aspect_impl,
    attr_aspects = ['deps'],
)

Vamos dividir o exemplo em partes e examinar cada uma delas individualmente.

Definição de aspecto

print_aspect = aspect(
    implementation = _print_aspect_impl,
    attr_aspects = ['deps'],
)

As definições de aspecto são semelhantes às definições de regras e são definidas usando a função aspect.

Assim como uma regra, um aspecto tem uma função de implementação que, neste caso, é _print_aspect_impl.

attr_aspects é uma lista de atributos de regra em que o aspecto é propagado. Nesse caso, o aspecto será propagado ao longo do atributo deps das regras às quais ele é aplicado.

Outro argumento comum para attr_aspects é ['*'], que propaga o aspecto para todos os atributos de uma regra.

Implementação de aspectos

def _print_aspect_impl(target, ctx):
    # Make sure the rule has a srcs attribute.
    if hasattr(ctx.rule.attr, 'srcs'):
        # Iterate through the files that make up the sources and
        # print their paths.
        for src in ctx.rule.attr.srcs:
            for f in src.files.to_list():
                print(f.path)
    return []

As funções de implementação de aspectos são semelhantes às funções de implementação de regras. Elas retornam provedores, podem gerar ações e usam dois argumentos:

  • target: o alvo ao qual o aspecto está sendo aplicado.
  • ctx: objeto ctx que pode ser usado para acessar atributos e gerar saídas e ações.

A função de implementação pode acessar os atributos da regra de destino por meio de ctx.rule.attr. Ele pode examinar provedores que são fornecidos pelo destino a que ele é aplicado (pelo argumento target).

Os aspectos são necessários para retornar uma lista de provedores. Neste exemplo, o aspecto não fornece nada, então retorna uma lista vazia.

Como invocar o aspecto usando a linha de comando

A maneira mais simples de aplicar um aspecto é na linha de comando, usando o argumento --aspects. Supondo que o aspecto acima tenha sido definido em um arquivo chamado print.bzl assim:

bazel build //MyExample:example --aspects print.bzl%print_aspect

aplicaria o print_aspect ao example de destino e a todas as regras de destino que são acessíveis de forma recursiva pelo atributo deps.

A sinalização --aspects usa um argumento, que é uma especificação do aspecto no formato <extension file label>%<aspect top-level name>.

Exemplo avançado

O exemplo a seguir demonstra o uso de um aspecto de uma regra de destino que conta arquivos em destinos, possivelmente filtrando-os por extensão. Ele mostra como usar um provedor para retornar valores, como usar parâmetros para transmitir um argumento para uma implementação de aspecto e como invocar um aspecto de uma regra.

Arquivo file_count.bzl:

FileCountInfo = provider(
    fields = {
        'count' : 'number of files'
    }
)

def _file_count_aspect_impl(target, ctx):
    count = 0
    # Make sure the rule has a srcs attribute.
    if hasattr(ctx.rule.attr, 'srcs'):
        # Iterate through the sources counting files
        for src in ctx.rule.attr.srcs:
            for f in src.files.to_list():
                if ctx.attr.extension == '*' or ctx.attr.extension == f.extension:
                    count = count + 1
    # Get the counts from our dependencies.
    for dep in ctx.rule.attr.deps:
        count = count + dep[FileCountInfo].count
    return [FileCountInfo(count = count)]

file_count_aspect = aspect(
    implementation = _file_count_aspect_impl,
    attr_aspects = ['deps'],
    attrs = {
        'extension' : attr.string(values = ['*', 'h', 'cc']),
    }
)

def _file_count_rule_impl(ctx):
    for dep in ctx.attr.deps:
        print(dep[FileCountInfo].count)

file_count_rule = rule(
    implementation = _file_count_rule_impl,
    attrs = {
        'deps' : attr.label_list(aspects = [file_count_aspect]),
        'extension' : attr.string(default = '*'),
    },
)

Arquivo BUILD.bazel:

load('//:file_count.bzl', 'file_count_rule')

cc_library(
    name = 'lib',
    srcs = [
        'lib.h',
        'lib.cc',
    ],
)

cc_binary(
    name = 'app',
    srcs = [
        'app.h',
        'app.cc',
        'main.cc',
    ],
    deps = ['lib'],
)

file_count_rule(
    name = 'file_count',
    deps = ['app'],
    extension = 'h',
)

Definição de aspecto

file_count_aspect = aspect(
    implementation = _file_count_aspect_impl,
    attr_aspects = ['deps'],
    attrs = {
        'extension' : attr.string(values = ['*', 'h', 'cc']),
    }
)

Este exemplo mostra como o aspecto se propaga pelo atributo deps.

attrs define um conjunto de atributos para um aspecto. Os atributos de aspecto público definem parâmetros e só podem ser dos tipos bool, int ou string. Para aspectos propagados por regras, os parâmetros int e string precisam ter values especificados. Esse exemplo tem um parâmetro chamado extension que pode ter '*', 'h' ou 'cc' como valor.

Para aspectos propagados por regras, os valores de parâmetro são retirados da regra que solicita o aspecto, usando o atributo da regra com o mesmo nome e tipo. (consulte a definição de file_count_rule).

Para aspectos de linha de comando, os valores dos parâmetros podem ser transmitidos usando a flag --aspects_parameters. A restrição values dos parâmetros int e string pode ser omitida.

Os aspectos também podem ter atributos privados dos tipos label ou label_list. Os atributos de marca própria podem ser usados para especificar dependências de ferramentas ou bibliotecas necessárias para ações geradas por aspectos. Não há um atributo particular definido neste exemplo, mas o snippet de código abaixo demonstra como transmitir uma ferramenta para um aspecto:

...
    attrs = {
        '_protoc' : attr.label(
            default = Label('//tools:protoc'),
            executable = True,
            cfg = "exec"
        )
    }
...

Implementação de aspectos

FileCountInfo = provider(
    fields = {
        'count' : 'number of files'
    }
)

def _file_count_aspect_impl(target, ctx):
    count = 0
    # Make sure the rule has a srcs attribute.
    if hasattr(ctx.rule.attr, 'srcs'):
        # Iterate through the sources counting files
        for src in ctx.rule.attr.srcs:
            for f in src.files.to_list():
                if ctx.attr.extension == '*' or ctx.attr.extension == f.extension:
                    count = count + 1
    # Get the counts from our dependencies.
    for dep in ctx.rule.attr.deps:
        count = count + dep[FileCountInfo].count
    return [FileCountInfo(count = count)]

Assim como uma função de implementação de regra, uma função de implementação de aspecto retorna uma estrutura de provedores que são acessíveis às dependências.

Neste exemplo, o FileCountInfo é definido como um provedor que tem um campo count. É uma prática recomendada definir explicitamente os campos de um provedor usando o atributo fields.

O conjunto de provedores para um aplicativo de aspecto A(X) é a união de provedores que vêm da implementação de uma regra para o destino X e da implementação do aspecto A. Os provedores que uma implementação de regra propaga são criados e congelados antes que os aspectos sejam aplicados e não podem ser modificados por um aspecto. Será um erro se um destino e um aspecto aplicado a ele fornecerem um provedor com o mesmo tipo, com as exceções de OutputGroupInfo (que é mesclada, desde que a regra e o aspecto especifiquem grupos de saída diferentes) e InstrumentedFilesInfo (que é tirada do aspecto). Isso significa que as implementações de aspecto podem nunca retornar DefaultInfo.

Os parâmetros e atributos particulares são transmitidos nos atributos do ctx. Este exemplo faz referência ao parâmetro extension e determina quais arquivos serão contados.

Para provedores recorrentes, os valores dos atributos em que o aspecto é propagado (da lista attr_aspects) são substituídos pelos resultados de uma aplicação do aspecto a eles. Por exemplo, se o destino X tiver Y e Z nas dependências, ctx.rule.attr.deps para A(X) será [A(Y), A(Z)]. Neste exemplo, ctx.rule.attr.deps são objetos de destino que são o resultado da aplicação do aspecto às "dependências" do destino original a que o aspecto foi aplicado.

No exemplo, o aspecto acessa o provedor FileCountInfo das dependências do destino para acumular o número transitivo total de arquivos.

Invocar o aspecto de uma regra

def _file_count_rule_impl(ctx):
    for dep in ctx.attr.deps:
        print(dep[FileCountInfo].count)

file_count_rule = rule(
    implementation = _file_count_rule_impl,
    attrs = {
        'deps' : attr.label_list(aspects = [file_count_aspect]),
        'extension' : attr.string(default = '*'),
    },
)

A implementação da regra demonstra como acessar o FileCountInfo usando o ctx.attr.deps.

A definição da regra demonstra como definir um parâmetro (extension) e atribuir a ele um valor padrão (*). Ter um valor padrão que não seja "cc", "h" ou "*" seria um erro devido às restrições aplicadas ao parâmetro na definição do aspecto.

Como invocar um aspecto usando uma regra de destino

load('//:file_count.bzl', 'file_count_rule')

cc_binary(
    name = 'app',
...
)

file_count_rule(
    name = 'file_count',
    deps = ['app'],
    extension = 'h',
)

Isso demonstra como transmitir o parâmetro extension para o aspecto usando a regra. Como o parâmetro extension tem um valor padrão na implementação da regra, ele é considerado opcional.extension

Quando o destino file_count é criado, nosso aspecto é avaliado por ele mesmo e por todos os destinos acessíveis recursivamente por deps.

Referências