m-mm-mmm编译命令
完成 lunch 后,最常见的 AOSP 构建入口是 m、mm 和 mmm。旧资料通常把它们描述为 envsetup.sh 中的 shell functions,并把 mma/mmma 解释成“额外构建依赖”的版本。 Android 17 当前实现已经变化:
- 五个命令位于
build/soong/bin/,是独立脚本; mm与mma内容完全相同;mmm与mmma内容完全相同;- 实际目录目标由 Soong UI 的
--build-mode解析; - 依赖由构建图决定,不再通过命令末尾的字母
a区分。
lunch目标与编译变体 已说明 Product、Release 和 Variant。 本文假设三元组已经有效,只讨论命令如何把模块名和源码目录转换成构建目标。
本文面向已经能完成 lunch、但经常把目录名、模块名和最终 Ninja 目标混为一谈的读者。边界止于 Soong UI 接收 build mode 和目录参数,不展开 Ninja 调度或设备部署;源码主线是独立脚本、 getConfigArgs、getTargetsFromDirs 到后续构建 action。读完后应能从命令入口判断 owner、解释 mm/mmm 的目录转换和失败时机,并选择最小命令验证目标是否正确。
1. 命令来源
1.1 脚本PATH
加载 build/envsetup.sh 后,build/soong/bin 被加入 PATH。envsetup.sh 中还保留提示: 这些命令过去定义在该文件中,现在已经迁移为独立脚本。
build/soong/bin/
├── m
├── mm
├── mmm
├── mma
├── mmma
├── allmod
├── dirmods
├── pathmod
└── outmod这意味着分析命令行为时,应先读对应脚本,再进入 Soong UI,而不是在 envsetup.sh 中搜索旧函数。
1.2 脚本路由
源码文件:build/soong/bin/m
_wrap_build "$TOP/build/soong/soong_ui.bash" \
--build-mode \
--all-modules \
--dir="$(pwd)" \
"$@"源码文件:build/soong/bin/mm
_wrap_build "$TOP/build/soong/soong_ui.bash" \
--build-mode \
--modules-in-a-dir \
--dir="$(pwd)" \
"$@"源码文件:build/soong/bin/mmm
_wrap_build "$TOP/build/soong/soong_ui.bash" \
--build-mode \
--modules-in-dirs \
--dir="$(pwd)" \
"$@"三个命令共享 soong_ui.bash --build-mode,区别只在 build action:
| 命令 | Build action | 目录来源 |
|---|---|---|
m | --all-modules | 当前目录仅用于记录调用位置 |
mm | --modules-in-a-dir | pwd |
mmm | --modules-in-dirs | pwd 加命令参数中的目录 |
require_top 会确认命令位于 AOSP 工作区中,soong_ui.bash 随后回到源码根目录启动 Soong UI。
2. m 命令
2.1 默认目标
mm 选择 BUILD_MODULES。不带模块参数时,构建系统按当前 lunch 配置执行默认目标。
2.2 Android.bp
m framework-minus-apex
m SystemUI
m services
m droid模块名来自 Android.bp 或 Make 兼容图,不一定等于目录名、Java package 或最终文件名。
2.3 常用参数
m -j8 SystemUI
m -k SystemUI services
m showcommands SystemUI
m dist这些参数继续交给 Soong build config 和后续 Ninja/Kati 阶段解释。m 本身只负责选择 build action 并转发参数。
2.4 make 与 m
envsetup.sh 仍包装了 make。在 AOSP 根目录且没有 -C 时,它通常转到 soong_ui.bash --make-mode;m 则显式使用新的 --build-mode --all-modules。
新文章和脚本应优先使用 m、mm、mmm 表达构建意图,不依赖 shell 中的 make wrapper 细节。
3. mm 命令
3.1 目标路径
cd frameworks/base/services/core
mmmm 将当前目录传给 Soong UI。getConfigArgs 会确认目录位于源码树内,寻找当前目录或适用父目录 中的 Android.bp/Android.mk,再转换为 MODULES-IN-* 目标。
例如:
frameworks/base/services/core
↓
MODULES-IN-frameworks-base-services-core目录目标依赖该目录下定义的模块及其构建依赖。它不是简单地对目录内每个源文件执行编译器。
3.2 MODULES-IN-*
mm services
mm -j8 services测试覆盖了“当前目录目标 + 显式模块参数”的组合。最终参数同时包含传入模块和 MODULES-IN-* 目录目标。
3.3 模块构建
如果当前目录没有构建文件,Soong 会向父目录查找适用边界。最终仍找不到时报告:
Build file not found for <directory>此时不要随意移动 Android.bp。先确认当前目录是否只是某个模块的深层源码目录,再用 pathmod <module> 或 gomod <module> 找模块定义位置。
4. mmm 命令
4.1 相对路径
mmm frameworks/base/services/core \
frameworks/base/packages/SystemUImmm 使用 --modules-in-dirs。命令参数中的普通值被当作目录;以 - 开头的参数和 key=value 仍作为构建参数保留。
4.2 hmm
hmm 给出的语法是:
mmm dir/:target1,target2例如:
mmm frameworks/base/packages/SystemUI/:SystemUI,SystemUI-testsSoong 使用冒号拆分目录和模块列表,模块之间使用逗号。冒号超过一个、目标列表包含空项或目录不存在 都会失败。
源码文件:build/soong/ui/build/config.go
相关函数/类型:getTargetsFromDirs
for _, dir := range dirs {
// 说明:冒号只允许出现一次,用来分隔目录和模块列表。
parts := strings.Split(dir, ":")
if len(parts) > 2 {
ctx.Fatalf("%s not in proper directory:target1,target2,... format", dir)
}
dir = filepath.Join(relDir, parts[0])
if _, err := os.Stat(dir); err != nil {
ctx.Fatalf("couldn't find directory %s", dir)
}
var newTargets []string
if len(parts) == 2 && parts[1] != "" {
newTargets = strings.Split(parts[1], ",")
if inList("", newTargets) {
ctx.Fatalf("%s not in proper directory:target1,target2,... format", dir)
}
}
// 说明:未指定模块时,目录会转换为MODULES-IN-*目标。
if len(newTargets) == 0 {
buildFile := findBuildFile(ctx, dir)
if buildFile == "" {
ctx.Fatalf("Build file not found for %s directory", dir)
}
newTargets = []string{
convertToTarget(filepath.Dir(buildFile), targetNamePrefix),
}
}
targets = append(targets, newTargets...)
}4.3 子目录路径
mmm 把调用时的 pwd 作为 --dir。目录参数会与该相对目录组合。为了减少歧义,跨较远目录构建 时建议先 croot,再传源码根相对路径。
5. mma/mmma语义
5.1 Android17别名
固定源码中:
build/soong/bin/mm == build/soong/bin/mma
build/soong/bin/mmm == build/soong/bin/mmma逐字节比较结果相同。hmm 也明确写着:
mma:Same asmm;mmma:Same asmmm。
5.2 旧资料的误区
历史上 mma/mmma 常被解释为“同时构建依赖”。当前 Soong 构建图本身会追踪依赖,mm 正在变得 更接近 mma,最终两者成为同一脚本。
阅读旧文章时,应以当前 tag 下脚本为准,不要根据命令名推断依赖策略。
6. Soong UI
6.1 动作选择约束
buildActionConfig 定义三个动作:
| Flag | BuildAction |
|---|---|
--all-modules | BUILD_MODULES |
--modules-in-a-dir | BUILD_MODULES_IN_A_DIRECTORY |
--modules-in-dirs | BUILD_MODULES_IN_DIRECTORIES |
调用还必须提供 --dir,并且 dir 必须位于源码树内部。多个 action 同时出现、缺少 action 或目录 越界都会直接失败。
6.2 目标转换测试
ui/build/config_test.go 覆盖了:
m传入显式模块时保持模块参数;mm将当前目录转换为MODULES-IN-*;- 深层目录找到父级构建文件;
mmm将多个目录转换为多个目标;directory:target1,target2只选择显式模块;- 缺少构建文件时产生错误;
GET-INSTALL-PATH使用独立目标前缀。
选择性运行 TestGetConfigArgsBuildModules* 已通过。它证明参数和目标转换,不证明所有编译器、 Kati、Ninja 或设备镜像任务都能成功。
7. 命令选择
8. 常见错误与定位
8.1 未执行 lunch
如果 TARGET_PRODUCT 未设置,构建无法确定产品配置。先检查:
echo "$TARGET_PRODUCT"
echo "$TARGET_RELEASE"
echo "$TARGET_BUILD_VARIANT"8.2 services目标
m frameworks/base/services 会把参数当成模块目标,而不是目录。构建目录应使用 mm 或 mmm。
8.3 mm
mm 以当前 pwd 为目录入口。先运行:
pwd
find .. -maxdepth 2 -name Android.bp -o -name Android.mk8.4 mmm路径错误
从子目录调用时,参数相对当前目录组合。跨模块构建优先:
croot
mmm path/from/source/root8.5 旧别名
在 Android 17 中它们只是别名。依赖是否构建由模块图和 Ninja 决定。
8.6 设备更新
这些命令把产物安装到 output tree,不会自动刷写设备。编译后如何定位产物、adb sync 或推送模块, 属于后续模块化编译专题。
9. 目标到产物
m、mm 和 mmm 的差异在于如何形成初始目标,而不是“编译强度”:
| 入口 | 初始信息 | 后续共同路径 |
|---|---|---|
m <module> | 显式模块或顶层目标 | Soong/Kati 图展开、Ninja 增量与并行执行 |
mm | 当前目录对应的 MODULES-IN-* | 同上 |
mmm <dirs> | 一个或多个目录,或目录内显式目标 | 同上 |
因此,一次局部编译要能解释四个映射:lunch 三元组选择了什么配置,命令产生了什么初始目标,构建图 展开了哪些依赖,最终产物落在哪个 output/install path。任一映射不清楚,都可能出现“命令成功但 编译错模块”或“产物生成但没有进入设备”的现象。
首个错误发生在目录转换、产品配置、Soong、Kati、Ninja 还是具体编译器,也应沿这条映射逐层定位。 产物定位与设备部署继续由 模块化编译 处理。
10. 两个目标转换断言
在 build/soong/ui/build 运行 TestGetConfigArgsBuildModules、 TestGetConfigArgsBuildModulesInDirectory 和多目录变体,输入分别是显式模块、当前目录和多个 目录;断言生成的 BUILD_MODULES* 参数只包含预期目标,并在缺少 Android.bp 时失败。这组测试 证明的是命令到目标的转换与拒绝条件,不证明编译器或 Ninja action 成功。
读者还可以在真实树中执行 mm 与 mmm path/to/dir:target,保存 Soong UI 的目标摘要,再用 outmod <target> 检查安装产物。若两次摘要的目标集合不同,问题仍在入口或目录解析层;产物检查 只能证明输出存在,不能证明设备已经加载它。
