使用ninja命令提高单模块编译效率

从Android 7.0开始,默认使用ninja进行编译。 从Android 8.0后,默认启用Soong、Android.bp。 在开发过程中,以前最经常使用的mmmma等单模块编译功能,现在变得及其耗时。

本文介绍如何用ninja命令,来提高单模块编译效率,缩短开发流程(到当年没有ninja的状态)。

Android 8.0的编译过程与问题

在原先Android 6.0纯Makefile编译的传统流程前,8.0版本新增了四个步骤:

  1. Soong的自举(bootstrap)。这个步骤会编译Soong的核心组件。
  2. 收集Android.bp并生成out/soong/build.ninja文件。
  3. 收集Android.mk并生成out/build-<product>.ninjaout/combined-<product>.ninja文件。
  4. 执行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 pullrepo 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等。


相关笔记