Skip to content

Soong构建链

从 soong_ui 的 bootstrap.ninja 追踪到 soong_build 的 Blueprint、mutator、变体和 Ninja action,解释 Soong 构建图如何生效。

基于android-17.0.0_r1
AndroidSoongBlueprintNinja

Soong构建链 ​

本文面向已经读过 Ninja构建系统、模块化编译 和 Android.bp属性链 的读者。你需要知道 Android.bp 是声明文件、Ninja 是 action 执行器,但不必先了解 Soong 的所有模块类型。

本文回答一个具体问题:执行一次 Soong 分析时,soong_ui 如何准备 soong_build,soong_build 如何把 BP 文件变成模块变体和依赖边,最终哪个对象把这些状态写成 Ninja 文件。本文不把 Kati 的 Make 求值、每个 cc_* 属性或 Ninja 的并发调度混进来;旧 Make 文件如何转换,见 Android.mk迁移链。

读完后,读者应该能从 build/soong/ui/build/soong.go 的 runSoong 追到 bootstrap.RunBlueprint,再定位一个模块的 DepsMutator、GenerateAndroidBuildActions 和最终 WriteBuildFile;也能解释为什么一个模块名会产生多个 variant,以及解析失败时哪些输出不能被当成有效构建图。

1. 两级启动 ​

Soong 有两个容易混淆的执行层。soong_ui 是构建编排者,负责写环境文件、生成 bootstrap 描述并调用 Ninja;soong_build 是被 bootstrap 产出的分析程序,负责读取 BP、运行 Blueprint context 并写出主 Soong Ninja。bootstrap 不是业务模块分析的替代品,而是先把分析程序本身构建出来。

1.1 编排入口 ​

固定源码中的 runSoong 每次进入时都会调用 bootstrapBlueprint,随后生成可追踪环境文件,再选择 Ninja、N2 或 Siso 执行 bootstrap.ninja。这里的 ninja 闭包把输出路径固定为 config.SoongOutDir()/bootstrap.ninja,所以 bootstrap 失败发生在 soong_build 运行之前。

源码文件:build/soong/ui/build/soong.go

相关函数/类型:runSoong

go
func runSoong(ctx Context, config Config, enforceNoSoongOutput bool) {
    e := ctx.BeginTrace(metrics.RunSoong, "soong")
    defer e.End()

    if err := migrateOutputSymlinks(ctx, config); err != nil {
        ctx.Fatalf("failed to migrate output directory to current TOP dir: %v", err)
    }

    bootstrapBlueprint(ctx, config)
    // ... 写入 available environment 并检查 used environment
    ninja := func(targets ...string) {
        // ... 根据 config.ninjaCommand 选择 ninja、n2 或 siso
        ninjaArgs = append(ninjaArgs, "-f", filepath.Join(
            config.SoongOutDir(), "bootstrap.ninja"))
        cmd := Command(ctx, config, e, "soong bootstrap", ninjaCmd, ninjaArgs...)
        cmd.RunAndStreamOrFatal()
    }
    // ...
}

这里有一个重要的生效时机:bootstrapBlueprint 是无条件调用的,但是否重新执行 soong_build 由 bootstrap Ninja 的输入、环境依赖和 epoch 文件决定。不能把每次调用 runSoong 都理解成完整重建。

1.2 分析入口 ​

soong_build/main.go 在启动后读取 --available_env,创建 Android 配置,注册模块、mutator 和 singleton,然后调用 runSoongOnlyBuild。真正的 Blueprint 主循环隐藏在 bootstrap.RunBlueprint,而不是 main 里手写一个“扫描所有 Android.bp”的循环。

源码文件:build/soong/cmd/soong_build/main.go

相关函数/类型:main

go
availableEnv := parseAvailableEnv()
configuration, err := android.NewConfig(cmdlineArgs, availableEnv)
maybeQuit(err, "")

ctx := newContext(configuration)
// ... 设置增量分析、variant 分裂和调试选项
ctx.Register()
finalOutputFile, ninjaDeps := runSoongOnlyBuild(ctx)

ninjaDeps = append(ninjaDeps, configuration.ProductVariablesFileName)
ninjaDeps = append(ninjaDeps, usedEnvFile)
writeDepFile(finalOutputFile, ctx.EventHandler, ninjaDeps)
writeUsedEnvironmentFile(configuration)
ctx.WriteGlobFile(shared.JoinPath(topDir, finalOutputFile), soongStartTime)

availableEnv 和 usedEnv 形成了分析输入的依赖边:配置代码只能通过 Config 读取被追踪的环境变量,下一次 soong_ui 可以据此判断是否需要重新分析。环境变化和模块源码变化不是同一种原因,但都会通过 Ninja 依赖让 soong_build 重新运行。

增量分析还有更严格的 cache gate。incrementalValid 同时比较已使用环境的 hash、产品变量文件时间戳和 soong_build 二进制时间戳;cache 文件不存在或 JSON 无法解析时,会回退到非增量分析。它复用的是分析缓存,不是跳过输入正确性检查。

源码文件:build/soong/cmd/soong_build/main.go

相关函数/类型:incrementalValid

go
newConfigCache.EnvDepsHash, err = proptools.CalculateHashReflection(data)
newConfigCache.ProductVariableFileTimestamp = getFileTimestamp(
    filepath.Join(topDir, cmdlineArgs.SoongVariables))
newConfigCache.SoongBuildFileTimestamp = getFileTimestamp(
    filepath.Join(topDir, config.HostToolDir(), "soong_build"))

file, err := os.Open(configCacheFile)
if err != nil && os.IsNotExist(err) {
    return &newConfigCache, false
}
// ... 解码旧 cache;仅当两个 ConfigCache 相等时返回 true

所以遇到“明明没改 BP,Soong 为什么重新分析”,应分别检查 used environment、产品变量和分析器二进制,而不是只比较 Android.bp 的 mtime。

2. 注册管线 ​

2.1 模块工厂 ​

Soong 不从 BP 文本动态加载 Go 类型。各 Go 包的注册函数在进程启动时把字符串模块类型和工厂放入全局注册表;android.Context.Register 再把这些工厂转交给 Blueprint context。这个阶段只建立“名字到工厂”的关系,还没有解析任何模块属性。

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

go
func RegisterModuleType(name string, factory ModuleFactory) {
    moduleTypes = append(moduleTypes, moduleType{name, factory})
    RegisterModuleTypeForDocs(name, reflect.ValueOf(factory))
}

func NewContext(config Config) *Context {
    ctx := &Context{blueprint.NewContext(), config}
    ctx.SetSrcDir(absSrcDir)
    ctx.SetIncrementalDBDir(config.SoongOutDir())
    ctx.AddSourceRootDirs(config.SourceRootDirs()...)
    return ctx
}

func (ctx *Context) Register() {
    for _, t := range moduleTypes {
        t.register(ctx)
    }
    collateGloballyRegisteredMutators().registerAll(ctx)
    collateGloballyRegisteredSingletons().registerAll(ctx)
}

模块工厂创建的对象会成为后续状态所有者;singleton 则负责跨模块输出,例如 phony、makevars、rawfiles 和 Ninja 依赖。把 singleton 当成普通模块会漏掉它们在 PrepareBuildActions 后才生成的全局规则。

以 cc_library 为例,真实注册入口位于 cc/library.go,而不是 Blueprint parser 的 switch。LibraryFactory 调用 NewLibrary(HostAndDeviceSupported) 创建同时具备 host/device 能力的模块;是否真的产生 host variant,还要等 BP 的 host_supported 和 arch transition 决定。

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

相关函数/类型:RegisterLibraryBuildComponents

go
func RegisterLibraryBuildComponents(ctx android.RegistrationContext) {
    ctx.RegisterModuleType("cc_library_static", LibraryStaticFactory)
    ctx.RegisterModuleType("cc_library_shared", LibrarySharedFactory)
    ctx.RegisterModuleType("cc_library", LibraryFactory)
    ctx.RegisterModuleType("cc_library_host_static", LibraryHostStaticFactory)
    ctx.RegisterModuleType("cc_library_host_shared", LibraryHostSharedFactory)
}

func LibraryFactory() android.Module {
    module, _ := NewLibrary(android.HostAndDeviceSupported)
    // ... 注册 shared/static SDK member types
    return module.Init()
}

