Android.bp属性链
读 Android.bp 时,最容易形成的错误模型是:它是一份“参数表”,Soong 读完每个字段就直接执行。真实链路更严格:Blueprint 先把文本变成 AST 和表达式,求值后由 proptools.UnpackProperties 把属性名映射到 Go struct;Soong 再通过 mutator 合并 defaults、创建变体、检查 visibility 和依赖,最后才让模块生成 Ninja action。
本文面向已经读过 lunch目标与编译变体、Ninja构建系统 和 模块化编译 的读者。需要知道模块、依赖和 Ninja action 的基本含义;不展开完整 Soong 模块目录、Kati、Ninja 执行器或每个 cc_* 属性的业务语义。本文的问题边界是:一段 BP 属性如何经过 parser、类型映射和 Soong mutator,改变一个模块的依赖和输出;Soong构建链 再讲全局 Soong 架构。
读完后,读者应能解释 srcs 为什么不是 Blueprint 的关键字、select() 为什么不能随意放进普通 Go 字段、defaults 为什么会产生依赖但不会生成自己的编译输出,以及 visibility 错误为什么是在依赖图阶段而不是词法阶段出现。
1. 生效边界
一段 BP 文本至少穿过四个 owner:Blueprint parser 持有语法树,Scope 持有顶层变量,Blueprint Context 持有模块工厂和模块对象,Soong mutator/模块实现持有合并后的业务状态。字段真正影响编译命令,要等 GenerateAndroidBuildActions。
图中的边界很重要:parser 能证明 name: "foo" 是合法属性表达式,却不知道 name 是否满足模块名规则;UnpackProperties 能证明值类型能放进某个字段,却不知道 shared_libs 指向的模块是否存在;只有后续 mutator 和模块实现才拥有这些业务判断。
| 层 | owner | 输入 | 结果 | 失败例子 |
|---|---|---|---|---|
| 语法 | parser | token、括号、逗号 | AST | 缺冒号、未闭合 map |
| 求值 | Scope/expression | 顶层变量、+、select | evaluated expression | 未定义变量、类型不一致 |
| 类型 | proptools | module properties、Go structs | 运行时属性对象 | 未知字段、string 写入 int |
| 图语义 | Soong mutator | defaults、依赖名、visibility | module graph/variants | 缺依赖、不可见依赖 |
| 产物 | 模块实现 | 已处理属性、依赖 provider | Ninja rule/output | 编译器参数或输入不成立 |
2. 语法节点
2.1 顶层定义
Blueprint parser 的顶层定义只有 assignment 和 module。parseDefinitions() 读取标识符后,根据下一个 token 分辨 =、+= 或模块的 {/(;它不认识 cc_library、srcs 或 shared_libs 的业务意义。
源码文件:build/blueprint/parser/parser.go
相关函数/类型:parser.parseDefinitions
switch p.tok {
case scanner.Ident:
ident := p.scanner.TokenText()
p.accept(scanner.Ident)
switch p.tok {
case '+':
p.accept('+')
defs = append(defs, p.parseAssignment(ident, pos, "+="))
case '=':
defs = append(defs, p.parseAssignment(ident, pos, "="))
case '{', '(':
defs = append(defs, p.parseModule(ident, pos))
default:
p.errorf("expected \"=\" or \"+=\" or \"{\" or \"(\"")
}
case scanner.EOF:
return
}foo { ... } 是兼容写法,parser 也接受 foo( ... );属性内部统一由 parseProperty 读取 name: value。模块类型字符串先保存到 parser.Module.Type,后面由 processModuleDef 到工厂表中查找。未知模块类型因此不是词法错误,而是模块处理错误。
2.2 表达式
AST 的 Expression 接口覆盖 bool、string、int64、list、map、变量、+ operator 和 select。这解释了为什么 BP 看起来像 JSON 却支持顶层变量拼接:JSON 只是字面量外形,Blueprint 还有作用域和表达式节点。
源码文件:build/blueprint/parser/ast.go
相关函数/类型:Expression 与 Type
type Expression interface {
Copy() Expression
Type() Type
Eval(scope *Scope) (Expression, error)
MarkReferencedVariables(scope *Scope)
}
const (
UnknownType Type = iota
BoolType
StringType
Int64Type
ListType
MapType
UnsetType
)+ 并不是 Make 的变量替换。evaluateOperator 会先分别求值,再按同类型执行字符串拼接、整数相加、列表追加或 map 合并;不同类型会在求值或后续类型检查中失败。map 合并还会递归处理同名属性,因此 defaults 的属性追加不能简单理解为文本拼接。
2.3 select分支
select() 在 parser 中仍然是 Select expression。parser 只校验分支模式不重复、default 位置和括号结构;它不决定 release flag 的当前值。求值时机由 Soong 注册的 configurable evaluator 决定,未绑定条件的 select 不能直接塞进普通 string/[]string 字段。
源码文件:build/blueprint/parser/parser.go
相关函数/类型:parser.parseSelect
result.Conditions = conditions
for p.tok != '}' {
c := &SelectCase{}
c.Patterns = append(c.Patterns, parseOnePattern())
c.ColonPos = p.scanner.Position
if !p.accept(':') {
return nil
}
if p.tok == scanner.Ident && p.scanner.TokenText() == "unset" {
c.Value = &UnsetProperty{Position: p.scanner.Position}
p.accept(scanner.Ident)
} else {
c.Value = p.parseExpression()
}
result.Cases = append(result.Cases, c)
}因此 select() 不是运行时 if,也不是 parser 读到哪一分支就丢掉其他分支。它把条件和所有候选值保留到可配置属性处理阶段;模块字段必须声明为 Soong/Blueprint 支持的 configurable 类型,才能在后续按 variant 或配置求值。
3. 变量作用域
3.1 文件变量
Context.parseOne() 在读取一个 BP 文件前禁止 subdirs、optional_subdirs 和 build 从父 scope 继承,然后调用 parser.ParseAndEval。assignment 会进入当前 Scope,module 属性则在同一个 scope 中求值。变量是文件及其子目录可见的构建描述状态,不是 shell 环境变量。
源码文件:build/blueprint/context.go
相关函数/类型:Context.parseOne
scope.DontInherit("subdirs")
scope.DontInherit("optional_subdirs")
scope.DontInherit("build")
file, errs = parser.ParseAndEval(filename, reader, scope)
if len(errs) > 0 {
return nil, nil, errs
}Scope.LookupVariable 先查当前 scope,再沿 parent 查找;导入包变量使用 pkg.Var,并要求导出名首字母大写。重复定义在 AddVariable 阶段失败,未定义变量在 Variable.Eval 失败。这里没有 Make 式全局环境,也没有在模块内声明变量的语法。
3.2 拼接规则
示例输入:下面的 Android.bp 由本文构造,不对应 AOSP 中的真实模块。
// 文件:某个 Android.bp(示例输入,不是 parser 实现)
common_flags = ["-Wall"]
cc_library {
name: "libsample",
cflags: common_flags + ["-Werror"],
}parser 只看到 Variable + List;Eval 从 scope 取出 common_flags,把两个 list 合并。Soong 的 cflags 是否最终传给编译器,则要看 cc 模块把属性读取到哪里。变量名相同但字段类型不同、变量值被重复赋值或值包含不支持的 expression,都会在构建分析阶段失败,而不是在 clang 执行时才发现。
4. 强类型映射
4.1 模块工厂
Blueprint 在 processModuleDef 中根据 moduleDef.Type 查找 ModuleFactory,调用工厂创建 Go module 和它登记的 property structs,然后交给 proptools.UnpackProperties。name 缺失、重复模块名和 unknown module type都是这一层或紧邻层的错误。
源码文件:build/blueprint/context.go
相关函数/类型:processModuleDef
factory, ok := moduleFactories[moduleDef.Type]
if !ok && scopedModuleFactories != nil {
factory, ok = scopedModuleFactories[moduleDef.Type]
}
if !ok {
return nil, []error{&BlueprintError{
Err: fmt.Errorf("unrecognized module type %q", moduleDef.Type),
Pos: moduleDef.TypePos,
}}
}
module := newModule(factory)
module.typeName = moduleDef.Type
propertyMap, errs := proptools.UnpackProperties(moduleDef.Properties, module.properties...)cc_library 因而不是 parser 关键字,而是 Soong 注册的工厂名。不同模块类型可以注册不同 Go struct,使用同一个 Blueprint 语法获得完全不同的 property schema。
4.2 字段匹配
UnpackProperties 把嵌套 map 展平为 foo.bar 路径,再反射遍历接收 struct。字段名通过 PropertyNameForField 转成 BP 名;字符串、bool、int、slice、嵌套 struct 和 configurable 字段分别进入不同转换分支。
源码文件:build/blueprint/proptools/unpack.go
相关函数/类型:UnpackProperties
func UnpackProperties(properties []*parser.Property, objects ...interface{}) (map[string]*parser.Property, []error) {
var unpackContext unpackContext
unpackContext.propertyMap = make(map[string]*packedProperty)
if !unpackContext.buildPropertyMap("", properties) {
return nil, unpackContext.errs
}
for _, obj := range objects {
valueObject := reflect.ValueOf(obj)
if !isStructPtr(valueObject.Type()) {
panic(fmt.Errorf("properties must be *struct, got %s", valueObject.Type()))
}
unpackContext.unpackToStruct("", valueObject.Elem())
if len(unpackContext.errs) >= maxUnpackErrors {
return nil, unpackContext.errs
}
}
var unusedNames []string
result := make(map[string]*parser.Property)
for name, v := range unpackContext.propertyMap {
if v.used {
result[name] = v.property
} else {
unusedNames = append(unusedNames, name)
}
}
if len(unusedNames) == 0 && len(unpackContext.errs) == 0 {
return result, nil
}
return nil, unpackContext.reportUnusedNames(unusedNames)
}函数先建立属性路径 map,再把同一组 parsed properties 交给每个注册 struct;最后收集未消费字段并转换成带源码位置的诊断。因此同一属性可以被多个 property struct 共同读取,但不能完全没有消费者。
源码文件:build/blueprint/proptools/unpack.go
相关函数/类型:unpackToStruct
if !propertyIsSet {
continue
}
packedProperty.used = true
if HasTag(field, "blueprint", "mutated") {
ctx.addError(&UnpackError{
fmt.Errorf("mutated field %s cannot be set in a Blueprint file", propertyName),
property.ColonPos,
})
continue
}blueprint:"mutated" 是一个关键边界:字段可以存在于运行时 struct 中,但 BP 不能直接设置,它只由 mutator 或模块逻辑修改。未知属性最终报告 unrecognized property,类型不匹配则报告 can't assign ...。这比“字段拼写错了,Soong 忽略它”严格得多。
4.3 cc属性
在 Soong cc 中,srcs、shared_libs 等字段属于具体 decorator/property struct,而不是通用 Blueprint 类型。字段经过 AddProperties 登记后才可被 UnpackProperties 接收;随后 DepsMutator 读取它们,GenerateAndroidBuildActions 读取编译器和 linker 需要的派生值。
源码文件:build/soong/cc/cc.go
相关函数/类型:CcInfo 与属性消费边界
type CcInfo struct {
LocalFlags LocalOrGlobalFlagsInfo
CompilerInfo *CompilerInfo
LinkerInfo *LinkerInfo
StlInfo *StlInfo
}
// 生成 provider 前,模块已把 BP 属性转成编译器和 linker 信息。
ccInfo.LinkerInfo.LibraryDecoratorInfo.SharedLibs =
properties.Shared_libs.GetOrDefault(ctx, nil)同一个 BP 字段可能是普通 slice,也可能是 proptools.Configurable[[]string];后者必须在当前 variant 的 evaluator 下调用 GetOrDefault。所以不能仅凭 BP 文本判断 select() 的最终值,要继续追到字段声明和读取点。
5. defaults合并
5.1 依赖建立
Soong 的 DefaultableModuleBase 保存 defaultsProperties 和待合并的 property structs。RegisterDefaultsPreArchMutators 先注册 defaults_deps,为每个 defaultable module 添加 defaults 依赖;defaults 模块本身也是图节点,但它不因此生成自己的编译输出。
源码文件:build/soong/android/defaults.go
相关函数/类型:defaults mutator registration
func RegisterDefaultsPreArchMutators(ctx RegisterMutatorsContext) {
ctx.BottomUp("defaults_deps", defaultsDepsMutator)
ctx.BottomUp("defaults", defaultsMutator).UsesCreateModule()
}
func defaultsDepsMutator(ctx BottomUpMutatorContext) {
if defaultable, ok := ctx.Module().(Defaultable); ok {
ctx.AddDependency(ctx.Module(), DefaultsDepTag,
defaultable.defaults().Defaults...)
}
}这个依赖用于让 mutator 找到 defaults 对象,不是最终 C/C++ linker 依赖。读图时如果把 defaults 当成 libfoo 一样的编译产物,会误判构建图。
5.2 继承顺序
defaultsMutator 沿 DefaultsDepTag 遍历依赖,收集传递 defaults 后调用 applyDefaults。普通属性用 proptools.PrependProperties,变量属性用匹配字段的 prepend;之后模块自己的字段仍然位于后面。对于列表,结果表现为 defaults 值在前、模块值在后;对于标量和嵌套字段,具体合并由 proptools 规则决定。
源码文件:build/soong/android/defaults.go
相关函数/类型:defaultsMutator
ctx.walkDeps(func(module, parent Module) bool {
if ctx.OtherModuleDependencyTag(module) == DefaultsDepTag {
if defaults, ok := module.(Defaults); ok {
if !seen[defaults] {
seen[defaults] = true
defaultsList = append(defaultsList, defaults)
return len(defaults.defaults().Defaults) > 0
}
} else {
ctx.PropertyErrorf("defaults", "module %s is not an defaults module",
ctx.OtherModuleName(module))
}
}
return false
})
defaultable.applyDefaults(ctx, defaultsList)固定测试用三层 defaults:transitive → defaults → test,最终 Foo 断言为 transitive, defaults, module。这证明的是属性合并顺序,不证明所有 Soong property 都采用同一种覆盖规则;例如 replace_instead_of_append tag 和 configurable 字段有专门分支。
5.3 hook时机
defaults 合并后调用 CallHookIfAvailable。这比普通 load hook 更晚,适合依赖已经被 defaults 填充的属性。若 hook 需要根据 defaults 结果创建模块或补依赖,应该追踪 DefaultableHookContext 的能力,而不是把 hook 当作 parser 回调。
6. 条件求值
6.1 arch与target
arch、target、multilib 这些常见块不是 Blueprint parser 的特殊语法;它们只是嵌套 map,字段是否存在由 Soong 模块 property struct 决定。Soong 在变体 mutator 中创建或选择对应 variant,再把 variant-specific properties 合并到该模块的运行时状态。
示例输入:下面的模块只用于展示嵌套属性,不对应 AOSP 中的真实文件。
cc_library {
name: "libsample",
srcs: ["common.cpp"],
arch: {
arm64: {
srcs: ["arm64.cpp"],
},
},
target: {
android: {
shared_libs: ["liblog"],
},
},
}这个片段能证明输入有嵌套 map,不能单独证明会生成几个 action。要回答“arm64 和 host 是否各有一份”,必须继续查 InitAndroidArchModule、variant mutator 以及该模块的 GenerateAndroidBuildActions。
6.2 configurable字段
Blueprint parser 的 Select 会被 UnpackProperties 转成 configurable property,而不是立即变成某个字符串列表。unpackToConfigurable 会检查 select 的候选类型是否与字段的 configured type 相容;普通字段接收到 select 会报错。
源码文件:build/blueprint/proptools/unpack.go
相关函数/类型:configurable 分支(逻辑节选)
if isConfigurable(fieldValue.Type()) {
configuredType := fieldValue.Interface().
(configurableReflection).configuredType()
unpackedValue, ok := ctx.unpackToConfigurable(
propertyName, property, fieldValue.Type(), configuredType)
if ok {
ExtendBasicType(fieldValue, unpackedValue.Elem(), Append)
}
} else if isStruct(fieldValue.Type()) {
if property.Value.Type() != parser.MapType {
ctx.addError(&UnpackError{
fmt.Errorf("can't assign %s value to map property %q",
property.Value.Type(), property.Name),
property.Value.Pos(),
})
}
}条件的“生效时机”因此分为两层:parser 先保留选择结构,variant/config evaluator 再把它投影到当前模块值。文章只凭 BP 片段无法判断 release_flag 的实际值,也不能把 default 分支写成所有产品都生效的事实。
7. 可见边界
7.1 规则解析
visibility 字符串在 Soong 的 visibility mutator 中解析为 rule 对象。//visibility:public 的 matches 永远返回 true,private 永远返回 false,//pkg:__subpackages__ 使用祖先路径判断。它们是对依赖方的匹配规则,不是文件系统权限。
源码文件:build/soong/android/visibility.go
type publicRule struct{}
func (r publicRule) matches(_ visibilityModuleReference) bool {
return true
}
type subpackagesRule struct {
pkgPrefix string
}
func (r subpackagesRule) matches(m visibilityModuleReference) bool {
return isAncestor(r.pkgPrefix, m.name.pkg)
}
func isAncestor(p1, p2 string) bool {
return strings.HasPrefix(p2, p1) &&
(len(p2) == len(p1) || p2[len(p1)] == '/')
}边界检查刻意避免简单 HasPrefix 的兄弟目录误判:foo 是 foo/bar 的祖先,却不是 fooo/bar 的祖先。这个细节直接影响 :__subpackages__ 的允许范围。
7.2 检查阶段
visibility 源码把检查拆成多阶段:先检查规则语法,再收集 package/module 信息,再建立每个 qualified module 的规则表,最后 top-down 遍历依赖调用 matches。因此一个字符串格式正确的 visibility 仍可能在依赖边上失败。
源码文件:build/soong/android/visibility.go
相关函数/类型:文件注释描述的四阶段 owner
// 1. bottom-up 校验规则语法
// 2. bottom-up 收集 package 信息
// 3. bottom-up 解析规则并建立 qualified module 映射
// 4. top-down 遍历依赖并执行规则匹配visibility_test.go 的 //visibility:private fixture 让同包依赖成功、子包和其他目录依赖失败;//top/nested fixture 还断言其不允许 sibling 和更深子目录。这些测试证明图边约束,不证明 Linux 文件权限或安装分区访问。
8. 依赖消费
8.1 名称到边
BP 中的 shared_libs: ["liblog"] 仍只是字符串列表。以 cc 模块为例,DepsMutator 读取属性并调用 AddDependency/variation dependency,把名字变成 Blueprint module graph 的边;这一步才会触发模块存在性、变体匹配和 visibility 检查。
源码文件:build/soong/cc/cc.go
相关函数/类型:Module.DepsMutator
func (c *Module) DepsMutator(actx android.BottomUpMutatorContext) {
if !c.Enabled(actx) {
return
}
ctx := &depsContext{
BottomUpMutatorContext: actx,
moduleContextImpl: moduleContextImpl{mod: c},
}
ctx.ctx = ctx
deps := c.deps(ctx)
for _, lib := range deps.StaticLibs {
depTag := libraryDependencyTag{Kind: staticLibraryDependency}
actx.AddVariationDependencies([]blueprint.Variation{
{Mutator: "link", Variation: "static"},
}, depTag, lib)
}
for _, lib := range deps.SharedLibs {
depTag := libraryDependencyTag{Kind: sharedLibraryDependency}
actx.AddVariationDependencies([]blueprint.Variation{
{Mutator: "link", Variation: "shared"},
}, depTag, lib)
}
// ... late libraries, generated sources, runtime deps and other tags.
}具体库的 dependency tag、variation 和 provider 要继续读对应 decorator;不能从 shared_libs 这个名字推断它一定是普通设备共享库。sdk_version、vendor、APEX、stubs 和 multilib 都可能改变最终依赖变体。
8.2 输出生成
依赖图稳定后,Blueprint/Soong 才调用每个 variant 的 GenerateAndroidBuildActions。模块将属性、依赖 provider 和 variant 信息转换为编译器命令、linker 命令和 output path;此时 srcs 已不再是 AST 字符串,而是经过 glob、模块输出引用和路径解析的输入集合。
源码文件:build/soong/android/module.go
相关函数/类型:Module 接口边界
type Module interface {
blueprint.Module
DepsMutator(BottomUpMutatorContext)
GenerateAndroidBuildActions(ModuleContext)
}这解释了排查顺序:模块属性拼写和类型先查 UnpackProperties,缺库和 visibility 查 mutator,编译命令内容和输出目录查 GenerateAndroidBuildActions 及其 decorator。把三个阶段混成“bp 语法错”会导致错误定位。
9. 失败路径
| 现象 | 所有者 | 典型源码入口 | 恢复动作 |
|---|---|---|---|
expected ... found | parser | parseDefinitions/parseProperty | 修正括号、冒号、逗号后重新分析 |
undefined variable | Scope/eval | LookupVariable、Variable.Eval | 修正顶层变量或导入范围 |
unrecognized module type | Blueprint context | processModuleDef | 使用已注册模块类型或注册工厂 |
unrecognized property | proptools | UnpackProperties | 修正字段名或确认模块类型 |
can't assign string to int | proptools | propertyToValue | 按 Go property 类型修改值 |
| defaults 缺失 | defaults mutator | defaultsDepsMutator | 补模块或允许缺失依赖的测试配置 |
| visibility denied | Soong visibility | rule matches/依赖检查 | 修改公开范围或依赖方 |
| 编译器输入错误 | 模块 decorator | DepsMutator/GenerateAndroidBuildActions | 查 variant、provider、路径和 action |
失败不是“回滚 BP 文件”。解析失败时不会产生可信模块图;分析阶段失败可能已经创建了部分 module objects,但构建系统以 action 失败结束;中断后的 .ninja 和中间目录由 Soong/ui 的下一次配置或清理逻辑处理,不应手工把残缺输出当成新图的证据。
10. 属性测试
10.1 解析测试
Blueprint parser_test.go 的 valid cases 分别输入 module、string、bool、int、list、nested map 和 list-of-maps,断言 AST 节点类型、属性名、位置和字面值。它证明 parser 保留结构化语法,不证明 Soong 某个模块接受这些字段。
源码文件:build/blueprint/parser/parser_test.go
相关函数/类型:valid parse case
foo {
name: "abc",
isGood: true,
num: 4,
stuff: ["asdf", "jkl;"],
}对应断言检查 Module.Type == "foo"、Property.Name == "name"、String.Value == "abc" 和 Bool.Value == true。当输入缺逗号、错误 token 或重复 select pattern 时,测试断言 parser 返回带位置的 ParseError。
10.2 属性测试
proptools/unpack_test.go 的输入包含 string、bool、slice、nested map、list-of-maps、错误类型和未知字段,输出是 Go struct;例如 s: "abc" 进入 S string,而 int: "abc" 进入类型错误。该测试证明“属性名被消费且类型匹配”,不证明字段后续会进入编译命令。
源码文件:build/blueprint/proptools/unpack_test.go
相关函数/类型:失败输入的关键断言
errors: []string{
`<input>:3:11: can't assign string value to int64 property "int"`,
`<input>:3:13: unrecognized property "missing"`,
}10.3 图检查
Soong defaults_test.go::TestDefaults 用三层 defaults 断言列表顺序;TestDefaultsPathProperties 让 defaults 含 :gen 路径,断言使用 defaults 的模块依赖 gen,defaults 模块自身不依赖 gen。这精确证明 defaults 的“属性/依赖消费”边界。
visibility_test.go 的 private fixture 则让同包成功、nested 和 other 失败,错误中包含被依赖模块的 qualified name。它证明检查发生在图边上;不证明模块文件在磁盘上的读权限。
11. 可执行复述
在固定源码 checkout 中,可以按以下顺序复述真实链路:
git -C build/blueprint show android-17.0.0_r1:parser/parser.go
git -C build/blueprint show android-17.0.0_r1:context.go
git -C build/blueprint show android-17.0.0_r1:proptools/unpack.go
git -C build/soong show android-17.0.0_r1:android/defaults.go
git -C build/soong show android-17.0.0_r1:android/visibility.go
git -C build/soong show android-17.0.0_r1:cc/cc.go给定一个真实模块,先取 Android.bp 中的 name、一个嵌套属性和一个依赖字段,然后回答:它对应哪一个 registered module factory?字段在什么 Go struct?是否由 defaults 或 configurable wrapper 接收?哪个 mutator 把依赖字符串变成 graph edge?哪个 GenerateAndroidBuildActions 读取最终值?若其中任何一步只能凭经验回答,说明还没有走完源码主线。
12. 边界收束
Android.bp 的“语法”不是一张字段清单,而是一条有 owner 和生效时机的流水线:parser 负责结构,Scope 负责表达式值,proptools 负责把公开文本映射到强类型字段,Soong mutator 负责 defaults、变体、依赖和 visibility,模块实现负责把状态写成 Ninja action。
本文不把 cc_library 的所有属性、Soong 全局并发调度、Kati 转换或 Ninja 执行细节塞进同一篇;也不把 select() 的配置值、srcs 的 glob 结果或最终编译参数从一段 BP 文本直接推断出来。要回答这些问题,必须继续阅读相应模块的 property struct、mutator、provider 和 build action 源码。
