概要
本文档介绍了“build variant”的实现,这是 Fuchsia 构建系统的一项功能,可用于构建主机和设备二进制文件的插桩或特别优化版本。
读者必须熟悉 GN 工具链() 实例,并且应已阅读以下文档:
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 支持。实际上,构建启用了 Address Sanitizer 的 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 工具链,那么它的任何变体工具链也会具有 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" 标记用于指示此变体创建执行运行时插桩(例如,清理器或分析器)的机器代码
所有有效的变体标记及其文档的列表位于 //build/toolchain/variant_tags.gni 中。
创建变体工具链时,全局 toolchain_variant.tags 值将包含从基本工具链继承的标记和从变体继承的标记。
工具链变体实例化
构建系统仅在需要时创建变体工具链。工具链和变体的组合数量非常多,如果一次性创建所有组合,会使 gn gen 的速度明显变慢。
构建系统不会急于创建所有变体,而是根据以下条件决定要创建哪些工具链变体:
select_variant全局变量中显示的变体选择器列表。作为
variant_toolchain_suite()模板的enable_variants实参出现的变体描述符名称的列表。即使select_variant为空,也极少用于强制启用少数变体。例如,用于构建 C 库的工具链的 ASan 和 UBSan 变体始终处于启用状态,因为在构建核心 Fuchsia IDK 时需要这些变体(请参阅
//sdk/lib/c/BUILD.gn)。variant_toolchain_suite()的exclude_variant_tags实参中显示的变体标记列表。很少用于排除特定变体,使其不应用于给定的基础工具链。例如,引导加载程序会排除带有
"instrumented"标记的变体,因为在启动设备时无法运行清理器或分析运行时(请参阅 `//
变体描述符
变体描述符是一个 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 标签。请注意,对于工具链变体的共享库工具链,此属性指向最终的基础工具链。示例://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:x64tags:一个自由格式的字符串列表,每个字符串都描述了当前工具链实例及其变体的属性。这只是工具链和变体标记的并集。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。这意味着,该工具链要么是共享库工具链(例如//build/toolchain/fuchsia:x64-shared),要么是直接生成此类代码的基本工具链(例如//zircon/kernel/lib/userabi/userboot:userboot_arm64)。with_shared:一个布尔值,如果当前工具链具有用于构建 ELF 共享库(例如//build/toolchain/fuchsia:x64)的 shlib 工具链,则为 true;或者当位于此类工具链(例如//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 程序二进制文件。由于构建的工作方式,这会生成${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