Abstract
This document explains the implementation of "build variants", a feature of the Fuchsia build system that allows building instrumented or specially-optimized versions of the host and device binaries.
The reader must be familiar with GN toolchain() instances and should have read the following documents:
Overview of build variants
The Fuchsia build defines several types of build variants, for example:
The
asan
andubsan
variants are used to build machine code with Clang's Address Sanitizer, and Undefined Behaviour Sanitizer, respectively. There is even anasan-ubsan
variant that combines both.The
coverage
variant is used to build machine code with Clang's instrumentation-based profiling enabled, to support code coverage collection.The
profile
variant is used to build instrumented code as well, but to support profile-guided optimization.The
thinlto
andlto
variants are used to build binaries with link-time optimization enabled.The
gcc
variant is used to build certain pieces of the Zircon kernel with the GCC compiler instead of Clang (which has been useful to weed out subtle machine code generation issues that can affect the kernel in very important ways).The
release
anddebug
variants, which are provided to override the default compilation mode, which is determined by the value of theis_debug
build configuration variable inargs.gn
.A few other variants for specialized needs, which are all defined in the
//build/config/BUILDCONFIG.gn
file, using conventions described in the rest of this document.
Generally speaking, a single build variant models:
A set of extra configs that define compiler, assembler or linker flags to be applied when building variant binaries and their dependencies.
A set of optional implicit dependencies to be added to the final variant binary targets in the build graph (i.e. executables, loadable modules and even sometimes shared libraries).
For example, let's consider the "asan" build variant, which is used to enable Clang's Address Sanitizer support. In practice, building a Fuchsia executable program with Address Sanitizer enabled requires (at a minimum):
Passing the
-fsanitize=address
flag both to the Clang compiler and linker when building the executable and all its dependencies (including the C library in the case of Fuchsia).The Asan runtime (
libclang_rt.asan.so
) being available at runtime, as well as its own dependencies (i.e. special prebuilt versions of thelibc++.so
,libc++abi.so
andlibunwind.so
binaries).
Base and variant toolchains
Build variants are always applied to a specific "base" toolchain, which
provides the default settings that are augmented by the variant itself.
This creates a new GN toolchain() instance,
called a "variant toolchain", which has its own root_out_dir
. For
example:
//build/toolchain:host_x64
is the base toolchain used to build host binaries, and itsroot_out_dir
is${root_build_dir}/host_x64
.//build/toolchain:host_x64-ubsan
is the variant toolchain created by applying theubsan
variant, and itsroot_out_dir
is${root_build_dir}/host_x64-ubsan
.//build/toolchain/fuchsia:x64
is the default toolchain (when targeting x64-based devices), used to build Fuchsia user-level binaries. Because it is the default, itsroot_out_dir
is the same asroot_build_dir
.//build/toolchain/fuchsia:x64-asan
is the variant toolchain created by applying theasan
variant to the default toolchain. Itsroot_out_dir
will be${root_build_dir}/x64-asan
.
As a general rule, //${tc_dir}:${tc_name}-${variant}
will be
a variant toolchain created by applying the ${variant}
variant to a base
toolchain named //${tc_dir}:${tc_name}
, and its root_out_dir
will
always be ${root_build_dir}/${tc_name}-${variant}
.
If a base toolchain has an shlib toolchain, then any of its variant toolchains will have one too. Finally, a single variant can be applied to several base toolchains.
For example //build/toolchain:host_x64-asan
and
//build/toolchain/fuchsia:x64-asan
are variant toolchains created
by applying the same asan
variant to the base toolchains used to
build host and Fuchsia device binaries.
The latter would also have //build/toolchain/fuchsia:x64-asan-shared
as
an shlib toolchain to generate ELF-based shared libraries.
Base toolchains must be defined in the build using either clang_toolchain_suite()
or zircon_toolchain_suite()
. Both templates end up calling variant_toolchain_suite()
which implements the magic that automatically creates variant toolchains when needed.
Toolchain and variant tags
Each base toolchain in the Fuchsia build can have a number of tags, which
are free-form strings describing properties of the toolchain. For example,
the "kernel"
tag is used to indicate that a toolchain is used to build kernel
artifacts (this is important because there is no C library, no standard C++
library and a few other constraints that are important for certain target
definitions).
The list of all valid toolchain tags is in
//build/toolchain/toolchain_tags.gni
.
Similarly, each variant definition has a number of tags, describing properties
of the variant. For example, the "instrumentation"
tag is used to
indicate that this variant creates machine code that performs runtime
instrumentation (e.g. sanitizers or profilers)
The list of all valid variant tags and their documentation is in
//build/toolchain/variant_tags.gni
.
When a variant toolchain is created, the global toolchain_variant.tags
value
will contain both the tags inherited from the base toolchain, and those inherited
from the variant.
Toolchain variant instantiation
The build system will only create variant toolchains when they are needed.
There is a very large number of possible toolchain+variant combinations,
and creating all of them at once would make gn gen
considerably slower.
Instead of eagerly creating all variants, the build system decides which toolchain variants to create based on the following conditions:
The list of variant selectors that appear in the
select_variant
global variable.The list of variants descriptor names that appear as the
enable_variants
argument of thevariant_toolchain_suite()
template. It is seldom used to force-enable a few variants even ifselect_variant
is empty.For example, the ASan and UBSan variants of the toolchain used to build the C library are always enabled, because these are needed when building the Core Fuchsia IDK (see
//zircon/system/ulib/c/BUILD.gn
).The list of variant tags that appear in the
exclude_variant_tags
argument ofvariant_toolchain_suite()
. It is seldom used to exclude specific variants from being applied to a given base toolchain.For example, the bootloader excludes variants with the
"instrumented"
tag, since it is not possible to run a sanitizer or profiling runtime while booting the device (see `//
Variant descriptors
A variant descriptor is a GN scope that describe the properties of
a given build variant to the build system. These are defined through
the known_variants
variable in //build/config/BUILDCONFIG.gn
, and
each scope should follow the following strict schema:
configs
: An optional list of GN config labels, that will be automatically added to every target with this variant.Note that for each config
${label}
, in this list, there must also be a target${label}_deps
, which each target built in this variant will automatically depend on. Most of the time, this will be an emptygroup()
.remove_common_configs
: An optional list of GN config labels that should be removed, if present, from any target built with this variant. This is sometimes necessary when some of the default configs that the build system sets up for binaries should not be used for a specific variant.remove_shared_configs
: An optional list of GN config labels, similar toremove_common_configs
, but will only apply when buildingshared_library()
targets and their dependencies.deps
: An optional list of GN target labels, which will be added as implicit dependencies to any linkable target that is built with this variant.name
: A string uniquely naming the variant descriptor, as typically used inselect_variant
. Ifname
is omitted,configs
must be non-empty and will be used to derive a name (by joining their names with dashes).tags
: An optional list of free-form strings describing properties of the variant (see toolchain and variant tags.toolchain_args
: An optional scope, where each variable defined in this scope overrides a build argument in the toolchain context of this variant.host_only
andtarget_only
: Optional scopes that can contain any of the fields above. These values are used only for host or target (i.e. device) toolchains, respectively. Any fields included here should not also be in the outer scope.
Here are a few examples:
Example variant descriptor with a single config
{
configs = [ "//build/config/lto" ]
tags = [ "lto" ]
}
The scope above defines a variant descriptor named "lto"
(since
there is no name
key in the scope, the name is deduced from the
values in configs
which here only contains a single item).
Applying this variant will add the //build/config/lto:lto
config,
defined in //build/config/lto/BUILD.gn
, and that file should also
contain a //build/config/lto:lto_deps
empty group if such a
config has no implicit dependencies. For example:
# //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.
}
This descriptor uses the "lto"
tag to indicate that this
variant performed link-time optimization. This tag can also
be used by the "thinlto"
descriptor, which would be using
a different config.
Example variant descriptor with several configs
{
configs = [
"//build/config/sanitizers:ubsan",
"//build/config/sanitizers:sancov",
]
remove_common_configs = [ "//build/config:no_rtti" ]
tags = [
"instrumented",
"instrumentation-runtime",
"kernel-excluded",
"sancov",
"ubsan",
]
}
This defines a variant descriptor named "ubsan-sancov"
(the
name is derived from the configs
list by joining the config
names with dashes), used to build machine code that detect
undefined behaviour at runtime, and collects code coverage
information at the same time.
Note that this also requires //build/config/sanitizers:ubsan_deps
and //build/config/sanitizers:sancov_deps
to be defined to list
implicit dependencies from these configs.
This uses remove_common_config
because //build/config:no_rtti
is part of the default configs of many base toolchains, but RTTI
must be enabled for UBSan instrumentation to work properly.
The list of tags used is also much more extensive. Note the
"kernel-excluded"
tag which is used to prevent this variant
to be applied to any kernel machine code.
Example variant descriptor with toolchain_args
{
name = "release"
toolchain_args = {
is_debug = false
}
}
This variant descriptor is named explicitly, and does not
add any configs or dependencies. On the other hand, it ensures
that the global build configuration variable is_debug
will
be set to false, which change how many default configs are
defined in a corresponding variant toolchain context.
Universal variants
A lesser known feature of the build system is called "universal variants". These are additional variant descriptors that combine with other known variants, they work as follows:
If
is_debug=false
is set inargs.gn
, meaning that all binaries should be built with maximal optimizations, then the"debug"
variant descriptor is defined by the build. This allows building specific targets in debug mode if necessary.Similarly, if
is_debug=true
(the default), then the"release"
variant descriptor is defined by the build. This allows building specific targets with full optimizations if necessary.Additionally, the universal variants above are combined with all other known variant descriptors automatically by the build. E.g. if
is_debug=false
, then the build will also create"asan-debug"
,"ubsan-debug"
,"thinlto-debug"
, etc. Ifis_debug=true
, then it will define"asan-release"
,"ubsan-release"
,"thinlto-release"
and so on instead.
Note that these variant descriptors are conditionally defined by the build,
based on the value of is_debug
. I.e. there is no "release"
variant and its
combinations when is_debug=false
, and there is no "debug"
variant and its
combinations when is_debug=true
!
The toolchain_variant
global variable
When in a BUILD.gn
or *.gni
file, the global toolchain_variant
variable
can be used to retrieve variant-related information for the current_toolchain
.
This is a scope with the following schema:
name
: Name of the build variant descriptor. This is an empty string in the context of a base toolchain, or the name of the variant descriptor that was used to created the current GNtoolchain()
instance otherwise.Examples names for various toolchain contexts:
//build/toolchain/fuchsia:x64 "" //build/toolchain/fuchsia:x64-shared "" //build/toolchain/fuchsia:x64-asan "asan" //build/toolchain/fuchsia:x64-asan-shared "asan"
base
: A fully-qualified GN label to the base toolchain for the current one. Note that for the shlib toolchain of a toolchain variant, this points to the final base toolchain. Examples://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
: A list of free-form strings, each one describing a property of the current toolchain instance and its variant. This is simply the union of toolchain and variant tags.instrumented
: A boolean flag which will be set to true iff thetags
list contains the"instrumentation"
tag value, provided as a convenience to replace a complicated testing instruction in GN like:if (toolchain_variant.tags + [ "instrumentation" ] - [ "instrumentation" ] != toolchain_variant.tags) { # toolchain is instrumented ... }
With:
if (toolchain_variant.instrumented) { # toolchain is instrumented ... }
is_pic_default
: A boolean that is true in a toolchain that can build ELF position independent code (PIC). This means either an shlib toolchain (e.g.//build/toolchain/fuchsia:x64-shared
), or a base toolchain that produces such code directly (e.g.//zircon/kernel/lib/userabi/userboot:userboot_arm64
).with_shared
: A boolean that is true if the current toolchain has an shlib toolchain to build ELF shared libraries (e.g.//build/toolchain/fuchsia:x64
) or when in such a toolchain (e.g.//build/toolchain/fuchsia:x64-shared
).configs
,remove_common_configs
,remove_shared_configs
: List of GN labels toconfig()
items, that come directly from the current variant descriptor, if any, or empty lists otherwise.deps
: List of GN labels to targets that are added as dependencies to any linkable target, inherited from the variant descriptor itself, if any.libprefix
: For instrumented variants, this is an installation prefix string for shared libraries, or an empty string otherwise. See the toolchain variant libprefix section for full details.exclude_variant_tags
: Used internally by the variant selection logic. Inherited from aclang_toolchain_suite()
orzircon_toolchain_suite()
call, or directly from a target definition. It is is a list of tags used to exclude variants to be applied to a base toolchain, or target, as is sometimes necessary.suffix
: This is"-${toolchain_variant.name}"
, or""
if name is empty. Used internally to simplify expansions without conditionals.supports_cpp
: A boolean that istrue
if this toolchain supports C/C++.supports_rust
: A boolean that istrue
if this toolchain supports Rust.is_basic
: A boolean that istrue
if this toolchain was created by thebasic_toolchain()
template, and therefore does not use any of the built-in support within GN for C/C++ and Rust. It only hascopy
oraction
targets.
The content of this global variable is seldom used by target definitions to alter their configuration based on the current toolchain context. This mostly happen for low-level targets, like the C library, kernel artifacts, or instrumentation runtime support that do not come as prebuilts.
Toolchain variant libprefix
In order to be able to mix instrumented and non-instrumented binaries in a single Fuchsia package, special steps must be performed by the build system:
The shared libraries that are built using an instrumented variant toolchain must be installed to
"lib/<variant>/"
instead of the default"lib/"
location.The executable binaries must be compiled with a linker argument like
"-Wl,-dynamic-linker=<variant>/ld.so.1"
, which overrides the default value ("ld.so.1"
, which is hard-coded in the Fuchsia clang prebuilt toolchain binaries).As a special case, fuzzing build variants use the non-fuzzing build variant name for the library sub-directory.
The toolchain_variant.libprefix
variable is defined in the following way to help
support all of this easily:
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)
This can be used to determine the install location as "lib/${toolchain_variant.libprefix}"
and the linker flag as "-Wl,-dynamic-linker=${toolchain_variant.libprefix}ld.so.1"
.
Variant selection
The Fuchsia build system supports selecting which build variants are
enabled, and to which individual targets, or groups of targets, they
apply. This is done by defining the select_variant
variable in the
build configuration file (args.gn
). Consider the following example:
# From out/default/args.gn
...
select_variant = [
{
label = [ "//src/sys/component_manager:bin" ]
variant = "release"
},
"host_asan",
"thinlto/blobfs",
"ubsan",
]
Each value in the list is an expression, called a variant selector which can be a scope or a string, used to configure how the build will apply variants to different sets of targets.
When select_variant
is defined and not an empty list, its value will
be used to determine how to build linkable targets like executables,
loadable modules and shared libraries that appear in the build graph
in the context of a base toolchain, as well as all their dependencies.
The variant selectors that appear in select_variant
are compared
in order, and the first one that matches the current target is selected.
As such, the example above means that:
The
//src/sys/component/manager:bin
program binary, and its dependencies should always be built with therelease
variant (NOTE: This example will result in an error atgn gen
time isis_debug=false
is in theargs.gn
file, because the"release"
variant will not exist in this case, see universal variants to see why).Host binaries should be built in the
"asan"
variant. Note that"host_asan"
is not a variant descriptor name, but a variant shortcut.The
blobfs
program device binary should always be built using the"thinlto"
variant, which performs link-time optimizations.All other device binaries should be built with the
"ubsan"
variant.
Variant selectors
A variant selector is a value that can appear in the global select_variant
build
configuration variable. They are used by the build system to control variant
selection when defining linkable targets in the context of base toolchains.
Three types of values are supported:
A scope that defines a set of matching criteria for a set of targets. The format of that scope is the following:
variant
: The name of a given variant descriptor that will be used if, and only if, the current target matches all the criteria defined in the rest of the scope.label
: If defined, this must be a list of qualified GN labels (with:
but without toolchain labels, e.g.//src/sys/foo:foo
).name
: If defined, a list of GN label target names (e.g. the name of the//src/sys/foo:bar
target is '"bar"`).dir
: If defined, a list of GN label directory paths (e.g. the path of the//src/sys/foo:bar
target is"//src/sys/foo"
).output_name
: If defined, a list of targetoutput_name
value (the default being itstarget_name
).target_type
: If defined, a list of strings matching the target type. Valid values are:"executable"
,"test"
,"loadable_module"
,"shared_library"
and a few others.testonly
: If defined, a boolean. If true the selector matches targets withtestonly=true
. If false, the selector matches targets withouttestonly=true
.host
: If defined, a boolean. If true the selector matches targets in a host toolchain. If false, the selector matches in the target toolchain.
A string that contains a simple name (e.g.
"asan"
) that points to a variant shortcut, which is an alias for a pre-existing selector scope value.For example, the
"coverage"
value is equivalent to the following scope:{ variant = "coverage" host = false }
A string that contains a variant shortcut name and an output name separated by a directory path (e.g.
"thintlo/blobfs"
). This is a convenience format that avoids writing an equivalent scope, which would look like this in the previous example as:{ variant = "thinlto" host = false output_name = [ "blobfs" ] }
The order of selectors in the select_variant
list is important: the first selector
that matches the current target wins and determines how said target will be built.
Variant shortcuts
In addition to variant descriptors, the build sets up a number of "shortcuts", which are named aliases for a few hard-coded variant selector scope values. The build adds a few hard-coded ones, and creates others from the list of known variants:
The
"host_asan"
shortcut is defined to build host binaries with the"asan"
variant descriptor, and is technically equivalent to the following list of selector scope values:# Definition for the `host_asan` variant shortcut [ { variant = "asan" host = true } ]
Similarly, there exists
host_asan-ubsan
,host_coverage
,host_profile
and several others.Every variant descriptor name has a corresponding shortcut that applies it exclusively to device binaries. I.e. the
"ubsan"
shortcut is equivalent to this list of one selector scope value:[ { variant = "ubsan" host = false } ]
This is the reason why using a variant descriptor name in
select_variant
only applies it to device binaries, as in:# Applies the `ubsan` variant to device binaries, not host ones! select_variant = [ "ubsan", ]
Similarly, a shortcut is defined for every universal variant and its cobinations, which again only apply them to device binaries.
This means, that assuming that
is_debug=true
inargs.gn
, the following would force all device binaries to be built in release mode, while the host ones would still be built in debug mode.is_debug = true select_variant = [ "release" ]
Which is equivalent to:
is_debug = true select_variant = [ { variant = "release" host = false } ]
A way to force host binaries to be compiled in release mode would be to use an explicit scope value, since there is no shortcut for this use case, as in:
is_debug = true select_variant = [ { variant = "release" host = true } ]
Variant target redirection
The variant_target()
template
The variant_target()
template defined in //build/config/BUILDCONFIG.gn
implements the core build variant selection mechanism.
This template should not be called directly from BUILD.gn
files, instead,
it is invoked by the wrapper templates defined by the Fuchsia build for
executable()
, loadable_module()
, shared_library()
and a few others
that correspond to linkable targets (i.e. those created with the static
linker).
What it does is, for each target, in each toolchain context of the build
graph, compare the content of select_variant
with the target's
properties (i.e. target type and a few additional arguments) to:
1) Compute the "builder toolchain" for the target, which is the GN toolchain instance that will be used to build the real binary, and its dependencies.
2) If the current toolchain is the builder toolchain, just build the target as usual.
3) Otherwise, create either a group()
or copy()
target that will
redirect (i.e. publicly depend) on the target in the builder toolchain.
Whether this is a group or a copy depends on subtle conditions
that are fully documented in the implementation of variant_target()
,
but see the following sub-section for some explanations.
The copy()
target is required to preserve the output location
of some linkable targets, while the group()
is used when this
is not needed.
Most of the time, an executable()
or loadable_module()
target
will require a copy()
, and a shared_library()
one will require
a group()
instead.
Output location of linkable variant binaries
A critical design limitation of the GN configuration language is that, with a few exceptions, a given target definition doesn't know anything about its dependencies, except for their labels. This is problematic because there are many cases where a given target needs to know where its dependencies' outputs are located, or what type of targets these dependencies really are.
To illlustrate this, let's consider the following example:
An
executable()
target named//src/my/program:bin
that generates a Fuchsia program binary namedmy_program
. Due to the way the build works this generates${root_build_dir}/exe.unstripped/my_program
and${root_build_dir}/my_program
, plus a few minor files (ignored here).An
action()
target named//src/my/program:verify_binary
used to parse the program binary to check it or extract information out of it (let's say it verifies its import symbol references). This target needs to depend on the first one, but also locate where the binary's output location, as in:
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,
]
Here, the action()
can guess the location of the program binary by
using get_label_info("<label>", "root_out_dir")
for its directory,
and hard-code the output_name
value in the action itself. This violates
abstraction layers, but this is necessary given GN's limitations.
When build variants are enabled, the actual output location of binary targets
will change depending on select_variant
. If variant redirection is implemented
with a simple group()
, the graph becomes:
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,
# ]
The problem is that the value of inputs
in the top-level action didn't change,
so its command will try to find the program binary at the old location
(${root_build_dir}/my_program
) instead of the new one
(${root_build_dir}/x64-asan/my-program
). Either the build will use a stale artifact,
or will fail due to a missing file.
Parsing select_variant
in the action itself would be too expensive, so solving
this situation, for executable and loadable module targets required a copy()
target, instead of group()
, to ensure that the unstripped binary is copied
to its original location. The graph becomes:
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,
]
With this setup, the build always succeeds, and the action command always processes the right binary.
All of this is done automatically in the build. The final effect is that dependent do not need to care whether their dependencies were built with a specific variant or not, they can rely on the output locations to be stable, at least for the unstripped binary paths.
Output location of ELF shared libraries
TBW
The special novariant
descriptor
TBW
Special global variables
The host_toolchain
and host_out_dir
global variables
TBW
The zircon_toolchain
variable
TBW
The variant()
template
TBW