这条路径说明“模块类型”和“模块变体”必须分开:cc_library 是工厂键;android_arm64_armv8-a_shared 是 mutator 后的具体构建实例。前者在注册阶段存在,后者只有依赖解析完成后才可查询。

2.2 变换分组 ​

Soong 把 mutator 分成 partial、pre-arch、pre-deps、post-deps、post-apex 和 final-deps 阶段。顺序不是展示用的分类,而是依赖关系:defaults 必须先于组件和 prebuilt 关联,架构 transition 必须发生在依赖 mutator 之前,visibility enforcement 又必须在依赖替换的特定阶段执行。

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

go
func collateGloballyRegisteredMutators() sortableComponents {
    return collateRegisteredMutators(
        prePartial, preArch, preDeps, postDeps, postApex, finalDeps)
}

// registerArchMutator is part of preDeps.
func registerArchMutator(ctx RegisterMutatorsContext) {
    ctx.Transition("os", &osTransitionMutator{})
    ctx.BottomUp("image_begin", imageMutatorBeginMutator)
    ctx.Transition("image", &imageTransitionMutator{})
    ctx.Transition("arch", &archTransitionMutator{})
}

2.3 单例消费者 ​

Blueprint 的 singleton 不是“单例模块”的别名。它有独立的 GenerateBuildActions,在模块 action 生成后写全局 Ninja 规则或汇总文件。WriteBuildFile 会先写模块 action,再写 singleton action;因此某个 singleton 依赖模块产生的 provider 或 output 时,必须等模块阶段完成。

3. 文件入图 ​

3.1 文件枚举 ​

bootstrap.RunBlueprint 通过 ctx.ListModulePaths(".") 枚举待解析文件,然后调用 ParseFileList。Blueprint 会根据 package include、source root 和 glob 结果决定哪些文件进入解析。它不是只打开根目录的一个 Android.bp。

源码文件:build/blueprint/bootstrap/command.go

相关函数/类型:RunBlueprint

go
ctx.SetModuleListFile(args.ModuleListFile)

var filesToParse []string
if f, err := ctx.ListModulePaths("."); err != nil {
    return nil, fmt.Errorf("could not enumerate files: %v", err)
} else {
    filesToParse = f
}

if blueprintFiles, errs := ctx.ParseFileList(".", filesToParse, config); len(errs) > 0 {
    return nil, colorizeErrs(errs)
} else {
    ninjaDeps = append(ninjaDeps, blueprintFiles...)
}

解析阶段的结果是 module definition 和文件依赖,不是最终变体。此时 name 缺失、属性类型不匹配或 BP 语法错误会终止后续阶段;没有合法 AST,就没有可供 mutator 消费的模块对象。

3.2 依赖边 ​

ResolveDependencies 先初始化 provider、更新模块声明的依赖,再按可兼容分组运行 mutator。以 cc.Module.DepsMutator 为例,shared_libs 和 static_libs 不只是字符串列表;它们会通过 AddVariationDependencies 请求特定 link variation 的依赖。

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

相关函数/类型:Module.DepsMutator

go
func (c *Module) DepsMutator(actx android.BottomUpMutatorContext) {
    if !c.Enabled(actx) {
        return
    }
    ctx := &depsContext{BottomUpMutatorContext: actx}
    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}
        name, version := StubsLibNameAndVersion(lib)
        variations := []blueprint.Variation{
            {Mutator: "link", Variation: "shared"},
        }
        AddSharedLibDependenciesWithVersions(
            ctx, c, variations, depTag, name, version, false)
    }
}

这里的消费者是 Blueprint dependency graph,生效时机是 ResolveDependencies,不是 GenerateAndroidBuildActions。如果依赖名存在但没有匹配的 variant,错误会在 variant 查找阶段暴露;如果只看 BP 文本,很容易漏掉 link: static/shared 这一层。

3.3 循环失败 ​

Blueprint 的依赖解析会检测循环。测试用例构造 A→B→C→A,并断言错误包含 encountered dependency cycle;这证明图构建不会把循环静默线性化。解析失败时 PrepareBuildActions 不应被当作可继续执行的恢复步骤。

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

相关函数/类型:Test_parallelVisit 的 cycle 子测试

go
// arrange: 构造 A、B、C 的依赖环,并让访问过程暂停在环上
// action: 运行 ResolveDependencies / parallel visit
// assert: 错误包含 "encountered dependency cycle"

