1. 程式人生 > >Android7.0 Ninja編譯原理

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目錄的編譯其實對普通開發者來說都有些優化的空間。對這部分的研究將會持續存在