Skip to content

构建条件链

追踪 select、arch 属性、product variables、soong_config 和 cc compilerFlags 如何把配置值变成 C/C++ 宏与源码分支。

基于android-17.0.0_r1
AndroidSoongselectsoong_configC Preprocessor

构建条件链 ​

本文面向已经读过 Android.bp属性链、Soong构建链 和 Soong模块类型 的读者。你需要知道 BP 属性会进入 Go struct、模块会产生 variant,但不必先熟悉全部产品变量。

本文回答:一个产品、架构或配置条件如何改变模块属性,最终如何变成编译器 -D 或源码中的 #if 分支。关键是区分四个对象:Blueprint select 是可配置属性表达式;arch/target 是 variant 属性合并;product_variables 与 soong_config 从产品配置取值;C/C++ 预处理器只消费最终 compiler command。本文不展开产品 makefile 的完整继承,那属于 产品配置链。

读完后,读者应能从一个 cflags: select(...) 或 arch.arm64.cflags 追到 Configurable.GetOrDefault、arch/variable mutator 和 baseCompiler.compilerFlags,再确认宏进入哪条编译命令;也能判断条件缺省、无匹配 case、错误类型和宏未定义分别发生在哪个阶段。

1. 四个阶段 ​

条件不会从产品变量直接“跳进”C 源码。配置系统先提供值,Soong evaluator 或 mutator 选择并合并属性,模块 consumer 生成 cflags,Clang 预处理器最后决定 #if 分支。任何一层缺失都会得到不同现象。

条件机制状态所有者生效阶段直接消费者
select()Configurable[T]属性读取时模块 action
arch/targetModuleBase variantarch mutator顶层 property struct
product_variablesProductVariables + variable mutatorpre-deps mutator模块属性
soong_configconfig namespace/module factoryload/property 应用包装模块属性
#if/#ifdefClang preprocessorcompile action 执行时translation unit

2. Select求值 ​

2.1 状态结构 ​

Blueprint 的 Configurable[T] 保存 condition、case、scope 和可能串接的 property value。它不是解析时立即得到的普通 list;属性 owner 在模块 action 中调用 Get 或 GetOrDefault 时,才用当前 evaluator 求最终值。

源码文件:build/blueprint/proptools/configurable.go

go
type Configurable[T ConfigurableElements] struct {
    marker         configurableMarker
    propertyName   string
    inner          *configurableInner[T]
    postProcessors *[][]postProcessor[T]
}

type singleConfigurable[T ConfigurableElements] struct {
    conditions []ConfigurableCondition
    cases      []ConfigurableCase[T]
    scope      *parser.Scope
}

这解释了为什么 BP parser 通过不等于条件已经生效:parser 只保存 case;module variant、release flag 或 config variable 的实际值必须由 evaluator 提供。

2.2 读取屏障 ​

GetOrDefault 调用 evaluate,匹配失败或类型错误会通过 PropertyErrorf 绑定到当前属性,并回落为空结果;只有 unset 才使用调用者传入的 default。错误不是悄悄选择 default。

源码文件:build/blueprint/proptools/configurable.go

相关函数/类型:Configurable.GetOrDefault

go
func (c *Configurable[T]) GetOrDefault(
    evaluator ConfigurableEvaluator, defaultValue T) T {
    result, err := c.evaluate(c.propertyName, evaluator)
    if err != nil {
        evaluator.PropertyErrorf(c.propertyName, "%s", err.Error())
        result = nil
    }
    if result != nil {
        return copyConfiguredValue(*result)
    }
    return defaultValue
}

conditions_default、default 和“属性未设置”不是同一个状态。阅读一个 select 时应列出 condition 值域、case、default 和 unset 后模块 consumer 的默认值。

3. 架构合并 ​

3.1 Variant先行 ​

arch 与 target 属性只对带 android:"arch_variant" tag 的字段生成 variant property struct。arch transition 先决定当前模块的 Arch、Os 和 image,之后 setArchProperties 才选择匹配 shard 并合并进顶层属性。

源码文件:build/soong/android/arch.go

相关函数/类型:ModuleBase.setArchProperties

go
func (m *ModuleBase) setArchProperties(ctx BottomUpMutatorContext) {
    arch := m.Arch()
    os := m.Os()
    for i := range m.archProperties {
        genProps := m.GetProperties()[i]
        if m.archProperties[i] == nil {
            continue
        }
        var propStructs []reflect.Value
        for _, archProperty := range m.archProperties[i] {
            shard := getArchProperties(ctx, archProperty, arch, os,
                m.Target().NativeBridge == NativeBridgeEnabled)
            propStructs = append(propStructs, shard...)
        }
        for _, propStruct := range propStructs {
            mergePropertyStruct(ctx, genProps, propStruct)
        }
    }
}

