使用ninja命令提高单模块编译效率
2018-02-09 14:16:53 +08 字数:3400 标签: Android Ninja从Android 7.0开始,默认使用ninja进行编译。
从Android 8.0后,默认启用Soong、Android.bp。
在开发过程中,以前最经常使用的mm
、mma
等单模块编译功能,现在变得及其耗时。
本文介绍如何用ninja
命令,来提高单模块编译效率,缩短开发流程(到当年没有ninja
的状态)。
Android 8.0的编译过程与问题 ¶
在原先Android 6.0纯Makefile编译的传统流程前,8.0版本新增了四个步骤:
- Soong的自举(bootstrap)。这个步骤会编译Soong的核心组件。
- 收集Android.bp并生成
out/soong/build.ninja
文件。 - 收集Android.mk并生成
out/build-<product>.ninja
与out/combined-<product>.ninja
文件。 - 执行Ninja文件,进行编译。这个
combined-*.ninja
文件,就是真正的执行入口。
后面的步骤就是自底向上编译Android所有模块,与以前相同。
生成Ninja的工具链关系大致如下:
Makefile/Android.mk -> Kati -> Ninja
Android.bp -> Blueprint -> Soong -> Ninja
在使用Ninja来做真正的编译以后,Android的编译、尤其是单模块编译,多出了许多问题,影响开发效率。
问题0:Ninja文件太大 ¶
在AOSP 8.0版本,out/build-<product>.ninja
文件超过400MB,out/soong/build.ninja
也超过200MB,实际的项目会更大。
out/combined-<product>.ninja
是对另外两个文件的组合,本身并不大。
比起out
目录下其它几十GB的产物,这可能不算是一个真正的问题。
但这些巨大的纯文本文件,会产生其它问题。
问题1:生成Ninja文件很慢 ¶
在收集Android.bp与Android.mk时,编译脚本需要去遍历整个代码库。 受限于硬盘IO速度,以及脚本的单线程实现,这个过程会比较耗时。 生成一个Ninja文件,在一个32核64GB的工作站,大约需要4~6分钟。 如果有多人同时编译,时间还会更长。
问题2:生成Ninja文件的场景很多 ¶
Android编译脚本重新生成Ninja文件的场景,比想象中要多。
如果只是在项目lunch不同产品参数时,生成不同的Ninja文件,这还可以接受。
实际上不仅如此,用mm
进行单模块编译时,也会生成一组Ninja文件,
命名规律为build-<product_name>-<path_to_Android.mk>.ninja
。
比如,在AOSP项目lunch aosp_arm64-eng
后,于system/netd
目录下,做mm
编译,会出现以下打印:
out/build-aosp_arm64-system_netd_Android.mk.ninja is missing, regenerating...
卡4~6分钟后,生成了out/build-aosp_arm64-system_netd_Android.mk.ninja
文件,并继续编译过程。
与原先Android 6.0即以前的mm
几分钟完成首次编译、几秒钟完成编译检查、一分钟内完成增量编译相比,这额外多出来的几分钟,冗长得不可接受。
好在,这类额外的文件大小,取决于依赖多少。
就system/netd
而言,有110MB,仅需额外使用1分钟。
此外,mmm
会额外生成另外一种Ninja文件:out/build-aosp_arm64-_system_netd_Android.mk.ninja
。
仔细看,可以发现,差别就是多了一个下划线_
!
这两个文件,不仅名称相近,而且内容相同,可能是一个bug。
有时,也会生成一种带SHA1的的文件,比如out/build-8015fee4a534af9c63f8512e71a7f51b.ninja
。
问题3:重新生成Ninja的条件很多 ¶
在system/netd
目录下,再次执行mm
,则大约9秒就得到结果:
...
ninja: no work to do.
然而,如果touch Android.mk
,再执行mm
,就需要重新生成Ninja文件。
system/netd/Android.mk was modified, regenerating...
已知的规则,只有编译配置文件被修改。
除了显式修改,还有类似git pull
、repo sync
这样的情况。
然而实际使用中,有更多未知因素,也会导致Ninja文件重新生成。
使用ninja能改进什么 ¶
使用ninja
,可以不经过make
,直接执行Ninja文件,完全避免重新生成,以及解析Makefile的运行开销。
此外,还能、或者说必须,指定更细致的执行内容。
mm
的意思,是执行当前目录下的所有编译模块。
Android.mk定义了几个模块,就会执行几个。
使用ninja
必须指定一个target,否则是全编译。
可以直接指定模块名,单独执行某个模块。
甚至可以指定具体的*.o
、*.jar
、*.dex
文件为target,避免编译整个模块。
(当然,make
也支持指定模块名,或更细致的target。
用Ninja只是为了避免重新生成,其实相比Android 6.0,执行速度未见得有多快。)
安装ninja ¶
ninja
的可执行文件,需要自行安装。
五种安装方法,详见《在Android平台开发环境安装ninja》。
使用ninja ¶
准备build.ninja ¶
make的默认编译文件是Makefile,而Ninja默认的编译文件则是build.ninja。
ninja
需要在Android项目根目录执行,那里当然是没有这个文件的,需要自行准备。
ln -s out/combined-aosp_arm64.ninja build.ninja
以上仍然以AOSP的lunch aosp_arm64-eng
为例。
如果有多个combined-*.ninja
文件,可以自行选择一个。
ninja
也支持-f
参数,可以临时指定一个Ninja文件来执行。
ninja -f out/combined-aosp_arm64.ninja <target>
找到模块名 ¶
在source build/envsetup.sh
后,有一些额外的function,可以帮助查找代码,详见hmm
。
其中mgrep
,对查找*.mk
文件,很有帮助。
而Android.bp,则还没做相关功能,不过也可以通过原版grep来实现。
比如,在AOSP的system/netd
目录下,查找所有Makefile里的模块名:
$ mgrep -w LOCAL_MODULE
./tests/dns_responder/Android.mk:20:LOCAL_MODULE := libnetd_test_dnsresponder
./tests/benchmarks/Android.mk:20:LOCAL_MODULE := netd_benchmark
./tests/Android.mk:20:LOCAL_MODULE := netd_integration_test
./netutils_wrappers/Android.mk:24:LOCAL_MODULE := netutils-wrapper-1.0
./netutils_wrappers/Android.mk:43:LOCAL_MODULE := netutils_wrapper_test
./server/Android.mk:24:LOCAL_MODULE := libnetdaidl_static
./server/Android.mk:41:LOCAL_MODULE := libnetdaidl
./server/Android.mk:63:LOCAL_MODULE := netd
./server/Android.mk:149:LOCAL_MODULE := ndc
./server/Android.mk:159:LOCAL_MODULE := netd_unit_test
再查找所有Android.bp里的模块名。
$ grep -rnws --include='*.bp' 'name:'
libnetdutils/Android.bp:2: name: "libnetdutils",
libnetdutils/Android.bp:26: name: "netdutils_test",
Android.bp:2: name: "libnetd_client_headers",
client/Android.bp:16: name: "libnetd_client",
有些Java层的模块,可能会没有指定LOCAL_MODULE
,而是指定LOCAL_PACKAGE_NAME
。
比如在packages/apps/Settings
,就可以执行以下命令来查找模块。
$ mgrep -w 'LOCAL_MODULE\|LOCAL_PACKAGE_NAME'
./tests/unit/Android.mk:20:LOCAL_PACKAGE_NAME := SettingsUnitTests
./tests/app/Android.mk:23:LOCAL_PACKAGE_NAME := SettingsTests
./tests/robotests/Android.mk:21:LOCAL_MODULE := SettingsRoboTests
./tests/robotests/Android.mk:32:LOCAL_MODULE := RunSettingsRoboTests
./Android.mk:7:LOCAL_MODULE := settings-logtags
./Android.mk:14:LOCAL_PACKAGE_NAME := Settings
原理大概是这样,而为了方便,可以用一个alias来简化查找。
alias findm="grep -rnws --include='*.[mb][kp]' 'LOCAL_MODULE\|LOCAL_PACKAGE_NAME\|name:'"
直接执行这条命令,可以得到一个findm
的alias。
把这一行添加到$HOME/.bashrc
中,在新的Bash里就可以直接使用了。
在system/netd
中,执行效果如下:
$ findm
tests/dns_responder/Android.mk:20:LOCAL_MODULE := libnetd_test_dnsresponder
tests/benchmarks/Android.mk:20:LOCAL_MODULE := netd_benchmark
tests/Android.mk:20:LOCAL_MODULE := netd_integration_test
libnetdutils/Android.bp:2: name: "libnetdutils",
libnetdutils/Android.bp:26: name: "netdutils_test",
netutils_wrappers/Android.mk:24:LOCAL_MODULE := netutils-wrapper-1.0
netutils_wrappers/Android.mk:43:LOCAL_MODULE := netutils_wrapper_test
server/Android.mk:24:LOCAL_MODULE := libnetdaidl_static
server/Android.mk:41:LOCAL_MODULE := libnetdaidl
server/Android.mk:63:LOCAL_MODULE := netd
server/Android.mk:149:LOCAL_MODULE := ndc
server/Android.mk:159:LOCAL_MODULE := netd_unit_test
Android.bp:2: name: "libnetd_client_headers",
client/Android.bp:16: name: "libnetd_client",
使用ninja执行单模块编译 ¶
必须要在项目根目录执行。
在source build/envsetup.sh
后,可以用croot
从任何位置直接回到项目根目录。
$ croot
$ ninja netd # Build netd
ninja: no work to do.
$ ninja netd_unit_test # Build test units of netd
[65/65] Install: out/target/product/aosp_arm64/data/nativetest/netd_unit_test/netd_unit_test
使用ninja执行单文件编译 ¶
要编译单个文件,先要找到对应的target,这一个原则是相同的。 但由于C/C++与Java的编译方式大不相同,因此分开举例介绍。
C/C++ ¶
开发过程中,如果单个文件编译出错了,可以在修改后,用ninja
单独编译这个文件。
$ echo error >> system/netd/server/Network.cpp # Make an error
$ ninja netd
[1/6] target C++: netd <= system/netd/server/Network.cpp
FAILED: out/target/product/aosp_arm64/obj/EXECUTABLES/netd_intermediates/Network.o
...
system/netd/server/Network.cpp:95:1: error: unknown type name 'error'
error
^
system/netd/server/Network.cpp:95:6: error: expected unqualified-id
error
^
2 errors generated.
ninja: build stopped: subcommand failed.
在修改了这个错误后,可以通过FAILED:
的提示,直接编译这个*.o
文件。
$ ninja out/target/product/aosp_arm64/obj/EXECUTABLES/netd_intermediates/Network.o
[1/1] target C++: netd <= system/netd/server/Network.cpp
这样,能最快地检查编译问题是否修复。
Java ¶
Java层的情况也类似,但没有办法做到像C/C++这么精确。
$ echo error >> packages/apps/Settings/src/com/android/settings/Settings.java # Make an error
$ ninja Settings
...
[3/9] Building with Jack: out/target/common/obj/APPS/Settings_intermediates/with-local/classes.dex
FAILED: out/target/common/obj/APPS/Settings_intermediates/with-local/classes.dex
...
ERROR: /home/yanqd1/workspace/aosp_arm64/packages/apps/Settings/src/com/android/settings/Settings.java:183.1: Syntax error on token "error", delete this token
ninja: build stopped: subcommand failed.
修复问题后,同样是根据FAILED:
这一行的提示,编译这个*.dex
文件。
$ ninja out/target/common/obj/APPS/Settings_intermediates/with-local/classes.dex
...
[3/3] Building with Jack: out/target/common/obj/APPS/Settings_intermediates/with-local/classes.dex
例外 ¶
以上的方法,仅对AOSP以及类AOSP的Git库生效。 这里所谓『类AOSP的Git库』,是指以Android.mk或Android.bp方式来组织编译的那些。
在实际的项目中,有很多Git库的编译方式不同于此,不能使用这里介绍的方法。 比如,Linux Kernel、高通nonhlos等。