1. 程式人生 > >Android Build System[一]

Android Build System[一]

更多幹貨,請關注微信公眾號: tmac_lover

1. 寫在最開始

Android Build System是AOSP(Android Open Source Project)中既重要又複雜的一部分,且涉及的知識點非常多,比如shell script, Makefile, python, aapt等等。但若從事Android ROM開發工作,需要通過修改編譯系統來實現系統的定製,所以適當瞭解AOSP編譯系統,對我們的工作會有非常大的幫助。

Android Build System非常龐大和複雜,一篇文章不能介紹清楚,所以後續我通過一系列的文章來講解,並用示例和原理相結合的方式來幫助大家深入理解編譯系統。Android的編譯是基於linux的,並且使用make命令編譯,需要有一些基本的Makefile知識,文章中會涉及到一些基本的Makefile知識,但若先對Makefile有一定的瞭解會有助於大家理解後續文章。

2. 建立環境

這裡的建立環境並不是講如何配置你的電腦,而是我們在編譯原始碼之前常執行的幾步操作。

$ source build/envsetup.sh
$ lunch

執行完這兩步之後,我們就可以使用lunch命令或者mm, mmm之類的命令編譯某個模組。

我知道有些朋友肯定發現了,如果不執行source build/envsetup.sh這條命令,或者已經在一個console裡執行過這條命令,但是在一個新開的console裡,仍然是無法編譯或者執行lunch和mm等命令。
本文以剖析這兩條命令來作為Android Build System系列的開端。

3. source build/envsetup.sh

source是一條標準的linux命令,相當於.命令,這條命令和

$ ./build/envsetup.sh

功能差不多。只不過source後加的指令碼不需要有可執行許可權。但是其結果也就是執行一遍build/envsetup.sh這個shell指令碼,而envsetup.sh腳本里定義了很多函式,執行完這個指令碼之後,就可以在當前console裡直接像使用linux命令一樣執行這些函式,比如lunch, mm, mmm等(使用hmm可以看到所有可用命令)。所以lunch, mmm它們實質上是一個shell函式,也就相當於shell指令碼。我們可以在build/envsetup.sh裡找到這些命令的實現原理。

當然envsetup.sh不僅僅只是現實了lunch, mmm等這些命令,它還有一個重要的功能就是找到所有當前原始碼支援的平板編譯選項,並放到一個數組中去,也就是後面使用lunch命令時列出來的那些選項。先來看看是如何實現的:

[build/envsetup.sh]

... ...

unset LUNCH_MENU_CHOICES
function add_lunch_combo()
{
    local new_combo=$1
    local c
    for c in ${LUNCH_MENU_CHOICES[@]} ; do
        if [ "$new_combo" = "$c" ] ; then
            return
        fi
    done
    LUNCH_MENU_CHOICES=(${LUNCH_MENU_CHOICES[@]} $new_combo)
}

add_lunch_combo aosp_arm-eng
add_lunch_combo aosp_arm64-eng
add_lunch_combo aosp_mips-eng
add_lunch_combo aosp_mips64-eng
add_lunch_combo aosp_x86-eng
add_lunch_combo aosp_x86_64-eng

... ...

for f in `test -d device && find -L device -maxdepth 4 -name 'vendorsetup.sh' 2> /dev/null | sort` \
         `test -d vendor && find -L vendor -maxdepth 4 -name 'vendorsetup.sh' 2> /dev/null | sort`
do
    echo "including $f"
    . $f
done

這裡做了下面幾件事:
* 使用add_lunch_combo函式添加了幾個預設的平臺編譯選項,比如: aosp_arm-eng …
* 在原始碼的device和vendor目錄下找到名為vendorsetup.sh的指令碼,並依次執行它們,需要注意的是,vendorsetup.sh檔案所在目錄層級不能超過4級
* device或者vendor目錄下的vendorsetup.sh裡其實也是通過add_lunch_combo xxx新增平臺編譯選項

上面程式碼中有add_lunch_combo的實現,它將所 有add_lunch_combo後的引數全部儲存到LUNCH_MENU_CHOICES這個陣列中去(${LUNCH_MENU_CHOICES[@]}是取陣列的所有元素的意思),後面lunch命令直接使用。

4. lunch

上面說過,lunch命令實質上也是envsetup.sh裡的一個函式,以我的程式碼選擇”aosp_mangosteen-userdebug”為例,看看它的實現:

[build/envsetup.sh]

