Skip to content

OUT路径链

追踪 OUT_DIR、PRODUCT_OUT、Soong .intermediates、安装路径和镜像输入的生成关系,区分 canonical 中间产物、legacy 兼容路径与符号文件消费者。

基于android-17.0.0_r1
AndroidSoongMakeOUT调试

OUT路径链 ​

本文面向已经读过 产品配置链、Soong构建链 和 Soong模块类型 的读者。你需要知道模块 action 会产生 android.Path,但不必先记住 out/ 的目录清单。

本文回答一个调试问题:给定一个模块和一个安装文件,如何从 OUT_DIR 反查它的 canonical 中间产物、安装路径、未 strip 输出和最终镜像输入?范围限定在 Android 17 的 Make/Kati 与 Soong 构建侧,不把某个具体产品当前磁盘上的目录快照当成稳定 AOSP 规则,也不展开设备运行时加载。

读完后,你应能解释 PRODUCT_OUT 与 SOONG_OUT_DIR 的所有者差异,顺着 PathForModuleOut 定位模块 variant 的 .intermediates,从 InstallFile / PackagingSpec 找到 out/target/product 中的文件,并判断 symbols、module-info.json、installed-files-*.txt 分别服务哪个消费者。

1. 路径模型 ​

OUT 不是一棵按文件名分类的静态树,而是多个 owner 共享的路径空间:Make 定义产品和主机根目录,Soong 为模块 variant 生成 canonical intermediates,安装/打包层再把部分输出映射到产品目录。

路径对象所有者生效时机主要消费者
OUT_DIRMake/Kati 环境配置初始化所有输出根路径
PRODUCT_OUTbuild/make/core/envsetup.mkTARGET_DEVICE 已确定后legacy 安装与镜像规则
SOONG_OUT_DIRMake 配置 + Soong configSoong 启动时Soong .intermediates 与变量文件
ModuleOutPathSoong module contextmodule action 生成时编译、链接、生成规则
PackagingSpecSoong packaging 层InstallFile / PackageFile 时安装树、checkbuild、镜像打包
module-info.jsonSoong singleton / Make taskmodule graph 收束时m <module>、IDE、调试脚本

2. 根目录 ​

2.1 Make变量 ​

Android 17 在 envsetup.mk 中建立产品和主机输出根目录。这里的变量是路径 owner,不是目录扫描结果。

源码文件:build/make/core/envsetup.mk

相关函数/类型:350-410, android-17.0.0_r1

makefile
TARGET_OUT_ROOT := $(OUT_DIR)/target
TARGET_PRODUCT_OUT_ROOT := $(TARGET_OUT_ROOT)/product
PRODUCT_OUT := $(TARGET_PRODUCT_OUT_ROOT)/$(TARGET_DEVICE)
.KATI_READONLY := TARGET_OUT_ROOT TARGET_PRODUCT_OUT_ROOT PRODUCT_OUT

SOONG_OUT_DIR := $(OUT_DIR)/soong
.KATI_READONLY := SOONG_OUT_DIR

HOST_OUT_ROOT := $(OUT_DIR)/host
HOST_OUT := $(HOST_OUT_ROOT)/$(HOST_OS)-$(HOST_PREBUILT_ARCH)
SOONG_HOST_OUT := $(HOST_OUT)
.KATI_READONLY := HOST_OUT SOONG_HOST_OUT

PRODUCT_OUT 依赖 TARGET_DEVICE,所以同一个 OUT_DIR 下切换产品会切换产品目录;SOONG_OUT_DIR 不按设备名再嵌套一层,Soong 通过 variant、module path 和产品 suffix 区分输出。不要根据 target/product 的存在反推某个模块一定由 Make 生成。

2.2 分区路径 ​

TARGET_OUT 等变量将产品目录与分区名拼接,并在配置阶段锁定。以 system 为例:

源码文件:build/make/core/envsetup.mk

makefile
ifeq ($(SANITIZE_TARGET),address)
  TARGET_OUT_INTERMEDIATES := $(PRODUCT_OUT)/obj_asan
else
  TARGET_OUT_INTERMEDIATES := $(PRODUCT_OUT)/obj
endif
TARGET_OUT_HEADERS := $(TARGET_OUT_INTERMEDIATES)/include