arch.arm64 并不是在一个 common module 上临时 if;它只会合并到 arm64 variant。依赖者请求 x86_64 variant 时会得到另一套 property state。

3.2 合并顺序 ​

匹配集合可能包含 OS、arch、multilib、native bridge 和 target-specific shard。list 属性通常追加,scalar 属性按 proptools 合并规则覆盖;带 variant_prepend 的字段改变列表方向。不能仅凭 BP 中块的视觉顺序推断最终 flags。

4. 产品变量 ​

4.1 反射入口 ​

传统 product_variables 由 VariableMutator 读取 ModuleBase 动态生成的 Product_variables struct,再与 Config.productVariables 对照。变量没设置、bool 为 false 或模块没有对应属性时会跳过;命中后才把变量属性追加到模块顶层属性。

源码文件:build/soong/android/variable.go

相关函数/类型:VariableMutator

go
variableValues := reflect.ValueOf(a.variableProperties).
    Elem().FieldByName("Product_variables")
productVariables := reflect.ValueOf(&mctx.Config().productVariables).Elem()

for i := 0; i < variableValues.NumField(); i++ {
    variableValue := variableValues.Field(i)
    name := variableValues.Type().Field(i).Name
    val := productVariables.FieldByName(name)
    if !val.IsValid() || val.Kind() != reflect.Ptr || val.IsNil() {
        continue
    }
    val = val.Elem()
    if val.Kind() == reflect.Bool && !val.Bool() {
        continue
    }
    if variableValue.IsZero() {
        continue
    }
    a.setVariableProperties(mctx, property, variableValue, val.Interface())
}

4.2 格式替换 ​

setVariableProperties 会先用变量值替换属性中的格式占位,再调用 AppendMatchingProperties 合并。因而 product variable 可以改变 string/list 内容;类型不匹配会产生 property error,而不是把任意值 stringify。

5. Soong配置 ​

5.1 类型包装 ​

soong_config_module_type 注册一个包装模块类型,它声明 base module type、namespace、variables 和可被条件修改的 properties。soong_config_bool_variable、string/value variable 是定义输入,不直接生成编译产物。

源码文件:build/soong/android/soong_config_modules.go

go
func RegisterSoongConfigModuleBuildComponents(ctx RegistrationContext) {
    ctx.RegisterModuleType("soong_config_module_type_import",
        SoongConfigModuleTypeImportFactory)
    ctx.RegisterModuleType("soong_config_module_type",
        SoongConfigModuleTypeFactory)
    ctx.RegisterModuleType("soong_config_string_variable",
        SoongConfigStringVariableDummyFactory)
    ctx.RegisterModuleType("soong_config_bool_variable",
        SoongConfigBoolVariableDummyFactory)
    ctx.RegisterModuleType("soong_config_value_variable",
        SoongConfigValueVariableDummyFactory)
}

5.2 缺省条件 ​

conditions_default 在 bool 未设置/false、string 未设置或值不在声明集合等条件下生效;如果某个具体 case 显式写空对象,则表示该值不应用 default。空 case 和缺 case 的语义不同。

5.3 配置所有者 ​

namespace/value 通常从产品 Make 配置进入 Soong config。模块作者应确认谁设置值、何时写入、允许值域和 default,而不是在 BP 中假设某个 vendor 变量总存在。未导入 module type 或 property 不在 allowlist 会在 load/property 阶段失败。

6. Flags消费 ​

6.1 最终读取 ​

cc 的 baseCompiler.compilerFlags 是条件属性到编译命令的关键消费者。它对 Cflags、Cppflags、Srcs 等 Configurable 调用 GetOrDefault(ctx, nil);此时 select 已按当前 variant 求值,arch/product properties 也已经合并。

源码文件:build/soong/cc/compiler.go

相关函数/类型:baseCompiler.compilerFlags

go
cflags := compiler.Properties.Cflags.GetOrDefault(ctx, nil)
cppflags := compiler.Properties.Cppflags.GetOrDefault(ctx, nil)
vendorcflags := compiler.Properties.Target.Vendor.Cflags.GetOrDefault(ctx, nil)
productcflags := compiler.Properties.Target.Product.Cflags.GetOrDefault(ctx, nil)

