Android編譯系統分析之幾個關鍵點(三)
已開通新的部落格,後續文字都會發到新部落格
Android 編譯系統解析系列文件
解析lunch的執行過程以及make執行過程中include檔案的順序
關注一些make執行過程中的幾個關鍵點
對一些獨特的語法結構進行解析
這篇文章的主要內容我們來分析關於模組配置檔案Android.mk載入的一些關鍵的知識點
躲不開的背景知識"mm"與"mmm"
在分析模組配置檔案Android.mk檔案的載入過程之前,我們需要先了解一段背景知識,那就是Android.mk的使用情景是什麼樣的?
還記得我們在前邊分析lunch的時候提到的原始碼的全編與模組編譯嗎?
控制全編與模組編譯的命令就在envsetup.sh檔案中定義,其中全編直接執行make,模組單編需要用到mm與mmm兩條命令,定義如下
function findmakefile()
{
TOPFILE=build/core/envsetup.mk
local HERE=$PWD
T=
while [ \( ! \( -f $TOPFILE \) \) -a \( $PWD != "/" \) ]; do
T=`PWD= /bin/pwd`
if [ -f "$T/Android.mk" ]; then
echo $T/Android.mk
\cd $HERE
return
fi
\cd ..
done
\cd $HERE
}
function mm()
{
local T=$(gettop)
local DRV=$(getdriver $T)
# If we're sitting in the root of the build tree, just do a
# normal make.
if [ -f build/core/envsetup.mk -a -f Makefile ]; then
$DRV make [email protected]
else
# Find the closest Android.mk file.
local M=$(findmakefile)
local MODULES=
local GET_INSTALL_PATH=
local ARGS=
# Remove the path to top as the makefilepath needs to be relative
local M=`echo $M|sed 's:'$T'/::'`
if [ ! "$T" ]; then
echo "Couldn't locate the top of the tree. Try setting TOP."
elif [ ! "$M" ]; then
echo "Couldn't locate a makefile from the current directory."
else
for ARG in [email protected]; do
case $ARG in
GET-INSTALL-PATH) GET_INSTALL_PATH=$ARG;;
esac
done
if [ -n "$GET_INSTALL_PATH" ]; then
MODULES=
ARGS=GET-INSTALL-PATH
else
MODULES=all_modules
[email protected]
fi
ONE_SHOT_MAKEFILE=$M $DRV make -C $T -f build/core/main.mk $MODULES $ARGS
fi
fi
}
先看mm命令,如果執行這條命令的路徑為TOP目錄,那麼就等價於直接使用make命令
如果不是,我們就以當前目錄為基點,遞迴向上查詢距離最近的Android.mk檔案,這個查詢的過程在findmakefile()
函式中定義
Android編譯系統在使用mm命令的時候為我們提供了一個引數,可以方便我們打印出所要編譯模組的最終安裝路徑,這個引數就是GET-INSTALL-PATH
,
- 如果編譯系統在檢查到正在使用mm時加了這個引數,我們就不執行編譯的操作,只打印這個引數的值
這個邏輯的實現其實只是將GET-INSTALL-PATH
定義為了一個target,我們使用mm時,會將這個target傳進去,從而呼叫到這個target定義的命令,我們後邊遇到模組解析程式碼的時候就會看到這個target相關程式碼
- 如果沒有這個引數,我們就指定編譯全部的模組(all_modules),然後將mm後邊的引數全部作為MAKEGOALS傳入
以上就是mm執行的全部過程,有三點需要注意:
- $DRV,這個變數的作用是加一些靜態分析的選項以及路徑
- ARG引數的新增可以讓我們使用-B這樣的make自帶的引數
- ONE_SHOT_MAKEFILE是區別全編與模組編譯的關鍵變數
對於mmm函式,使用方法為直接指定Android.mk所在的資料夾,除此之外最終呼叫的命令與mm是一樣的,有興趣的讀者可以自己來試著解析
瞭解了使用方法之後,mm在呼叫的時候會傳入ONE_SHOT_MAKEFILE
引數,這個引數是區別全編和模組編譯的重點,接下來我們來具體看看這個引數帶來的實質的影響
模組檔案載入解析過程
如果讀過我之前make解析文章的同學一定還記得include的順序,沒錯,Android.mk檔案的解析的主要程式碼是在build/core/main.mk檔案中,附程式碼如下:
# Before we go and include all of the module makefiles, stash away
# the PRODUCT_* values so that later we can verify they are not modified.
stash_product_vars:=true
ifeq ($(stash_product_vars),true)
$(call stash-product-vars, __STASHED)
endif
ifneq ($(ONE_SHOT_MAKEFILE),)
# We've probably been invoked by the "mm" shell function
# with a subdirectory's makefile.
include $(ONE_SHOT_MAKEFILE)
# Change CUSTOM_MODULES to include only modules that were
# defined by this makefile; this will install all of those
# modules as a side-effect. Do this after including ONE_SHOT_MAKEFILE
# so that the modules will be installed in the same place they
# would have been with a normal make.
CUSTOM_MODULES := $(sort $(call get-tagged-modules,$(ALL_MODULE_TAGS)))
FULL_BUILD :=
# Stub out the notice targets, which probably aren't defined
# when using ONE_SHOT_MAKEFILE.
NOTICE-HOST-%: ;
NOTICE-TARGET-%: ;
# A helper goal printing out install paths
.PHONY: GET-INSTALL-PATH
GET-INSTALL-PATH:
@$(foreach m, $(ALL_MODULES), $(if $(ALL_MODULES.$(m).INSTALLED), \
echo 'INSTALL-PATH: $(m) $(ALL_MODULES.$(m).INSTALLED)';))
else # ONE_SHOT_MAKEFILE
ifneq ($(dont_bother),true)
#
# Include all of the makefiles in the system
#
# Can't use first-makefiles-under here because
# --mindepth=2 makes the prunes not work.
subdir_makefiles := \
$(shell build/tools/findleaves.py --prune=$(OUT_DIR) --prune=.repo --prune=.git $(subdirs) Android.mk)
$(foreach mk, $(subdir_makefiles), $(info including $(mk) ...)$(eval include $(mk)))
endif # dont_bother
endif # ONE_SHOT_MAKEFILE
# Now with all Android.mks loaded we can do post cleaning steps.
include $(BUILD_SYSTEM)/post_clean.mk
ifeq ($(stash_product_vars),true)
$(call assert-product-vars, __STASHED)
endif
在真正的呼叫ONE_SHOT_MAKEFILE
變數判斷全編還是模組編譯之前,我們還有一件事需要做:
暫存PRODUCT_*系列變數(stash-product-vars
)
這項操作的用意很明顯,我們對於Product級的配置已經結束,接下來載入的模組級別的配置是不能影響干擾到Product的相關配置,所以我們需要暫存變數,來方便後邊比對是否修改了這些變數(assert-product-vars
)
從這個操作我們也可以看出,Android編譯系統對於Product配置在載入模組配置檔案Android.mk檔案之前就已經結束,從include檔案順序表中我們可以看到也就是在lunch的全部宣告週期做完了Product的配置的載入,這裡我們之所以不說是完成配置,而是完成載入,是因為對於PRODUCT_COPY_FILES這個變數我們還有操作需要處理,這塊內容我們會在後序的文章中說明
接下來我們又遇到一個新的關鍵字TAG,如果編寫過模組程式碼,那麼對這個TAG應該不陌生,常用的定義有user,eng,tests,optional等,你可以指定對應的TAG,使得它在指定的編譯型別中生效
這裡使用一個get-tagged-modules
函式來根據我們當前的編譯的varient來挑選出合適的模組加入待編譯列表
瞭解這個函式之前,我們需要知道傳入的引數ALL_MODULE_TAGS
的作用是什麼
我們之前在解析各編譯檔案的作用時曾經提到過,envsetup.mk的作用主要是定義一些編譯系統需要用到的巨集,而definitions.mk檔案則是用來定義一些公有的函式,這些公有函式主要用在模組編譯規則檔案Android.mk的編寫,所以在遇到ALL_MODULE_TAGS
這個變數,我們首先想到的就是去definitions.mk檔案中檢視,我們發現
definitions.mk中定義了ALL_MODULE_TAGS
以及操作這個變數的相關函式,但是真正的為這個變數賦值的操作發生在base_rules.mk中,那麼這個base_rules.mk與definitions.mk之間是什麼關係呢?
一個Android.mk的示例
我們選擇一個Android.mk來看看include之後發生了什麼
示例Android.mk檔案內容:
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := \
$(call all-java-files-under, src)
LOCAL_PACKAGE_NAME := Nfc
LOCAL_JNI_SHARED_LIBRARIES := libnfc_mt6605_jni libmtknfc_dynamic_load_jni
LOCAL_PROGUARD_ENABLED := disabled
include $(BUILD_PACKAGE)
以上檔案是一個NFC模組的Android.mk檔案,我們從前邊執行mm的流程可以得知,如果我們在NFC模組規則檔案Android.mk檔案所在的目錄下執行mm,實際執行的操作是找到這個Android.mk檔案,並將這個檔案賦值給ONE_SHOT_MAKEFILE,然後在main.mk檔案中載入進來,也就是我們會在main.mk檔案中依次執行以上檔案的內容:
- 使用$(CLEAR_VARS)清零各變數
- 定義幾個以LOCAL_*開頭的變數
- 載入一個對應的編譯規則檔案$(BUILD_PACKAGE)
我們就以這個檔案為例,來解析一下一般Android.mk檔案載入的流程:
首先$(CLEAR_VARS)對應的是一個makefile檔案clear_vars.mk,內容是對各個LOCAL_*變數的清零操作,這個巨集的定義是在config.mk檔案中,也就是在載入模組規則檔案Android.mk檔案之前
然後$(BUILD_PACKAGE)也是一個makefile檔案package.mk,內容是關於對一個package編譯的規則,Android編譯系統定義了一系列的巨集來將編譯各種型別模組的規則打包,我們只需要在每個模組定義的最後引用就可以
瞭解以上兩點,我們就可以使用之前分析make命令執行流程的方法來分析這個示例檔案被include到main.mk之後的執行過程,以下是include Android.mk檔案之後的include檔案順序:
#模組編譯時的include順序,以package.mk為例
config.mk (
62-97: BUILD_SYSTEM_INTERNAL_FILE (
CLEAR_VARS:clear_vars.mk
......
BUILD_STATIC_LIBRARY:static_library.mk
BUILD_SHARED_LIBRARY:shared_library.mk
BUILD_PACKAGE:package.mk (
6:include multilib.mk
53:include module_arch_supported.mk
55:include package_internal.mk (
204:include android_manifest.mk
207:include java.mk (
307:include base_rules.mk (
165:include configure_module_stem.mk
688:include $(BUILD_NOTICE_FILE)
)
314:include dex_preopt_odex_install.mk
)
356:include install_jni_libs.mk (
81:include install_jni_libs_internal.mk
)
)
)
BUILD_PHONY_PACKAGE:phone_package.mk
......
BUILD_PREBUILT:prebuilt.mk
......
)
)
從以上的include順序圖中,我們可以很清晰的發現base_rulse.mk的身影,這裡我們只關心ALL_MODULE_TAGS
,所以直接來看base_rules.mk檔案中對這個變數的處理:
my_module_tags := $(LOCAL_MODULE_TAGS)
LOCAL_UNINSTALLABLE_MODULE := $(strip $(LOCAL_UNINSTALLABLE_MODULE))
my_module_tags := $(sort $(my_module_tags))
ifeq (,$(my_module_tags))
my_module_tags := optional
endif
# User tags are not allowed anymore. Fail early because it will not be installed
# like it used to be.
ifneq ($(filter $(my_module_tags),user),)
$(warning *** Module name: $(LOCAL_MODULE))
$(warning *** Makefile location: $(LOCAL_MODULE_MAKEFILE))
$(warning * )
$(warning * Module is attempting to use the 'user' tag. This)
$(warning * used to cause the module to be installed automatically.)
$(warning * Now, the module must be listed in the PRODUCT_PACKAGES)
$(warning * section of a product makefile to have it installed.)
$(warning * )
$(error user tag detected on module.)
endif
# Only the tags mentioned in this test are expected to be set by module
# makefiles. Anything else is either a typo or a source of unexpected
# behaviors.
ifneq ($(filter-out debug eng tests optional samples,$(my_module_tags)),)
$(warning unusual tags $(my_module_tags) on $(LOCAL_MODULE) at $(LOCAL_PATH))
endif
從上到下,依次說明了這幾件事:
- 如果
LOCAL_MODULE_TAG
未定義,那麼預設使用optional - user這個TAG已經廢棄,如果需要定義這個TAG,可以將其加入到PRODUCT_PACKAGES變數中
- TAG只能是
debug
,eng
,tests
,optional
,samples
這幾個
我們在這裡拿到tag之後,就需要對其進行處理:
# Keep track of all the tags we've seen.
ALL_MODULE_TAGS := $(sort $(ALL_MODULE_TAGS) $(my_module_tags))
# Add this module to the tag list of each specified tag.
# Don't use "+=". If the variable hasn't been set with ":=",
# it will default to recursive expansion.
$(foreach tag,$(my_module_tags),\
$(eval ALL_MODULE_TAGS.$(tag) := \
$(ALL_MODULE_TAGS.$(tag)) \
$(LOCAL_INSTALLED_MODULE)))
這個示例中Android.mk只定義了一個模組,所以這裡的ALL_MODULE_TAGS就是Android.mk定義的LOCAL_MODULTE_TAG
,如果是多個模組,這裡就是多個模組的綜合
然後使用不同的TAG字尾,將對應TAG的模組賦值給ALL_MODULE_TAGS.$(tag)
,這裡模組只有一個,所以其值也是唯一,這樣對應TAG的模組我們就可以拿到了
我們回過頭繼續看CUSTOM_MODULE的值是需要get-tagged-modules
取出來的,我們來看這個函式:
define modules-for-tag-list
$(sort $(foreach tag,$(1),$(ALL_MODULE_TAGS.$(tag))))
endef
# Same as modules-for-tag-list, but operates on
# ALL_MODULE_NAME_TAGS.
# $(1): tag list
define module-names-for-tag-list
$(sort $(foreach tag,$(1),$(ALL_MODULE_NAME_TAGS.$(tag))))
endef
# Given an accept and reject list, find the matching
# set of targets. If a target has multiple tags and
# any of them are rejected, the target is rejected.
# Reject overrides accept.
# $(1): list of tags to accept
# $(2): list of tags to reject
#TODO(dbort): do $(if $(strip $(1)),$(1),$(ALL_MODULE_TAGS))
#TODO(jbq): as of 20100106 nobody uses the second parameter
define get-tagged-modules
$(filter-out \
$(call modules-for-tag-list,$(2)), \
$(call modules-for-tag-list,$(1)))
endef
get-tagged-modules
有兩個引數,第一個引數對應的是我們想要取出的模組的tag,第二個引數對應我們不想取出的模組對應的tag,獲取CUSTOM_MODULE時,只傳入了我們想要取出的模組tag,所以我們我們看到,對於傳入的要取出的對應tag的模組,我們只是從ALL_MODULE_TAGS對應tag字尾中取出即可
雖然一個簡單的模組編譯規則繞了這麼一大圈,但是這只是對於單個模組而言,這套模組編譯系統的強大之處對於多個模組編譯才能真正體現出來,也就是我們進行全編的時候才會見識到它真正的威力
ALL_DEFAULT_INSTALLED_MODULES
挑選完模組之後,我們就看到前邊解析mm時要到的列印目標模組安裝路徑的那個target,然後模組的單編也就完成了引數的傳入
回過頭來我們看看全編過程對Android.mk檔案的處理,當include全部的Android.mk之後,我們會發現所有的模組檔案都各自被這兩條語句包括著:
include $(CLEAR_VARS)
......
include $(BUILD_PACKAGE)
我們前邊已經講到過CLEAR_VARS就是一個全部LOCAL_*變數的清零操作的mk合集,而BUILD_*這型別的檔案定義了各種型別的模組的編譯規則
這裡還有一個dont_bother
的問題,dont_bother
的出現,是因為Android編譯系統定義了一些特殊的目標,在編譯這些目標時,不需要載入Android.mk檔案,具體的定義在build/core/main.mk
# These goals don't need to collect and include Android.mks/CleanSpec.mks
# in the source tree.
dont_bother_goals := clean clobber dataclean installclean \
help out \
snod systemimage-nodeps \
stnod systemtarball-nodeps \
userdataimage-nodeps userdatatarball-nodeps \
cacheimage-nodeps \
vendorimage-nodeps \
ramdisk-nodeps \
bootimage-nodeps
ifneq ($(filter $(dont_bother_goals), $(MAKECMDGOALS)),)
dont_bother := true
endif
包括clean, help, 以及一些image的打包等,這些目標都是獨立的,不需要依賴,因此對於程式碼相關的模組是不需要參與編譯的
找到系統所有的Android.mk
對於全編過程中所有模組檔案的載入我們用到了findleaves.py
import os
import sys
def perform_find(mindepth, prune, dirlist, filename):
result = []
pruneleaves = set(map(lambda x: os.path.split(x)[1], prune))
for rootdir in dirlist:
rootdepth = rootdir.count("/")
for root, dirs, files in os.walk(rootdir, followlinks=True):
# prune
check_prune = False
for d in dirs:
if d in pruneleaves:
check_prune = True
break
if check_prune:
i = 0
while i < len(dirs):
if dirs[i] in prune:
del dirs[i]
else:
i += 1
# mindepth
if mindepth > 0:
depth = 1 + root.count("/") - rootdepth
if depth < mindepth:
continue
# match
if filename in files:
result.append(os.path.join(root, filename))
del dirs[:]
return result
def usage():
sys.stderr.write("""Usage: %(progName)s [<options>] <dirlist> <filename>
Options:
--mindepth=<mindepth>
Both behave in the same way as their find(1) equivalents.
--prune=<dirname>
Avoids returning results from inside any directory called <dirname>
(e.g., "*/out/*"). May be used multiple times.
""" % {
"progName": os.path.split(sys.argv[0])[1],
})
sys.exit(1)
def main(argv):
mindepth = -1
prune = []
i=1
while i<len(argv) and len(argv[i])>2 and argv[i][0:2] == "--":
arg = argv[i]
if arg.startswith("--mindepth="):
try:
mindepth = int(arg[len("--mindepth="):])
except ValueError:
usage()
elif arg.startswith("--prune="):
p = arg[len("--prune="):]
if len(p) == 0:
usage()
prune.append(p)
else:
usage()
i += 1
if len(argv)-i < 2: # need both <dirlist> and <filename>
usage()
dirlist = argv[i:-1]
filename = argv[-1]
results = list(set(perform_find(mindepth, prune, dirlist, filename)))
results.sort()
for r in results:
print r
if __name__ == "__main__":
main(sys.argv)
雖然函式內容很少,但是我們可以從中看出一些細節實現,所以我們在這裡簡單分析一下:
首先來看使用方法,就是定義的函式usage(),我們從
Usage: %(progName)s [<options>] <dirlist> <filename>
可以看出函式後加兩個可選引數和兩個不可省略引數,我們分開來看他們的規則
- 可選引數
- –mindepth:相對於查詢目錄的最淺深度,如果沒有達到,不會查詢filename,可以有多個引數,取最後一個定義
- –prune:略過查詢的目錄,可以定義多個目錄
- 不可省略引數
- dirlist:要執行查詢操作的目錄列表,需要有1個或多個引數
- filename:引數唯一,需要執行查詢的檔名
Android編譯系統在這裡使用的命令是
build/tools/findleaves.py --prune=$(OUT_DIR) --prune=.repo --prune=.git $(subdirs) Android.mk
也就是排除了out,.repo,.git三個目錄,在根目錄下查詢Android.mk檔案
main函式為入口函式,在perform_find函式之前主要是對引數進行處理,將需要排除的目錄放入prune陣列中,執行實際查詢的函式主要在perform_find中,我們來看
def perform_find(mindepth, prune, dirlist, filename):
result = []
pruneleaves = set(map(lambda x: os.path.split(x)[1], prune))
for rootdir in dirlist:
rootdepth = rootdir.count("/")
for root, dirs, files in os.walk(rootdir, followlinks=True):
# prune
check_prune = False
for d in dirs:
if d in pruneleaves:
check_prune = True
break
if check_prune:
i = 0
while i < len(dirs):
if dirs[i] in prune:
del dirs[i]
else:
i += 1
# mindepth
if mindepth > 0:
depth = 1 + root.count("/") - rootdepth
if depth < mindepth:
continue
# match
if filename in files:
result.append(os.path.join(root, filename))
del dirs[:]
return result
這個函式主要做了以下幾件事:
- 首先記錄了目錄深度,用於在後邊判斷是否達到最淺目錄深度(mindepth)
- 然後遍歷傳入的目錄,也就是原始碼根目錄
- 檢查是否需要在當前目錄略過指定的目錄(加快搜索速度)
- 如果檢查到需要略過的目錄,刪掉當前目錄下的子目錄中所有指定的目錄
- 判斷是否達到最淺的搜尋深度
- 如果檔名匹配,將它放到reslut陣列中
- 返回查詢到的所有的Android.mk
函式很簡單,但是需要注意兩點:
- 如果最小目錄深度mindepth沒有達到,那麼不會匹配當前目錄的檔案,直到達到目錄深度才會執行開始匹配
- 如果當前資料夾下匹配到了Android.mk,那麼就清空子目錄列表,也就是停止繼續向下查詢
以上兩點是很重要的,第一點可以讓我們可以自由的控制從哪個目錄深度開始查詢,第二點可以讓我們自由的拓展目錄的深度與廣度,可以自由的控制深度目錄下的模組的編譯與否,Android編譯系統提供了兩個函式來搭配這種查詢方式all-makefiles-under
與first-makefiles-under
一個小插曲
指令碼解析完了,這裡還有一個小插曲說明一下:
findleaves.py是一個python指令碼,這點我們已經瞭解,你可能會想到為什麼不是一個shell指令碼,額,沒錯,你想的沒錯,這之前確實是一個bash指令碼,09年的8月份被替換掉了,google支援python的時間還真是久遠,原因是因為可以大大縮短解析的時間
下邊作者當初的提交,讓我們來看看來究竟比bash強大在什麼地方
作者在提交裡說明,使用python重寫的原因有兩個
- 可以使用多重prune來排除目錄
- 有效的縮短查詢時間
確實,從作者的提交記錄來看,從30秒縮減到不到1秒,確實提升很多,我們看到這裡已對python頂禮膜拜,敬仰之情滔滔不絕
然並xxx
其實shell指令碼該實現的也都已經實現,並且在搜尋效能上不僅不差python,還更勝一籌,下圖是實際的對比結果
作者當初選擇python重寫,一者可能是因為喜愛python,另一個原因可能是他根本不會用那個shell指令碼…
好了,小插曲過後,我們回過頭來繼續研究,我們讀了兩個指令碼之後,也明白了Android.mk檔案的搜尋條件,簡而言之:
從$(subdirs)也就是原始碼根目錄開始搜尋,排除.repo,out和.git目錄,搜尋各個目錄下的Android.mk並列印,關於其中的查詢規則,前邊已經說明,我們不再贅述
結束語
至此,關於Android.mk相關的內容也已經解析完畢,對於模組編譯的內容的解析可能不是那麼深,因為模組編譯有單獨的一套規則,且相對獨立,在一般的系統開發中的出問題的可能性比較小,所以對於這方面待日後遇到問題再來詳細補充