1. 程式人生 > >Android編譯系統分析之幾個關鍵點(一)

Android編譯系統分析之幾個關鍵點(一)

已開通新的部落格,後續文字都會發到新部落格

http://www.0xfree.top


Android 編譯系統解析系列文件

解析lunch的執行過程以及make執行過程中include檔案的順序

關注一些make執行過程中的幾個關鍵點

對一些獨特的語法結構進行解析


我們首先來看看今天的主角,以下這段程式碼就是解析AndroidProducts.mk以及其內容的關鍵程式碼

###################
build/core/product_config.mk
###################

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

# Find the product config makefile for the current product.
# all_product_configs consists items like:
# <product_name>:<path_to_the_product_makefile>
# or just <path_to_the_product_makefile> 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 Can not 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

# Sanity check
$(check-all-products)

ifneq ($(filter dump-products, $(MAKECMDGOALS)),)
$(dump-products)
$(error done)
endif

# 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 :=

該載入哪裡的AndroidProducts.mk檔案?

我們將之前的程式碼拆著來看,首先看AndroidProducts.mk檔案是如何被載入到Android整個編譯環境中的

###################
build/core/product_config.mk
###################

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

這裡判斷構建的目標是不是APP,對於獨立APP的編譯,只需要載入核心目錄下(build/target/product)AndroidProducts.mk檔案即可,如果是構建整個系統,那麼需要載入所有的AndroidProducts.mk檔案

TARGET_BUILD_APPS這個變數可以通過tapas命令指定(具體命令使用方式請參見envsetup.sh),也可以通過"APP-<appname>" 來指定

這個變數預設為空,也就是編譯整個系統,我們可以在載入環境變數之後通過使用printconfig命令來檢視我們是否設定過TARGET_BUILD_APPS變數

取得編譯系統中所有的AndroidProducts.mk

get-all-product-makefiles函式定義在build/core/product.mk檔案中,是get-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函式返回的是整個編譯系統中所有AndroidProducts.mk的集合

####################
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

如上:掃描device與vendor目錄下6層深度的子目錄下的所有AndroidProducts.mk檔案,以及build/target/product/AndroidProducts.mk檔案,從這裡我們可以看出,這裡的得到的最後結果帶有相對於原始碼根目錄的相對路徑,類似以下格式:

device/htc/flounder/AndroidProducts.mk
device/meizu/m86/AndroidProducts.mk
device/samsung/avl7420/AndroidProducts.mk

....

注:SRC_TARGET_DIR=build/target

處理AndroidProducts.mk

接下來要對掃描出的AndroidProducts.mk檔案進行處理

####################
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

我們注意到以上程式碼有一行是對LOCAL_DIR進行了定義,這個定義的原因是因為AndroidProducts.mk檔案中定義的格式像下邊這樣:

ifeq ($(TARGET_PRODUCT),meizu_m86)
    PRODUCT_MAKEFILES := $(LOCAL_DIR)/meizu_m86.mk
endif

我們還記得上邊掃描得出的所有AndroidProducts.mk的集合是帶有相對路徑的,所以我們這裡可以通過dir獲取路徑,然後置換為下一行include對應AndroidProducts.mk中的LOCAL_DIR,這樣我們就得到了我們真正要載入的makefile檔案,就是我們配置一個device需要用到的檔案(例:meizu_m86.mk)

這樣在將所有的AndroidProducts.mk檔案中的內容解析完畢之後,我們就得到了一份使用sort排序並去重的product_makefile檔案列表

注意: 這裡我們並未區分我們要編譯的product_makefile,也就是說這是一個包含全部product_makefile的列表

取得current_makefile(當前lunch機型的配置檔案)

###################
build/core/product_config.mk
###################

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),\
    #then-1
        $(eval all_product_makefiles += $(_cpm_word2))\
        $(if $(filter $(TARGET_PRODUCT),$(_cpm_word1)),\
        #then-2
            $(eval current_product_makefile += $(_cpm_word2)),),\
    #else  
        $(eval all_product_makefiles += $(f))\
        $(if $(filter $(TARGET_PRODUCT),$(basename $(notdir $(f)))),\
        #then-3
            $(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))

關於makefile中IF的語法:
$(if <condition>, <then-part>, <else-part> )

從前邊的程式碼我們可以知道all_product_configs是代表device以及vendor所有的AndroidProducts.mk檔案中變數PRODUCT_MAKEFILES的值的集合,這個PRODUCT_MAKEFILES值包括兩種情況

  1. <product_name>:<path_to_the_product_makefile>
  2. <path_to_the_product_makefile>