4. 变体展开 ​

4.1 选择层级 ​

archTransitionMutator 的注释给出了实际选择层级:先选 OS class,再按 compile_multilib 或模块默认值选 multilib,最后把每个 Target 变成 variation。host_supported、device_supported、分区 image、recovery/ramdisk 和 native bridge 都会过滤候选集合。

4.2 架构变体 ​

在 Android 17,split 先从配置的 Targets[os] 得到候选目标,再调用 decodeMultilibTargets。若模块安装到 recovery 或 ramdisk,会限制为 primary target;如果没有支持的 target,模块被禁用并返回空 variation,而不是生成一个假的架构产物。

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

相关函数/类型:archTransitionMutator.split

go
multilib, extraMultilib := decodeMultilib(ctx, base)
targets, err := decodeMultilibTargets(multilib, osTargets, prefer32)
if err != nil {
    ctx.ModuleErrorf("%s", err.Error())
}
if len(targets) == 0 {
    base.Disable()
    return []string{""}, nil
}

targetNames := make([]string, len(targets))
targetMapping := make(map[string]Target, len(targets))
for i, target := range targets {
    targetNames[i] = target.ArchVariation()
    targetMapping[targetNames[i]] = targets[i]
}

splitAll 还会受到 SOONG_SPLIT_OPT_IN_VARIANTS_ON_DEMAND 和 RELEASE_SOONG_ARCH_VARIANT_ON_DEMAND 影响。因而“一个 cc_library 一定生成 arm64、arm、host 三个变体”不是稳定事实;它取决于模块支持能力、产品 target、multilib 和构建 flag。

4.3 变体测试 ​

android/arch_test.go 的 TestArchMutator 使用测试模块和不同 compile_multilib 设置,读取 ModuleVariantsForTests 断言实际 variation 名称;同文件还覆盖 recovery、host-cross、native bridge 和 arch_variant 属性过滤。这个测试证明变体选择函数在给定 TestConfig 下的结果,不证明任意产品配置都拥有同样的 target 列表。

它的输入是一段真实 BP fixture:foo 使用默认 device 能力,bar 打开 host_supported,baz 关闭 device,qux 固定 32 位。normal case 的断言因此能逐项回答属性对 variant 集合的影响。

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

相关函数/类型:TestArchMutator

go
bp := `
    module { name: "foo" }
    module { name: "bar", host_supported: true }
    module { name: "baz", device_supported: false }
    module {
        name: "qux",
        host_supported: true,
        compile_multilib: "32",
    }
`

fooVariants := []string{
    "android_arm64_armv8-a",
    "android_arm_armv7-a-neon",
}
// action: RunTest 后调用 ModuleVariantsForTests("foo")
// assert: enabled variants 与 fooVariants 完全相等

测试还包含 host-only 配置:它把 config.Targets[Android] 设为 nil,断言 device-only 的 foo 无 variant,而 host-enabled 模块仍保留 build OS variant。这是条件边界的对应测试,能防止把 normal case 外推为所有构建环境。

5. 动作生成 ​

5.1 模块动作 ​

当依赖和变体准备好后,Blueprint 的 PrepareBuildActions 调用每个模块的 GenerateBuildActions。Soong 的 ModuleBase 再把调用转给当前 variant 的 GenerateAndroidBuildActions,因此同一个 Go 模块对象可能按多个 variant 生成不同输出。

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

相关函数/类型:ModuleBase 的接口约定

go
// ModuleBase 实现 Blueprint 的 GenerateBuildActions,
// 并对每个待构建 variant 调用 GenerateAndroidBuildActions。
type Module interface {
    blueprint.Module
    GenerateAndroidBuildActions(ModuleContext)
}

5.2 C/C++消费者 ​

cc.Module.GenerateAndroidBuildActions 先把当前 variant 的依赖转换成路径,再计算 compiler/linker/STL/sanitize flags,后续才创建编译和链接 action。这里的 depsToPaths 是依赖图到文件输入的边界:DepsMutator 处理的是模块关系,GenerateAndroidBuildActions 消费的是已经解析的 provider 和路径。

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

相关函数/类型:Module.GenerateAndroidBuildActions

