使用distcc分布式编译加速Android NDK原生项目编译生成

随着项目规模的增大,源代码文件增多,结构越来越复杂,导致项目编译链接速度变慢是一件让人非常头痛的事!

在Windows上我们用Visual Studio可以使用IncrediBuild (http://www.incredibuild.com/) 这个非常好用的分布式编译工具,配合其自带的VS Add-In可以很方便的将大型项目的编译工作负担分布到网络上的其它机器完成,极大的缩短了项目编译时间,提高工作效率!

不过遗憾的是IncrediBuild目前只支持Windows系统和VS等一些编译环境,对于Android, iOS等交叉编译的移动平台开发环境就无能为力了。

其实对于linux系OS上还是有可用的分布式编译解决方案的,就是接下来我要说的这个distcc,项目介绍请猛击这里:https://code.google.com/p/distcc/

本来目前的开发环境是Windows+Eclipse+ADT+Android SDK+Android NDK+Cygwin这样的,所以一开始的想法是试试能不能混合Windows和Linux环境共同分担编译压力,这个想法最初是由于看到有资料说distcc可以在Cygwin环境下编译运行。

不过后来我发现我拿到的distcc-3.2rc1在Cygwin环境下编译报错:

cc1: warnings being treated as errors
src/util.c: 在函数‘dcc_get_dns_domain’中:
src/util.c:270:9: 错误:数组下标类型为‘char’
src/util.c: 在函数‘dcc_tokenize_string’中:
src/util.c:795:9: 错误:数组下标类型为‘char’
Makefile:448: recipe for target `src/util.o’ failed
make: *** [src/util.o] Error 1

先是这个警告当错误处理,然后用./configure –disable-Werror重新生成一次make编译又提示:

In file included from /usr/lib/gcc/i686-pc-cygwin/4.5.3/../../../../include/w32api/oleidl.h:7:0,
from /usr/lib/gcc/i686-pc-cygwin/4.5.3/../../../../include/w32api/ole2.h:38,
from /usr/lib/gcc/i686-pc-cygwin/4.5.3/../../../../include/w32api/wtypes.h:12,
from /usr/lib/gcc/i686-pc-cygwin/4.5.3/../../../../include/w32api/winscard.h:10,
from /usr/lib/gcc/i686-pc-cygwin/4.5.3/../../../../include/w32api/windows.h:97,
from src/exec.c:65:
src/rpc.h:27:5: 错误:与‘dcc_x_result_header’类型冲突
src/rpc.h:27:5: 附注:‘dcc_x_result_header’的上一个声明在此
src/rpc.h:28:5: 错误:与‘dcc_r_result_header’类型冲突
src/rpc.h:28:5: 附注:‘dcc_r_result_header’的上一个声明在此
src/rpc.h:47:5: 错误:与‘dcc_r_request_header’类型冲突
src/rpc.h:47:5: 附注:‘dcc_r_request_header’的上一个声明在此
Makefile:448: recipe for target `src/exec.o’ failed
make: *** [src/exec.o] Error 1

看了一下,发现大概是socket头文件的win32 linux不兼容导致的,抑或是源码中没有对win32的include进行分情况处理,总之就是没法编译最新版本的源码。后来发现cygwin的setup中有2.18.3-1的bin和source,下载下来试了一下是可以在Cygwin下正常执行的,不过考虑到部分linux机器上还是要使用新版本的,为了保持所有编译服务器版本一致(PS:这点distcc和IncrediBuild的概念正好相反,前者认为负责执行编译工作的为服务端,而后者认为是客户端agent!),外加一直对Cygwin这种模拟linux环境做法的效率不是很感冒,最后还是决定放弃Windows端了,有兴趣的朋友可以试一下,理论上只要不涉及新版distcc加入的pump模式的话,跨版本的客户端和服务器应该也是没有问题的。

接下来开始转战linux!

首先是配置Android开发环境,测试用的linux系统为RHEL RedHat Enterprise Linux 5.5,由于我只是想将项目的native部分(/jni)进行分布式编译处理,不考虑Java部分,所以SDK的配置就免了,直接wget最新版本的NDK for linux:http://dl.google.com/android/ndk/android-ndk-r8d-linux-x86.tar.bz2解压缩,调用./ndk-build -C /proj/尝试编译项目代码,结果报错提示libstdc++.so.6的GLIBCXX3.4.10(记忆中好像是这个版本号…),搜索了一下发现是由于libstdc++.so.6符号连接的libstdc++库版本过低导致的,于是开始折腾linux的源更新,什么下载源码编译啊,配置yum自动更新啊,折腾了半天也没给libstdc++.so升级成功,最后直接从网上down了一份人家编译好的so:http://download.csdn.net/detail/dqswuyundong/3379734(同样还是没记错的话…切记不要搞错32bit 64bit版本),放到lib位置然后ln -s重新建立libstdc++.so.6的符号连接到新的so,然后再试,发现又提示libz.so.1的错误(依然是没记错的话,貌似是最后运行一个什么strip程序的时候,libz.so.1: no version information available…),看了一下,依旧是版本过低问题,这次直接到:http://www.zlib.net/下载1.2.7版本的源码编译安装,然后重做符号链接,编译,终于这回pass了,然后又换了一下toolchain到clang3.1,加入APP_ABI armeabi armeabi-v7a两份so生成(具体操作可以看之前的这篇:http://blog.k-res.net/%E8%AE%BE%E8%AE%A1%E5%BC%80%E5%8F%91/cc/android-ndk-arm-thumb-toolchain-switch-clang-llvm.html),这样下来生成一次时间大概是不到30min!

下一步开始设置distcc!

从distcc项目主页:https://code.google.com/p/distcc/downloads/list拿下最新的distcc-3.2rc1的源码后./configure, make, sudo make install,ok一切都很顺利!然后启动distccd服务:distccd –daemon –allow 0.0.0.0/0,这里我犯懒,允许了所有地址的链接。设置一下编译服务器列表,如export DISTCC_POTENTIAL_HOSTS='localhost red green blue',或者直接修改distcc的hosts文件(/usr/local/etc/distcc/hosts)均可,然后回到distcc的源码文件夹,试了一下pump make -j8 CC=distcc,貌似没有问题,不过由于本来编译就没花多长时间,换上分布式也没什么感觉,不管它,开始设置android ndk编译…

这里过程中的各种失败尝试我就省略了吧,什么改toolchain下编译器实体为到distcc的符号链接啦,改make file啦之类的…最后发现的一个比较省事且可行的方法(在读了ndk-build的bash脚本和build/core中的各种mk文件之后)就是修改build/core/definitions.mk这个文件中对编译器的定义,一般gcc的make可以通过指定CC=XXX,CXX=XXX的方式指定编译所需编译器,而我试了在ndk-build的最后实际执行make时加入CC=distcc或是修改项目的Android.mk Application.mk均没有效果,读了半天ndk内置的mk文件后发现是被保存在了definitions.mk这个文件中,并且是以_CC命名的,通过加入_CC=distcc确实可以改变make执行时的编译器命令,不过这样是不行的,由于NDK还会根据其它东西设置具体执行的编译器路径,导致单纯的修改_CC=distcc后会根据arch之类的让distcc执行默认的arm编译器armv5te-none-linux-androideabi之类的,这当然会由于找不到编译器而失败了,tail /var/log/message会发现110错误,也就是man里面说的compiler not found!

于是干脆直接修改definitions.mk中下面这段

# This assumes that many variables have been pre-defined:
# _SRC: source file
# _OBJ: destination file
# _CC: ‘compiler’ command
# _FLAGS: ‘compiler’ flags
# _TEXT: Display text (e.g. “Compile++ thumb”, must be EXACTLY 15 chars long)
#
define ev-build-file
$$(_OBJ): PRIVATE_SRC      := $$(_SRC)
$$(_OBJ): PRIVATE_OBJ      := $$(_OBJ)
$$(_OBJ): PRIVATE_DEPS     := $$(call host-path,$$(_OBJ).d)
$$(_OBJ): PRIVATE_MODULE   := $$(LOCAL_MODULE)
$$(_OBJ): PRIVATE_TEXT     := “$$(_TEXT)”
$$(_OBJ): PRIVATE_CC       := $$(_CC) <=这里改为distcc$$(_CC)
$$(_OBJ): PRIVATE_CFLAGS   := $$(_FLAGS)

这样修改后,所有ndk-build的make后执行的compiler路径前都会加上个”distcc “,也就是distcc man里的指定编译器的方式,另外由于这里会提交clang编译器的完整路径给distcc,所以其它服务机上的编译器路径也应保持一直(查到的资料上说有个什么merge可以自动做到,不过对于NDK来说还是手动在每台机上解压一次好了,毕竟还带着其它乱七八糟的东西)。然后再次执行ndk-build,并加入V=1参数,可以看到执行的编译命令已经在最前面加上了distcc,但是会提示服务器编译失败,然后fallback到本地编译成功,这可不是我想要的!查看了一下log,发现是distccd在调用NDK的clang编译器时出现了permission denied错误,检查了一下发现到clang的完整路径上层有没有all执行的权限的夹子,于是chmod a+x给上权限再试,终于ok了!
可以看到服务端的log中会有类似:

Mar 14 09:49:03 localhost distccd[3054]: (dcc_job_summary) client: 127.0.0.1:48579 COMPILE_OK exit:0 sig:0 core:0 ret:0 time:1603ms /root/android-ndk-r8d/toolchains/llvm-3.1/prebuilt/linux-x86/bin/clang /root/libs/jni/../freetype-2.3.9/src/sfnt/sfnt.c

这样的COMPILE_OK记录,clean后试了一下,两台Xeon 4核x3210机器用-j8的参数,同时起8个编译线程编译刚才的项目时间可以缩短到大概9min左右!效果还是比较明显的。

最后,还试了一下新版distcc加入的pump模式,说是可以将头文件预处理过程也发到各个编译机上执行,能进一步的提高效率。于是将hosts中的编译机地址后加上,cpp,lzo参数,然后修改ndk-build最后一行,在调用make前加上pump ,执行编译,发现提示这样的错误:

WARNING include server: Preprocessing locally. Include server not covering: Could not locate name of translation unit: [‘/root/android-ndk-r8d/toolchains/arm-linux-androideabi-4.6/prebuilt/linux-x86’, ‘armv5te-none-linux-androideabi’, ‘/root/libs/jni/../boost_1_48_0/libs/regex/src/icu.cpp’]. for translation unit ‘unknown translation unit’
distcc[956] (dcc_talk_to_include_server) Warning: include server gave up analyzing
distcc[956] (dcc_build_somewhere) Warning: failed to get includes from include server, preprocessing locally

看了一下大概还是类似之前distcc根据arch寻找默认编译器之类的错误,不过这次找了很久也没发现怎么让pump模式的distcc处理include时也一样使用NDK工具选定的clang编译器,最后还是残念了,先有个普通模式用着吧,以后有时间再继续研究,哪位朋友要是有这方面的经验也麻烦分享一下,感激不尽!

PS:Android NDK貌似是从8b开始支持独立运行(standalone)编译器的模式了,也就是说可以脱离ndk-build编译native代码,不过目前好像还不支持llvm的clang,这里:http://www.kandroid.org/ndk/docs/STANDALONE-TOOLCHAIN.html。这对于distcc来说或许可以更加简化设置操作,不用再去擅自修改内置mk文件,而且对于pump模式来说没准也能更轻松的实现!

最后的最后,写这篇博文的时的一些错误信息、位置之类内容都是靠记忆的,外加本人水平有限,写的有点啰嗦,还可能会有不准确的地方,欢迎留言!

博主友情提示:

如在您的评论中需要提及如QQ号、微信号、电子邮件地址或其他隐私敏感信息,欢迎通过>>博主专用加密工具<<加密后发布,原文只有博主可以看到。