Android Build System[一]

来源:互联网 发布:博采网络好不好 编辑:程序博客网 时间:2024/06/01 16:09

更多干货,请关注微信公众号: tmac_lover

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_CHOICESfunction 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-engadd_lunch_combo aosp_arm64-engadd_lunch_combo aosp_mips-engadd_lunch_combo aosp_mips64-engadd_lunch_combo aosp_x86-engadd_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"    . $fdone

这里做了下面几件事:
* 使用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))endifINTERNAL_PRODUCT := $(call resolve-short-product-name, $(TARGET_PRODUCT))ifneq ($(current_product_makefile),$(INTERNAL_PRODUCT))    errorendifTARGET_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 Tfbuild/core/main.mkDASH_ARGS MODULESARGS,还是一个make命令。注意make后加了-C T,packages/apps/使mmm.makefmakefileCT是必要的

6. 总结

本文以介绍lunch和mmm命令作为编译系统的开篇,特别是lunch命令,虽然它没有编译任何文件,但是它仍然使用了make命令,并且加载了一大堆的makefile,后面编译都以lunch为基础,并使用了lunch设置的很多变量。

原创粉丝点击