CheckBadCompilerFlags(ctx, "cflags", cflags)
CheckBadCompilerFlags(ctx, "cppflags", cppflags)

esc := proptools.NinjaAndShellEscapeList
flags.Local.CFlags = append(flags.Local.CFlags, esc(cflags)...)
flags.Local.CppFlags = append(flags.Local.CppFlags, esc(cppflags)...)

6.2 分区附加 ​

同一个 module type 的 vendor/product/recovery variant 会在 consumer 中追加各自 Target.*.Cflags。ctx.inVendor() 等判断读取的是当前 variant state,不是运行设备上的动态分区状态。

源码文件:build/soong/cc/compiler.go

go
if ctx.inVendor() {
    flags.Local.CFlags = append(flags.Local.CFlags, esc(vendorcflags)...)
}
if ctx.inProduct() {
    flags.Local.CFlags = append(flags.Local.CFlags, esc(productcflags)...)
}
if ctx.inRecovery() {
    flags.Local.CFlags = append(flags.Local.CFlags,
        esc(compiler.Properties.Target.Recovery.Cflags)...)
}

6.3 宏注入 ​

当最终 cflags 含 -DFEATURE_X=1 时,Ninja action 把它传给 Clang。Soong 不解释 FEATURE_X 的 C 语义;预处理器才根据 token 决定条件分支。若 BP 条件选择正确但源文件没包含相关代码或宏拼写不同,构建可能成功却没有预期功能。

7. 预处理器 ​

7.1 固有宏 ​

部分宏来自头文件或编译器,而非 BP -D。Bionic 的 sys/cdefs.h 固定定义 __BIONIC__,__LP64__ 由编译 target 决定,_FORTIFY_SOURCE 则通常来自构建 flags;三者 owner 不同。

源码文件:bionic/libc/include/sys/cdefs.h

c
#define __BIONIC__ 1

#if defined(__LP64__)
#define __WORDSIZE 64
#else
#define __WORDSIZE 32
#endif

#if defined(_FORTIFY_SOURCE) && _FORTIFY_SOURCE > 0
#if !defined(__clang_analyzer__)
#define __BIONIC_FORTIFY 1
#endif
#endif

7.2 分支结果 ​

预处理发生在每个 translation unit。相同源文件若属于两个 variant,可能收到不同的 -D、target triple 和内建宏,从而生成不同对象文件。这是“条件编译”真正删除或保留代码的阶段。

8. 失败分流 ​

8.1 条件错误 ​

select condition 类型不匹配、case 缺 default 或 evaluator 取值失败,会报 BP property error;此时不要查 C 源码,因为编译 action 尚未可靠生成。

8.2 合并错误 ​

arch/product variable 属性未带 arch_variant、目标字段不存在或 property 类型不匹配,会在 mutator/merge 阶段失败。看到某 variant 缺 flags 时,先确认它是否实际存在和 enabled。

8.3 Flag错误 ​

CheckBadCompilerFlags 会拒绝不允许的参数;Ninja/shell escaping 又可能暴露错误引号。应查看最终 rule args,而不是只读 BP 文本。

8.4 宏错误 ​

编译成功但功能分支未生效时,检查 compile command 中是否真的有 -D、宏值是 1 还是仅定义、源码使用 #ifdef 还是 #if FEATURE,以及内建 target 宏是否与预期一致。

9. 验证路径 ​

用源码搜索重新走一遍主链:

bash
rg -n "type Configurable|GetOrDefault|func .*evaluate" build/blueprint/proptools
rg -n "setArchProperties|VariableMutator|setVariableProperties" build/soong/android
rg -n "compilerFlags|GetOrDefault.*Cflags|Target.Vendor.Cflags" build/soong/cc

测试边界应分层:Blueprint configurable 测试给定 condition/case 并断言结果或 property error;Soong arch/variable 测试给定 BP 和 TestConfig 并断言 variant 属性;cc 测试读取具体 variant 的 compile rule args。它们能证明属性到 flag,不证明运行时功能。

最后选一个真实模块,记录 condition 输入、匹配 case、variant 名、最终 cflags 和一处 #if。如果五项无法逐一定位,就还没有证明条件真正生效。

10. 边界收束 ​

构建条件是一条跨所有者的数据链,而不是三种语法的清单。select 延迟属性求值,arch/product mutator 合并 variant 状态,cc consumer 生成 compiler flags,Clang 才裁剪源码。调试必须从失败阶段向上下游检查,不能看到 #ifdef 就假设产品变量直接控制它。