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 T−fbuild/core/main.mkDASH_ARGS MODULESARGS,还是一个make命令。注意make后加了-C T,因为我们可能在packages/apps/下使用mmm.编译,而make−f指定的makefile文件是源码根目录下的相对路径,所以−CT是必要的
6. 总结
本文以介绍lunch和mmm命令作为编译系统的开篇,特别是lunch命令,虽然它没有编译任何文件,但是它仍然使用了make命令,并且加载了一大堆的makefile,后面编译都以lunch为基础,并使用了lunch设置的很多变量。