Android7.0 Ninja編譯原理
#############################################
本文為極度寒冰原創,轉載請註明出處 #############################################引言
使在Android N的系統上,初次使用了Ninja的編譯系統。對於Ninja,最初的印象是用在了Chromium open source code的編譯中,在chromium的編譯環境中,使用ninja -C out/Default chrome命令,就可以利用原始碼編譯出chrome的apk。對使用者而言,拋開對原理的探究,最直觀的印象莫過於可以清楚的看到自己當前編譯的進度。同時,對android而言,也可以感受到編譯速度的提升帶來的便捷。本文將深入分析Ninja的編譯原理,以及android上面的編譯改變。
正因為這個改變,所以在編譯android N的code的時候需要使用OpenJDK8
編譯系統的記憶體最少需要12G,建議16G,否則會出現JVM不足的錯誤。
8G記憶體的機器可以通過增大JVM預設值的方法來解決,但是經過測試,還是會偶爾出現JVM不足的錯誤
exportJAVA_OPTS='-Xmx4096M'
概念簡介
名詞:
Ninja |
Blueprint |
Soong |
Ninja
Ninja是一個致力於速度的小型編譯系統(類似於Make);
如果把其他編譯系統比做高階語言的話,Ninja就是組合語言
主要有兩個特點:
1、可以通過其他高階的編譯系統生成其輸入檔案;
2、它的設計就是為了更快的編譯;
使用Kati把makefile轉換成Ninja files,然後用Ninja編譯
在不久的將來,當不再用Makefile(Android.mk)時,Kati將被去掉
ninja核心是由C/C++編寫的,同時有一部分輔助功能由python和shell實現。由於其開源性,所以可以利用ninja的開原始碼進行各種個性化的編譯定製。
Blueprint, Soong
Blueprint和Soong是用於一起把Blueprint 檔案轉換為Ninja檔案。 將來需要寫Blueprint檔案(Android.bp),轉換為Android.soong.mk(也可以直接寫),然後轉換為Ninja檔案(build.ninja)然後用Ninja編譯。
如果Android.mk和Android.bp同時存在,Android.mk會被忽略。
如果Android.bp的同級目錄下有Android.soong.mk也會被include
1.ckati可執行檔案的生成
在android系統中,目前還未完全切換到Ninja編譯,編譯的入口仍然是make命令, 如下commands以nexus為例:
source build/envsetup.sh
choosecombo
make -j4
在這邊可以看到,最終編譯使用的命令仍然是make.
既然是make,那就在編譯中首先include到的就是build/core/main.mk了,在main.mk中,我們可以清楚的看到對Ninja的呼叫:
relaunch_with_ninja :=
ifneq ($(USE_NINJA),false)
ifndef BUILDING_WITH_NINJA
relaunch_with_ninja := true
endif
endif
由於USE_NINJA預設沒有定義,所以一定會進入到這個選項中,並且將relaunch_with_ninja置為true。這樣的話,就會進入到下面的重要操作語句,去include ninja的makefile. 並且在out目錄下生成ninja_build的檔案,顯示當前是使用了ninja的編譯系統。
ifeq ($(relaunch_with_ninja),true)
# Mark this is a ninjabuild.
$(shell mkdir -p $(OUT_DIR)&& touch $(OUT_DIR)/ninja_build)
includebuild/core/ninja.mk
else # !relaunch_with_ninja
ifndef BUILDING_WITH_NINJA
# Remove ninja build mark ifit exists.
$(shell rm -f $(OUT_DIR)/ninja_build)
endif
在include build/core/ninja.mk的語句執行後,我們就可以看到真正定義ninja的地方了。由於前面簡介講了ninjia是基於開源專案編譯出來的輕便的編譯工具,所以這邊google肯定也對ninjia進行了修改,編譯,並且最終生成了一個可執行的應用程式。在simba6專案中,我們可以在prebuilts/ninja/linux-x86下面找到這個可執行的應用程式ninja。我們可以簡單的執行這個ninja的命令,比如ninja –h, 就可以瞭解到這個command的基本用法, 也可以看到本版本的ninja使用的base version為1.6.0。
./ninja -h
usage: ninja [options][targets...]
if targets are unspecified,builds the 'default' target (see manual).
options:
--version print ninja version ("1.6.0")
-C DIR change to DIR before doing anything else
-f FILE specify input build file [default=build.ninja]
-j N run N jobs in parallel [default=6, derived from CPUs available]
-k N keep going until N jobs fail [default=1]
-l N do not start new jobs if the load average is greater than N
-n dry run (don't run commands but act like they succeeded)
-v show all command lines while building
-d MODE enable debugging (use -d list to list modes)
-t TOOL run a subtool (use -t list to list subtools)
terminates toplevel options; further flagsare passed to the tool
-w FLAG adjust warnings (use -w list to list warnings)
---------------------------------------------------------------------------------------------------
在聲明瞭ninjia可執行程式的目錄之後,緊接著在mk中就提及了kati的定義。
KATI ?= $(HOST_OUT_EXECUTABLES)/ckati
目標KATI是利用原始碼編譯生成的一個可執行的程式。原始碼在build/kati資料夾中。這同樣是一個開源的程式碼。
開源網站的地址為:
我們可以clone下來最新的code,或者在原始碼build/kati中直接按下面的步驟來編譯ckati的可執行程式。
一、新增軟體源
sudoadd-apt-repository ppa:ubuntu-toolchain-r/test
sudo apt-get update
二、安裝版本的命令:
sudo apt-get install gcc-4.8 g++-4.8
三、檢視本地版本
四、切換版本
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.660
sudo update-alternatives --install /usr/bin/gcc gcc/usr/bin/gcc-4.8 40
sudo update-alternatives --install /usr/bin/g++ g++/usr/bin/g++-4.6 60
sudo update-alternatives --install /usr/bin/g++ g++/usr/bin/g++-4.8 40
這裡的4.6是你本機之前的版本。
sudo update-alternatives --config gcc
sudo update-alternatives --config g++
選擇你需要的版4.8.
在選擇好了之後,執行make,即可開始編譯。編譯後會在根目錄生成ckati的可執行程式。
當然,android原始碼是利用makefile來做的,我們可以看到ninja.mk中對ckati的makefile進行了呼叫。
檔案地址:include build/kati/Makefile.ckati
在kati的makefile中,我們可以看到真正去編譯kati的過程。
# Rule to build ckati intoKATI_BIN_PATH
$(KATI_BIN_PATH)/ckati:$(KATI_CXX_OBJS) $(KATI_CXX_GENERATED_OBJS)
@mkdir -p $(dir [email protected])
$(KATI_LD) -std=c++11 $(KATI_CXXFLAGS) [email protected] $^ $(KATI_LIBS)
# Rule to build normal sourcefiles into object files in KATI_INTERMEDIATES_PATH
$(KATI_CXX_OBJS) :$(KATI_INTERMEDIATES_PATH)/%.o: $(KATI_SRC_PATH)/%.cc
@mkdir -p $(dir [email protected])
$(KATI_CXX) -c -std=c++11 $(KATI_CXXFLAGS)-o [email protected] $<
# Rule to build generatedsource files into object files in KATI_INTERMEDIATES_PATH
$(KATI_CXX_GENERATED_OBJS):$(KATI_INTERMEDIATES_PATH)/%.o: $(KATI_INTERMEDIATES_PATH)/%.cc
@mkdir -p $(dir [email protected])
$(KATI_CXX)-c -std=c++11 $(KATI_CXXFLAGS) -o [email protected] $<
這個呼叫簡單解釋一下,就是編譯kati是需要依賴與KATI_CXX_OBJS和 KATI_CXX_GENERATED_OBJS這兩個變數。 KATI_CXX_OBJS的生成依賴於 KATI_INTERMEDIATES_PATH下的.o,而.o檔案的生成又依賴與KATI_INTERMEDIATES_PATH下的.cc檔案。在生成了所有依賴的.o檔案之後,會link成編譯所需的ckati檔案。具體的命令為:$(KATI_LD) -std=c++11$(KATI_CXXFLAGS) -o [email protected] $^ $(KATI_LIBS)
這樣的話,就完成了ckati可執行檔案的生成。
流程圖可以簡單歸結如下:
2. 解析並使用ninja
ckati檔案生成之後,我們接著來看是如何使用的。
接著回到ninja.mk檔案中,如下是具體的呼叫。
$(KATI_BUILD_NINJA): $(KATI) $(MAKEPARALLEL)$(DUMMY_OUT_MKS) $(SOONG_ANDROID_MK) FORCE
@echo Running kati to generatebuild$(KATI_NINJA_SUFFIX).ninja... +$(hide)$(KATI_MAKEPARALLEL) $(KATI) --ninja --ninja_dir=$(OUT_DIR)--ninja_suffix=$(KATI_NINJA_SUFFIX) --regen --ignore_dirty=$(OUT_DIR)/%--no_ignore_dirty=$(SOONG_ANDROID_MK) --ignore_optional_ include=$(OUT_DIR)/%.P--detect_android_echo $(KATI_FIND_EMULATOR) -f build/core/main.mk $(KATI_GOALS)--gen_all_targets BUILDING_WITH_NINJA=true SOONG_ANDROID_MK=$(SOONG_ANDROID_MK)
可以看到使用kati,並且將很多的引數傳入到了ckati中。
在kati檔案的main函式中,可以看到接受了這些引數並且進行處理。
檔案地址:build/kati/main.cc
main函式:
int main(int argc, char*argv[]) {
if (argc >= 2 && !strcmp(argv[1],"--realpath")) {
HandleRealpath(argc - 2, argv + 2);
return 0;
}
Init();
string orig_args;
for (int i = 0; i < argc; i++) {
if (i)
orig_args += ' ';
orig_args += argv[i];
}
g_flags.Parse(argc, argv);
FindFirstMakefie();
if (g_flags.makefile == NULL)
ERROR("*** No targets specified and nomakefile found.");
// This depends on command line flags.
if (g_flags.use_find_emulator)
InitFindEmulator();
int r = Run(g_flags.targets, g_flags.cl_vars,orig_args);
Quit();
return r;
}
argv接受到了傳入的引數後,經過處理,轉化為了string,傳入orig_args變數,並且呼叫Run函式來進行後續的處理。Run函式是kati程式的核心,用於各種檔案的生成,流程的執行以及處理。我們這邊只對重點內容進行分析。
任何的編譯都脫離不了環境變數的支援,在編譯的第一步,肯定要對環境變數進行設定。
在run函式的開始,就利用Linux標準C介面來進行了環境變數的讀取和設定。
具體操作為:
extern "C" char**environ;
…
for (char** p = environ; *p; p++) {
SetVar(*p, VarOrigin::ENVIRONMENT);
}
如果我們prinf列印*p的值,可以很清楚的看到該環境變數的設定。這邊只擷取部分環境變數用於說明該問題:
printf("*p = %s \n", *p);
/*
*p =XDG_SESSION_PATH=/org/freedesktop/DisplayManager/Session0
*p =BUILD_ENV_SEQUENCE_NUMBER=10
*p =XDG_SEAT_PATH=/org/freedesktop/DisplayManager/Seat0
*p =ANDROID_BUILD_PATHS=/data/android_N/out/host/linux-x86/bin:/data/android_N/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9/bin:/data/android_N/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.9/bin:/data/android_N/development/scripts:/data/android_N/prebuilts/devtools/tools:/data/android_N/external/selinux/prebuilts/bin:/data/android_N/prebuilts/android-emulator/linux-x86_64:
*p = SSH_AUTH_SOCK=/tmp/keyring-941KY1/ssh
*p =MAKELEVEL=1
*p =DEFAULTS_PATH=/usr/share/gconf/ubuntu.default.path
*p =SESSION_MANAGER=local/chao:@/tmp/.ICE-unix/1835,unix/chao:/tmp/.ICE-unix/1835
*p =TARGET_BUILD_APPS=
* */
在設定完環境變數以後,就會開始對makefile進行部分的解析。這邊有個重要函式為
static voidReadBootstrapMakefile(const vector<Symbol>& targets,
vector<Stmt*>* stmts) {
…
}
函式初始定義了一些基本的變數,比如GCC,G++,SHELL,MAKE等。並且會去解析當前編譯機器所擁有的cpu的核數,且進行合理分配。
以下是一些具體初始化的變數:
/*
* bootstrap =
*
* CC?=cc
* CXX?=g++
* AR?=ar
* MAKE_VERSION?=3.81
* KATI?=ckati
* SHELL=/bin/sh
* .c.o:
* $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c -o [email protected] $<
* .cc.o:
* $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c -o [email protected] $<
* MAKE?=make -j2
* MAKECMDGOALS?=
· CURDIR:=/data/android_N
並且會將這些字串轉換為對應的node結構體,儲存在記憶體變數中,方便編譯的時候使用。
for (Stmt* stmt :bootstrap_asts) {
printf("stmt = %s\n\n", stmt->DebugString().c_str());
stmt->Eval(ev);
}
以下擷取部分log:
/*
stmt =AssignStmt(lhs=CC rhs=cc (cc) opstr=QUESTION_EQ dir= loc=*bootstrap*:0)
stmt = AssignStmt(lhs=CXX rhs=g++ (g++) opstr=QUESTION_EQ dir=loc=*bootstrap*:0)
stmt =AssignStmt(lhs=AR rhs=ar (ar) opstr=QUESTION_EQ dir= loc=*bootstrap*:0)
stmt =AssignStmt(lhs=MAKE_VERSION rhs=3.81 (3.81) opstr=QUESTION_EQ dir= loc=*bootstrap*:0)
stmt = AssignStmt(lhs=KATI rhs=ckati (ckati) opstr=QUESTION_EQ dir=loc=*bootstrap*:0)
stmt =AssignStmt(lhs=SHELL rhs=/bin/sh (/bin/sh) opstr=EQ dir=loc=*bootstrap*:0)
stmt =RuleStmt(expr=.c.o: term=0 after_term=(null) loc=*bootstrap*:0)
stmt =CommandStmt(Expr(SymRef(CC), ,SymRef(CFLAGS), , SymRef(CPPFLAGS), , SymRef(TARGET_ARCH), -c -o , SymRef(@), , SymRef(<)), loc=*bootstrap*:0)
stmt =RuleStmt(expr=.cc.o: term=0 after_term=(null) loc=*bootstrap*:0)
*/
在環境變數,編譯引數都設定成功後,就會開始GenerateNinja的重要操作。
GenerateNinja的函式定義在了ninja.cc中,以下是函式的具體實現。
void GenerateNinja(constvector<DepNode*>& nodes,
Evaluator* ev,
const string&orig_args,
double start_time) {
NinjaGenerator ng(ev, start_time);
ng.Generate(nodes, orig_args);
} 該函式初始化了一個 NinjaGenerator的結構體,並且繼續呼叫 Generate的方法。
void Generate(const vector<DepNode*>&nodes,
const string& orig_args){
unlink(GetNinjaStampFilename().c_str());
PopulateNinjaNodes(nodes);
GenerateNinja();
GenerateShell();
GenerateStamp(orig_args);
}
Generate 函式非常的重要, PopulateNinjaNodes會對前面include的makefile進行解析,並且將node進行整理。正如前面分析的link的程式會依賴.o一樣,這裡基本會將所依賴的.o;.a; .so進行歸類,包含了所有檔案下面的目錄。這裡舉一些簡單擷取的例子:
node =out/host/linux-x86/obj/STATIC_LIBRARIES/libcutils_intermediates/strlcpy.
node =out/host/linux-x86/obj/STATIC_LIBRARIES/libcutils_intermediates/threads.o
node =out/host/linux-x86/obj/STATIC_LIBRARIES/libcutils_intermediates/dlmalloc_stubs.o
…..
node =out/host/linux-x86/obj/SHARED_LIBRARIES/libcryptohost_intermediates/src/crypto/evp/sign.o
node =out/host/linux-x86/obj/SHARED_LIBRARIES/libcrypto-host_intermediates/src/crypto/ex_data.o
node = out/host/linux-x86/obj/SHARED_LIBRARIES/libcrypto-host_intermediates/src/crypto/hkdf/hkdf.o
node = out/host/linux-x86/obj/SHARED_LIBRARIES/libcrypto-host_intermediates/src/crypto/hmac/hmac.o
….
node = out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/core/java/android/app/IBackupAgent.java
node = out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/core/java/android/app/IInstrumentationWatcher.java
node = out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/core/java/android/app/INotificationManager.java
node = out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/core/java/android/app/IProcessObserver.java
node = out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/core/java/android/app/ISearchManager.java
….
node = out/host/linux-x86/obj32/SHARED_LIBRARIES/libchrome_intermediates/base/base64.o
node = out/host/linux-x86/obj32/SHARED_LIBRARIES/libchrome_intermediates/base/base64url.o
node = out/host/linux-x86/obj32/SHARED_LIBRARIES/libchrome_intermediates/base/base_switches.o
node = out/host/linux-x86/obj32/SHARED_LIBRARIES/libchrome_intermediates/base/bind_helpers.o
node = out/host/linux-x86/obj32/SHARED_LIBRARIES/libchrome_intermediates/base/build_time.o
…
node = out/target/product/generic/obj/SHARED_LIBRARIES/libhardware_intermediates/hardware.o
node = out/target/product/generic/obj/SHARED_LIBRARIES/libhardware_intermediates/import_includes
node = out/target/product/generic/obj/lib/libandroidfw.so.to
node = out/target/product/generic/obj/lib/libandroidfw.so
node = out/target/product/generic/symbols/system/lib/libandroidfw.so ...
在整理好了依賴之後,會將所有的步驟寫入檔案中。具體的操作為 GenerateNinja函式所實現。
GenerateNinja() {
....
fp_ = fopen(GetNinjaFilename().c_str(), "wb"); ...
fprintf(fp_, "# Generated by kati %s\n",kGitVersion);
fprintf(fp_, "\n");
…
for (const ostringstream& buf : bufs) {
fprintf(fp_, "%s", buf.str().c_str());
}
…
fclose(fp_);
}
在GenerateNinja函式中,會建立並寫入一個檔案, 這個檔案依賴於build target的制定。比如在nexus的編譯中,會在out目錄下生成
Build-aosp_arm.ninja檔案,
該檔案會非常大,但是這個也就是編譯的基礎和ninja可以明確知道自己所編譯的操作步數的由來。
具體的流程圖:
3. 總結
Ninja編譯帶來的改變是巨大的,但是通過本文的分析,可以預見到後續的變化會更大且會一直存在。Android.bp何時可以完全取代makefile,ninja編譯時的test目錄的編譯其實對普通開發者來說都有些優化的空間。對這部分的研究將會持續存在