切面

报告问题 查看源代码 每夜 build · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

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

通过方面,您可以使用其他信息和操作来增强构建依赖项图。以下是一些在哪些典型场景中方面可能很有用:

  • 集成了 Bazel 的 IDE 可以使用 aspect 收集有关项目的信息。
  • 代码生成工具可以利用方面以与目标无关的方式在其输入上执行操作。例如,BUILD 文件可以指定 protobuf 库定义的层次结构,并且特定于语言的规则可以使用方面来附加操作,以便为特定语言生成 protobuf 支持代码。

切面基础知识

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 文件)的操作,并将信息(例如这些工件的位置和名称)传递给 provider 中这些目标的反向依赖项。

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

因此,将 A 方面应用于目标 X 这一单个操作会产生目标原始依赖项图的“阴影图”,如下图所示:

使用 Aspect 构建图表

图 2. 使用切面构建图。

只有沿着传播集的属性的边会被阴影,因此在本例中,runtime_deps 边不会被阴影。然后,系统会对阴影图中的所有节点调用一个方面实现函数,这与对原始图的节点调用规则实现类似。

简单示例

此示例演示了如何递归输出规则及其具有 deps 属性的所有依赖项的源文件。该图展示了切面实现、切面定义以及如何从 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'],
)

我们将该示例拆分成多个部分,分别研究每个部分。

切面定义

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

切面定义与规则定义类似,是使用 aspect 函数定义的。

与规则一样,切面也具有实现函数,在本示例中为 _print_aspect_impl

attr_aspects 是切面传播的规则属性列表。在这种情况下,该方面将沿其应用到的规则的 deps 属性传播。

attr_aspects 的另一个常见参数是 ['*'],它会将相应方面传播到规则的所有属性。

方面实现

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

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

  • target:将切面应用到的目标
  • ctxctx 对象,可用于访问属性并生成输出和操作。

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

需要切面才能返回提供程序的列表。在此示例中,切面不提供任何内容,因此它返回一个空列表。

使用命令行调用切面

应用方面最简单的方法是在命令行中使用 --aspects 参数。假设上述方面是在名为 print.bzl 的文件中定义的,如下所示:

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

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

--aspects 标志接受一个参数,该参数是采用 <extension file label>%<aspect top-level name> 格式的宽高比规范。

高级示例

以下示例演示了如何使用目标规则中的某个方面来统计目标中的文件数,并可能按扩展名对这些文件进行过滤。其中介绍了如何使用提供程序来返回值、如何使用参数将参数传递给切面实现,以及如何从规则调用切面。

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

切面定义

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

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

attrs 为切面定义一组属性。公共切面属性的类型为 string,称为参数。参数必须指定 values 属性。此示例包含一个名为 extension 的参数,其值可以是“*”“h”或“cc”。

切面的参数值取自与请求切面的规则同名的字符串属性(请参阅 file_count_rule 的定义)。带参数的切面不能通过命令行使用,因为没有语法来定义参数。

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

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

方面实现

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

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

在此示例中,FileCountInfo 定义为具有一个字段 count 的提供程序。最佳实践是使用 fields 属性明确定义提供程序的字段。

切面应用 A(X) 的提供程序集是实现目标 X 的规则和实现切面 A 后得到的提供程序的集合。规则实现传播的提供程序会在应用方面之前创建并冻结,并且无法通过方面进行修改。如果目标和应用于它的方面各自提供相同类型的提供程序,则会发生错误,OutputGroupInfo(只要规则和方面指定不同的输出组,就会合并)和 InstrumentedFilesInfo(从方面获取)除外。这意味着,aspect 实现绝不能返回 DefaultInfo

参数和私有属性通过 ctx 的属性进行传递。此示例引用 extension 参数,并确定要统计哪些文件。

对于返回提供程序,用于传播切面的属性值(从 attr_aspects 列表)会替换为对该切面应用该切面的结果。例如,如果目标 X 的依赖项中包含 Y 和 Z,则 A(X) 的 ctx.rule.attr.deps 将为 [A(Y), A(Z)]。在此示例中,ctx.rule.attr.deps 是 Target 对象,是将此 aspect 应用于已应用此 aspect 的原始 target 的“依赖项”的结果。

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

从规则调用方面

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

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

规则定义演示了如何定义参数 (extension) 并为其指定默认值 (*)。请注意,由于在方面定义中对参数施加了限制,因此如果默认值不是“cc”“h”或“*”中的一种,则会出错。

通过目标规则调用 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 递归访问。