function lunch()
{
    local answer

    // 如果lunch命令後接有引數,賦值給answer
    if [ "$1" ] ; then
        answer=$1
    else // 否則將所有平臺編譯選項打印出來,並等待輸入
        print_lunch_menu
        read answer
    fi

    local selection=

    if [ -z "$answer" ]    // 如果answer的值是0,設定aosp_arm-eng為預設
    then
        selection=aosp_arm-eng
    elif (echo -n $answer | grep -q -e "^[0-9][0-9]*$")
        // answer是數字的情況,從LUNCH_MENU_CHOICES陣列中選擇
    then
        if [ $answer -le ${#LUNCH_MENU_CHOICES[@]} ]
        then
            selection=${LUNCH_MENU_CHOICES[$(($answer-1))]}
        fi
    elif (echo -n $answer | grep -q -e "^[^\-][^\-]*-[^\-][^\-]*$")
        // answer也可以直接是名字,但要符合規則
    then
        selection=$answer
    fi

    export TARGET_BUILD_APPS=

    // 結果的前半部分aosp_mangosteen作為產品名,通過check_product函式檢測它是否可用
    local product=$(echo -n $selection | sed -e "s/-.*$//")
    check_product $product

    // 結果的後半部分userdebug作為編譯變數,通過check_variant函式檢測它是否可用
    // variant必須嚴格的是user userdebug eng這幾個中的一個,否則lunch結束
    local variant=$(echo -n $selection | sed -e "s/^[^\-]*-//")
    check_variant $variant


    if [ -z "$product" -o -z "$variant" ]
    then
        echo
        return 1
    fi

    // export幾個重要的環境變數,後面make使用
    export TARGET_PRODUCT=$product
    export TARGET_BUILD_VARIANT=$variant
    export TARGET_BUILD_TYPE=release

    echo

    // 設定一些重要的環境變數
    set_stuff_for_environment
    // lunch完成結束後,列印所有和平臺相關重要的環境變數
    printconfig
}

從上面lunch函式的程式碼註釋可以看到,lunch實際上就是確定編譯平臺和編譯選項,並且設定了一些關鍵環境變數。下面來詳細講下lunch中幾個關鍵的地方。

4.1 check_product

小小的check_product函式,後面引入了一大堆makefile,並且執行這個函式過程中,會設定相當多你眼熟的環境變數,並且會檢查lunch選擇的平臺是否可用,來看一看它是如何實現的吧。

check_product設定一些變數後,呼叫get_build_var函式,所以check_product $product相當於:

TARGET_PRODUCT=$1 \
TARGET_BUILD_VARIANT= \
TARGET_BUILD_TYPE= \
TARGET_BUILD_APPS= \
CALLED_FROM_SETUP=true BUILD_SYSTEM=build/core \
make -f build/core/config.mk dumpvar-TARGET_DEVICE > /dev/null

直接點,就是以build/core/config.mk作為makefile執行make命令,目標是dumpvar-TARGET_DEVICE。這裡TARGET_PRODUCT就是lunch時選擇的名字裡’-‘前的部分,即aosp_mangosteen。

先看看build/core/config.mk裡對其它makefile檔案的依賴關係:

  • 這裡只列出了部分makefile包含關係,這些makefile裡同時還定義了非常多的變數,比如BUILD_STATIC_LIBRARY, TARGET_OUT_JAVA_LIBRARIES等等,感興趣可以自己去看一下
  • 圖中board_config_mk的定義如下:
board_config_mk := \
    $(strip $(wildcard \
        $(SRC_TARGET_DIR)/board/$(TARGET_DEVICE)/BoardConfig.mk \
        $(shell test -d device && find device -maxdepth 4 -path '*/$(TARGET_DEVICE)/BoardConfig.mk') \
        $(shell test -d vendor && find vendor -maxdepth 4 -path '*/$(TARGET_DEVICE)/BoardConfig.mk') \
    ))

也就是$(TARGET_DEVICE)目錄下的BoardConfig.mk檔案。從圖中可以看到,TARGET_DEVICE變數的值是在build/core/product_config.mk裡確定的,在這個檔案中有一段很重要的程式碼來確定TARGET_DEVICE:

... ...
// 找到所有的AndroidProducts.mk檔案
all_product_configs := $(get-all-product-makefiles)

current_product_makefile :=
all_product_makefiles :=
$(foreach f, $(all_product_configs),\
    $(eval _cpm_words := $(subst :,$(space),$(f)))\
    $(eval _cpm_word1 := $(word 1,$(_cpm_words)))\
    $(eval _cpm_word2 := $(word 2,$(_cpm_words)))\
    $(if $(_cpm_word2),\
        $(eval all_product_makefiles += $(_cpm_word2))\
        $(if $(filter $(TARGET_PRODUCT),$(_cpm_word1)),\
            $(eval current_product_makefile += $(_cpm_word2)),),\
        $(eval all_product_makefiles += $(f))\
        $(if $(filter $(TARGET_PRODUCT),$(basename $(notdir $(f)))),\
            $(eval current_product_makefile += $(f)),)))
_cpm_words :=
_cpm_word1 :=
_cpm_word2 :=
current_product_makefile := $(strip $(current_product_makefile))
all_product_makefiles := $(strip $(all_product_makefiles))

... ...

ifneq (,$(filter product-graph dump-products, $(MAKECMDGOALS)))
...
else
    ifndef current_product_makefile
        error
    endif
    ifneq (1,$(words $(current_product_makefile)))
        error
    endif
// MAKECMDGOALS = dumpvar-TARGET_DEVICE, 所以走這個分支
$(call import-products, $(current_product_makefile))
endif

INTERNAL_PRODUCT := $(call resolve-short-product-name, $(TARGET_PRODUCT))
ifneq ($(current_product_makefile),$(INTERNAL_PRODUCT))
    error
endif

TARGET_DEVICE := $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_DEVICE)

這段程式碼如果根據傳入的TARGET_PRODUCT = aosp_mangosteen來查詢它需要的makefile檔案,如果能夠找到,然後就設定TARGET_DEVICE;否則,直接報錯,結束lunch。

  • 函式get-all-product-makefiles定義在build/core/product.mk裡,它的作用是返回device, vendor, build/target目錄下所有AndroidProducts.mk檔案中PRODUCT_MAKEFILES變數的值的列表
  • all_product_makefiles的值是all_product_configs裡所有.mk檔案路徑的列表
  • current_product_makefile是根據TARGET_PRODUCT = aosp_mangosteen來找到的目標.mk路徑,檔名它必須與TARGET_PRODUCT的值相同,在我們這裡它的值是device/mstar/mangosteen/aosp_mangosteen.mk
  • MAKECMDGOALS是make定義的一個全域性變數,它的值是make命令指定的目標,在這裡MAKECMDGOALS = dumpvar-TARGET_DEVICE
  • 函式import-products定義在build/core/product.mk裡, 它的作用是把current_product_makefile的值賦給PRODUCTS,並且將current_product_makefile代表的.mk檔案載入進來, 並將它裡的變數值儲存到變數PRODUCTS.$(current_product_makefile).xxx中,
    xxx就是.mk檔案中每個變數名字,比如 PRODUCTS.\$(INTERNAL_PRODUCT).PRODUCT_DEVICE = PRODUCT_DEVICE = mangosteen
  • 函式resolve-short-product-name定義在build/core/product.mk裡,它將TARGET_PRODUCT的值和PRODUCTS列表裡的.mk檔案中PRODUCT_NAME的值一一對比,返回和TARGET_PRODUCT值相同PRODUCT_NAME變數所在的那個.mk檔案的路徑
  • 根據上面的分析TARGET_DEVICE的值,其實就是device/mstar/mangosteen/aosp_mangosteen.mk檔案中PRODUCT_DEVICE的值,它的值mangosteen
  • 如果上面的正常步驟中有任何一項沒找到或者匹配不成功,則lunch就以失敗結束

再回頭看看那條make語句: make -f build/core/config.mk dumpvar-TARGET_DEVICE > /dev/null, 結合build/core/dumpvar.mk中下面這段看:

dumpvar_goals := \
    $(strip $(patsubst dumpvar-%,%,$(filter dumpvar-%,$(MAKECMDGOALS))))
ifdef dumpvar_goals

  absolute_dumpvar := $(strip $(filter abs-%,$(dumpvar_goals)))
  ifdef absolute_dumpvar
      ...
  else
    DUMPVAR_VALUE := $($(dumpvar_goals))
    dumpvar_target := dumpvar-$(dumpvar_goals)
  endif

.PHONY: $(dumpvar_target)
$(dumpvar_target):
    @echo $(DUMPVAR_VALUE)
  • 上一段有說過,MAKECMDGOALS在這裡就是dumpvar-TARGET_DEVICE
  • 這段程式碼中,dumpvar_goals = TARGET_DEVICE;DUMPVAR_VALUE = $(TARGET_DEVICE) = mangosteen; dumpvar_target = dumpvar-mangosteen
  • 所以make -f build/core/config.mk dumpvar-TARGET_DEVICE > /dev/null最終就是一句”echo $(DUMPVAR_VALUE)”, 因為”> /dev/null”將make輸出的正確資訊重定向到/dev/null, 所以正常情況下,在console裡看不到任何輸出

到這裡check_product函式就執行完了。

4.2 set_stuff_for_environment

函式set_stuff_for_environment裡有一個比較重要的函式setpaths(),它的作用是往PATH環境變數中新增一些路徑,讓我們可以在執行過lunch的console裡使用Android原始碼裡的一些工具。

function setpaths() {
    ... ...
    export ANDROID_BUILD_PATHS=$(get_build_var ANDROID_BUILD_PATHS):$ANDROID_TOOLCHAIN:$ANDROID_TOOLCHAIN_2ND_ARCH:$ANDROID_DEV_SCRIPTS:

    export PATH=$ANDROID_BUILD_PATHS$PATH
    ... ...
}

可以使用echo $ANDROID_BUILD_PATHS 命令檢視新增了哪些目錄。

4.3 printconfig

printconfig函式的核心是呼叫”get_build_var report_config”, 和check_product函式流程類似,在build/core/dumpvar.mk裡可以看到:

ifneq ($(PRINT_BUILD_CONFIG),)
HOST_OS_EXTRA:=$(shell python -c "import platform; print(platform.platform())")
$(info ============================================)
$(info   PLATFORM_VERSION_CODENAME=$(PLATFORM_VERSION_CODENAME))
$(info   PLATFORM_VERSION=$(PLATFORM_VERSION))
$(info   TARGET_PRODUCT=$(TARGET_PRODUCT))
$(info   TARGET_BUILD_VARIANT=$(TARGET_BUILD_VARIANT))
$(info   TARGET_BUILD_TYPE=$(TARGET_BUILD_TYPE))
$(info   TARGET_BUILD_APPS=$(TARGET_BUILD_APPS))
$(info   TARGET_ARCH=$(TARGET_ARCH))
$(info   TARGET_ARCH_VARIANT=$(TARGET_ARCH_VARIANT))
$(info   TARGET_CPU_VARIANT=$(TARGET_CPU_VARIANT))
$(info   TARGET_2ND_ARCH=$(TARGET_2ND_ARCH))
$(info   TARGET_2ND_ARCH_VARIANT=$(TARGET_2ND_ARCH_VARIANT))
$(info   TARGET_2ND_CPU_VARIANT=$(TARGET_2ND_CPU_VARIANT))
$(info   HOST_ARCH=$(HOST_ARCH))
$(info   HOST_OS=$(HOST_OS))
$(info   HOST_OS_EXTRA=$(HOST_OS_EXTRA))
$(info   HOST_BUILD_TYPE=$(HOST_BUILD_TYPE))
$(info   BUILD_ID=$(BUILD_ID))
$(info   OUT_DIR=$(OUT_DIR))
$(info ============================================)
endif

有沒有發現,這就是lunch命令執行成功之後,console裡打印出來的一串資訊。

到這裡lunch命令就執行完了,總得來說,lunch就是根據我們的輸入,找到對應平臺的所有相關makefile, 並根據makefile裡定義的變數值,設定編譯需要的變數的值;同時也新增一些原始碼中的目錄到PATH中,方便使用原始碼目錄中的一些命令。

5. mmm命令

這裡順帶再簡單介紹下mmm命令,如果你看懂了lunch命令的原理後,mmm也就很容易了。我們都知道使用mmm可以編譯單個模組,比如:mmm packages/apps/Launcher3

function mmm() {
    ... ...

    for DIR in $DIRS ; do

        MODULES=`echo $DIR | sed -n -e 's/.*:\(.*$\)/\1/p' | sed 's/,/ /'`
        if [ "$MODULES" = "" ]; then
            MODULES=all_modules
        fi
        ... ...
    done

    ONE_SHOT_MAKEFILE="$MAKEFILE" $DRV make -C $T -f build/core/main.mk $DASH_ARGS $MODULES $ARGS

    ... ...
}
  • mmm先從引數中分離出需要編譯模組的目錄,mmm可以指定多個目錄下的模組進行編譯,比如:mmm packages/apps/Launcher3 packages/apps/Launcher2
  • 如果Android.mk中有多個module, 可以通過命令指定編譯某一個module, 使用:和目錄分開, 例如: mmm packages/apps/Launcher3:Launcher3; 若不指定,則預設編譯Android.mk中所有的module
  • 可以看到mmm最終是定義了一個變數ONE_SHOT_MAKEFILE,然後使用make -C Tfbuild/core/main.mkDASH_ARGS MODULESARGS,還是一個make命令。注意make後加了-C T,packages/apps/使mmm.makefmakefileCT是必要的

6. 總結

本文以介紹lunch和mmm命令作為編譯系統的開篇,特別是lunch命令,雖然它沒有編譯任何檔案,但是它仍然使用了make命令,並且載入了一大堆的makefile,後面編譯都以lunch為基礎,並使用了lunch設定的很多變數。