go
func (c *Module) GenerateAndroidBuildActions(actx android.ModuleContext) {
    ctx := moduleContextFromAndroidModuleContext(actx, c)
    c.logtagsPaths = android.PathsForModuleSrc(actx, c.Properties.Logtags)

    deps := c.depsToPaths(ctx)
    if ctx.Failed() {
        return
    }

    flags := Flags{Toolchain: c.toolchain(ctx)}
    if c.compiler != nil {
        flags = c.compiler.compilerFlags(ctx, flags, deps)
    }
    if c.linker != nil {
        flags = c.linker.linkerFlags(ctx, flags)
    }
    if c.stl != nil {
        flags = c.stl.flags(ctx, flags)
    }
    // 后续阶段由 compiler/linker 生成具体 BuildParams。
}

如果 depsToPaths 设置了失败状态,函数立即返回,避免用不完整的依赖路径继续生成 action。这是失败清理边界:Soong 不会撤销已经写出的 Ninja 文件来“修复”一个 action;正确恢复是修正输入后重新分析和重新写出。

Blueprint 在调用模块 action 时还包了一层 panic 与 missing dependency 防护。它先标记 startedGenerateBuildActions,把 panic 转成带模块上下文的错误,调用结束后计算 provider hash;如果模块留下未处理的 missing deps,则把它们转换为构建错误。只有无错误时才把模块记录的 Ninja 文件依赖送回汇总通道。

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

相关函数/类型:generateModuleBuildActions 内部

go
module.startedGenerateBuildActions = true
func() {
    defer func() {
        if r := recover(); r != nil {
            in := fmt.Sprintf("GenerateBuildActions for %s", module)
            if err, ok := r.(panicError); ok {
                err.addIn(in)
                mctx.error(err)
            } else {
                mctx.error(newPanicErrorf(r, in))
            }
        }
    }()
    // ... 增量缓存恢复和 module clone
    module.logicModule.GenerateBuildActions(mctx)
    module.calculateProviderHash()
}()
module.finishedGenerateBuildActions = true

if len(mctx.errs) > 0 {
    errsCh <- mctx.errs
    return true
}

provider 也有阶段约束。Context.ModuleProvider 的注释明确:如果在相应 mutator 或 GenerateBuildActions pass 完成前读取,会 panic。provider 因而不是随时可读的全局 map,而是由生产阶段和消费阶段共同约束的模块状态。

5.3 全局动作 ​

模块动作生成完成后,Blueprint 再调用 singleton 的 GenerateBuildActions。PrepareBuildActions 源码明确把两组依赖分开收集;随后 WriteBuildFile 写 header、全局变量、规则、模块 action 和 singleton action。只有 buildActionsReady 为真时才允许写文件。

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

相关函数/类型:PrepareBuildActions / WriteBuildFile

go
depsModules, errs = c.generateModuleBuildActions(config, c.liveGlobals)
if len(errs) > 0 {
    return
}
depsSingletons, errs = c.generateSingletonBuildActions(config, c.singletonInfo, c.liveGlobals)
if len(errs) > 0 {
    return
}

// WriteBuildFile
if !c.buildActionsReady {
    err = ErrBuildActionsNotReady
    return
}
if err = c.writeAllModuleActions(nw, shardNinja, ninjaFileName); err != nil {
    return
}
err = c.writeAllSingletonActions(nw)

6. 文件写出 ​

6.1 主循环 ​

bootstrap.RunBlueprint 把五个阶段串起来:枚举文件、解析、解析依赖、生成 action、写 Ninja。StopBeforePrepareBuildActions 和 StopBeforeWriteNinja 是测试和文档模式使用的停止屏障,它们说明这些阶段是可观察边界,而不是一段不可分的函数。

源码文件:build/blueprint/bootstrap/command.go

相关函数/类型:RunBlueprint

go
ctx.ParseFileList(".", filesToParse, config)
ctx.ResolveDependencies(config)
if stopBefore == StopBeforePrepareBuildActions {
    return ninjaDeps, nil
}
ctx.PrepareBuildActions(config)
if stopBefore == StopBeforeWriteNinja {
    return ninjaDeps, nil
}
ctx.WriteBuildFile(out, !strings.Contains(args.OutFile, "bootstrap.ninja"), args.OutFile)

