切面

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

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

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

Aspect 基础知识

BUILD 文件提供了项目源代码的说明:哪些源 文件是项目的一部分,应从这些文件构建哪些工件(目标),这些文件之间的依赖关系是什么等。Bazel 使用 此信息执行 build,也就是说,它会找出生成工件所需的一组操作(例如运行编译器或链接器),并 执行这些操作。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's 的传播列表中的所有属性)中引用的所有目标。

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

使用切面构建图表

图 2.包含 Aspect 的 build 图。

被遮盖的边仅是沿传播集中的属性的边,因此在此 示例中,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 属性 定义了参数,并且只能是 boolintstring 类型。 对于规则传播的 Aspect,intstring 参数必须指定 values。此示例有一个名为 extension 的参数,该参数允许将“*”“h”或“cc”作为值。

对于规则传播的 Aspect,参数值取自请求 Aspect 的规则,使用具有相同名称和类型的规则的属性。 (请参阅 file_count_rule 的定义)。

对于命令行 Aspect,可以使用 --aspects_parameters 标志传递参数值。可以省略 intstring 参数的 values 限制。

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 目标时,我们的 Aspect 将针对 自身以及可通过 deps 以递归方式访问的所有目标进行评估。

参考