AOSP编译数据库
compile_commands.json 常被描述为“把真实编译命令导出给 IDE”。这个说法在 AOSP 中只对了一半。 Android 17 的 Soong 并不读取 Ninja 的执行日志,也不为同一个源文件保留全部架构和 variant;它遍历 已经进入本次 Soong 图的 C/C++ 模块,从 CcInfoProvider 取出源码和工具参数,再为每个源文件组装 一条面向编辑工具的记录。
理解这一区别,才能解释三类常见问题:为什么文件不在数据库里,为什么数据库里的参数与终端中看到的 Ninja 命令不完全相同,以及为什么换了产品或只执行 mm 后跳转突然失准。本文承接 Android Studio与AOSP索引的 IDE 消费链,也需要读者了解 lunch目标与编译变体和 Ninja构建系统。本文不展开 clangd 配置、 Android Studio 插件或 C/C++ 模块的全部 flag 计算;重点是 compdb 从 Soong 图到 JSON 文件的真实主线。 本文面向已经知道 compile_commands.json 用途、但还不能解释 AOSP 中记录如何形成的读者。读完后, 应能从 GenerateBuildActions 追到 CcInfoProvider、variant 去重、JSON 写入和软链接消费者,并通过 一个真实源文件对照 compdb 条目与 Ninja action,明确数据库能证明和不能证明的构建事实。
1. 生成入口
入口位于 build/soong/cc/compdb.go。包初始化时通过 RegisterParallelSingletonType("compdb_generator", ...) 注册 singleton。singleton 的含义是: 它不属于某一个 cc_library,而是在模块图完成后访问所有模块,将分散的编译信息汇总成一个文件。
GenerateBuildActions 首先检查 SOONG_GEN_COMPDB。没有启用时立即返回,不创建空文件,也不更新旧文件。 因此“文件存在”只证明某次构建生成过它,不证明当前构建启用了生成器。
推荐的全图入口是:
source build/envsetup.sh
lunch <product>-<release>-<variant>
SOONG_GEN_COMPDB=1 m nothingnothing 仍会运行 Soong 图生成,只是不要求构建产品产物。若使用 mm、mmm 等有限范围构建,官方 docs/compdb.md 明确说明数据库只包含进入该次分析范围的模块。覆盖范围由构建入口拥有,不由 JSON 消费者补全。
2. 模块收集
生成器调用 VisitAllModuleProxies 遍历当前图。只有模块能提供 CcInfoProvider,且 CompilerInfo 非空时才进入 generateCompdbProject。纯 Java 模块、只有链接信息而没有编译源码的 模块,以及未进入当前产品图的 C/C++ 模块都不会自然产生记录。
源码文件:build/soong/cc/compdb.go
相关函数/类型:GenerateBuildActions
ctx.VisitAllModuleProxies(func(module android.ModuleProxy) {
if ccModule, ok := android.OtherModuleProvider(ctx, module, CcInfoProvider); ok {
if ccModule.CompilerInfo != nil {
generateCompdbProject(ctx, module, ccModule, entries)
}
}
})环境变量 SOONG_GEN_COMPDB_MODULES 还能按模块名进一步过滤。代码用空白拆分模块名并构造 map;过滤器 非空时,名称不在 map 中的模块直接跳过。它适合缩小大型图的工具输入,但不能被误读为依赖闭包:生成器 只按名字判断当前模块,不会因为选中一个模块而自动把所有依赖模块都加入数据库。
| 条件 | owner | 结果 |
|---|---|---|
| 当前 lunch target | Soong Config | 决定可见产品、架构和 variant |
| 构建范围 | m/mm/mmm 入口 | 决定进入分析的模块范围 |
CcInfoProvider | C/C++模块 | 提供源码和编译参数 |
SOONG_GEN_COMPDB_MODULES | compdb singleton | 对已访问模块再次过滤 |
| 源文件去重 map | compdb singleton | 同一路径只保留一条记录 |
3. 参数组装
generateCompdbProject 读取 CompilerInfo.Srcs,再根据扩展名选择编译器。.c 使用 clang, .cpp、.cc、.cxx、.mm 使用 clang++,汇编使用 clang;.o 被跳过,未知扩展名会记录日志 并按汇编路径处理。如果 ${config.ClangBin} 无法求值,编译器路径降级为 /bin/false,让消费者 无法误把不完整工具路径当成可执行编译器。
getArguments 按固定顺序拼接参数:
compiler
→ global/local common flags
→ global/local C flags
→ C++ flags 或 C-only flags
→ system include flags
→ no-override flags
→ source file每个 flag 先经 ctx.Eval 展开 Soong 变量,再由 strings.Fields 拆分。求值失败时保留原字符串,说明 数据库生成器优先提供可诊断结果,而不是在单个变量上终止整个图。
这里的 arguments 是字符串数组,不是需要 shell 再解析的 command 字符串。消费者应逐项传递参数, 不能自行按空格重新切分。AOSP 生成器也没有填充 output;该字段带 omitempty,所以通常不会出现在 JSON 中。
4. 四个字段
每条 compDbEntry 定义了 directory、arguments、file 和可选 output:
| 字段 | AOSP来源 | 消费方式 | 边界 |
|---|---|---|---|
directory | AOSP checkout绝对路径 | 解析相对 include 和 source | checkout 移动后会过期 |
arguments | CcInfo 中的 tooling flags | clangd/libclang 建立编译上下文 | 不等于完整 Ninja action |
file | CompilerInfo.Srcs | 记录索引键 | 同一路径只保留一条 |
output | 当前生成器不设置 | 可用于区分多条命令 | AOSP 输出通常缺失 |
Directory 使用 AbsSrcDirForExistingUseCases(),File 通常是仓库相对路径。消费者把两者结合才能 定位文件。把数据库复制到另一台机器后只改 JSON 所在位置没有意义,其中的绝对 checkout 路径、编译器 路径和 OUT_DIR include 仍指向原环境。
5. 去重与variant
生成器用 map[string]compDbEntry,键是 src.String():
源码文件:build/soong/cc/compdb.go
相关函数/类型:generateCompdbProject
if _, exists := entries[src.String()]; !exists {
entries[src.String()] = compDbEntry{...}
}注释明确说明它只需要每个文件一条记录,不关心记录来自哪个 module 或 ISA。一个源文件同时进入 host、 device、arm64、x86_64、vendor 或 recovery variant 时,数据库不会保存全部组合,也没有用 output 区分它们。保留下来的条目适合语言工具进行一般解析,但不能证明它代表你正在调查的那个最终产物。
因为 JSON slice 从 Go map 的 values 构造,条目顺序不应作为稳定接口。版本控制或缓存系统若直接比较文本, 可能看到只有顺序变化的差异;可靠消费者应按 file 建索引并比较字段语义。
6. 写入与链接
文件固定写到:
${OUT_DIR}/soong/development/ide/compdb/compile_commands.json生成器创建目录后直接 os.Create 目标文件,再用 json.Encoder 写入。创建、关闭或编码失败都会 log.Fatalf;但它没有先写临时文件再原子 rename。因此主机进程被取消、磁盘写满或异常退出时,旧文件 可能已被截断,而不能把“路径仍存在”当作完整性证明。
SOONG_LINK_COMPDB_TO 由 ui/build/createCompDbSymlink 消费。它在 Ninja 阶段之后删除目标目录中 已有的 compile_commands.json,再创建指向 Soong 输出的软链接。若 os.Symlink 失败,只打印消息; 源文件可能有效,而便捷链接不存在。这个链接不是第二份数据库,也不拥有更新状态。
7. 失败与恢复
| 现象 | 需要检查的 owner | 恢复方式 |
|---|---|---|
| 文件不存在 | SOONG_GEN_COMPDB、Soong 是否运行 | 在正确 lunch 环境执行全图生成 |
| 只有少量源文件 | 构建入口和 module filter | 清除过滤并用 m nothing 重新生成 |
| 某文件缺失 | 当前产品图、CcInfoProvider、源扩展名 | 确认模块和 variant 实际包含该源文件 |
| 宏或 include 错误 | lunch target 与保留的 variant | 对照真实 Ninja action,不外推 compdb 条目 |
编译器是 /bin/false | ${config.ClangBin} 求值 | 修复构建环境和工具链路径 |
| JSON 解析失败 | 生成过程、磁盘空间和取消 | 删除损坏输出后完整重建 |
| 顶层软链接缺失 | SOONG_LINK_COMPDB_TO 和链接权限 | 先验证源 JSON,再重新创建链接 |
取消生成时没有需要回滚的设备状态,但需要清理可能截断的 host 文件。恢复动作应重跑生成器,而不是手工 补写某条命令;手工 JSON 会脱离 Soong owner,并在下一次生成时被覆盖。
8. 生成测试
Android 17 的 compdb.go 没有独立的 compdb_test.go。这意味着不能声称已有单元测试直接证明 JSON 去重、写入和软链接行为。可用的对应测试来自相邻层:cc_test.go 对 cppflags、Clang flag 过滤和 variant 条件进行输入/断言测试,证明 CcInfo 参数确实受模块属性与产品配置影响;Android Studio与AOSP索引 使用的 ide_query C++ prober 则实际消费 compdb,断言活动源文件可以得到编译参数和生成 proto include。
这些测试共同证明“上游 flags 会变化”和“下游 analyzer 能消费数据库”,但没有证明以下内容:map 输出 顺序稳定、同源多 variant 选择符合某个产品意图、写入中断后文件原子恢复,以及每个厂商模块都进入当前 图。上述边界必须由源码分支和实际生成实验验证。
9. 可执行验证
先生成并确认 JSON 可解析:
source build/envsetup.sh
lunch <product>-<release>-<variant>
SOONG_GEN_COMPDB=1 SOONG_GEN_COMPDB_DEBUG=1 m nothing
jq 'length' out/soong/development/ide/compdb/compile_commands.json选择一个真实源文件,检查唯一记录和关键参数:
jq --arg file 'system/core/init/init.cpp' '
[.[] | select(.file == $file)] |
{count: length, entry: .[0]}
' out/soong/development/ide/compdb/compile_commands.json验证时应回答四个问题:该条目属于哪个 lunch 图,arguments 中的宏和 include 从哪个模块配置进入, 同一源文件是否还有其他 variant,以及数据库生成时间是否晚于相关 Android.bp 和产品配置。随后再用 ninja -t commands 或构建 verbose 输出对照真实 action,区分“工具分析参数”和“最终构建命令”。
完成这条链路后,读者应能从一个错误跳转反查到 CcInfoProvider、生成器去重和构建范围,而不是把 compile_commands.json 当作脱离 Soong 的静态配置。它是当前构建图的可消费快照,不是构建系统本身。