TARGET_OUT := $(PRODUCT_OUT)/$(TARGET_COPY_OUT_SYSTEM)
TARGET_OUT_EXECUTABLES := $(TARGET_OUT)/bin
TARGET_OUT_SHARED_LIBRARIES := $(TARGET_OUT)/lib64
TARGET_OUT_JAVA_LIBRARIES := $(TARGET_OUT)/framework
TARGET_OUT_APPS := $(TARGET_OUT)/app
TARGET_OUT_APPS_PRIVILEGED := $(TARGET_OUT)/priv-app
TARGET_OUT_ETC := $(TARGET_OUT)/etc

这些变量描述“安装树中的目标位置”,不是编译器的中间目录。TARGET_OUT_INTERMEDIATES 是 legacy Make 语义;Soong 模块通常使用自己的 PathForModuleOut,这正是旧稿把 obj/SHARED_LIBRARIES 当成所有模块 canonical 路径时容易出错的地方。

2.3 配置屏障 ​

Make 将根路径标记为 Kati readonly。之后的规则可以读取 PRODUCT_OUT,但不能悄悄把产品目录改成另一个设备;改变产品必须重新执行配置阶段。

3. Soong中间产物 ​

3.1 输出类型 ​

Soong 的 OutputPath 携带 output root 和完整路径;PathForIntermediates 明确把路径放进 .intermediates。这两个类型让路径操作经过统一校验,而不是在模块里手写字符串。

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

相关函数/类型:1448-1472, android-17.0.0_r1

go
type OutputPath struct {
    basePath
    outDir   string
    fullPath string
}

func (p OutputPath) withRel(rel string) OutputPath {
    p.basePath = p.basePath.withRel(rel)
    p.fullPath = filepath.Join(p.fullPath, rel)
    return p
}

// build/soong/android/paths.go:1596-1608
func PathForIntermediates(ctx PathContext, paths ...string) OutputPath {
    path, err := validatePath(paths...)
    if err != nil {
        reportPathError(ctx, err)
    }
    return PathForOutput(ctx, ".intermediates", path)
}

PathForIntermediates 的 consumer 是 Soong action;它不等于 PRODUCT_OUT/obj。路径类型保留 output root,便于同一套模块代码在 device、host 和不同 variant 下复用。

3.2 模块目录 ​

模块级 canonical 路径由 module directory、module name 和 module subdir 组成:

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

go
func pathForModuleOut(ctx ModuleOutPathContext) OutputPath {
    return PathForOutput(ctx, ".intermediates",
        ctx.ModuleDir(), ctx.ModuleName(), ctx.ModuleSubDir())
}

func PathForModuleOut(ctx ModuleOutPathContext, paths ...string) ModuleOutPath {
    p, err := validatePath(paths...)
    if err != nil {
        reportPathError(ctx, err)
    }
    return ModuleOutPath{
        OutputPath: pathForModuleOut(ctx).withRel(p),
    }
}

例如 cc 模块在 system/core/libutils 下生成的路径会包含该目录和模块名;具体 variant、arch 和 subdir 还会由 module context 参与。文章不把某个模块的实际目录名硬编码成所有产品都相同。

3.3 路径校验 ​

validatePath 会在构造 Soong output path 时拒绝不安全路径。模块不能通过 ../ 把输出写到任意源目录或另一个模块的 intermediates;这也是使用 Path API 而不是字符串拼接的原因。

4. 编译输出 ​

4.1 C++库 ​

以 shared library 为例,Soong 先以 PathForModuleOut(ctx, fileName) 建立最终模块输出,再根据是否需要 strip 把未 strip 文件放到 unstripped/,并让 stripper 产生安装用文件。

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

go
outputFile := android.PathForModuleOut(ctx, fileName)
if library.stripper.NeedsStrip(ctx) {
    strippedOutputFile := outputFile
    outputFile = android.PathForModuleOut(ctx, "unstripped", fileName)
    library.stripper.StripExecutableOrSharedLib(
        ctx, outputFile, strippedOutputFile, stripFlags)
}
library.unstrippedOutputFile = outputFile

这里的 owner 是 libraryDecorator。strip 发生时,原始 outputFile 作为 stripped destination,变量随后改指向 unstripped/<fileName>;因此 library.unstrippedOutputFile 明确保存未 strip producer。安装或后续链接使用的路径还可能经过版本注入等后续变换。两者都在 Soong module intermediates,不应直接等同为 PRODUCT_OUT/symbols/...。

4.2 二进制 ​

