Skip to content

Android.bp属性链

追踪 Blueprint parser 与 Soong 如何把 Android.bp 文本变成强类型属性、defaults 合并、依赖边界和构建动作。

基于android-17.0.0_r1
AndroidAndroid.bpBlueprintSoong构建系统

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输入结果失败例子
语法parsertoken、括号、逗号AST缺冒号、未闭合 map
求值Scope/expression顶层变量、+、selectevaluated expression未定义变量、类型不一致
类型proptoolsmodule properties、Go structs运行时属性对象未知字段、string 写入 int
图语义Soong mutatordefaults、依赖名、visibilitymodule graph/variants缺依赖、不可见依赖
产物模块实现已处理属性、依赖 providerNinja rule/output编译器参数或输入不成立

2. 语法节点 ​

2.1 顶层定义 ​

Blueprint parser 的顶层定义只有 assignment 和 module。parseDefinitions() 读取标识符后,根据下一个 token 分辨 =、+= 或模块的 {/(;它不认识 cc_library、srcs 或 shared_libs 的业务意义。

源码文件:build/blueprint/parser/parser.go

相关函数/类型:parser.parseDefinitions

go
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

go
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

go
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

go
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 中的真实模块。

make
// 文件:某个 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

go
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

go
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

go
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 与属性消费边界

go
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

go
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

go
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 中的真实文件。

make
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 分支(逻辑节选)

go
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

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

go
// 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

go
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 接口边界

go
type Module interface {
    blueprint.Module
    DepsMutator(BottomUpMutatorContext)
    GenerateAndroidBuildActions(ModuleContext)
}

这解释了排查顺序:模块属性拼写和类型先查 UnpackProperties,缺库和 visibility 查 mutator,编译命令内容和输出目录查 GenerateAndroidBuildActions 及其 decorator。把三个阶段混成“bp 语法错”会导致错误定位。

9. 失败路径 ​

现象所有者典型源码入口恢复动作
expected ... foundparserparseDefinitions/parseProperty修正括号、冒号、逗号后重新分析
undefined variableScope/evalLookupVariable、Variable.Eval修正顶层变量或导入范围
unrecognized module typeBlueprint contextprocessModuleDef使用已注册模块类型或注册工厂
unrecognized propertyproptoolsUnpackProperties修正字段名或确认模块类型
can't assign string to intproptoolspropertyToValue按 Go property 类型修改值
defaults 缺失defaults mutatordefaultsDepsMutator补模块或允许缺失依赖的测试配置
visibility deniedSoong visibilityrule matches/依赖检查修改公开范围或依赖方
编译器输入错误模块 decoratorDepsMutator/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

go
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

相关函数/类型:失败输入的关键断言

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 中,可以按以下顺序复述真实链路:

bash
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 源码。