已开通新的博客,后续文字都会发到新博客
https://www.0xforee.top
Android 编译系统解析系列文档
编译系统入口envsetup.sh解析
解析lunch的执行过程以及make执行过程中include文件的顺序
关注一些make执行过程中的几个关键点
对一些独特的语法结构进行解析
这篇文章的主要内容我们来分析关于模块配置文件Android.mk加载的一些关键的知识点
在分析模块配置文件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 $@
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 $@; 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
ARGS=$@
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
,
这个逻辑的实现其实只是将GET-INSTALL-PATH
定义为了一个target,我们使用mm时,会将这个target传进去,从而调用到这个target定义的命令,我们后边遇到模块解析代码的时候就会看到这个target相关代码
以上就是mm执行的全部过程,有三点需要注意:
对于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来看看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文件中依次执行以上文件的内容:
我们就以这个文件为例,来解析一下一般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
未定义,那么默认使用optionaldebug
,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的打包等,这些目标都是独立的,不需要依赖,因此对于代码相关的模块是不需要参与编译的
对于全编过程中所有模块文件的加载我们用到了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:
--mindepth=
Both behave in the same way as their find(1) equivalents.
--prune=
Avoids returning results from inside any directory called
(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 i2 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 and
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 [
可以看出函数后加两个可选参数和两个不可省略参数,我们分开来看他们的规则
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
这个函数主要做了以下几件事:
函数很简单,但是需要注意两点:
以上两点是很重要的,第一点可以让我们可以自由的控制从哪个目录深度开始查找,第二点可以让我们自由的拓展目录的深度与广度,可以自由的控制深度目录下的模块的编译与否,Android编译系统提供了两个函数来搭配这种查找方式all-makefiles-under
与first-makefiles-under
脚本解析完了,这里还有一个小插曲说明一下:
findleaves.py是一个python脚本,这点我们已经了解,你可能会想到为什么不是一个shell脚本,额,没错,你想的没错,这之前确实是一个bash脚本,09年的8月份被替换掉了,google支持python的时间还真是久远,原因是因为可以大大缩短解析的时间
下边作者当初的提交,让我们来看看来究竟比bash强大在什么地方
作者在提交里说明,使用python重写的原因有两个
确实,从作者的提交记录来看,从30秒缩减到不到1秒,确实提升很多,我们看到这里已对python顶礼膜拜,敬仰之情滔滔不绝
然并xxx
其实shell脚本该实现的也都已经实现,并且在搜索性能上不仅不差python,还更胜一筹,下图是实际的对比结果
作者当初选择python重写,一者可能是因为喜爱python,另一个原因可能是他根本不会用那个shell脚本…
好了,小插曲过后,我们回过头来继续研究,我们读了两个脚本之后,也明白了Android.mk文件的搜寻条件,简而言之:
从$(subdirs)也就是源码根目录开始搜索,排除.repo,out和.git目录,搜索各个目录下的Android.mk并打印,关于其中的查找规则,前边已经说明,我们不再赘述
至此,关于Android.mk相关的内容也已经解析完毕,对于模块编译的内容的解析可能不是那么深,因为模块编译有单独的一套规则,且相对独立,在一般的系统开发中的出问题的可能性比较小,所以对于这方面待日后遇到问题再来详细补充