编译目标与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目标会依赖其他子目标,比如bootimage、systemimage、userdataimage等。你想想看,如果你只想改个内核,完全没必要编译整个系统——直接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这个名字在两个文件中必须完全一致。
LOCAL_MODULE := libfoo,而Android.bp里写了name: "libfoo",结果编译时报重复定义。原因是Android.mk默认会加lib前缀,而Android.bp不会。所以一定要检查模块名的最终形式。
4. 知识体系与核心逻辑
下面这张图展示了编译目标与Makefile集成的整体流程:
这张图展示了从你敲下make命令到最终生成镜像的完整流程。我个人觉得,理解这个流程比记住具体命令更重要。因为只有知道了构建系统是怎么工作的,你才能灵活地应对各种问题。
5. 实战建议
最后,给你几个我实际项目中的建议:
- 新模块用Android.bp:除非你有条件编译的需求,否则Android.bp更简洁、更快。
- 老模块别急着迁移:Android.mk还能用,而且很多第三方库还是用mk写的。强行迁移可能引入新bug。
- 混合使用时注意模块名:这是最容易出错的地方。我建议你在Android.bp里显式指定
name,不要依赖默认命名规则。 - 善用
make snod:如果你只改了文件列表或分区大小,用make snod可以跳过编译,直接打包镜像,速度飞快。
name: "libfoo",而Android.mk里也定义了LOCAL_MODULE := libfoo。编译时Soong和Kati都解析到了这个模块,但一个认为是静态库,一个认为是动态库,最后链接阶段报错。查了整整一天才找到原因。所以,模块名和类型一定要统一。
好了,关于编译目标和Makefile集成,今天就聊到这里。记住,选对目标、理解构建流程、注意混合使用的细节,你就能在AOSP编译中游刃有余。