编译目标与Makefile集成:make命令详解、编译目标(droid、sdk、kernel等)、Android.mk与Android.bp的混合使用

说实话,很多刚接触AOSP的同学,第一反应就是敲个make然后等它编译完。但你真的知道make后面跟的那些参数是什么意思吗?droid、sdk、kernel这些目标到底干了什么?还有,Android.mk和Android.bp混着用会不会打架?

嗯,今天我们就来把这些事情彻底讲清楚。我会结合我这些年踩过的坑,给你把这块掰开揉碎了说。

1. make命令:不只是敲个回车

在AOSP根目录下执行make,默认会编译整个系统。但实际项目中,我们很少这么干——太慢了。我个人的习惯是,先搞清楚我要编译什么,再选对应的目标。

基本的make命令格式是这样的:

make [目标名] [变量赋值]

举个例子:

make droid -j16
make sdk -j8
make kernel -j32

-j后面的数字是并行编译的线程数。我建议你设成CPU核心数的1.5到2倍。设太小了慢,设太大了容易把机器搞死机——我曾经在32核的服务器上设了-j64,结果内存爆了,编译到一半直接OOM。

小技巧: 如果你不确定设多少,可以用nproc命令查看CPU核心数,然后乘以2。比如make -j$(nproc --all)

2. 编译目标详解:droid、sdk、kernel

AOSP里定义了很多编译目标。每个目标对应一组模块的集合。说白了,就是告诉构建系统:我要编译哪些东西。

目标名 说明 典型用途
droid 默认目标,编译完整的Android系统镜像 刷机、测试完整系统
sdk 编译Android SDK(包括android.jar等) 应用开发、SDK定制
kernel 编译Linux内核 内核开发、驱动调试
bootimage 只编译boot.img 快速验证内核或ramdisk修改
systemimage 只编译system.img 修改系统服务或库后快速验证
snod 跳过重新编译,直接打包镜像 只修改了文件列表或分区大小

这里我要特别说一下droid。它其实是make的默认目标,定义在build/make/core/Makefile里。你执行make不带参数,就相当于执行make droid。它会编译所有在PRODUCT_PACKAGES里列出的模块。

为什么会这样?因为AOSP的构建系统是分层的。droid目标会依赖其他子目标,比如bootimagesystemimageuserdataimage等。你想想看,如果你只想改个内核,完全没必要编译整个系统——直接make kernel就行了。

核心要点: 编译目标就是一组模块的集合。选对目标,能省下大量时间。

3. Android.mk与Android.bp的混合使用

这个问题我经常被问到。Android.mk是老的构建文件格式,Android.bp是新的。两者可以共存吗?答案是:可以,但有讲究。

先说说我的经历。有一次我在一个项目里,发现某个模块用Android.mk写的,另一个模块用Android.bp写的,结果编译时老是报依赖找不到。查了半天,发现是Android.bp里引用了Android.mk定义的模块名,但大小写写错了。嗯,这种坑真的很隐蔽。

3.1 两种格式的对比

特性 Android.mk Android.bp
语法 Makefile语法(变量、函数) Blueprint语法(类似JSON)
条件判断 支持(ifeq, ifdef等) 不支持(需用Soong插件)
性能 较慢(解析复杂) 较快(解析简单)
推荐场景 遗留模块、需要条件编译 新模块、纯编译逻辑

3.2 混合使用的规则

Android.bp最终会被Soong编译系统解析,而Android.mk会被Kati解析。两者在同一个构建过程中是共存的。但要注意以下几点:

  • 模块名必须唯一:同一个模块名不能在Android.mk和Android.bp中同时定义,否则会冲突。
  • 依赖可以跨格式:Android.bp可以依赖Android.mk定义的模块,反之亦然。但模块名必须完全匹配。
  • Android.mk可以引用Android.bp的变量:通过$(call my-dir)等函数,可以获取到Android.bp中定义的路径。

举个例子,假设你有一个老模块用Android.mk写的:

# Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := my_old_lib
LOCAL_SRC_FILES := my_old_lib.c
include $(BUILD_SHARED_LIBRARY)

然后新模块用Android.bp写,想依赖它:

// Android.bp
cc_library_shared {
    name: "my_new_lib",
    srcs: ["my_new_lib.c"],
    shared_libs: ["my_old_lib"],  // 依赖Android.mk定义的模块
}

这样是没问题的。但要注意,my_old_lib这个名字在两个文件中必须完全一致。

警告: 我曾经遇到过一个情况,Android.mk里定义了LOCAL_MODULE := libfoo,而Android.bp里写了name: "libfoo",结果编译时报重复定义。原因是Android.mk默认会加lib前缀,而Android.bp不会。所以一定要检查模块名的最终形式。

4. 知识体系与核心逻辑

下面这张图展示了编译目标与Makefile集成的整体流程:

编译目标与Makefile集成核心逻辑 用户执行 make 命令 解析编译目标(droid/sdk/kernel等) 解析 Android.mk(Kati)和 Android.bp(Soong) 依赖分析 & 模块排序 并行编译 & 生成镜像 用户层 目标层 解析层 依赖层 编译层

这张图展示了从你敲下make命令到最终生成镜像的完整流程。我个人觉得,理解这个流程比记住具体命令更重要。因为只有知道了构建系统是怎么工作的,你才能灵活地应对各种问题。

5. 实战建议

最后,给你几个我实际项目中的建议:

  • 新模块用Android.bp:除非你有条件编译的需求,否则Android.bp更简洁、更快。
  • 老模块别急着迁移:Android.mk还能用,而且很多第三方库还是用mk写的。强行迁移可能引入新bug。
  • 混合使用时注意模块名:这是最容易出错的地方。我建议你在Android.bp里显式指定name,不要依赖默认命名规则。
  • 善用make snod:如果你只改了文件列表或分区大小,用make snod可以跳过编译,直接打包镜像,速度飞快。
避坑指南: 我曾经在一个项目里,把Android.mk和Android.bp混用,结果发现某个模块在Android.bp里定义了name: "libfoo",而Android.mk里也定义了LOCAL_MODULE := libfoo。编译时Soong和Kati都解析到了这个模块,但一个认为是静态库,一个认为是动态库,最后链接阶段报错。查了整整一天才找到原因。所以,模块名和类型一定要统一。

好了,关于编译目标和Makefile集成,今天就聊到这里。记住,选对目标、理解构建流程、注意混合使用的细节,你就能在AOSP编译中游刃有余。


公众号:蓝海资料掘金营,微信deep3321