Android.mk迁移链
把 Android.mk 改成 Android.bp,真正困难的不是把 LOCAL_SRC_FILES 改名成 srcs,而是判断旧 Make 文件中的动态求值、条件、路径和自定义 recipe 是否有 Blueprint/Soong 的等价 owner。AOSP 的 androidmk 不是完整 Make 执行器,而是一个有限的 AST 转换器:它只把自己认识的 include、变量和模块类型改写成 BP;不认识的结构会带着原文和错误注释进入输出。
本文面向已经读过 Android.bp属性链、Ninja构建系统 和 模块化编译 的读者。需要知道 Make 变量、模块依赖和 Ninja action 的基本概念;不展开全部 Make 变量、Kati 执行器、Soong 模块全集或产品配置迁移。文章只回答:一个 Android.mk 如何从 Make AST 进入 androidmk 映射器,哪些结果可以直接交给 Soong,哪些必须人工重构,以及如何证明迁移没有改变产物边界。
1. 迁移边界
androidmk 的输入 owner 是 Make parser,输出 owner 是 Blueprint printer;Soong 并不在转换命令中执行原 Make 变量和 recipe。迁移完成后,新的 Android.bp 还要重新经过 Blueprint parser、property unpack、mutator 和模块 build actions。
转换成功只意味着 ConvertFile 没有返回 errors,并不意味着新 BP 的依赖、变体、安装路径和运行时行为已经等价。Soong 需要重新验证这些消费者。
| 问题 | 转换器能证明 | 转换器不能证明 |
|---|---|---|
| 语法 | Make AST 可读、BP AST 可打印 | 旧 recipe 的 shell 行为 |
| 字段 | 白名单变量映射到 BP 字段 | 目标模块是否支持该字段语义 |
| 条件 | 少数已知条件翻成 target/product 结构 | 任意 ifeq 的逻辑等价 |
| 路径 | LOCAL_PATH 形式可相对化 | 动态 wildcard、shell 计算的完整文件集 |
| 产物 | 生成可供 Soong 继续解析的 BP | 新旧编译产物和安装结果一致 |
2. Make解析
2.1 节点类型
androidmk/parser/ast.go 把 Make 输入归纳为 assignment、comment、directive、rule 和 variable。它保存变量插值而不是立刻执行:MakeString 由字符串片段和 Variable 列表组成,能够保留 $(LOCAL_PATH)/foo 这样的结构。
源码文件:build/soong/androidmk/parser/ast.go
type Assignment struct {
Target *MakeString
Name *MakeString
Value *MakeString
Type string // "=" 或 "+="
}
type Directive struct {
Name string
Args *MakeString
}
type Rule struct {
Target *MakeString
Prerequisites *MakeString
Recipe string
}
type MakeString struct {
Strings []string
Variables []Variable
}Rule 的 recipe 会被 parser 保存,但 androidmk 并不会把任意 recipe 变成 genrule。这正是旧稿把“自定义 Make 规则可自动转 genrule”写得过宽的原因:语法上能解析,不代表转换器有安全的输出映射。
2.2 变量插值
MakeString.Value(scope) 才会把片段和变量值拼起来;Words() 负责按空白切分,同时保留跨变量边界的单词。转换器依赖这些结构区分字符串属性和列表属性。
源码文件:build/soong/androidmk/parser/make_strings.go
相关函数/类型:MakeString.Value
func (ms *MakeString) Value(scope Scope) string {
if len(ms.Strings) == 0 {
return ""
}
ret := unescape(ms.Strings[0])
for i := range ms.Strings[1:] {
ret += ms.Variables[i].Value(scope)
ret += unescape(ms.Strings[i+1])
}
return ret
}转换器如果能在当前 scope 中求出一个 Make function,就会把它转成 BP literal;不能求出的变量会保留为 Blueprint variable。遇到需要执行 shell、用户自定义函数或动态文件系统查询的表达式,则不能把“当前主机的一次结果”冒充成稳定 BP。
2.3 规则与指令
parser 测试覆盖 ifeq directive、带 recipe 的 rule 和空行合并。它证明 Make 语法树能够保存条件和 recipe 的位置,但不为 androidmk 赋予转换语义。
源码文件:build/soong/androidmk/parser/parser_test.go
in := `all:
echo first line
echo second line`
// 断言:一个 Rule,Recipe 为两个 echo 行,中间空行被归并。如果输入含普通 build rule,androidmk 主循环会把它当作未支持节点处理;只有 include $(BUILD_*) 这类模块边界进入模块转换函数。
3. 模块映射
3.1 include入口
ConvertFile 遍历 parser 节点。遇到 include 时先调用 mapIncludePath;CLEAR_VARS 重置当前 module,忽略的 include(例如 subdirs)被跳过,BUILD_* 则创建 BP module。这里的顺序保留了 Make 文件中“清变量、赋值、结束模块”的边界。
源码文件:build/soong/androidmk/androidmk/androidmk.go
相关函数/类型:ConvertFile
case *mkparser.Directive:
switch x.Name {
case "include", "-include":
module, ok := mapIncludePath(x.Args.Value(file.scope))
if !ok {
file.errorf(x, "unsupported include")
continue
}
switch module {
case clearVarsPath:
resetModule(file)
case includeIgnoredPath:
continue
default:
handleModuleConditionals(file, x, conds)
makeModule(file, module)
}
}CLEAR_VARS 并不是 Soong 运行时模块;它只是转换器内部把上一段 LOCAL_* 状态清空的信号。把它迁移成 BP 模块会制造错误节点。
3.2 模块类型
真实映射表由 moduleTypes 持有。它覆盖常见 C/C++、Java、APK、RRO 和 CTS 类型,并标注部分结果还要交给 bpfix 二次改写。
源码文件:build/soong/androidmk/androidmk/android.go
相关函数/类型:moduleTypes
var moduleTypes = map[string]string{
"BUILD_SHARED_LIBRARY": "cc_library_shared",
"BUILD_STATIC_LIBRARY": "cc_library_static",
"BUILD_EXECUTABLE": "cc_binary",
"BUILD_NATIVE_TEST": "cc_test",
"BUILD_JAVA_LIBRARY": "java_library_installable",
"BUILD_PACKAGE": "android_app",
"BUILD_RRO_PACKAGE": "runtime_resource_overlay",
// ... host、prebuilt、CTS 等映射
}BUILD_JAVA_LIBRARY 映射为 java_library_installable 后可能由 bpfix 改成 java_library;所以 stdout 中出现的初始模块类型不是最终语义的唯一证据。必须查看 bpfix steps 和最终打印的 BP。
3.3 属性白名单
变量映射通过 rewriteProperties 和 addStandardProperties 注册,而不是通过字符串相似度自动猜测。下面是固定源码中的真实映射片段:
源码文件:build/soong/androidmk/androidmk/values.go
"LOCAL_MODULE": "name",
"LOCAL_SRC_FILES": "srcs",
"LOCAL_SRC_FILES_EXCLUDE": "exclude_srcs",
"LOCAL_SHARED_LIBRARIES": "shared_libs",
"LOCAL_STATIC_LIBRARIES": "static_libs",
"LOCAL_LDLIBS": "host_ldlibs",
"LOCAL_CFLAGS": "cflags",
"LOCAL_CPPFLAGS": "cppflags",
"LOCAL_CERTIFICATE": "certificate",表面相似不等于语义等价。源码把 LOCAL_LDLIBS 映射到 host_ldlibs,说明该转换器的目标是宿主 linker 属性;不能把旧稿中“LOCAL_LDLIBS 等于 Android 设备 ldflags”继续当作普遍结论。
4. 路径转换
4.1 local与global
classifyLocalOrGlobalPath 只把 $(LOCAL_PATH) 或 $(LOCAL_PATH)/... 归类为 local,其余字面路径和变量归类为 global。localizePaths 进一步要求路径只能是 LOCAL_PATH 形式,否则返回 Only $(LOCAL_PATH)/.. values are allowed。
源码文件:build/soong/androidmk/androidmk/values.go
func classifyLocalOrGlobalPath(value bpparser.Expression) (string, bpparser.Expression, error) {
switch v := value.(type) {
case *bpparser.Variable:
if v.Name == "LOCAL_PATH" {
return "local", &bpparser.String{Value: "."}, nil
}
return "global", value, nil
case *bpparser.Operator:
if v.Type() != bpparser.StringType || v.Operator != '+' {
return "global", value, nil
}
variable, ok := v.Args[0].(*bpparser.Variable)
if !ok || variable.Name != "LOCAL_PATH" {
return "global", value, nil
}
return "local", v.Args[1], nil
default:
return "global", value, nil
}
}这解释了转换结果中 LOCAL_C_INCLUDES := $(LOCAL_PATH)/include 变成 local_include_dirs: ["include"],而 system/core/include 变成 include_dirs。如果原路径来自 $(shell ...) 或复杂变量拼接,分类器无法证明它的相对目录,应该人工确认。
4.2 列表拆分
splitAndAssign 先把 Make 字符串转成 BP list/operator,再依据分类函数拆成多个字段。它保留变量表达式和 + 结构,而不是强行展开当前环境。
源码文件:build/soong/androidmk/androidmk/values.go
相关函数/类型:splitAndAssign
func splitAndAssign(ctx variableAssignmentContext,
splitFunc listSplitFunc, namesByClassification map[string]string) error {
val, err := makeVariableToBlueprint(ctx.file, ctx.mkvalue, bpparser.ListType)
if err != nil {
return err
}
lists, err := splitBpList(val, splitFunc)
if err != nil {
return err
}
for classification, name := range namesByClassification {
if component, ok := lists[classification]; ok && !emptyList(component) {
if err = setVariable(ctx.file, ctx.append, ctx.prefix, name, component, true); err != nil {
return err
}
}
}
return nil
}转换后的列表可能仍含 BP variable 或 operator;这不是转换失败,而是把可静态表达的动态部分延迟给 Blueprint。真正需要运行 shell 或读取未声明文件的动态性不能靠此函数获得等价性。
5. 条件迁移
5.1 支持集合
androidmk 不接受任意 ifeq。conditionalTranslations 只列出少量能映射到 Soong 条件的固定表达式;遇到不在 map 中的条件,转换器调用 errorf("unsupported conditional"),并把后续受影响结构标记为不可自动迁移。
源码文件:build/soong/androidmk/androidmk/androidmk.go
if _, ok := conditionalTranslations[args]; ok {
newCond := conditional{args, eq}
conds = append(conds, &newCond)
} else {
file.errorf(x, "unsupported conditional")
conds = append(conds, nil)
continue
}LOCAL_* 属性在一个支持的条件内还会被写到条件前缀,例如 target/arch 对应的嵌套 map;全局 assignment 在条件中则直接报错,因为它无法安全地表达为 BP 文件变量。
5.2 嵌套限制
在 module 内遇到嵌套条件,源码明确报 unsupported nested conditional in module。这不是因为 Make parser 不能解析嵌套,而是映射器没有为任意条件栈建立等价 Blueprint property tree。
源码文件:build/soong/androidmk/androidmk/androidmk.go
if file.inModule {
if assignmentCond == nil {
assignmentCond = &newCond
} else {
file.errorf(x, "unsupported nested conditional in module")
}
}迁移动作应是先人工把条件归并为 arch、target、product_variables 或 select,再运行 androidmk;不应把生成的错误注释删除后假设条件仍然存在。
5.3 结果选择
条件迁移有三种不同处理:产品选择模块可以移出 Make 条件并保持模块始终定义;架构差异通常进入 arch;设备/厂商变量需要 soong_config_module_type 或 select。这些不是机械替换,必须确认谁拥有该配置以及何时生效。
6. 错误输出
6.1 错误保留
转换器的 errorf 不会静默丢弃失败节点。它把错误文本加入输出,并把原 Make 节点逐行作为注释插入 BP:
源码文件:build/soong/androidmk/androidmk/androidmk.go
相关函数/类型:bpFile.errorf
func (f *bpFile) errorf(failedNode mkparser.Node, message string, args ...interface{}) {
orig := failedNode.Dump()
message = fmt.Sprintf(message, args...)
f.addErrorText(fmt.Sprintf("// ANDROIDMK TRANSLATION ERROR: %s", message))
for _, line := range strings.Split(orig, "\n") {
f.insertExtraComment("// " + line)
}
}输出因此可能“看起来像合法 BP”,但文件内已经包含翻译错误注释,命令也会返回非零。人工迁移必须把这些注释当作未完成项,而不是格式噪声。
6.2 CLI退出
命令入口读取单个文件,调用 ConvertFile,先把 output 写到 stdout,再把 errors 写到 stderr,最后以 os.Exit(1) 结束。输出和错误可以同时存在,这是为了保留可审查的部分转换结果。
源码文件:build/soong/androidmk/cmd/androidmk.go
相关函数/类型:main
output, errs := androidmk.ConvertFile(os.Args[1], bytes.NewBuffer(b))
if len(output) > 0 {
fmt.Print(output)
}
if len(errs) > 0 {
for _, err := range errs {
fmt.Fprintln(os.Stderr, "ERROR: ", err)
}
os.Exit(1)
}因此 shell 脚本必须检查退出码,不能只判断 Android.bp 是否生成。
7. 属性核对
7.1 C/C++
以下映射来自固定 values.go 白名单,可作为审查起点,而不是自动等价承诺:
| Make字段 | 转换字段 | 审查点 |
|---|---|---|
LOCAL_SRC_FILES | srcs | 变量和生成文件是否仍存在 |
LOCAL_SHARED_LIBRARIES | shared_libs | 变体、stubs、APEX 可见性 |
LOCAL_STATIC_LIBRARIES | static_libs | 链接顺序、whole/static 语义 |
LOCAL_C_INCLUDES | local_include_dirs/include_dirs | LOCAL_PATH 分类是否正确 |
LOCAL_EXPORT_C_INCLUDE_DIRS | export_include_dirs | 依赖者是否真的需要导出 |
LOCAL_LDLIBS | host_ldlibs | 仅能按转换器定义理解为宿主库 |
LOCAL_SANITIZE | sanitize.* | 仅支持白名单 sanitizer |
7.2 Java与APK
Java 映射同样由白名单决定:LOCAL_PACKAGE_NAME 和 LOCAL_MODULE 进入 name,LOCAL_JAVA_LIBRARIES 进入 libs,LOCAL_STATIC_JAVA_LIBRARIES 进入 static_libs,LOCAL_RESOURCE_DIR 经 localize 进入 resource_dirs。certificate、privileged 和 proguard 字段还会受到最终模块类型和产品策略影响,不能只看字段名。
7.3 预置模块
LOCAL_MODULE_CLASS 会通过 prebuiltClass 改写 BUILD_PREBUILT scope,之后 include 选择对应预置模块类型。一个 LOCAL_MODULE_CLASS := ETC 的预置文件和一个 APK 预置不是同一消费者;迁移后应检查 prebuilt_etc、android_app_import 等最终类型,而不是只检查 name。
8. 迁移验证
8.1 旧链记录
迁移前先在同一产品和 variant 下构建旧 Make 模块,记录 module-info、安装路径、依赖库、生成文件和测试输入。只记录“构建成功”不足以证明等价,因为新 BP 可能成功但少了一个 required 或改变了安装分区。
8.2 转换审查
# 固定 checkout 中构建或取得 androidmk
m androidmk
# 保留 stdout 与 stderr,退出码不能忽略
set -o pipefail
androidmk path/to/Android.mk > path/to/Android.bp.new \
2> path/to/androidmk.stderr
status=$?
test "$status" -eq 0
# 检查转换器留下的未完成项
rg -n 'ANDROIDMK TRANSLATION (ERROR|WARNING)' \
path/to/Android.bp.new path/to/androidmk.stderr转换结果先写 .new,不要直接覆盖现有 BP。处理完错误注释后用 bpfmt 格式化,再执行 Soong 单模块分析。
8.3 新链验证
bpfmt -d path/to/Android.bp.new
m <module>
atest <related-test>
# 对 native 产物检查 ABI 和依赖
llvm-readelf -d out/.../lib<name>.so | grep NEEDED关键断言应按 owner 分层:Blueprint 无未知字段,Soong 无缺失/不可见依赖,Ninja action 的输入和 flags 符合预期,最终 artifact 的安装路径、ABI 和测试行为与旧链一致。没有旧产物或完整构建日志时,不能把“新 BP 可解析”写成迁移完成。
8.4 中断恢复
转换器本身是单次进程,没有事务日志;stdout 可能已经写出部分 BP,stderr 也可能记录错误。恢复时删除 .new 和旧错误日志后重新转换,避免把部分输出拼接到下一次结果。Soong 构建失败则按其 declared outputs 重新生成,不手工复制旧 .o 或 APK。
9. 测试边界
9.1 基础映射
androidmk_test.go 的基础测试输入 include $(CLEAR_VARS)、LOCAL_MODULE、LOCAL_SRC_FILES_EXCLUDE 和 BUILD_SHARED_LIBRARY,断言输出为 cc_library_shared、name 和 exclude_srcs,并保留注释。它证明白名单映射和注释位置,不证明复杂 Make 函数。
9.2 路径映射
测试分别输入 $(LOCAL_PATH)、$(LOCAL_PATH)/include、system/core/include 和混合变量,断言 local/global include 字段的拆分。这证明路径分类器的当前规则;如果路径由 shell、wildcard 或复杂函数产生,测试没有覆盖,不能自动外推。
9.3 失败映射
失败 fixture 使用不支持的 conditional、普通 rule 和不支持函数,断言输出包含 ANDROIDMK TRANSLATION ERROR。这个对应测试非常关键:错误节点不会消失,转换命令也不会伪装成功。
10. 边界收束
Android.mk 到 Android.bp 不是语法替换,而是 owner 迁移:Make parser 的动态字符串和 recipe 必须变成 Blueprint 的静态属性、Soong 的 mutator 状态或独立 genrule;androidmk 只能负责有明确映射的部分。最可靠的流程是记录旧链产物,运行转换器并检查退出码和错误注释,人工重构条件/路径/规则,最后用新 Soong action、安装路径、ABI 和测试做反向验证。
本文不声称 Android.mk 与 Android.bp 可以在同一目录任意共存,也不声称字段名相似就具有运行时等价性。共存、Make/Kati 调度和产品选择属于更高层构建图,需要结合具体 release 的 build/make、Soong/ui 和产品配置源码判断。
