切面

本页介绍了使用 Aspect 的基础知识和优势,并提供了 简单示例和高级示例。

借助 Aspect,您可以向构建依赖关系图添加其他信息 和操作。以下是一些典型场景,在这些场景中,Aspect 非常有用:

  • 集成 Bazel 的 IDE 可以使用 Aspect 来收集有关项目的信息。
  • 代码生成工具可以利用 Aspect 以与 目标无关的方式对其输入执行操作。protobuf

Aspect 基础知识

BUILD 文件提供了项目源代码的说明:哪些源 文件是项目的一部分,应从这些文件构建哪些工件(目标),这些文件之间的依赖关系是什么等。Bazel 使用 此信息执行构建,也就是说,它会找出生成工件所需的一组操作(例如运行编译器或链接器)并 执行这些操作。Bazel 通过在目标之间构建依赖关系 图并访问此图来收集这些操作。

请考虑以下 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'], ...)

BUILD 文件定义了下图所示的依赖关系图:

构建图表

图 1。BUILD 文件依赖关系图。

Bazel 会通过为上述示例中的每个 目标调用相应 规则(在本例中为“java_library”)的实现函数来分析此依赖关系图。规则实现函数会生成用于构建工件(例如 .jar 文件)的操作,并将这些工件的位置和名称等信息传递给这些目标的反向依赖项(在 提供程序中)。

Aspect 与规则类似,因为它们都有一个实现函数,该函数会 生成操作并返回提供程序。不过,它们的强大之处在于 为其构建依赖关系图的方式。Aspect 具有实现 和它沿其传播的所有属性的列表。假设 Aspect A 沿名为“deps”的属性 传播。此 Aspect 可以应用于 目标 X,从而生成 Aspect 应用节点 A(X)。在应用期间, Aspect A 会以递归方式应用于 X 在其“deps” 属性(A 的传播列表中的所有属性)中引用的所有目标。

因此,将 Aspect A 应用于目标 X 的单个操作会生成 下图所示的目标的原始依赖关系图的“影子图”:

使用切面构建图表

图 2.包含 Aspect 的构建图。

被遮盖的边仅是沿传播集中的属性的边,因此在此 示例中,runtime_deps 边未被遮盖。然后,系统会在影子图中的所有节点上调用 Aspect 实现函数,类似于在原始图的节点上调用规则实现的方式。

简单示例

此示例演示了如何以递归方式打印规则及其所有具有 deps 属性的依赖项的源文件。它展示了 Aspect 实现、Aspect 定义以及如何从 Bazel 命令行调用 Aspect 。

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'],
)

让我们将示例分解为各个部分,并单独检查每个部分。

Aspect 定义

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

Aspect 定义与规则定义类似,并且使用 aspect 函数定义。

与规则一样,Aspect 也有一个实现函数,在本例中为 _print_aspect_impl

attr_aspects 是 Aspect 沿其传播的规则属性的列表。 在本例中,Aspect 将沿其所应用的规则的 deps 属性传播。

attr_aspects 的另一个常见实参是 ['*'],它会将 Aspect 传播到规则的所有属性。

Aspect 实现

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 []

Aspect 实现函数与规则实现 函数类似。它们会返回 提供程序,可以生成 操作,并接受两个实参:

  • target:Aspect 所应用的 目标
  • ctxctx 对象,可用于访问属性 以及生成输出和操作。

实现函数可以通过 ctx.rule.attr访问目标规则的属性。它可以检查目标(通过 target 实参)提供的提供程序。

Aspect 必须返回提供程序的列表。在此示例中,Aspect 不提供任何内容,因此它会返回一个空列表。

使用命令行调用 Aspect

应用 Aspect 的最简单方法是通过命令行使用 --aspects 实参。假设上述 Aspect 在名为 print.bzl 的文件中定义,则:

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

会将 print_aspect 应用于目标 example 以及可通过 deps 属性以递归方式访问的所有 目标规则。

--aspects 标志接受一个实参,该实参是 Aspect 的规范,格式为 <extension file label>%<aspect top-level name>

高级示例

以下示例演示了如何使用目标规则中的 Aspect 来统计目标中的文件数,并可按扩展名进行过滤。它展示了如何使用提供程序返回值,如何使用形参将 实参传递到 Aspect 实现中,以及如何从规则调用 Aspect。

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 = '*'),
    },
)

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',
)

Aspect 定义

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

此示例展示了 Aspect 如何通过 deps 属性传播。

attrs 定义了 Aspect 的一组属性。公共 Aspect 属性 的类型为 string,称为形参。形参必须指定values 属性。此示例有一个名为 extension 的形参,该形参允许将“*”“h”或“cc”作为值。

Aspect 的形参值取自与请求 Aspect 的规则同名的字符串属性(请参阅 file_count_rule 的定义)。带有形参的 Aspect 无法通过命令行使用,因为没有用于定义形参的语法。

Aspect 还允许具有类型为 labellabel_list 的私有属性。私有标签属性可用于指定对 Aspect 生成的操作所需的工具或库的依赖项。在此示例中未定义私有属性,但以下代码段演示了如何将工具传递给 Aspect:

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

Aspect 实现

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

与规则实现函数一样,Aspect 实现函数 会返回一个提供程序结构,其依赖项可以访问该结构。

在此示例中,FileCountInfo 定义为一个具有一个 字段 count 的提供程序。最佳做法是使用 fields 属性显式定义 提供程序的字段。

Aspect 应用 A(X) 的提供程序集是来自目标 X 的规则实现和来自 Aspect A 实现的提供程序 的并集。规则实现传播的提供程序是在应用 Aspect 之前创建和冻结的,无法从 Aspect 进行修改。如果目标和应用于该目标的 Aspect 各自 提供具有相同类型的提供程序,则会出错,但 OutputGroupInfo (只要 规则和 Aspect 指定不同的输出组,就会合并)和 InstrumentedFilesInfo (取自 Aspect)除外。这意味着 Aspect 实现绝不能返回 DefaultInfo

形参和私有属性在 ctx 的属性中传递。此示例引用了 extension 形参,并确定 要统计哪些文件。

对于返回提供程序,Aspect 沿其传播的属性(来自 attr_aspects 列表)的值将替换为对它们应用 Aspect 的结果。例如,如果目标 X 在其依赖项中包含 Y 和 Z,则 ctx.rule.attr.deps 的 A(X) 将为 [A(Y), A(Z)]。 在此示例中,ctx.rule.attr.deps 是目标对象,这些对象是将 Aspect 应用于已应用 Aspect 的原始目标的“依赖项”的结果。

在该示例中,Aspect 从 目标的依赖项访问 FileCountInfo 提供程序,以累积文件的总传递数。

从规则调用 Aspect

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 = '*'),
    },
)

规则实现演示了如何访问 FileCountInfo 通过 ctx.attr.deps

规则定义演示了如何定义形参 (extension) 并为其指定默认值 (*)。请注意,如果默认值 不是“cc”“h”或“*”之一,则会出错,因为 Aspect 定义中对形参施加了 限制。

通过目标规则调用 Aspect

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

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

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

这演示了如何通过规则将 extension 形参传递到 Aspect 中 。由于 extension 形参在 规则实现中具有默认值,因此 extension 将被视为可选形参。

构建 file_count 目标时,系统将针对 自身以及可通过 deps 以递归方式访问的所有目标评估我们的 Aspect。