也就是在上邊程式碼中then-1做出判斷,我們一般都是使用的第二種情況,所以我們就解析一下else的情況,else主要做了兩步處理

  1. 將所有的product_makefile檔案加入到all_product_makefiles變數中
  2. 通過TARGET_PRODUCT來解析出對應的product_makefile檔案

通過以上兩步,我們可以得到一個包含全部device,vendor下的product_makefile檔案的變數all_product_makefiles以及當前我們需要編譯的product_makefile的變數current_product_makeifle

以上的示例也提醒我們,AndroidProducts.mk檔案內容中指向的product_makefile名稱必須標準

匯入PRODUCT變數

###################
build/core/product_config.mk
###################

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 Can not 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

# Sanity check
$(check-all-products)

ifneq ($(filter dump-products, $(MAKECMDGOALS)),)
$(dump-products)
$(error done)
endif

# 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

在上邊所示的程式碼中,google也給出了除錯product_makefile的方式:

  1. MAKECMDGOALS中如果包含dump-products,那麼執行$(dump-products)命令列印所有的PRODUCT_XXXX變數,具體規則定義位於build/core/product.mk檔案
  2. MAKECMDGOALS中如果包含product-graph,那麼google會在out目錄生成一個pdf和svg檔案,這兩個檔案內包含了所有的product_makefile檔案之間的相互依賴關係,具體規則定義位於build/core/tasks/product-graph.mk

一般的編譯來說,是呼叫import-products匯入當前的我們要編譯的機型配置,也就是這個current_product_makefile這個變數的值

注意current_product_makefile這個值是唯一的,否則會報錯

重要說明:
對於各個目錄下定義的PRODUCT_開頭的相同的變數都會在import-products中得到處理(或者說展開),在處理完畢之後,對於各個變數的獲取,我們都可以在如下格式的變數中獲取到
PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_XXXX

  • 其中INTERNAL_PRODUCT值為上邊current_product_makefile的值,類似device/meizu/m86/meizu_m86.mk
  • PRODUCT_XXXXX表示PRODUCT_COPY_FILESPRODUCT_LOCALES等變數

讓我們接著來看看import-products幹了什麼

####################
build/core/product.mk
####################

define import-products
    $(call import-nodes,PRODUCTS,$(1),$(_product_var_list))
endef

我們記錄一下傳入的引數:$(1) = $(current_product_makefile)

實際呼叫import-nodes匯入傳入的引數,這裡的_product_var_list是以PRODUCT開頭的一系列的變數的列舉

####################
build/core/product.mk
####################

_product_var_list := \
    PRODUCT_NAME \
    PRODUCT_MODEL \
    PRODUCT_LOCALES \
    PRODUCT_AAPT_CONFIG \
    PRODUCT_AAPT_PREF_CONFIG \
    PRODUCT_AAPT_PREBUILT_DPI \
    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 \

    ......