cc/binary.go 使用同样的双输出策略:未 strip 文件先生成,stripper 输出可安装文件;额外的 versioned、unversioned 和校验文件是特定模块语义,不是所有 executable 的固定目录。

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

go
outputFile := android.PathForModuleOut(ctx, fileName)
if binary.stripper.NeedsStrip(ctx) {
    strippedOutputFile := outputFile
    outputFile = android.PathForModuleOut(ctx, "unstripped", fileName)
    binary.stripper.StripExecutableOrSharedLib(
        ctx, outputFile, strippedOutputFile, stripFlags)
}
binary.unstrippedOutputFile = outputFile

4.3 输出标签 ​

Soong action 可以用 SetOutputFiles 为同一模块登记不同标签,例如 unstripped 与 stripped_all。标签是 module API,读者应通过 m <module>、dumpvars 或 module-info 追踪它们,而不是假设目录名一定叫 symbols。

5. 安装映射 ​

5.1 InstallFile ​

编译输出进入产品树的关键不是复制命令本身,而是 InstallFile 创建安装路径和 packaging spec。Soong 在需要时生成复制 rule,同时保留 source、destination、owner 和 variant 元数据。

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

go
func (m *moduleContext) installFile(
    installDir InstallPath, name string, srcPath Path,
    deps []InstallPath, executable, hooks, checkbuild bool,
    extraZip *extraFilesZip) InstallPath {
    // ... reject installed paths as srcPath and run install hooks
    fullInstallPath := installDir.Join(m, name)
    if m.requiresFullInstall() {
        // ... katiInstall or direct copy rule, depending on KatiEnabled()
        m.installFiles = append(m.installFiles, fullInstallPath)
    }
    m.packageFile(fullInstallPath, srcPath, executable,
        m.requiresFullInstall(), extraZip)
    return fullInstallPath
}

实际源码还会处理 executable、extra zip、implicit deps、checkbuild 和 Kati enabled/disabled 分支;上段保留核心状态变化。fullInstallPath 是产品树中的 legacy 可见路径,srcPath 仍是 Soong intermediates 中的 producer 输出。

5.2 PackagingSpec ​

PackagingSpec 保存一个文件如何被打包,而不是只保存目标字符串:

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

go
type PackagingSpec struct {
    srcPath           Path
    symlinkTarget     string
    executable        bool
    partition         string
    skipInstall       bool
    fullInstallPath   InstallPath
    owner             string
    variation         string
    prebuilt          bool
    extraZip          OptionalPath
}

func (p *PackagingSpec) Owner() string { return p.owner }
func (p *PackagingSpec) Variation() string { return p.variation }

镜像打包 consumer 可以据此区分同一模块的不同 variant、symlink 和 prebuilt;因此 out/target/product/<device>/system/... 是 packaging 结果,不是模块源码直接写出的 canonical output。

5.3 Soong-only差异 ​

当 KatiEnabled() 为 false 时,Soong 会直接为安装路径生成 cp rule;源码仍保留 katiInstall,用于 module-info、合规等半 legacy 消费者,但不会把它当作实际安装动作。这解释了为什么 soong-only 构建中同一个文件可能同时出现在 .intermediates 与产品 staging 语义里,却只有一个真正的安装 producer。

6. 产品产物 ​

6.1 镜像输入 ​

Make 的 build/make/core/Makefile 为每个分区定义 installed-files-*.txt 与 image target。以 system image 为例,installed-files 文件先于 image 进入依赖图,image rule 再消费完整文件树和元数据。

源码文件:build/make/core/Makefile

相关函数/类型:3770-3785, 3790-3870

makefile
INSTALLED_FILES_FILE := $(PRODUCT_OUT)/installed-files.txt

$(INSTALLED_FILES_FILE): $(FULL_SYSTEMIMAGE_DEPS)
    # ... FILESLIST 生成 JSON,FILESLIST_UTIL 生成 txt

systemimage_intermediates := \
  $(call intermediates-dir-for,PACKAGING,system)
BUILT_SYSTEMIMAGE := $(systemimage_intermediates)/system.img
$(BUILT_SYSTEMIMAGE): $(INSTALLED_FILES_FILE) $(INTERNAL_SYSTEMIMAGE_FILES)
    # ... build-systemimage-target 生成 system image

INSTALLED_SYSTEMIMAGE_TARGET := $(PRODUCT_OUT)/system.img

installed-files.txt 是调试和增量分析证据;BUILT_SYSTEMIMAGE 先出现在 legacy obj/PACKAGING 中间目录,INSTALLED_SYSTEMIMAGE_TARGET 才是产品根目录下的 system.img。文件清单先描述输入集合,镜像 rule 后执行封装和安装映射。

6.2 其他分区 ​

Android 17 的 build/make/core/Makefile 对 vendor、product、system_ext、odm、vendor_dlkm、system_dlkm 等分区分别维护 installed-files 文件和 image target。不能从某一产品的目录快照推出所有分区一定存在;是否生成由产品变量、板级能力和具体目标决定。

6.3 产品元数据 ​

module-info.json 位于产品输出路径,但它不是镜像文件清单。Soong singleton 收集各模块的 ModuleInfoJSON,排序后写入模块元数据;installed-files-*.txt 则描述分区文件输入。二者 consumer 不同:前者服务模块查询与调试,后者服务镜像构建和体积分析。

7. 符号路径 ​

7.1 未strip所有者 ​

Soong C/C++ 模块公开 UnstrippedOutputFile provider,ABI dump、fuzz 打包和 Android.mk 转换可以读取它。这个 provider 记录 producer 的真实路径,可能是 .intermediates/.../unstripped/...,不承诺一定已经复制到 symbols/。

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

go
func (c *Module) UnstrippedOutputFile() android.Path {
    if c.linker != nil {
        return c.linker.unstrippedOutputFilePath()
    }
    return nil
}

7.2 symbols边界 ​

symbols/ 是部分产品打包、调试或 legacy 流程使用的可见目录,不是 Soong 的通用 output root。源码中的 consumer 是否把 unstripped 文件复制到 symbols,取决于具体模块和产品规则;排查崩溃时应先查 module provider / Ninja edge,再查产品目录下是否存在符号副本。

7.3 反查命令 ​

bash
# 先获取 canonical 输出与安装变量
source build/envsetup.sh
lunch aosp_x86_64 trunk_staging userdebug
get_build_var PRODUCT_OUT
get_build_var SOONG_OUT_DIR
get_build_var TARGET_OUT_INTERMEDIATES

# 在模块规则中查 producer 与标签
rg -n "PathForModuleOut|UnstrippedOutputFile|InstallFile|SetOutputFiles" \
  build/soong/cc build/soong/android

# 在生成的 Ninja 中反查一个具体文件
rg -n "libfoo|installed-files-system|module-info" \
  out/soong/build.*.ninja out/target/product/*/*.ninja 2>/dev/null

8. 生成时序 ​

9. 故障分流 ​

10. 产物反查 ​

给定模块 libfoo,按以下顺序验证,而不是直接在 out/ 里全文搜索:

  1. 用 get_build_var PRODUCT_OUT、get_build_var SOONG_OUT_DIR 和 get_build_var TARGET_OUT_INTERMEDIATES 确认本次配置的根路径。
  2. 在 Soong 源码或生成 Ninja 中定位 PathForModuleOut 对应的 module/variant producer。
  3. 检查模块是否登记 InstallFile 或其他 packaging spec;没有安装动作时,产品目录不存在是正常结果。
  4. 若产品文件存在,读取相应 installed-files-<partition>.txt,确认它是否进入镜像输入集合。
  5. 若需要符号,读取 UnstrippedOutputFile 对应路径,再确认产品是否有额外 symbols copy;不要把 stripped 文件反向当作符号源。

Soong 的 paths_test.go 反向验证了 module output、Make install path 和 .intermediates 路径的相对关系;module_context 与 module_info_json 相关测试验证安装登记和元数据生成。它们证明路径 API 与构建记录,不证明任意产品都生成同一组镜像。

11. 边界收束 ​

排查 OUT 的正确顺序是“先 owner,后目录”:Make 决定根路径,Soong PathForModuleOut 决定模块 canonical intermediate,InstallFile/PackagingSpec 决定是否进入产品树,installed-files 与 image rule 决定是否封装成镜像,debug consumer 决定是否需要 unstripped 或 symbols 副本。

因此,out/target/product/<device>/obj 不是所有模块的源码入口,symbols/ 不是所有未 strip 文件的唯一来源,system.img 也不是模块编译成功的直接证明。只有把 producer、路径状态和下游 consumer 串起来,OUT 才能从目录清单变成可验证的源码导航。