Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android編譯系統環境初始化過程分析

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

編輯:關於Android編程

Android源代碼在編譯之前,要先對編譯環境進行初始化,其中最主要就是指定編譯的類型和目標設備的型號。Android的編譯類型主要有eng、userdebug和user三種,而支持的目標設備型號則是不確定的,它們由當前的源碼配置情況所決定。為了確定源碼支持的所有目標設備型號,Android編譯系統在初始化的過程中,需要在特定的目錄中加載特定的配置文件。接下來本文就對上述的初始化過程進行詳細分析。

 

對Android編譯環境進行初始化很簡單,分為兩步。第一步是打開一個終端,並且將build/envsetup.sh加載到該終端中:

 

$ . ./build/envsetup.sh 
including device/asus/grouper/vendorsetup.sh
including device/asus/tilapia/vendorsetup.sh
including device/generic/armv7-a-neon/vendorsetup.sh
including device/generic/armv7-a/vendorsetup.sh
including device/generic/mips/vendorsetup.sh
including device/generic/x86/vendorsetup.sh
including device/lge/mako/vendorsetup.sh
including device/samsung/maguro/vendorsetup.sh
including device/samsung/manta/vendorsetup.sh
including device/samsung/toroplus/vendorsetup.sh
including device/samsung/toro/vendorsetup.sh
including device/ti/panda/vendorsetup.sh
including sdk/bash_completion/adb.bash
從命令的輸出可以知道,文件build/envsetup.sh在加載的過程中,又會在device目錄中尋找那些名稱為vendorsetup.sh的文件,並且也將它們加載到當前終端來。另外,在sdk/bash_completion目錄下的adb.bash文件也會加載到當前終端來,它是用來實現adb命令的bash completion功能的。也就是說,加載了該文件之後,我們在運行adb相關的命令的時候,通過按tab鍵就可以幫助我們自動完成命令的輸入。關於bash completion的知識,可以參考官方文檔: http://www.gnu.org/s/bash/manual/bash.html#Programmable-Completion。
第二步是執行命令lunch,如下所示:

 

 

$ lunch

You're building on Linux

Lunch menu... pick a combo:
     1. full-eng
     2. full_x86-eng
     3. vbox_x86-eng
     4. full_mips-eng
     5. full_grouper-userdebug
     6. full_tilapia-userdebug
     7. mini_armv7a_neon-userdebug
     8. mini_armv7a-userdebug
     9. mini_mips-userdebug
     10. mini_x86-userdebug
     11. full_mako-userdebug
     12. full_maguro-userdebug
     13. full_manta-userdebug
     14. full_toroplus-userdebug
     15. full_toro-userdebug
     16. full_panda-userdebug

Which would you like? [full-eng] 
我們看到lunch命令輸出了一個Lunch菜單,該菜單列出了當前Android源碼支持的所有設備型號及其編譯類型。例如,第一項“full-eng”表示的設備“full”即為模擬器,並且編譯類型為“eng”即為工程機。

 

當我們選定了一個Lunch菜單項序號(1-16)之後,按回車鍵,就可以完成Android編譯環境的初始化過程。例如,我們選擇1,可以看到以下輸出:

 

Which would you like? [full-eng] 1

============================================
PLATFORM_VERSION_CODENAME=REL
PLATFORM_VERSION=4.2
TARGET_PRODUCT=full
TARGET_BUILD_VARIANT=eng
TARGET_BUILD_TYPE=release
TARGET_BUILD_APPS=
TARGET_ARCH=arm
TARGET_ARCH_VARIANT=armv7-a
HOST_ARCH=x86
HOST_OS=linux
HOST_OS_EXTRA=Linux-3.8.0-31-generic-x86_64-with-Ubuntu-13.04-raring
HOST_BUILD_TYPE=release
BUILD_ID=JOP40C
OUT_DIR=out
============================================
我們可以看到,lunch命令幫我們設置好了很多環境變量。通過設置這些環境變量,就配置好了Android編譯環境。

 

通過圖1我們就可以直觀地看到Android編譯環境初始化完成後,我們所獲得的東西:

width=654

圖1 Android編譯環境初始化完成之後

總體來說,Android編譯環境初始化完成之後,獲得了以下三樣東西:

1. 將vendor和device目錄下的vendorsetup.sh文件加載到了當前終端;

2. 新增了lunch、m、mm和mmm等命令;

3. 通過執行lunch命令設置好了TARGET_PRODUCT、TARGET_BUILD_VARIANT、TARGET_BUILD_TYPE和TARGET_BUILD_APPS等環境變量。

接下來我們就主要分析build/envsetup.sh文件的加載過程以及lunch命令的執行過程。

一. 文件build/envsetup.sh的加載過程

文件build/envsetup.sh是一個bash shell腳本,從它裡面定義的函數hmm可以知道,它提供了lunch、m、mm和mmm等命令供我們初始化編譯環境或者編譯Android源碼。

函數hmm的實現如下所示:

 

function hmm() {
cat <-
- tapas:   tapas [  ...] [arm|x86|mips] [eng|userdebug|user]
- croot:   Changes directory to the top of the tree.
- m:       Makes from the top of the tree.
- mm:      Builds all of the modules in the current directory.
- mmm:     Builds all of the modules in the supplied directories.
- cgrep:   Greps on all local C/C++ files.
- jgrep:   Greps on all local Java files.
- resgrep: Greps on all local res/*.xml files.
- godir:   Go to the directory containing a file.

Look at the source to view more functions. The complete list is:
EOF
    T=$(gettop)
    local A
    A=
    for i in `cat $T/build/envsetup.sh | sed -n /^function /s/function ([a-z_]*).*//p | sort`; do
      A=$A $i
    done
    echo $A
}
我們在當前終端中執行hmm命令即可以看到函數hmm的完整輸出。

 

函數hmm主要完成三個工作:

1. 調用另外一個函數gettop獲得Android源碼的根目錄T。

2. 通過cat命令顯示一個Here Document,說明$T/build/envsetup.sh文件加載到當前終端後所提供的主要命令。

3. 通過sed命令解析$T/build/envsetup.sh文件,並且獲得在裡面定義的所有函數的名稱,這些函數名稱就是$T/build/envsetup.sh文件加載到當前終端後提供的所有命令。

注意,sed命令是一個強大的文本分析工具,它以行為單位為執行文本替換、刪除、新增和選取等操作。函數hmm通過執行以下的sed命令來獲得在$T/build/envsetup.sh文件定義的函數的名稱:

 

sed -n /^function /s/function ([a-z_]*).*//p
它表示對所有以“function ”開頭的行,如果緊接在“function ”後面的字符串僅由字母a-z和下橫線(_)組成,那麼就將這個字符串提取出來。這正好就對應於shell腳本裡面函數的定義。

 

文件build/envsetup.sh除了定義一堆函數之外,還有一個重要的代碼段,如下所示:

 

# Execute the contents of any vendorsetup.sh files we can find.
for f in `/bin/ls vendor/*/vendorsetup.sh vendor/*/*/vendorsetup.sh device/*/*/vendorsetup.sh 2> /dev/null`
do
    echo including $f
    . $f
done
unset f

這個for循環遍歷vendor目錄下的一級子目錄和二級子目錄以及device目錄下的二級子目錄中的vendorsetup.sh文件,並且通過source命令(.)將它們加載當前終端來。vendor和device相應子目錄下的vendorsetup.sh文件的實現很簡單,它們主要就是添加相應的設備型號及其編譯類型支持到Lunch菜單中去。

例如,device/samsung/maguro目錄下的vendorsetup.sh文件的實現如下所示:

 

add_lunch_combo full_maguro-userdebug
它調用函數add_lunch_combo添加一個名稱為“full_maguro-userdebug”的菜單項到Lunch菜單去。

 

函數add_lunch_combo定義在build/envsetup.sh文件中,它的實現如下所示:

 

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的參數保存在位置參數$1中,接著又保存在一個本地變量new_combo中,用來表示一個要即將要添加的Lunch菜單項。函數首先是在數組LUNCH_MENU_CHOICES中檢查要添加的菜單項是否已經存在。只有在不存在的情況下,才會將它添加到數組LUNCH_MENU_CHOICES中去。注意,${LUNCH_MENU_CHOICES[@]}表示數組LUNCH_MENU_CHOICES的所有元素。

 

數組LUNCH_MENU_CHOICES是定義在文件build/envsetup.sh的一個全局變量,當文件build/envsetup.sh被加載的時候,這個數組會被初始化為化full-eng、full_x86-eng、vbox_x86-eng和full_mips-eng,如下所示:

 

# add the default one here
add_lunch_combo full-eng
add_lunch_combo full_x86-eng
add_lunch_combo vbox_x86-eng
add_lunch_combo full_mips-eng
這樣當文件build/envsetup.sh加載完成之後,數組LUNCH_MENU_CHOICES就包含了當前源碼支持的所有設備型號及其編譯類型,於是當接下來我們執行lunch命令的時候,就可以通過數組LUNCH_MENU_CHOICES看到一個完整的Lunch籐蔓。

 

二. lunch命令的執行過程

lunch命令實際上是定義在文件build/envsetup.sh的一個函數,它的實現如下所示:

 

function lunch()
{
    local answer

    if [ $1 ] ; then
        answer=$1
    else
        print_lunch_menu
        echo -n Which would you like? [full-eng] 
        read answer
    fi

    local selection=

    if [ -z $answer ]
    then
        selection=full-eng
    elif (echo -n $answer | grep -q -e ^[0-9][0-9]*$)
    then
        if [ $answer -le ${#LUNCH_MENU_CHOICES[@]} ]
        then
            selection=${LUNCH_MENU_CHOICES[$(($answer-1))]}
        fi
    elif (echo -n $answer | grep -q -e ^[^-][^-]*-[^-][^-]*$)
    then
        selection=$answer
    fi

    if [ -z $selection ]
    then
        echo
        echo Invalid lunch combo: $answer
        return 1
    fi

    export TARGET_BUILD_APPS=

    local product=$(echo -n $selection | sed -e s/-.*$//)
    check_product $product
    if [ $? -ne 0 ]
    then
        echo
        echo ** Don't have a product spec for: '$product'
        echo ** Do you have the right repo manifest?
        product=
    fi

    local variant=$(echo -n $selection | sed -e s/^[^-]*-//)
    check_variant $variant
    if [ $? -ne 0 ]
    then
        echo
        echo ** Invalid variant: '$variant'
        echo ** Must be one of ${VARIANT_CHOICES[@]}
        variant=
    fi

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

    export TARGET_PRODUCT=$product
    export TARGET_BUILD_VARIANT=$variant
    export TARGET_BUILD_TYPE=release

    echo

    set_stuff_for_environment
    printconfig
}

函數lunch的執行邏輯如下所示:

1. 檢查是否帶有參數,即位置參數$1是否等於空。如果不等於空的話,就表明帶有參數,並且該參數是用來指定要編譯的設備型號及其編譯類型的。如果等於空的話,那麼就調用另外一個函數print_lunch_menu來顯示Lunch菜單項,並且通過調用read函數來等待用戶輸入。無論通過何種方式,最終變量answer的值就保存了用戶所指定的備型號及其編譯類型。

2. 對變量answer的值的合法性進行檢查。如果等於空的話,就將它設置為默認值“full-eng”。如果不等於空的話,就分為三種情況考慮。第一種情況是值為數字,那麼就需要確保該數字的大小不能超過Lunch菜單項的個數。在這種情況下,會將輸入的數字索引到數組LUNCH_MENU_CHOICES中去,以便獲得一個用來表示設備型號及其編譯類型的文本。第二種情況是非數字文本,那麼就需要確保該文本符合-的形式,其中表示設備型號,而表示編譯類型 。第三種情況是除了前面兩種情況之外的所有情況,這是非法的。經過合法性檢查後,變量selection代表了用戶所指定的備型號及其編譯類型,如果它的值是非法的,即它的值等於空,那麼函數lunch就不往下執行了。

3. 接下來是解析變量selection的值,也就是通過sed命令將它的值提取出來,並且分別保存在變量product和variant中。提取出來的product和variant值有可能是不合法的,因此需要進一步通過調用函數check_product和check_variant來檢查。一旦檢查失敗,也就是函數check_product和check_variant的返回值$?等於非0,那麼函數lunch就不往下執行了。

4. 通過以上合法性檢查之後,就將變量product和variant的值保存在環境變量TARGET_PRODUCT和TARGET_BUILD_VARIANT中。此外,另外一個環境變量TARGET_BUILD_TYPE的值會被設置為release,表示此次編譯是一個release版本的編譯。另外,前面還有一個環境變量TARGET_BUILD_APPS,它的值被函數lunch設置為空,用來表示此次編譯是對整個系統進行編譯。如果環境變量TARGET_BUILD_APPS的值不等於空,那麼就表示此次編譯是只對某些APP模塊進行編譯,而這些APP模塊就是由環境變量TARGET_BUILD_APPS來指定的。

5. 調用函數set_stuff_for_environment來配置環境,例如設置Java SDK路徑和交叉編譯工具路徑等。

6. 調用函數printfconfig來顯示已經配置好的編譯環境參數。

在上述執行過程中,函數check_product、check_variant和printconfig是比較關鍵的,因此接下來我們就繼續分析它們的實現。

函數check_product定義在文件build/envsetup.sh中,它的實現如下所示:

 

# check to see if the supplied product is one we can build
function check_product()
{
    T=$(gettop)
    if [ ! $T ]; then
        echo Couldn't locate the top of the tree.  Try setting TOP. >&2
        return
    fi
    CALLED_FROM_SETUP=true BUILD_SYSTEM=build/core 
        TARGET_PRODUCT=$1 
        TARGET_BUILD_VARIANT= 
        TARGET_BUILD_TYPE= 
        TARGET_BUILD_APPS= 
        get_build_var TARGET_DEVICE > /dev/null
    # hide successful answers, but allow the errors to show
}
函數gettop用來返回Android源代碼工程的根目錄。函數check_product需要在Android源代碼工程根目錄或者子目錄下調用。否則的話,函數check_product就出錯返回。

 

接下來函數check_product設置幾個環境變量,其中最重要的是前面三個CALLED_FROM_SETUP、BUILD_SYSTEM和TARGET_PRODUCT。環境變量CALLED_FROM_SETUP的值等於true表示接下來執行的make命令是用來初始化Android編譯環境的。環境變量BUILD_SYSTEM用來指定Android編譯系統的核心目錄,它的值被設置為build/core。環境變量TARGET_PRODUCT用來表示要檢查的產品名稱(也就是我們前面說的設備型號),它的值被設置為$1,即函數check_product的調用參數。

最後函數check_product調用函數get_build_var來檢查由環境變量TARGET_PRODUCT指定的產品名稱是否合法,注意,它的調用參數為TARGET_DEVICE。

函數get_build_var定義在文件build/envsetup.sh中,它的實現如下所示:

 

# Get the exact value of a build variable.
function get_build_var()
{
    T=$(gettop)
    if [ ! $T ]; then
        echo Couldn't locate the top of the tree.  Try setting TOP. >&2
        return
    fi
    CALLED_FROM_SETUP=true BUILD_SYSTEM=build/core 
      make --no-print-directory -C $T -f build/core/config.mk dumpvar-$1
}
這裡就可以看到,函數get_build_var實際上就是通過make命令在Android源代碼工程根目錄中執行build/core/config.mk文件,並且將make目標設置為dumpvar-$1,也就是dumpvar-TARGET_DEVICE。

 

文件build/core/config.mk的內容比較多,這裡我們只關注與產品名稱合法性檢查相關的邏輯,這些邏輯也基本上涵蓋了Android編譯系統初始化的邏輯,如下所示:

 

......

# ---------------------------------------------------------------
# Define most of the global variables.  These are the ones that
# are specific to the user's build configuration.
include $(BUILD_SYSTEM)/envsetup.mk

# Boards may be defined under $(SRC_TARGET_DIR)/board/$(TARGET_DEVICE)
# or under vendor/*/$(TARGET_DEVICE).  Search in both places, but
# make sure only one exists.
# Real boards should always be associated with an OEM vendor.
board_config_mk := 
    $(strip $(wildcard 
        $(SRC_TARGET_DIR)/board/$(TARGET_DEVICE)/BoardConfig.mk 
        device/*/$(TARGET_DEVICE)/BoardConfig.mk 
        vendor/*/$(TARGET_DEVICE)/BoardConfig.mk 
    ))
ifeq ($(board_config_mk),)
  $(error No config file found for TARGET_DEVICE $(TARGET_DEVICE))
endif
ifneq ($(words $(board_config_mk)),1)
  $(error Multiple board config files for TARGET_DEVICE $(TARGET_DEVICE): $(board_config_mk))
endif
include $(board_config_mk)

......

include $(BUILD_SYSTEM)/dumpvar.mk
上述代碼主要就是將envsetup.mk、BoardConfig,mk和dumpvar.mk三個Makefile片段文件加載進來。其中,envsetup.mk文件位於$(BUILD_SYSTEM)目錄中,也就是build/core目錄中,BoardConfig.mk文件的位置主要就是由環境變量TARGET_DEVICE來確定,它是用來描述目標產品的硬件模塊信息的,例如CPU體系結構。環境變量TARGET_DEVICE用來描述目標設備,它的值是在envsetup.mk文件加載的過程中確定的。一旦目標設備確定後,就可以在$(SRC_TARGET_DIR)/board/$(TARGET_DEVICE)、device/*/$(TARGET_DEVICE)和vendor/*/$(TARGET_DEVICE)目錄中找到對應的BoradConfig.mk文件。注意,變量SRC_TARGET_DIR的值等於build/target。最後,dumpvar.mk文件也是位於build/core目錄中,它用來打印已經配置好的編譯環境信息。

 

接下來我們就通過進入到build/core/envsetup.mk文件來分析變量TARGET_DEVICE的值是如何確定的:

 

# Read the product specs so we an get TARGET_DEVICE and other
# variables that we need in order to locate the output files.
include $(BUILD_SYSTEM)/product_config.mk
它通過加載另外一個文件build/core/product_config.mk文件來確定變量TARGET_DEVICE以及其它與目標產品相關的變量的值。

 

文件build/core/product_config.mk的內容很多,這裡我們只關注變量TARGET_DEVICE設置相關的邏輯,如下所示:

 

......

ifneq ($(strip $(TARGET_BUILD_APPS)),)
# An unbundled app build needs only the core product makefiles.
all_product_configs := $(call get-product-makefiles,
    $(SRC_TARGET_DIR)/product/AndroidProducts.mk)
else
# Read in all of the product definitions specified by the AndroidProducts.mk
# files in the tree.
all_product_configs := $(get-all-product-makefiles)
endif

# all_product_configs consists items like:
# :
# or just  in case the product name is the
# same as the base filename of the product config makefile.
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)))
# Import all product makefiles.
$(call import-products, $(all_product_makefiles))
else
# Import just the current product.
ifndef current_product_makefile
$(error Cannot locate config makefile for product $(TARGET_PRODUCT))
endif
ifneq (1,$(words $(current_product_makefile)))
$(error Product $(TARGET_PRODUCT) ambiguous: matches $(current_product_makefile))
endif
$(call import-products, $(current_product_makefile))
endif  # Import all or just the current product makefile

......

# Convert a short name like sooner into the path to the product
# file defining that product.
#
INTERNAL_PRODUCT := $(call resolve-short-product-name, $(TARGET_PRODUCT))
ifneq ($(current_product_makefile),$(INTERNAL_PRODUCT))
$(error PRODUCT_NAME inconsistent in $(current_product_makefile) and $(INTERNAL_PRODUCT))
endif
current_product_makefile :=
all_product_makefiles :=
all_product_configs :=

# Find the device that this product maps to.
TARGET_DEVICE := $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_DEVICE)

......
上述代碼的執行邏輯如下所示:

 

1. 檢查環境變量TARGET_BUILD_APPS的值是否等於空。如果不等於空,那麼就說明此次編譯不是針對整個系統,因此只要將核心的產品相關的Makefile文件加載進來就行了,否則的話,就要將所有與產品相關的Makefile文件加載進來的。核心產品Makefile文件在$(SRC_TARGET_DIR)/product/AndroidProducts.mk文件中指定,也就是在build/target/product/AndroidProducts.mk文件,通過調用函數get-product-makefiles可以獲得。所有與產品相關的Makefile文件可以通過另外一個函數get-all-product-makefiles獲得。無論如何,最終獲得的產品Makefie文件列表保存在變量all_product_configs中。

2. 遍歷變量all_product_configs所描述的產品Makefile列表,並且在這些Makefile文件中,找到名稱與環境變量TARGET_PRODUCT的值相同的文件,保存在另外一個變量current_product_makefile中,作為需要為當前指定的產品所加載的Makefile文件列表。在這個過程當中,上一步找到的所有的產品Makefile文件也會保存在變量all_product_makefiles中。注意,環境變量TARGET_PRODUCT的值是在我們執行lunch命令的時候設置並且傳遞進來的。

3. 如果指定的make目標等於product-graph或者dump-products,那麼就將所有的產品相關的Makefile文件加載進來,否則的話,只加載與目標產品相關的Makefile文件。從前面的分析可以知道,此時的make目標為dumpvar-TARGET_DEVICE,因此接下來只會加載與目標產品,即$(TARGET_PRODUCT),相關的Makefile文件,這是通過調用另外一個函數import-products實現的。

4. 調用函數resolve-short-product-name解析環境變量TARGET_PRODUCT的值,將它變成一個Makefile文件路徑。並且保存在變量INTERNAL_PRODUCT中。這裡要求變量INTERNAL_PRODUCT和current_product_makefile的值相等,否則的話,就說明用戶指定了一個非法的產品名稱。

5. 找到一個名稱為PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_DEVICE的變量,並且將它的值保存另外一個變量TARGET_DEVICE中。變量PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_DEVICE是在加載產品Makefile文件的過程中定義的,用來描述當前指定的產品的名稱。

上述過程主要涉及到了get-all-product-makefiles、import-products和resolve-short-product-name三個關鍵函數,理解它們的執行過程對理解Android編譯系統的初始化過程很有幫助,接下來我們分別分析它們的實現。

函數get-all-product-makefiles定義在文件build/core/product.mk中,如下所示:

 

#
# Returns the sorted concatenation of all PRODUCT_MAKEFILES
# variables set in all AndroidProducts.mk files.
# $(call ) isn't necessary.
#
define get-all-product-makefiles
$(call get-product-makefiles,$(_find-android-products-files))
endef
它首先是調用函數_find-android-products-files來找到Android源代碼目錄中定義的所有AndroidProducts.mk文件,然後再調用函數get-product-makefiles獲得在這裡AndroidProducts.mk文件裡面定義的產品Makefile文件。

 

函數_find-android-products-files也是定義在文件build/core/product.mk中,如下所示:

 

#
# Returns the list of all AndroidProducts.mk files.
# $(call ) isn't necessary.
#
define _find-android-products-files
$(shell test -d device && find device -maxdepth 6 -name AndroidProducts.mk) 
  $(shell test -d vendor && find vendor -maxdepth 6 -name AndroidProducts.mk) 
  $(SRC_TARGET_DIR)/product/AndroidProducts.mk
endef
從這裡就可以看出,Android源代碼目錄中定義的所有AndroidProducts.mk文件位於device、vendor或者build/target/product目錄或者相應的子目錄(最深是6層)中。

 

函數get-product-makefiles也是定義在文件build/core/product.mk中,如下所示:

 

#
# Returns the sorted concatenation of PRODUCT_MAKEFILES
# variables set in the given AndroidProducts.mk files.
# $(1): the list of AndroidProducts.mk files.
#
define get-product-makefiles
$(sort 
  $(foreach f,$(1), 
    $(eval PRODUCT_MAKEFILES :=) 
    $(eval LOCAL_DIR := $(patsubst %/,%,$(dir $(f)))) 
    $(eval include $(f)) 
    $(PRODUCT_MAKEFILES) 
   ) 
  $(eval PRODUCT_MAKEFILES :=) 
  $(eval LOCAL_DIR :=) 
 )
endef
這個函數實際上就是遍歷參數$1所描述的AndroidProucts.mk文件列表,並且將定義在這些AndroidProucts.mk文件中的變量PRODUCT_MAKEFILES的值提取出來,形成一個列表返回給調用者。

 

例如,在build/target/product/AndroidProducts.mk文件中,變量PRODUCT_MAKEFILES的值如下所示:

 

# Unbundled apps will be built with the most generic product config.
ifneq ($(TARGET_BUILD_APPS),)
PRODUCT_MAKEFILES := 
    $(LOCAL_DIR)/full.mk 
    $(LOCAL_DIR)/full_x86.mk 
    $(LOCAL_DIR)/full_mips.mk
else
PRODUCT_MAKEFILES := 
    $(LOCAL_DIR)/core.mk 
    $(LOCAL_DIR)/generic.mk 
    $(LOCAL_DIR)/generic_x86.mk 
    $(LOCAL_DIR)/generic_mips.mk 
    $(LOCAL_DIR)/full.mk 
    $(LOCAL_DIR)/full_x86.mk 
    $(LOCAL_DIR)/full_mips.mk 
    $(LOCAL_DIR)/vbox_x86.mk 
    $(LOCAL_DIR)/sdk.mk 
    $(LOCAL_DIR)/sdk_x86.mk 
    $(LOCAL_DIR)/sdk_mips.mk 
    $(LOCAL_DIR)/large_emu_hw.mk
endif
這裡列出的每一個文件都對應於一個產品。

 

我們再來看函數import-products的實現,它定義在文件build/core/product.mk中,如下所示:

 

#
# $(1): product makefile list
#
#TODO: check to make sure that products have all the necessary vars defined
define import-products
$(call import-nodes,PRODUCTS,$(1),$(_product_var_list))
endef
它調用另外一個函數import-nodes來加載由參數$1所指定的產品Makefile文件,並且指定了另外兩個參數PRODUCTS和$(_product_var_list)。其中,變量_product_var_list也是定義在文件build/core/product.mk中,它的值如下所示:

 

 

_product_var_list := 
    PRODUCT_NAME 
    PRODUCT_MODEL 
    PRODUCT_LOCALES 
    PRODUCT_AAPT_CONFIG 
    PRODUCT_AAPT_PREF_CONFIG 
    PRODUCT_PACKAGES 
    PRODUCT_PACKAGES_DEBUG 
    PRODUCT_PACKAGES_ENG 
    PRODUCT_PACKAGES_TESTS 
    PRODUCT_DEVICE 
    PRODUCT_MANUFACTURER 
    PRODUCT_BRAND 
    PRODUCT_PROPERTY_OVERRIDES 
    PRODUCT_DEFAULT_PROPERTY_OVERRIDES 
    PRODUCT_CHARACTERISTICS 
    PRODUCT_COPY_FILES 
    PRODUCT_OTA_PUBLIC_KEYS 
    PRODUCT_EXTRA_RECOVERY_KEYS 
    PRODUCT_PACKAGE_OVERLAYS 
    DEVICE_PACKAGE_OVERLAYS 
    PRODUCT_TAGS 
    PRODUCT_SDK_ADDON_NAME 
    PRODUCT_SDK_ADDON_COPY_FILES 
    PRODUCT_SDK_ADDON_COPY_MODULES 
    PRODUCT_SDK_ADDON_DOC_MODULES 
    PRODUCT_DEFAULT_WIFI_CHANNELS 
    PRODUCT_DEFAULT_DEV_CERTIFICATE 
    PRODUCT_RESTRICT_VENDOR_FILES 
    PRODUCT_VENDOR_KERNEL_HEADERS 
    PRODUCT_FACTORY_RAMDISK_MODULES 
    PRODUCT_FACTORY_BUNDLE_MODULES
它描述的是在產品Makefile文件中定義在各種變量。

 

函數import-nodes定義在文件build/core/node_fns.mk中,如下所示:

 

#
# $(1): output list variable name, like PRODUCTS or DEVICES
# $(2): list of makefiles representing nodes to import
# $(3): list of node variable names
#
define import-nodes
$(if 
  $(foreach _in,$(2), 
    $(eval _node_import_context := _nic.$(1).[[$(_in)]]) 
    $(if $(_include_stack),$(eval $(error ASSERTION FAILED: _include_stack 
                should be empty here: $(_include_stack))),) 
    $(eval _include_stack := ) 
    $(call _import-nodes-inner,$(_node_import_context),$(_in),$(3)) 
    $(call move-var-list,$(_node_import_context).$(_in),$(1).$(_in),$(3)) 
    $(eval _node_import_context :=) 
    $(eval $(1) := $($(1)) $(_in)) 
    $(if $(_include_stack),$(eval $(error ASSERTION FAILED: _include_stack 
                should be empty here: $(_include_stack))),) 
   ) 
,)
endef
這個函數主要是做了三件事情:

 

1. 調用函數_import-nodes-inner將參數$2描述的每一個產品Makefile文件加載進來。

2. 調用函數move-var-list將定義在前面所加載的產品Makefile文件裡面的由參數$3指定的變量的值分別拷貝到另外一組獨立的變量中。

3. 將參數$2描述的每一個產品Makefile文件路徑以空格分隔保存在參數$1所描述的變量中,也就是保存在變量PRODUCTS中。

上述第二件事情需要進一步解釋一下。由於當前加載的每一個文件都會定義相同的變量,為了區分這些變量,我們需要在這些變量前面加一些前綴。例如,假設加載了build/target/product/full.mk這個產品Makefile文件,它裡面定義了以下幾個變量:

 

# Overrides
PRODUCT_NAME := full
PRODUCT_DEVICE := generic
PRODUCT_BRAND := Android
PRODUCT_MODEL := Full Android on Emulator
當調用了函數move-var-list對它進行解析後,就會得到以下的新變量:

 

 

PRODUCTS.build/target/product/full.mk.PRODUCT_NAME := full
PRODUCTS.build/target/product/full.mk.PRODUCT_DEVICE := generic
PRODUCTS.build/target/product/full.mk.PRODUCT_BRAND := Android
PRODUCTS.build/target/product/full.mk.PRODUCT_MODEL := Full Android on Emulator
正是由於調用了函數move-var-list,我們在build/core/product_config.mk文件中可以通過PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_DEVICE來設置變量TARGET_DEVICE的值。

 

回到build/core/config.mk文件中,接下來我們再看BoardConfig.mk文件的加載過程。前面提到,當前要加載的BoardConfig.mk文件由變量TARGET_DEVICE來確定。例如,假設我們在運行lunch命令時,輸入的文本為full-eng,那麼build/target/product/full.mk就會被加載,並且我們得到TARGET_DEVICE的值就為generic,接下來加載的BoradConfig.mk文件就會在build/target/board/generic目錄中找到。

BoardConfig.mk文件定義的信息可以參考build/target/board/generic/BoardConfig.mk文件的內容,如下所示:

 

# config.mk
#
# Product-specific compile-time definitions.
#

# The generic product target doesn't have any hardware-specific pieces.
TARGET_NO_BOOTLOADER := true
TARGET_NO_KERNEL := true
TARGET_ARCH := arm

# Note: we build the platform images for ARMv7-A _without_ NEON.
#
# Technically, the emulator supports ARMv7-A _and_ NEON instructions, but
# emulated NEON code paths typically ends up 2x slower than the normal C code
# it is supposed to replace (unlike on real devices where it is 2x to 3x
# faster).
#
# What this means is that the platform image will not use NEON code paths
# that are slower to emulate. On the other hand, it is possible to emulate
# application code generated with the NDK that uses NEON in the emulator.
#
TARGET_ARCH_VARIANT := armv7-a
TARGET_CPU_ABI := armeabi-v7a
TARGET_CPU_ABI2 := armeabi
ARCH_ARM_HAVE_TLS_REGISTER := true

HAVE_HTC_AUDIO_DRIVER := true
BOARD_USES_GENERIC_AUDIO := true

# no hardware camera
USE_CAMERA_STUB := true

# Enable dex-preoptimization to speed up the first boot sequence
# of an SDK AVD. Note that this operation only works on Linux for now
ifeq ($(HOST_OS),linux)
  ifeq ($(WITH_DEXPREOPT),)
    WITH_DEXPREOPT := true
  endif
endif

# Build OpenGLES emulation guest and host libraries
BUILD_EMULATOR_OPENGL := true

# Build and enable the OpenGL ES View renderer. When running on the emulator,
# the GLES renderer disables itself if host GL acceleration isn't available.
USE_OPENGL_RENDERER := true
它描述了產品的Boot Loader、Kernel、CPU體系結構、CPU ABI和Opengl加速等信息。

 

再回到build/core/config.mk文件中,它最後加載build/core/dumpvar.mk文件。加載build/core/dumpvar.mk文件是為了生成make目標,以便可以對這些目標進行操作。例如,在我們這個情景中,我們要執行的make目標是dumpvar-TARGET_DEVICE,因此在加載build/core/dumpvar.mk文件的過程中,就會生成dumpvar-TARGET_DEVICE目標。

文件build/core/dumpvar.mk的內容也比較多,這裡我們只關注生成make目標相關的邏輯:

 

......

# The dumpvar stuff lets you say something like
#
#     CALLED_FROM_SETUP=true 
#       make -f config/envsetup.make dumpvar-TARGET_OUT
# or
#     CALLED_FROM_SETUP=true 
#       make -f config/envsetup.make dumpvar-abs-HOST_OUT_EXECUTABLES
#
# The plain (non-abs) version just dumps the value of the named variable.
# The abs version will treat the variable as a path, and dumps an
# absolute path to it.
#
dumpvar_goals := 
    $(strip $(patsubst dumpvar-%,%,$(filter dumpvar-%,$(MAKECMDGOALS))))
ifdef dumpvar_goals

  ifneq ($(words $(dumpvar_goals)),1)
    $(error Only one dumpvar- goal allowed. Saw $(MAKECMDGOALS))
  endif

  # If the goal is of the form dumpvar-abs-VARNAME, then
  # treat VARNAME as a path and return the absolute path to it.
  absolute_dumpvar := $(strip $(filter abs-%,$(dumpvar_goals)))
  ifdef absolute_dumpvar
    dumpvar_goals := $(patsubst abs-%,%,$(dumpvar_goals))
    ifneq ($(filter /%,$($(dumpvar_goals))),)
      DUMPVAR_VALUE := $($(dumpvar_goals))
    else
      DUMPVAR_VALUE := $(PWD)/$($(dumpvar_goals))
    endif
    dumpvar_target := dumpvar-abs-$(dumpvar_goals)
  else
    DUMPVAR_VALUE := $($(dumpvar_goals))
    dumpvar_target := dumpvar-$(dumpvar_goals)
  endif

.PHONY: $(dumpvar_target)
$(dumpvar_target):
    @echo $(DUMPVAR_VALUE)

endif # dumpvar_goals

......
我們在執行make命令時,指定的目示會經由MAKECMDGOALS變量傳遞到Makefile中,因此通過變量MAKECMDGOALS可以獲得make目標。

 

上述代碼的邏輯很簡單,例如,在我們這個情景中,指定的make目標為dumpvar-TARGET_DEVICE,那麼就會得到變量DUMPVAR_VALUE的值為$(TARGET_DEVICE)。TARGET_DEVICE的值在前面已經被設置為“generic”,因此變量DUMPVAR_VALUE的值就等於“generic”。此外,變量dumpvar_target的被設置為“dumpvar-TARGET_DEVICE”。最後我們就可以得到以下的make規則:

.PHONY dumpvar-TARGET_DEVICE
dumpvar-TARGET_DEVICE:
    @echo generic
至此,在build/envsetup.sh文件中定義的函數check_product就分析完成了。看完了之後,小伙伴們可能會問,前面不是說這個函數是用來檢查用戶輸入的產品名稱是否合法的嗎?但是這裡沒看出哪一段代碼給出了true或者false的答案啊。實際上,在前面分析的build/core/config.mk和build/core/product_config.mk等文件的加載過程中,如果發現輸入的產品名稱是非法的,也就是找不到相應的產品Makefile文件,那麼就會通過調用error函數來產生一個錯誤,這時候函數check_product的返回值$?就會等於非0值。

 

接下來我們還要繼續分析在build/envsetup.sh文件中定義的函數check_variant的實現,如下所示:

 

VARIANT_CHOICES=(user userdebug eng)

# check to see if the supplied variant is valid
function check_variant()
{
    for v in ${VARIANT_CHOICES[@]}
    do
        if [ $v = $1 ]
        then
            return 0
        fi
    done
    return 1
}
這個函數的實現就簡單多了。合法的編譯類型定義在數組VARIANT_CHOICES中,並且它只有三個值user、userdebug和eng。其中,user表示發布版本,userdebug表示帶調試信息的發布版本,而eng表標工程機版本。

 

最後,我們再來分析在build/envsetup.sh文件中定義的函數printconfig的實現,如下所示:

 

function printconfig()
{
    T=$(gettop)
    if [ ! $T ]; then
        echo Couldn't locate the top of the tree.  Try setting TOP. >&2
        return
    fi
    get_build_var report_config
}
對比我們前面對函數check_product的分析,就會發現函數printconfig的實現與這很相似,都是通過調用get_build_var來獲得相關的信息,但是這裡傳遞給函數get_build_var的參數為report_config。

 

我們跳過前面build/core/config.mk和build/core/envsetup.mk等文件對目標產品Makefile文件的加載,直接跳到build/core/dumpvar.mk文件來查看與report_config這個make目標相關的邏輯:

 

......

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

ifneq ($(dumpvar_goals),report_config)
PRINT_BUILD_CONFIG:=
endif

......

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   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
變量PRINT_BUILD_CONFIG定義在文件build/core/envsetup.mk中,默認值設置為true。當make目標為report-config的時候,變量PRINT_BUILD_CONFIG的值就會被設置為空。因此,接下來就會打印一系列用來描述編譯環境配置的變量的值,也就是我們執行lunch命令後看到的輸出。注意,這些環境配置相關的變量量都是在加載build/core/config.mk和build/core/envsetup.mk文件的過程中設置的,就類似於前面我們分析的TARGET_DEVICE變量的值的設置過程。

 

至此,我們就分析完成Android編譯系統環境的初始化過程了。從分析的過程可以知道,Android編譯系統環境是由build/core/config.mk、build/core/envsetup.mk、build/core/product_config.mk、AndroidProducts.mk和BoardConfig.mk等文件來完成的。這些mk文件涉及到非常多的細節,而我們這裡只提供了一個大體的骨架和脈絡,希望能夠起到拋磚引玉的作用。

有了Android編譯系統環境的初始化過程知識之後,在接下來的一篇文章中,老羅將繼續分析Android編譯系統提供的m/mm/mmm編譯命令,敬請關注!更多信息也可以關注老羅的新浪微博:http://weibo.com/shengyangluo

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved