5

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

 3 years ago
source link: https://note.qidong.name/2018/02/android-ninja-tips/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

2018-02-09 14:16:53 +08  字数:3400  标签: Android 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等。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK