Skip to content

Android.mk迁移链

追踪 androidmk 与 Make parser 如何解析 Android.mk、映射 Android.bp,以及自动转换的失败边界与等价验证。

基于android-17.0.0_r1
AndroidAndroid.mkAndroid.bpandroidmkSoong

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

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

go
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

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

go
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

go
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

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

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

go
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

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

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

go
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

go
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_FILESsrcs变量和生成文件是否仍存在
LOCAL_SHARED_LIBRARIESshared_libs变体、stubs、APEX 可见性
LOCAL_STATIC_LIBRARIESstatic_libs链接顺序、whole/static 语义
LOCAL_C_INCLUDESlocal_include_dirs/include_dirsLOCAL_PATH 分类是否正确
LOCAL_EXPORT_C_INCLUDE_DIRSexport_include_dirs依赖者是否真的需要导出
LOCAL_LDLIBShost_ldlibs仅能按转换器定义理解为宿主库
LOCAL_SANITIZEsanitize.*仅支持白名单 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 转换审查 ​

bash
# 固定 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 新链验证 ​

bash
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 和产品配置源码判断。