1. 程式人生 > >Android原始碼編譯命令m/mm/mmm/make分析

Android原始碼編譯命令m/mm/mmm/make分析

        在前文中,我們分析了Android編譯環境的初始化過程。android編譯環境初始化完成後,我們就可以用m/mm/mmm/make命令編譯原始碼了。當然,這要求每一個模組都有一個Android.mk檔案。Android.mk實際上是一個Makefile指令碼,用來描述模組編譯資訊。Android編譯系統通過整合Android.mk檔案完成編譯過程。本文就對Android原始碼的編譯過程進行詳細分析。

《Android系統原始碼情景分析》一書正在進擊的程式設計師網(http://0xcc0xcd.com)中連載,點選進入!

        從前面Android編譯系統環境初始化過程分析

這篇文章可以知道,lunch命令其實是定義在build/envsetup.sh檔案中的函式lunch提供的。與lunch命令一樣,m、mm和mmm命令也分別是由定義在build/envsetup.sh檔案中的函式m、mm和mmm提供的,而這三個函式又都是通過make命令來對原始碼進行編譯的。事實上,命令m就是對make命令的簡單封裝,並且是用來對整個Android原始碼進行編譯,而命令mm和mmm都是通過make命令來對Android原始碼中的指定模組進行編譯。接下來我們就先分別介紹一下函式m、mm和mmm的實現,然後進一步分析它們是如何通過make命令來編譯程式碼的。

        函式m的實現如下所示:

  1. function m()  
  2. {  
  3.     T=$(gettop)  
  4.     if [ "$T" ]; then  
  5.         make -C $T [email protected]  
  6.     else  
  7.         echo "Couldn't locate the top of the tree.  Try setting TOP."  
  8.     fi  
  9. }  
       函式m呼叫函式gettop得到的是Android原始碼根目錄T。在執行make命令的時候,先通過-C選項指定工作目錄為T,即Android原始碼根目錄,接著又將執行命令m指定的引數
[email protected]
作為命令make的引數。從這裡就可以看出,命令m實際上就是對命令make的簡單封裝。

       函式mm的實現如下所示:

  1. function mm()  
  2. {  
  3.     # If we're sitting in the root of the build tree, just do a  
  4.     # normal make.  
  5.     if [ -f build/core/envsetup.mk -a -f Makefile ]; then  
  6.         make [email protected]  
  7.     else  
  8.         # Find the closest Android.mk file.  
  9.         T=$(gettop)  
  10.         local M=$(findmakefile)  
  11.         # Remove the path to top as the makefilepath needs to be relative  
  12.         local M=`echo $M|sed 's:'$T'/::'`  
  13.         if [ ! "$T" ]; then  
  14.             echo "Couldn't locate the top of the tree.  Try setting TOP."  
  15.         elif [ ! "$M" ]; then  
  16.             echo "Couldn't locate a makefile from the current directory."  
  17.         else  
  18.             ONE_SHOT_MAKEFILE=$M make -C $T all_modules [email protected]  
  19.         fi  
  20.     fi  
  21. }  
        函式mm首先是判斷當前目錄是否就是Android原始碼根目錄,即當前目錄下是否存在一個build/core/envsetup.mk檔案和一個Makefile檔案。如果是的話,就將命令mm當作是一個普通的make命令來執行。否則的話,就呼叫函式findmakefile從當前目錄開始一直往上尋找是否存在一個Android.mk檔案。如果在尋找的過程中,發現了一個Android.mk檔案,那麼就獲得它的絕對路徑,並且停止上述尋找過程。

        由於接下來執行make命令時,我們需要指定的是要編譯的Android.mk檔案的相對於Android原始碼根目錄路徑,因此函式mm需要將剛才找到的Android.mk絕對檔案路徑M中與Android原始碼根目錄T相同的那部分路徑去掉。這是通過sed命令來實現的,也就是將字串M前面與字串T相同的子串刪掉。

        最後,將找到的Android.mk檔案的相對路徑設定給環境變數ONE_SHOT_MAKE,表示接下來要對它進行編譯。另外,函式mm還將make命令目標設定為all_modules。這是什麼意思呢?我們知道,一個Android.mk檔案同時可以定義多個模組,因此,all_modules就表示要對前面指定的Android.mk檔案中定義的所有模組進行編譯。

         函式mmm的實現如下所示:

  1. function mmm()  
  2. {  
  3.     T=$(gettop)  
  4.     if [ "$T" ]; then  
  5.         local MAKEFILE=  
  6.         local MODULES=  
  7.         local ARGS=  
  8.         local DIR TO_CHOP  
  9.         local DASH_ARGS=$(echo "[email protected]" | awk -v RS=" " -v ORS=" " '/^-.*$/')  
  10.         local DIRS=$(echo "[email protected]" | awk -v RS=" " -v ORS=" " '/^[^-].*$/')  
  11.         for DIR in $DIRS ; do  
  12.             MODULES=`echo $DIR | sed -n -e 's/.*:.$/\1/p' | sed 's/,/ /'`  
  13.             if [ "$MODULES" = "" ]; then  
  14.                 MODULES=all_modules  
  15.             fi  
  16.             DIR=`echo $DIR | sed -e 's/:.*//' -e 's:/$::'`  
  17.             if [ -f $DIR/Android.mk ]; then  
  18.                 TO_CHOP=`(cd -P -- $T && pwd -P) | wc -c | tr -d ' '`  
  19.                 TO_CHOP=`expr $TO_CHOP + 1`  
  20.                 START=`PWD= /bin/pwd`  
  21.                 MFILE=`echo $START | cut -c${TO_CHOP}-`  
  22.                 if [ "$MFILE" = "" ] ; then  
  23.                     MFILE=$DIR/Android.mk  
  24.                 else  
  25.                     MFILE=$MFILE/$DIR/Android.mk  
  26.                 fi  
  27.                 MAKEFILE="$MAKEFILE $MFILE"  
  28.             else  
  29.                 if [ "$DIR" = snod ]; then  
  30.                     ARGS="$ARGS snod"  
  31.                 elif [ "$DIR" = showcommands ]; then  
  32.                     ARGS="$ARGS showcommands"  
  33.                 elif [ "$DIR" = dist ]; then  
  34.                     ARGS="$ARGS dist"  
  35.                 elif [ "$DIR" = incrementaljavac ]; then  
  36.                     ARGS="$ARGS incrementaljavac"  
  37.                 else  
  38.                     echo "No Android.mk in $DIR."  
  39.                     return 1  
  40.                 fi  
  41.             fi  
  42.         done  
  43.         ONE_SHOT_MAKEFILE="$MAKEFILE" make -C $T $DASH_ARGS $MODULES $ARGS  
  44.     else  
  45.         echo "Couldn't locate the top of the tree.  Try setting TOP."  
  46.     fi  
  47. }  
        函式mmm的實現就稍微複雜一點,我們詳細解釋一下。

        首先,命令mmm可以這樣執行:

  1. $ mmm <dir-1> <dir-2> ... <dir-N>[:module-1,module-2,...,module-M]    
        其中,dir-1、dir-2、dir-N都是包含有Android.mk檔案的目錄。在最後一個目錄dir-N的後面可以帶一個冒號,冒號後面可以通過逗號分隔一系列的模組名稱module-1、module-2和module-M,用來表示要編譯前面指定的Android.mk中的哪些模組。

        知道了命令mmm的使用方法之後 ,我們就可以分析函式mmm的執行邏輯了:

        1. 呼叫函式gettop獲得Android原始碼根目錄。

        2. 通過命令awk將執行命令mmm時指定的選項引數提取出來,也就是將以橫線“-”開頭的字串提取出來,並且儲存在變數DASH_ARGS中。

        3. 通過命令awk將執行命令mmm時指定的非選項引數提取出來,也就是將非以橫線“-”開頭的字串提取出來,並且儲存在變數DIRS中。這裡得到的實際上就是跟在命令mmm後面的字串“<dir-1> <dir-2> ... <dir-N>[:module-1,module-2,...,module-M]”。

        4. 變數DIRS儲存的字串可以看成是一系以空格分隔的子字串,因此,就可以通過一個for迴圈來對這些子字府串進行遍歷。每一個子字串DIR描述的都是一個包含有Android.mk檔案的目錄。對每一個目錄DIR執行以下操作:

        4.1 由於目錄DIR後面可能會通過冒號指定有模組名稱,因此就先通過兩個sed命令來獲得這些模組名稱。第一個sed命令獲得的是一系列以逗號分隔的模組名稱列表,第二個sed命令用來將前面獲得的以逗號分隔的模組名稱列表轉化為以空格分隔的模組名稱列表。最後,獲得的以空格分隔的模組名稱列表儲存在變數MODULES中。由於目錄DIR後面也可能不指定有模組名稱,因此前面得到的變數MODULES的值就會為空。在這種情況下,需要將變數MODULES的值設定為“all_modules”,表示要編譯的是所有模組。

        4.2 通過兩個sed命令獲得真正的目錄DIR。第一個sed命令將原來DIR字串後面的冒號以及冒號後面的模組列表字串刪掉。第二個sed命令將執行前面一個sed命令獲得的目錄後面的"/"斜線去掉,最後就得到一個末尾不帶有斜線“/”的路徑,並且儲存在變數DIR中。

        4.3 如果變數DIR描述的是一個真正的路徑,也就是在該路徑下存在一個Android.mk檔案,那麼就進行以下處理:

        4.3.1 統計Android原始碼根目錄T包含的字元數,並且將這個字元數加1,得到的值儲存在變數TO_CHOP中。

        4.3.2 通過執行/bin/pwd命令獲得當前執行命令mmm的目錄START。

        4.3.3 通過cut命令獲得當前目錄START相對於Android原始碼根目錄T的路徑,並且儲存在變數MFILE中。

        4.3.4 如果變數MFILE的值等於空,就表明是在Android原始碼根目錄T中執行mmm命令,這時候就表明變數DIR描述的就是相對Android原始碼根目錄T的一個目錄,這時候指定的Android.mk檔案相對於Android原始碼根目錄T的路徑就為$DIR/Android.mk。

        4.3.5 如果變數MFILE的值不等於空,就表明是在Android原始碼根目錄T的某一個子目錄中執行mmm命令,這時候$MFILE/$DIR/Android.mk表示的Android.mk檔案路徑才是相對於Android原始碼根目錄T的。

        4.3.6 將獲得的Android.mk路徑MFILE附加在變數MAKEFILE描述的字串的後面,並且以空格分隔。

        4.4 如果變數DIR描述的不是一個真正的路徑,並且它的值等於"snod"、"showcomands"、“dist”或者“incrementaljavac”,那麼它描述的其實是make修飾命令。這四個修飾命令的含義分別如下所示:

        4.4.1 snod是“systemimage with no dependencies”的意思,表示忽略依賴性地重新打包system.img。

        4.4.2 showcommands表示顯示編譯過程中執行的命令。

        4.4.3 dist表示將編譯後產生的釋出檔案拷貝到out/dist目錄中。

        4.4.4 incrementaljavac表示對Java原始檔採用增量式編譯,也就是如果一個Java檔案如果沒有修改過,那麼就不要重新生成對應的class檔案。

        5. 上面的for迴圈執行完畢,變數MAKEFILE儲存的是要編譯的Android.mk檔案列表,它們都是相對於Android原始碼根目錄的路徑,變數DASH_ARGS儲存的是原來執行mmm命令時帶的選項引數,變數MODULES儲存的是指定要編譯的模組名稱,變數ARGS儲存的是修飾命令。其中,變數MAKEFILE的內容通過環境變數ONE_SHOT_MAKEFILE傳遞給make命令,而其餘變數都是通過引數的形式傳遞給make命令,並且變數MODULES作為make命令的目標。

        明白了函式m、mm和mmm的實現之後,我們就可以知道:

        1. mm和mmm命令是類似的,它們都是用來編譯某些模組。

        2. m命令用來編譯所有模組。

        如果我們理解了mm或者mmm命令的編譯過程,那麼自然也會明白m命令的編譯過程,因為所有模組的編譯過程就等於把每一個模組的編譯都編譯出來,因此,接下來我們就選擇具有代表性的、常用的編譯命令mmm來分析Android原始碼的編譯過程,如圖1所示:


圖1 mmm命令的編譯過程

       函式mmm在Android原始碼根目錄執行make命令的時候,沒有通過-f指定Makefile檔案,因此預設就使用Android原始碼根目錄下的Makefile檔案,它的內容如下所示:

  1. ### DO NOT EDIT THIS FILE ###  
  2. include build/core/main.mk  
  3. ### DO NOT EDIT THIS FILE ###  

       它僅僅是將build/core/main.mk檔案載入進來。build/core/main.mk是Android編譯系統的入口檔案,它通過載入其它的mk檔案來對Android原始碼中的各個模組進行編譯,以及將編譯出來的檔案打包成各種映象檔案。以下就是build/core/main.mk檔案的主要內容:

  1. ......  
  2. # This is the default target.  It must be the first declared target.  
  3. .PHONY: droid  
  4. DEFAULT_GOAL := droid  
  5. $(DEFAULT_GOAL):  
  6. ......  
  7. # Set up various standard variables based on configuration  
  8. # and host information.  
  9. include $(BUILD_SYSTEM)/config.mk  
  10. ......  
  11. # Bring in standard build system definitions.  
  12. include $(BUILD_SYSTEM)/definitions.mk  
  13. ......  
  14. # These targets are going to delete stuff, don't bother including  
  15. # the whole directory tree if that's all we're going to do  
  16. ifeq ($(MAKECMDGOALS),clean)  
  17. dont_bother := true  
  18. endif  
  19. ifeq ($(MAKECMDGOALS),clobber)  
  20. dont_bother := true  
  21. endif  
  22. ifeq ($(MAKECMDGOALS),dataclean)  
  23. dont_bother := true  
  24. endif  
  25. ifeq ($(MAKECMDGOALS),installclean)  
  26. dont_bother := true  
  27. endif  
  28. # Bring in all modules that need to be built.