6.2 输出依赖 ​

RunBlueprint 返回 ninjaDeps,soong_build 把它与产品变量文件、usedEnv 文件一起写入输出的 depfile。这样,BP 文件、glob 目录、产品变量或实际使用的环境变量变化时,Ninja 能重新触发分析;action 输出本身不是唯一的重分析条件。

7. 失败分支 ​

7.1 启动失败 ​

bootstrap 阶段失败时,soong_build 尚未运行,应该检查 .bootstrap/bootstrap.ninja 的输入、Go 工具链和环境依赖。不要拿一个旧的 out/soong/build.ninja 推断本次分析已经成功,因为旧文件可能仍然存在。

7.2 分析失败 ​

BP 语法、缺失模块、循环依赖、不可匹配 variation 或 mutator 错误都会在 ParseFileList 或 ResolveDependencies 返回错误。RunBlueprint 只有在这些阶段成功后才进入 PrepareBuildActions;因此失败日志中出现模块名,不等于该模块已经生成了新的 Ninja action。

7.3 动作失败 ​

如果 GenerateAndroidBuildActions 报 property、依赖路径或 flags 错误,PrepareBuildActions 返回错误,WriteBuildFile 不应被视为成功输出。已经存在的旧 Ninja 文件仍可能可读,但它不代表当前输入。恢复方式是保留错误上下文,修复 BP/产品配置或模块实现后重新运行同一阶段。

8. 追踪实验 ​

8.1 源码定位 ​

可以用下面的只读搜索把本文主线重新走一遍:

bash
rg -n "func runSoong|bootstrapBlueprint|func main" \
  build/soong/ui/build/soong.go build/soong/cmd/soong_build/main.go
rg -n "func RunBlueprint|ParseFileList|ResolveDependencies|PrepareBuildActions|WriteBuildFile" \
  build/blueprint/bootstrap/command.go build/blueprint/context.go
rg -n "registerArchMutator|archTransitionMutator|func \(c \*Module\) DepsMutator|GenerateAndroidBuildActions" \
  build/soong/android/mutator.go build/soong/android/arch.go build/soong/cc/cc.go

第一组定位编排和进程入口,第二组定位 Blueprint 屏障,第三组定位变体与 C/C++ 消费者。若搜索结果来自其他 release,应先回到本文 frontmatter 的固定 tag,不要混用分支代码。

8.2 测试断言 ​

源码测试提供两类对应测试。blueprint/context_test.go 的循环用例把依赖环 arrange 出来,运行访问/解析后 assert encountered dependency cycle;android/arch_test.go 的 TestArchMutator 则从 TestContext 读取模块 variants,与预期的架构字符串比较。前者证明失败不会静默通过,后者证明变体命名和选择遵循测试配置。

这些测试没有证明完整产品的所有模块都能生成 Ninja,也没有证明编译器命令或最终 APK/镜像内容。要验证产物,必须在固定 product/variant 下比较 Soong action、安装路径和实际 artifact;不能把 unit test 的 variant 字符串当成系统构建成功。

8.3 读者复述 ​

拿一个实际 cc_library_shared 模块,沿 RegisterModuleType 找到工厂,沿 DepsMutator 找到 shared/static 依赖的 variation 请求,再沿 archTransitionMutator 确认当前 target,最后在 GenerateAndroidBuildActions 中定位 flags 和输出创建点。若模块失败,先判断它发生在 bootstrap、解析、依赖、变体还是 action 阶段;阶段决定要看的文件和可以信任的输出。

9. 边界分析 ​

Soong 的核心不是“读取 BP 后直接写 Ninja”,而是把声明拆成多个生效阶段:注册决定对象类型,解析建立定义,mutator 决定依赖和 variant,模块消费者把 provider 转成路径与 flags,singleton 汇总全局规则,最后 writer 才序列化 Ninja。每个阶段都有自己的 owner、输入和失败边界。

本文没有把 Kati、combined Ninja 的最终拼接、Ninja/Siso 执行器内部或远程构建服务展开;这些对象位于 Soong 生成主 Ninja 之后。也没有声称所有环境变量变化都会重跑完整图,源码只证明实际通过 Config 读取并写入 usedEnv 的变量参与依赖跟踪。