Android Build System[一]

更多干货,请关注微信公众号: 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_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 Tfbuild/core/main.mk DASH_ARGS MODULES ARGS,还是一个make命令。注意make后加了-C T,packages/apps/使mmm.makefmakefileC T是必要的

6. 总结

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

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值