概要
本文档介绍了“build 变体”的实现。这是 Fuchsia build 系统的一项功能,可用于构建插桩或经过特殊优化的主机和设备二进制文件版本。
读者必须熟悉 GN toolchain() 实例,并且应已阅读以下文档:
build 变体概览
Fuchsia build 定义了几种类型的 build 变体,例如:
asan
和ubsan
变体分别用于使用 Clang 的 Address Sanitizer 和 Undefined Behaviour Sanitizer 构建机器码。甚至还有一种asan-ubsan
变体,可将这两种方式结合使用。coverage
变体用于在启用 Clang 的基于插桩的性能分析的情况下构建机器代码,以支持代码覆盖率收集。profile
变体也用于构建插桩代码,但是为了支持配置文件引导型优化。thinlto
和lto
变体用于构建启用了链接时间优化的二进制文件。gcc
变体用于使用 GCC 编译器(而非 Clang)构建 Zircon 内核的某些部分,这有助于排除可能以非常重要的方式影响内核的细微机器码生成问题。适用于特殊需求的其他一些变体,这些变体均在
//build/config/BUILDCONFIG.gn
文件中定义,并采用本文档其余部分中所述的惯例。
一般来说,单个 build 变体可用于模拟以下情况:
一组额外的配置,用于定义在构建变体二进制文件及其依赖项时要应用的编译器、汇编器或链接器标志。
一组可选的隐式依赖项,要添加到 build 图中的最终变体二进制目标(即可执行文件、可加载模块,有时甚至是共享库)。
例如,我们来考虑一下“asan”build 变体,它用于启用 Clang 的 Address Sanitizer 支持。在实践中,若要构建启用了 AddressSanitizer 的 Fuchsia 可执行程序,至少需要满足以下条件:
在构建可执行文件及其所有依赖项(对于 Fuchsia,包括 C 库)时,将
-fsanitize=address
标志同时传递给 Clang 编译器和链接器。运行时可用的 Asan 运行时 (
libclang_rt.asan.so
),以及其自己的依赖项(即libc++.so
、libc++abi.so
和libunwind.so
二进制文件的特殊预构建版本)。
基础和变体工具链
build 变体始终应用于特定的“基础”工具链,该工具链提供由变体本身增强的默认设置。这会创建一个新的 GN toolchain() 实例,称为“变体工具链”,它具有自己的 root_out_dir
。例如:
//build/toolchain:host_x64
是用于构建宿主二进制文件的基础工具链,其root_out_dir
为${root_build_dir}/host_x64
。//build/toolchain:host_x64-ubsan
是通过应用ubsan
变体创建的变体工具链,其root_out_dir
为${root_build_dir}/host_x64-ubsan
。//build/toolchain/fuchsia:x64
是默认的工具链(以基于 x64 的设备为目标平台时),用于构建 Fuchsia 用户级二进制文件。由于它是默认值,因此其root_out_dir
与root_build_dir
相同。//build/toolchain/fuchsia:x64-asan
是通过将asan
变体应用于默认工具链而创建的变体工具链。其root_out_dir
将为${root_build_dir}/x64-asan
。
一般而言,//${tc_dir}:${tc_name}-${variant}
将是通过将 ${variant}
变体应用于名为 //${tc_dir}:${tc_name}
的基础工具链创建的变体工具链,其 root_out_dir
始终为 ${root_build_dir}/${tc_name}-${variant}
。
如果基本工具链具有 shlib 工具链,则其任何变体工具链也将具有一个。最后,一个变体可以应用于多个基础工具链。
例如,//build/toolchain:host_x64-asan
和 //build/toolchain/fuchsia:x64-asan
是通过将相同的 asan
变体应用于用于构建主机和 Fuchsia 设备二进制文件的基础工具链而创建的变体工具链。
后者还将 //build/toolchain/fuchsia:x64-asan-shared
用作 shlib 工具链来生成基于 ELF 的共享库。
必须在 build 中使用 clang_toolchain_suite()
或 zircon_toolchain_suite()
定义基础工具链。这两个模板最终都会调用 variant_toolchain_suite()
,后者会实现在需要时自动创建变体工具链的魔法。
工具链和变体标记
Fuchsia build 中的每个基础工具链都可以包含多个标记,这些标记是用于描述工具链属性的自由格式字符串。例如,"kernel"
标记用于指明工具链用于构建内核工件(这一点很重要,因为没有 C 库、标准 C++ 库,以及对某些目标定义至关重要的一些其他约束条件)。
所有有效的工具链标记的列表位于 //build/toolchain/toolchain_tags.gni
中。
同样,每个款式/规格定义都包含多个标记,用于描述款式/规格的属性。例如,"instrumentation"
标记用于指明此变体会创建用于执行运行时插桩(例如 sanitizer 或性能分析器)的机器代码
如需查看所有有效变体标记及其文档,请参阅 //build/toolchain/variant_tags.gni
。
创建变体工具链时,全局 toolchain_variant.tags
值将同时包含从基础工具链继承的标记和从变体继承的标记。
工具链变体实例化
构建系统仅在需要时创建变体工具链。可用的工具链和变体组合非常多,如果一次创建所有这些组合,gn gen
的速度会明显变慢。
构建系统不会立即创建所有变体,而是会根据以下条件决定要创建哪些工具链变体:
select_variant
全局变量中显示的变体选择器列表。显示为
variant_toolchain_suite()
模板的enable_variants
参数的变体描述符名称列表。即使select_variant
为空,也鲜少用于强制启用某些变体。例如,用于构建 C 库的工具链的 ASan 和 UBSan 变体始终处于启用状态,因为在构建 Core Fuchsia IDK 时需要这些变体(请参阅
//zircon/system/ulib/c/BUILD.gn
)。variant_toolchain_suite()
的exclude_variant_tags
参数中显示的变体标记列表。它很少用于排除将特定变体应用于给定基础工具链的操作。例如,引导加载程序会排除带有
"instrumented"
标记的变体,因为在启动设备时无法运行 sanitizer 或性能分析运行时(请参阅 `//
款式/规格描述符
变体描述符是 GN 作用域,用于向构建系统描述给定 build 变体的属性。这些范围通过 //build/config/BUILDCONFIG.gn
中的 known_variants
变量进行定义,并且每个范围都应遵循以下严格的架构:
configs
:一组可选的 GN 配置标签,系统会自动将其添加到具有此变体的每个目标。请注意,对于此列表中的每个配置
${label}
,还必须有一个目标${label}_deps
,此变体中构建的每个目标都将自动依赖于此目标。大多数情况下,这将是一个空的group()
。remove_common_configs
:GN 配置标签的可选列表,应从使用此变体构建的任何目标中移除(如果存在)。当构建系统为二进制文件设置的某些默认配置不适用于特定变体时,有时需要执行此操作。remove_shared_configs
:GN 配置标签的可选列表,与remove_common_configs
类似,但仅在构建shared_library()
目标及其依赖项时应用。deps
:GN 目标标签的可选列表,将作为隐式依赖项添加到使用此变体构建的任何可链接目标。name
:用于唯一命名变体描述符的字符串,通常在select_variant
中使用。如果省略name
,configs
必须不为空,并且将用于派生名称(通过将其名称用短划线连接)。tags
:可选的自由形式字符串列表,用于描述变体的属性(请参阅工具链和变体标记)。toolchain_args
:一个可选作用域,其中此作用域中定义的每个变量都会替换此变体工具链上下文中的构建参数。host_only
和target_only
:可选镜,可包含上述任一字段。这些值分别仅适用于主机或目标(即设备)工具链。此处包含的任何字段都不应出现在外部作用域中。
示例如下:
包含单个配置的变体描述符示例
{
configs = [ "//build/config/lto" ]
tags = [ "lto" ]
}
上述作用域定义了一个名为 "lto"
的变体描述符(由于作用域中没有 name
键,因此该名称是根据 configs
中的值推断出来的,而 configs
中仅包含一个项)。
应用此变体将添加 //build/config/lto/BUILD.gn
中定义的 //build/config/lto:lto
配置,如果此类配置没有隐式依赖项,该文件还应包含 //build/config/lto:lto_deps
空组。例如:
# //build/config/lto/BUILD.gn
config("lto") {
cflags = [ "-flto" ]
asmflags = cflags
ldflags = cflags
rustflags = [ "-Clto=fat" ]
}
group("lto_deps") {
# Implicit dependencies for "lto" config.
# This is an empty group since there are none.
}
此描述符使用 "lto"
标记来指示此变体执行了链接时间优化。"thinlto"
描述符也可以使用此标记,它将使用不同的配置。
包含多个配置的变体描述符示例
{
configs = [
"//build/config/sanitizers:ubsan",
"//build/config/sanitizers:sancov",
]
remove_common_configs = [ "//build/config:no_rtti" ]
tags = [
"instrumented",
"instrumentation-runtime",
"kernel-excluded",
"sancov",
"ubsan",
]
}
这会定义一个名为 "ubsan-sancov"
的变体描述符(该名称是通过将配置名称用短划线连接而从 configs
列表派生的),用于构建机器代码,以便在运行时检测未定义的行为,同时收集代码覆盖率信息。
请注意,这还需要定义 //build/config/sanitizers:ubsan_deps
和 //build/config/sanitizers:sancov_deps
,以列出这些配置中的隐式依赖项。
此方法使用 remove_common_config
,因为 //build/config:no_rtti
是许多基础工具链的默认配置的一部分,但必须启用 RTTI,才能让 UBSan 插桩正常运行。
所使用的代码列表也更为广泛。请注意 "kernel-excluded"
标记,该标记用于防止将此变体应用于任何内核机器代码。
包含 toolchain_args
的变体描述符示例
{
name = "fully_optimized"
toolchain_args = {
optimize = "speed"
}
}
此变体描述符已明确命名,且未添加任何配置或依赖项。另一方面,它可确保全局 build 配置变量 optimize
会设置为“speed”,这会更改在相应变体工具链上下文中定义的默认配置数量。
toolchain_variant
全局变量
在 BUILD.gn
或 *.gni
文件中,全局 toolchain_variant
变量可用于检索 current_toolchain
的变体相关信息。这是具有以下架构的镜重:
name
:build 变体描述符的名称。在基准工具链上下文中,此值为空字符串;否则,此值为用于创建当前 GNtoolchain()
实例的变体描述符的名称。各种工具链上下文的示例名称:
//build/toolchain/fuchsia:x64 "" //build/toolchain/fuchsia:x64-shared "" //build/toolchain/fuchsia:x64-asan "asan" //build/toolchain/fuchsia:x64-asan-shared "asan"
base
:指向当前工具链的基准工具链的完全限定 GN 标签。请注意,对于工具链变体的 shlib 工具链,此值指向最终的基础工具链。示例://build/toolchain/fuchsia:x64 //build/toolchain/fuchsia:x64 //build/toolchain/fuchsia:x64-asan //build/toolchain/fuchsia:x64 //build/toolchain/fuchsia:x64-shared //build/toolchain/fuchsia:x64 //build/toolchain/fuchsia:x64-asan-shared //build/toolchain/fuchsia:x64
tags
:一个由自由形式字符串组成的列表,每个字符串都描述了当前工具链实例及其变体的属性。这只是工具链和变体标记的联合。instrumented
:一个布尔标志,如果tags
列表包含"instrumentation"
标记值,则将设置为 true;此标志的提供是为了方便在 GN 中替换复杂的测试说明,例如:if (toolchain_variant.tags + [ "instrumentation" ] - [ "instrumentation" ] != toolchain_variant.tags) { # toolchain is instrumented ... }
替换为:
if (toolchain_variant.instrumented) { # toolchain is instrumented ... }
is_pic_default
:一个布尔值,在可以构建 ELF 位置无关代码 (PIC) 的工具链中为 true。这意味着,要么是 shlib 工具链(例如//build/toolchain/fuchsia:x64-shared
),要么是直接生成此类代码的基础工具链(例如//zircon/kernel/lib/userabi/userboot:userboot_arm64
)。with_shared
:一个布尔值,如果当前工具链具有用于构建 ELF 共享库的 shlib 工具链(例如//build/toolchain/fuchsia:x64
),或者当前处于此类工具链(例如//build/toolchain/fuchsia:x64-shared
)中,则为 true。configs
、remove_common_configs
、remove_shared_configs
:config()
项的 GN 标签列表,直接来自当前的变体描述符(如果有),否则为空列表。deps
:添加为任何可链接目标的依赖项的目标的 GN 标签列表,从变体描述符本身继承(如果有)。libprefix
:对于插桩变体,这是共享库的安装前缀字符串,否则为空字符串。如需了解完整详情,请参阅工具链变体 libprefix 部分。exclude_variant_tags
:变体选择逻辑在内部使用。从clang_toolchain_suite()
或zircon_toolchain_suite()
调用继承,或直接从目标定义继承。这是一个标记列表,用于排除要应用于基本工具链或目标的变体(有时需要)。suffix
:此值为"-${toolchain_variant.name}"
,如果名称为空,则为""
。在内部用于简化不带条件的展开。supports_cpp
:一个布尔值,如果此工具链支持 C/C++,则为true
。supports_rust
:一个布尔值,如果此工具链支持 Rust,则为true
。is_basic
:一个布尔值,如果此工具链由basic_toolchain()
模板创建,则为true
,因此不会使用 GN 中针对 C/C++ 和 Rust 的任何内置支持。它只有copy
或action
目标。
目标定义很少使用此全局变量的内容来根据当前工具链上下文更改其配置。这类问题通常发生在低级目标(例如 C 库、内核工件或非预构建的插桩运行时支持)上。
工具链变体 libprefix
为了能够在单个 Fuchsia 软件包中混合使用插桩二进制文件和非插桩二进制文件,构建系统必须执行特殊步骤:
使用插桩变体工具链构建的共享库必须安装到
"lib/<variant>/"
,而不是默认的"lib/"
位置。必须使用
"-Wl,-dynamic-linker=<variant>/ld.so.1"
等链接器参数编译可执行二进制文件,该参数会替换默认值("ld.so.1"
,在 Fuchsia clang 预构建工具链二进制文件中硬编码)。作为一种特殊情况,模糊测试 build 变体会使用库子目录的非模糊测试 build 变体名称。
toolchain_variant.libprefix
变量以如下方式定义,以便轻松支持所有这些操作:
variant name libdir libprefix note
no variant ---> lib/ "" (default target toolchain)
thinlto ---> lib/ "" (uninstrumented)
asan-ubsan ---> lib/asan-ubsan/ "asan-ubsan/" (instrumented)
asan-fuzzer ---> lib/asan/ "asan/" (instrumented + fuzzing)
这可用于将安装位置确定为 "lib/${toolchain_variant.libprefix}"
,并将链接器标志确定为 "-Wl,-dynamic-linker=${toolchain_variant.libprefix}ld.so.1"
。
变体选择
Fuchsia 构建系统支持选择启用哪些 build 变体,以及这些变体适用于哪些单个目标或目标组。为此,您可以在 build 配置文件 (args.gn
) 中定义 select_variant
变量。请参考以下示例:
# From out/default/args.gn
...
select_variant = [
{
label = [ "//src/sys/component_manager:bin" ]
variant = "lto"
},
"host_asan",
"thinlto/blobfs",
"ubsan",
]
列表中的每个值都是一个表达式,称为变体选择器,可以是作用域或字符串,用于配置 build 将如何将变体应用于不同的目标集。
当 select_variant
已定义且不为空列表时,其值将用于确定如何构建可链接的目标(例如可执行文件、可加载模块和在基本工具链上下文中显示在 build 图中的共享库),以及它们的所有依赖项。
系统会按顺序比较 select_variant
中显示的变体选择器,并选择第一个与当前目标匹配的选择器。因此,上述示例表示:
//src/sys/component/manager:bin
程序二进制文件及其依赖项应始终使用lto
变体进行构建。主机二进制文件应在
"asan"
变体中构建。请注意,"host_asan"
不是变体描述符名称,而是变体快捷方式。blobfs
程序设备二进制文件应始终使用"thinlto"
变体构建,该变体会执行链接时间优化。所有其他设备二进制文件都应使用
"ubsan"
变体构建。
变体选择器
变体选择器是可以出现在全局 select_variant
build 配置变量中的值。构建系统会在基本工具链上下文中定义可链接的目标时使用这些变量来控制变体选择。
支持三种类型的值:
用于为一组目标定义一组匹配条件的范围。该镜区的格式如下:
variant
:给定变体描述符的名称。仅当当前目标与范围其余部分中定义的所有条件匹配时,才会使用该名称。label
:如果已定义,此参数必须是符合条件的 GN 标签的列表(包含:
,但不包含工具链标签,例如//src/sys/foo:foo
)。name
:如果已定义,则为 GN 标签目标名称的列表(例如,//src/sys/foo:bar
目标的名称为“bar”)。dir
:GN 标签目录路径列表(例如,//src/sys/foo:bar
目标的路径为"//src/sys/foo"
)。如果未定义,则为空。output_name
:如果已定义,则为目标output_name
值的列表(默认为其target_name
)。target_type
:如果已定义,则为与目标类型匹配的字符串列表。有效值包括:"executable"
、"test"
、"loadable_module"
、"shared_library"
和其他一些值。testonly
:如果已定义,则为布尔值。如果为 true,则选择器会与testonly=true
匹配目标。如果为 false,选择器会与不含testonly=true
的目标匹配。host
:如果已定义,则为布尔值。如果为 true,选择器会与主机工具链中的目标匹配。如果为 false,则选择器在目标工具链中匹配。
包含指向变体快捷方式(即现有选择器范围值的别名)的简单名称(例如
"asan"
)的字符串。例如,
"coverage"
值等同于以下范围:{ variant = "coverage" host = false }
一个字符串,其中包含变体快捷方式名称和输出名称,以目录路径(例如
"thintlo/blobfs"
)分隔。这是一种便捷的格式,可避免编写等效的范围,在前面的示例中,该格式如下所示:{ variant = "thinlto" host = false output_name = [ "blobfs" ] }
select_variant
列表中选择器的顺序非常重要:第一个与当前目标匹配的选择器会胜出,并决定相应目标的构建方式。
变体快捷键
除了变体描述符之外,build 还会设置一些“快捷方式”,这些快捷方式是一些硬编码变体选择器范围值的命名别名。build 会添加一些硬编码的变体,并根据已知变体列表创建其他变体:
"host_asan"
快捷方式定义为使用"asan"
变体描述符构建主机二进制文件,在技术上等同于以下选择器作用域值列表:# Definition for the `host_asan` variant shortcut [ { variant = "asan" host = true } ]
同样,还有
host_asan-ubsan
、host_coverage
、host_profile
和其他一些方法。每个变体描述符名称都有一个对应的快捷方式,用于将其专门应用于设备二进制文件。也就是说,
"ubsan"
快捷方式等同于以下包含一个选择器范围值的列表:[ { variant = "ubsan" host = false } ]
因此,在
select_variant
中使用变体描述符名称只会将其应用于设备二进制文件,如以下示例所示:# Applies the `ubsan` variant to device binaries, not host ones! select_variant = [ "ubsan", ]
变体目标重定向
variant_target()
模板
//build/config/BUILDCONFIG.gn
中定义的 variant_target()
模板会实现核心 build 变体选择机制。
不应直接从 BUILD.gn
文件调用此模板,而是由 Fuchsia build 为 executable()
、loadable_module()
、shared_library()
以及与可链接的目标(即使用静态链接器创建的目标)对应的其他一些目标定义的封装容器模板调用。
它的作用是,在 build 图的每个工具链上下文中,针对每个目标,将 select_variant
的内容与目标的属性(即目标类型和一些其他参数)进行比较,以便:
1) 计算目标的“构建器工具链”,即用于构建真实二进制文件及其依赖项的 GN 工具链实例。
2) 如果当前工具链是构建器工具链,请照常构建目标。
3) 否则,请创建一个 group()
或 copy()
目标,以便重定向(即公开依赖)到构建器工具链中的目标。
这是一个组还是副本取决于一些细微的条件,这些条件在 variant_target()
的实现中已记录详尽,但请参阅以下小节,了解一些相关说明。
必须使用 copy()
目标来保留某些可链接目标的输出位置,而在不需要时使用 group()
。
大多数情况下,executable()
或 loadable_module()
目标需要 copy()
,而 shared_library()
目标需要 group()
。
可链接变体二进制文件的输出位置
GN 配置语言的一个关键设计限制是,除了少数例外情况外,给定目标定义对其依赖项一无所知,除了其标签外。这会带来问题,因为在许多情况下,给定目标需要知道其依赖项的输出位置,或者这些依赖项的实际类型。
为便于说明,我们来看以下示例:
一个名为
//src/my/program:bin
的executable()
目标,用于生成名为my_program
的 Fuchsia 程序二进制文件。由于 build 的运作方式,这会生成${root_build_dir}/exe.unstripped/my_program
和${root_build_dir}/my_program
,以及一些次要文件(此处忽略)。一个名为
//src/my/program:verify_binary
的action()
目标,用于解析程序二进制文件以检查其内容或从中提取信息(假设它会验证其导入符号引用)。此目标需要依赖于第一个目标,但还需要找到二进制文件的输出位置,如下所示:
action("//src/my/program:verify_imports")
script = "check-my-imports.py"
deps = [ "//src/my/program:bin" ]
inputs = [ get_label_info(deps[0], "root_out_dir") + "/my_program" ]
...
|
| deps
|
v
executable("//src/my/program:bin")
output_name = "my_program"
# outputs: [
${root_build_dir}/exe.unstripped/my_program,
${root_build_dir}/my_program,
]
在这里,action()
可以通过为其目录使用 get_label_info("<label>", "root_out_dir")
来推测程序二进制文件的位置,并在操作本身中对 output_name
值进行硬编码。这违反了抽象层的规定,但鉴于 GN 的限制,这是必要的。
启用 build 变体后,二进制目标的实际输出位置将会因 select_variant
而异。如果使用简单的 group()
实现变体重定向,则图表将变为:
action("//src/my/program:verify_imports")
script = "check-my-imports.py"
deps = [ "//src/my/program:bin" ]
inputs = [ get_label_info(deps[0], "root_out_dir") + "/my_program" ]
...
|
| deps
|
v
group("//src/my/program:bin")
|
| public_deps
|
v
executable("//src/my/program:bin(//build/toolchain/fuchsia:x64-asan")
output_name = "my_program"
# outputs: [
# ${root_build_dir}/x64-asan/exe.unstripped/my_program,
# ${root_build_dir}/x64-asan/my_program,
# ]
问题在于,顶级操作中的 inputs
值没有更改,因此其命令会尝试在旧位置 (${root_build_dir}/my_program
) 而不是新位置 (${root_build_dir}/x64-asan/my-program
) 中查找程序二进制文件。构建将使用过时的工件,或者因文件缺失而失败。
在操作本身中解析 select_variant
的开销太高,因此,对于可执行和可加载模块目标,解决此问题需要使用 copy()
目标(而非 group()
),以确保未剥离的二进制文件被复制到其原始位置。图表变为:
action("//src/my/program:verify_imports")
script = "check-my-imports.py"
deps = [ "//src/my/program:bin" ]
inputs = [ get_label_info(deps[0], "root_out_dir") + "/my_program" ]
...
|
| deps
|
v
copy("//src/my/program:bin")
outputs = [ "${root_build_dir}/my_program" ]
sources = [ "${root_build_dir}/x64-asan/my_program" ]
|
| public_deps
|
v
executable("//src/my/program:bin(//build/toolchain/fuchsia:x64-asan")
output_name = "my_program"
# outputs: [
${root_build_dir}/x64-asan/exe.unstripped/my_program,
${root_build_dir}/x64-asan/my_program,
]
采用这种设置后,构建始终会成功,并且操作命令始终会处理正确的二进制文件。
所有这些操作都会在 build 中自动完成。最终结果是,依赖项无需关心其依赖项是使用特定变体构建的还是未使用,它们可以依赖于输出位置的稳定性,至少对于未剥离的二进制文件路径而言是如此。
ELF 共享库的输出位置
TBW
特殊的 novariant
描述符
TBW
特殊全局变量
host_toolchain
和 host_out_dir
全局变量
TBW
zircon_toolchain
变量
TBW
variant()
模板
TBW