我們再來看看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)]]==<<) \
    # _node_import_context := _nic.PRODUCT.[[device/meizu/m86/meizu_m86.mk]]#
    $(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

import-nodes函式主要做了以下幾件事:

  • current_product_makefile定義了一個變數,這個變數的作用其實就是用來標示唯一性的,在後邊的程式碼中會將這個變數當前綴使用
  • 使用_import-nodes-inner函式做具體的解析工作
  • 將上一步解析完畢的變數變換字首
  • 將解析過的current_product_makefile都新增到PRODUCTS變數中

重要提示
move-var-list用法很簡單:
$(call move-var-list,SRC,DST,A B):變更A和B的字首SRC到DST

我們之前提到過,在所有的PRODUCT_XXXX變數都展開之後,也就是import-products current_product_makefile之後,所有的PRODUCT_XXXX變數都會被集中到以PRODUCTS.$(INTERNAL_PRODUCT)為字首的對應的變數中,這個操作就是使用move-var-list來完成的

下邊是_import-nodes-innner函式中將要使用到的變數的列表表示:

$(_node_import_context) $(_in) $(3) $(2) $(1)
_nic.PRODUCTS.[[device/meizu/m86/meizu_m86.mk]] device/meizu/m86/meizu_m86.mk $(_product_var_list) device/meizu/m86/meizu_m86.mk PRODUCTS

重要提示

  • 對於$(_node_import_context)所代表的內容為了方便敘述,我們定義為公有字首,對於每次編譯的目標,公有的字首是唯一的
  • 對於$(_in)或者$(2)中表示的內容,我們定義為私有字首,對於同一編譯目標不同makefile檔案中的相同PRODUCT_XXX變數,我們都會使用公有字首+私有字首作為字首來區分

我們繼續來看

####################
build/core/node_fns.mk
####################
#
# $(1): context prefix
# $(2): list of makefiles representing nodes to import
# $(3): list of node variable names
#

define _import-nodes-inner
  $(foreach _in,$(2), \
    $(if $(wildcard $(_in)), \
      $(if >>==$($(1).$(_in).seen==<<), \
        $(eval ### "skipping already-imported $(_in)") \
       , \
        $(eval $(1).$(_in).seen := true) \
        $(call >>==_import-node,$(1),$(strip $(_in)),$(3)==<<) \
       ) \
     , \
      $(error $(1): "$(_in)" does not exist) \
     ) \
   )
endef

重要提示

  • wildcard是一個萬用字元的關鍵字,這裡用來判斷$(_in)檔案是否存在
  • 這裡我們看到有一個foreach迴圈,這個在第一次的時候因為引數之後current_product_makefile,所以不會用到,後邊我們在用到繼承性的時候,因為會有繼承多個product的情況發生,所以需要foreach這個函式來遍歷

這裡會有一個變數($(1).$(_in).seen)來標示檔案的內容是否已經匯入,以86為例,變數以及內容分別為

$(1) $(_in) $(3)
_nic.PRODUCTS.[[device/meizu/m86/meizu_m86.mk]] device/meizu/m86/meizu_m86.mk _product_var_list

很長很變態…

_import-nodes-inner函式只是來判斷是否匯入過檔案,如果沒有匯入,使用_import_node來實際匯入

####################
build/core/node_fns.mk
####################
#
# $(1): context prefix
# $(2): makefile representing this node
# $(3): list of node variable names
#
# _include_stack contains the list of included files, with the most recent files first.

define _import-node
  $(eval _include_stack := $(2) $$(_include_stack))
  $(call clear-var-list, $(3))
  $(eval LOCAL_PATH := $(patsubst %/,%,$(dir $(2))))
  $(eval MAKEFILE_LIST :=)
  $(eval >>==include $(2)==<<)
  $(eval _included := $(filter-out $(2),$(MAKEFILE_LIST)))
  $(eval MAKEFILE_LIST :=)
  $(eval LOCAL_PATH :=)
  $(call copy-var-list, $(1).$(2), $(3))
  $(call clear-var-list, $(3))

  $(eval $(1).$(2).inherited := \
      $(call >>==get-inherited-nodes,$(1).$(2),$(3))==<<)
  $(call >>==_import-nodes-inner,$(1),$($(1).$(2).inherited),$(3)==<<)

  $(call >>==_expand-inherited-values,$(1),$(2),$(3)==<<)

  $(eval $(1).$(2).inherited :=)
  $(eval _include_stack := $(wordlist 2,9999,$$(_include_stack)))
endef

知識點
MAKEFILE_LIST
這個變數內容包含,在環境變數中指定的,命令列中指定的,以及make指定makefile檔案時,使用include包含進的檔案,這三者所組成的列表

_import-node函式主要做了以下幾件事:

  1. 將找到的最新檔案入棧,此時棧應該是空的,也就是說meizu_m86.mk是第一個入棧的
  2. 清除所有的PRODUCT_開頭的變數的值
  3. include meizu_m86.mk meizu_m86.mk是整個機型的配置入口,此處開始處理
  4. 將處理過的檔案新增到_included變數中
  5. 將第三步include進來的檔案中的PRODUCT_開頭的變數使用copy-var-list函式新增 ( 1 ) . (1). (2)字首,這裡就是公有字首+私有字首
  6. 取得帶有@inherit字首的變數,然後去掉@inherit字首,然後排序去重,獲得繼承第一層的makefile檔案的列表,記錄到公有字首+私有字首+inherited變數中
  7. 通過_import-nodes-inner來迴圈獲取上一步得到的繼承列表,將所有層次的繼承的文j件都獲取到,最後得到一個解除@inherit字首的包含所有繼承層次的makefile檔案列表
  8. _expand_-inherited-values展開上一步得到的這些檔案中_product_var_list中變數的值
  9. 清空繼承列表與_include_stack

這裡第1步到第7部,以及加上前邊的_import-nodes-inner一起構成了遞迴,我們在遞迴展開這些變數的最後一步時,會呼叫_expand-inherited-values來從最深層次的繼承一直展開到最淺層次的繼承,也就是第一層繼承,要明白最深層次與之後淺層次的makefile中變數的關係,是最深覆蓋最淺?還是最淺覆蓋最深?或者二者疊加?我們就需要看_expand-inherited-values的具體內容

什麼是inherit?

在看最後一個函式的內容之前,我們還需要了解一個知識點,在第5步的時候,出現了一個新的概念inherit,也就是繼承,我們經常會在product_makefile中看到inherit-product函式就是繼承的一個典型的應用,我們先來看看它是怎麼用的,然後再往下看具體的解析過程

define inherit-product
  $(foreach v,$(_product_var_list), \
      $(eval $(v) := $($(v)) $(INHERIT_TAG)$(strip $(1)))) \
  $(eval inherit_var := \
      PRODUCTS.$(strip $(word 1,$(_include_stack))).INHERITS_FROM) \
  $(eval $(inherit_var) := $(sort $($(inherit_var)) $(strip $(1)))) \
  $(eval inherit_var:=) \
  $(eval ALL_PRODUCTS := $(sort $(ALL_PRODUCTS) $(word 1,$(_include_stack))))
endef

這個函式在build/core/product.mk中定義,後邊的引數都是跟一個makefile名稱
示例:
$(call inherit-product, $(SRC_TARGET_DIR)/product/full_base.mk)

這個函式主要做了以下幾件事:

  1. 為_product_var_list中所有變數挨個加上@inherit xxxmakefile的字首,這個xxxmakfile就是傳入的$(1)變數值
  2. 組織當前正被處理(include)的檔案內容的繼承列表,具體是這樣的:
    • 我們用PRODUCTS.當前makefile.INHERITS_FROM這個變數來存放當前makefile的繼承列表
    • 然後這個檔案中的每一個使用inherit-product函式繼承的makefile,都會被加到以上變數中
      這一步的具體過程就是以下:
      PRODUCTS.當前makefile.INHERITS_FORM :=$(sort $(PRODUCT.當前makefile.INHERITS_FROM) last_makefile )
  3. 將當前正在處理(include)的makefile加入到變數ALL_PRODUCTS中

瞭解了這個背景知識之後,我們可以得出以下幾點:

  • 我們注意到之前分析的_import-node第三步有一個include product_makefile檔案的操作,這裡我們分析的inherit-product函式就在這個include的操作中被呼叫
  • include操作是發生在import-node函式中,因此include的makefile也會被加入到_include_stack中
  • _product_var_list中的每個變數也都加入了帶有@inherit字首的所繼承的xxxmakefile
  • inherit-product函式中還提供了一個INHERIT_FROM的變數,這個變數的相關用法,我們可以在build/core/tasks/product-graph.mk見到

由以上內容得知,在這裡我們只需要明白呼叫inherit-product函式只是添加了@inherit:這個字首就行,當然從這裡我們也可以看出,如果一個makefile檔案中inherit兩次同一個makefile,也會被在這裡去重

我們繼續向下來看是如何解析加入@inherit字首的這些變數

_get-inherited-nodes的內容也很簡單

define get-inherited-nodes
$(sort \
  $(subst $(INHERIT_TAG),, \
    $(filter $(INHERIT_TAG)%, \
      $(foreach v,$(2),$($(1).$(v))) \
 )))
endef
  1. 將_product_var_list中的變數挨個取出,這裡的變數已經添加了字首,因此要使用字首取出,也就是類似_nic.PRODUCTS.[[device/meizu/m86/meizu_m86.mk]].device/meizu/m86/meizu_m86.mk所代表的product檔案來取出var變數的值
  2. 過濾出帶有@inherit字首的變數值
  3. 將字首去掉,然後排序去重,最後得到一份繼承的makefile的list

這個函式有點複雜,我們在這裡說明一下:

for迴圈之後,在_product_var_list中的全部變數(PRODUCT_LOCALES,PRODUCT_FILES等)中帶有@inheri字首的內容都會被取出來,然後去掉字首,排序去重,我們之前解析過使用@inherit字首的函式inherit-product,知道呼叫函式之後,所有的PRODUCT_XXX都會繼承@inherite標識後邊加入的makefile,因此,這一塊的內容其實只是將我們之前include的makefile檔案中所繼承的(也就是呼叫inherit-product後的引數)所有makefile做了一個集合,你呼叫了幾個inherit-product,也就有幾個繼承,這個函式的返回值最後是要記錄在公共字首+私有字首+inherited這個變數中的,來表示某個makefile檔案的繼承性的

經過以上3步之後,我們可以(filter過濾出了帶@inherit字首的變數,故變數原本的值沒有在這個列表中)去除@inherit字首的繼承的makefile列表的集合,並且此集合是排序去重過的,這個集合被賦值給了$(1).$(2).inherited

也就是之前我們說到的以makefile絕對路徑來區分的字首,然後又會重複呼叫_import-nodes-inner這個函式,這個函式我們已經解析過了,只是用來判斷是否解析過傳入的檔案列表,實際將同樣的引數傳入了_import-node這個函式來進行解析

總的來說_import-node_import-nodes-innerget-inherited-nodes在不停的迴圈,將每次get-inherit-nodes得到的去除了@inherit的makefile重新放入迴圈中,然後解析出這個makefile檔案的所有的_product_var_list所對應的繼承關係,將其中_product_var_list中的變數的值都加上某一字首,這個字首就是_include_stack棧中最近的一個makefile,因此不需要擔心不同的makefile檔案中的同一變數會互相覆蓋,他們都會以不同的makefile作為字首來標示

也就是在_expand-inherited-values函式之前,我們都會得到相如以下型別的變數:

_nic.PRODUCTS.[[device/meizu/m86/meizu_m86.mk]].last_makefile.PRODUCT_xxxx := aaa bbb @inherit xxxmakefile @inherit yyymakefile

展開繼承的變數

我們來看最後一個函式_expand-inherited-values,我們將傳入_expand-inherited-values的引數列舉出來,方便後邊檢視

$(1) $(2) $(3)
_nic.PRODUCTS.[[device/meizu/m86/meizu_m86.mk]] last_makefile $(_product_var_list)

假設,我們這裡的$(2),也就是last_makefile,是example.txt舉例的最深層次的makefile檔案,也就是build/target/product/embedded.mk檔案,這個檔案內容中已經不包含繼承關係,因此_get_inherited_nodes返回的內容為空,_import-node-inner也什麼都不做,我們可以直接看_expand-inherited-values

仔細看傳入的引數,其實就是傳入_import_node的三個引數,其實就是公有字首,私有字首,以及一個PRODUCT_xxx的列表,我們可以用這三個引數組成我們呼叫_expand-inherited-values函式之前的那種型別的變數

接下來我們來實際解析這個函式

define _expand-inherited-values
  $(foreach v,$(3), \
    $(eval ### "Shorthand for the name of the target variable") \
    $(eval _eiv_tv := $(1).$(2).$(v)) \
    $(eval ### "Get the list of nodes that this variable inherits") \
    $(eval _eiv_i := \
        $(sort \
            $(patsubst $(INHERIT_TAG)%,%, \
                $(filter $(INHERIT_TAG)%, $($(_eiv_tv)) \
     )))) \
    $(foreach i,$(_eiv_i), \
      $(eval ### "Make sure that this inherit appears only once") \
      $(eval $(_eiv_tv) := \
          $(call uniq-word,$($(_eiv_tv)),$(INHERIT_TAG)$(i))) \
      $(eval ### "Expand the inherit tag") \
      $(eval $(_eiv_tv) := \
          $(strip \
              >>==$(patsubst $(INHERIT_TAG)$(i),$($(1).$(i).$(v)), \
                  $($(_eiv_tv)))==<<)) \
      $(eval ### "Clear the child so DAGs don't create duplicate entries" ) \
      $(eval $(1).$(i).$(v) :=) \
      $(eval ### "If we just inherited ourselves, it's a cycle.") \
      $(if $(filter $(INHERIT_TAG)$(2),$($(_eiv_tv))), \
        $(warning Cycle detected between "$(2)" and "$(i)" for context "$(1)") \
        $(error import of "$(2)" failed) \
      ) \
     ) \
   ) \
   $(eval _eiv_tv :=) \
   $(eval _eiv_i :=)
endef

這個函式做了以下幾件事:

  1. _eiv_tv 將_product_var_list中的所有變數都添加了公有字首+私有字首
  2. _eiv_i 表示當前正在處理的makefile檔案的繼承關係makefile列表,是通過過濾@inherit這個字首拿到的,顯而易見,這個是從倒數第二深的makefile檔案起作用,因為最深層次的makefile變數中不包含繼承關係
  3. 使用uniq-word來確保只繼承了一次,這種繼承的檢查發生在上層與緊挨著的下層之間
  4. 展開@inherit表示的變數,其實也就是使用比他深一層次的makefile檔案的對應的PRODUCT_XXX變數替換@inherit這個識別符號
  5. 檢查是否迴圈繼承(自己繼承了自己)

我們接著看uniq-word這個函式的實現

define uniq-word
$(strip \
  $(if $(filter-out 0 1,$(words $(filter $(2),$(1)))), \
    $(eval h := |||$(subst $(space),|||,$(strip $(1)))|||) \
    $(eval h := $(subst |||$(strip $(2))|||,|||$(space)|||,$(h))) \
    $(eval h := $(word 1,$(h)) $(2) $(wordlist 2,9999,$(h))) \
    $(subst |||,$(space),$(h)) \
   , \
    $(1) \
 ))
endef

程式碼比較簡單,讀者可以根據前邊的內容來分析這個函式的實際作用,不再贅述

我們繼續往下看,還記得之前我們通過不停的呼叫_import-node_import-nodes-innerget-inherited-nodes函式構建了所有有繼承關係的makefile自己的變數的值的關係,所以我們在這裡展開的時候,直接將@inherit:last_makefile替換為PRODUCT.last_makefile.PRODUCT_xxx的值,這裡就簡單的展開結束,因此我們最後得到就是所有繼承變數的綜合起來的內容

檢查所有的product

最後還剩下一點簡單的程式碼,是用來解析出一個short-name後邊來使用

# Sanity check
$(check-all-products)

ifneq ($(filter dump-products, $(MAKECMDGOALS)),)
$(dump-products)
$(error done)
endif

# 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

首先,check-all-products來檢查全部products

define check-all-products
$(if ,, \
  $(eval _cap_names :=) \
  $(foreach p,$(PRODUCTS), \
    $(eval pn := $(strip $(PRODUCTS.$(p).PRODUCT_NAME))) \
    $(if $(pn),,$(error $(p): PRODUCT_NAME must be defined.)) \
    $(if $(filter $(pn),$(_cap_names)), \
      $(error $(p): PRODUCT_NAME must be unique; "$(pn)" already used by $(strip \
          $(foreach \
            pp,$(PRODUCTS),
              $(if $(filter $(pn),$(PRODUCTS.$(pp).PRODUCT_NAME)), \
                $(pp) \
               ))) \
       ) \
     ) \
    $(eval _cap_names += $(pn)) \
    $(if $(call is-c-identifier,$(pn)),, \
      $(error $(p): PRODUCT_NAME must be a valid C identifier, not "$(pn)") \
     ) \
    $(eval pb := $(strip $(PRODUCTS.$(p).PRODUCT_BRAND))) \
    $(if $(pb),,$(error $(p): PRODUCT_BRAND must be defined.)) \
    $(foreach cf,$(strip $(PRODUCTS.$(p).PRODUCT_COPY_FILES)), \
      $(if $(filter 2 3,$(words $(subst :,$(space),$(cf)))),, \
        $(error $(p): malformed COPY_FILE "$(cf)") \
       ) \
     ) \
   ) \
)
endef

這裡簡單呼叫了resolve-short-product-name的函式,然後將引數傳入_resolve-short-product-name,我們直接來看

define _resolve-short-product-name
  $(eval pn := $(strip $(1)))
  $(eval p := \
      $(foreach p,$(PRODUCTS), \
          $(if $(filter $(pn),$(PRODUCTS.$(p).PRODUCT_NAME)), \
            $(p) \
       )) \
   )
  $(eval p := $(sort $(p)))
  $(if $(filter 1,$(words $(p))), \
    $(p), \
    $(if $(filter 0,$(words $(p))), \
      $(error No matches for product "$(pn)"), \
      $(error Product "$(pn)" ambiguous: matches $(p)) \
    ) \
  )
endef
define resolve-short-product-name
$(strip $(call _resolve-short-product-name,$(1)))
endef

以上程式碼也很簡單,就是針對對應的product_makefile來獲取對應的PRODUCT_NAME,然後定義為短product_name

至此,我們關於AndroidProduct.mk檔案的關鍵點的解析已經完成.