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_DIR | Make/Kati 环境 | 配置初始化 | 所有输出根路径 |
PRODUCT_OUT | build/make/core/envsetup.mk | TARGET_DEVICE 已确定后 | legacy 安装与镜像规则 |
SOONG_OUT_DIR | Make 配置 + Soong config | Soong 启动时 | Soong .intermediates 与变量文件 |
ModuleOutPath | Soong module context | module action 生成时 | 编译、链接、生成规则 |
PackagingSpec | Soong packaging 层 | InstallFile / PackageFile 时 | 安装树、checkbuild、镜像打包 |
module-info.json | Soong singleton / Make task | module 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
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_OUTPRODUCT_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
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
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
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
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
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 = outputFile4.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
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
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
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.imginstalled-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
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 反查命令
# 先获取 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/null8. 生成时序
9. 故障分流
10. 产物反查
给定模块 libfoo,按以下顺序验证,而不是直接在 out/ 里全文搜索:
- 用
get_build_var PRODUCT_OUT、get_build_var SOONG_OUT_DIR和get_build_var TARGET_OUT_INTERMEDIATES确认本次配置的根路径。 - 在 Soong 源码或生成 Ninja 中定位
PathForModuleOut对应的 module/variant producer。 - 检查模块是否登记
InstallFile或其他 packaging spec;没有安装动作时,产品目录不存在是正常结果。 - 若产品文件存在,读取相应
installed-files-<partition>.txt,确认它是否进入镜像输入集合。 - 若需要符号,读取
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 才能从目录清单变成可验证的源码导航。
