在Android开发环境中编译一个目标时,一般要执行下面三行命令:
$ . build/envsetup.sh
$ lunch <product_name>-<build_variant>
$ make [module]
这三行命令是什么意思呢?下面逐一介绍。
envsetup.sh是个shell脚本,位于build目录下,第一行命令便是执行这个脚本。执行脚本有多种方式,那为什么要使用句点来执行呢?传统的执行脚本方式有下面两种:
$ bash envsetup.sh
$ chmod +x envsetup.sh; ./envsetup.sh
上面两种执行方式的效果一样,都是在当前shell的子shell中进行的,类似于一个子进程,其结果是在脚本执行结束之后,脚本中的全局变量、函数等在当前shell是访问不到的,脚本的执行结果也不会影响当前shell,但是,这个脚本从名字上来看是做一些环境配置相关的事情,事实上也确实如此,这样的话脚本执行结果应该对当前shell有效,而句点命令就满足这个要求,另外,“source”命令也有同样的效果,可以替代句点命令。
以Android 6.0 marshmallow版本为例,这个脚本有1560行代码,里面都有些什么东西呢?这个脚本的一大特点就是定义了一系列函数,几乎占据了脚本的全部内容,这些函数可以在脚本内部调用,使用句点命令执行脚本之后,这些函数也可以在当前shell调用。下面先罗列一下这些函数的名字,多达70个,它们以空格分隔。
addcompletions add_lunch_combo cgrep check_product check_variant choosecombo chooseproduct choosetype choosevariant core coredump_enable coredump_setup cproj croot findmakefile get_abs_build_var getbugreports get_build_var getdriver getlastscreenshot get_make_command getprebuilt getscreenshotpath getsdcardpath gettargetarch gettop ggrep godir hmm is isviewserverstarted jgrep key_back key_home key_menu lunch _lunch m make mangrep mgrep mm mma mmm mmma pez pid printconfig print_lunch_menu provision qpid rcgrep resgrep runhat runtest sepgrep set_java_home setpaths set_sequence_number set_stuff_for_environment settitle sgrep smoketest stacks startviewserver stopviewserver systemstack tapas tracedmdump treegrep
有这么多的函数,在shell中常用的也就几个,这些函数在脚本的第一个函数hmm中作了具体说明如下:
Invoke ". build/envsetup.sh" from your shell to add the following functions to your environment:
- lunch: lunch <product_name>-<build_variant>
- tapas: tapas [<App1> <App2> ...] [arm|x86|mips|armv5|arm64|x86_64|mips64] [eng|userdebug|user]
- croot: Changes directory to the top of the tree.
- m: Makes from the top of the tree.
- mm: Builds all of the modules in the current directory, but not their dependencies.
- mmm: Builds all of the modules in the supplied directories, but not their dependencies.
To limit the modules being built use the syntax: mmm dir/:target1,target2.
- mma: Builds all of the modules in the current directory, and their dependencies.
- mmma: Builds all of the modules in the supplied directories, and their dependencies.
- provision: Flash device with all required partitions. Options will be passed on to fastboot.
- cgrep: Greps on all local C/C++ files.
- ggrep: Greps on all local Gradle files.
- jgrep: Greps on all local Java files.
- resgrep: Greps on all local res/*.xml files.
- mangrep: Greps on all local AndroidManifest.xml files.
- mgrep: Greps on all local Makefiles files.
- sepgrep: Greps on all local sepolicy files.
- sgrep: Greps on all local source files.
- godir: Go to the directory containing a file.
当我们执行这个脚本的时候,与这些函数都没什么关系,能够执行到的代码有下面几行。
首先,是一个if语句,判断当前shell即SHELL这个系统变量是否为bash,不是bash时给出警告:
if [ "x$SHELL" != "x/bin/bash" ]; then
case `ps -o command -p $$` in
*bash*)
;;
*)
echo "WARNING: Only bash is supported, use of other shell would lead to erroneous results"
;;
esac
fi
然后,通过一个for循环查找device、vendor、product这几个目录下名为vendorsetup.sh的脚本,test命令测试被查找对象是否为一个目录,为目录时执行find命令开始查找,包括链接文件,目录查找深度为4,标准错误输出重定向到/dev/null,sort排序增加了下面echo命令的可读性,文件找到后立刻通过句点命令执行这个文件,for循环结束后unset变量f以防止全局变量的不良影响。
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` \
`test -d product && find -L product -maxdepth 4 -name 'vendorsetup.sh' 2> /dev/null | sort`
do
echo "including $f"
. $f
done
unset f
最后,执行addcompletions函数,如果shell不是bash或者bash的版本小于3的话将直接return,该函数的真正目的是执行sdk/adb_completion目录下所有以.bash结尾的脚本:
addcompletions
addcompletions函数的定义如下:
function addcompletions()
{
local T dir f
if [ -z "${BASH_VERSION}" ]; then
return
fi
if [ ${BASH_VERSINFO[0]} -lt 3 ]; then
return
fi
dir="sdk/bash_completion"
if [ -d ${dir} ]; then
for f in `/bin/ls ${dir}/[a-z]*.bash 2> /dev/null`; do
echo "including $f"
. $f
done
fi
}
下面是envsetup.sh这个脚本执行的输出,显示了执行的外部脚本的路径:
including device/asus/deb/vendorsetup.sh
including device/asus/flo/vendorsetup.sh
including device/asus/fugu/vendorsetup.sh
including device/generic/mini-emulator-arm64/vendorsetup.sh
including device/generic/mini-emulator-armv7-a-neon/vendorsetup.sh
including device/generic/mini-emulator-mips/vendorsetup.sh
including device/generic/mini-emulator-x86_64/vendorsetup.sh
including device/generic/mini-emulator-x86/vendorsetup.sh
including device/htc/flounder/vendorsetup.sh
including device/huawei/angler/vendorsetup.sh
including device/lge/bullhead/vendorsetup.sh
including device/lge/hammerhead/vendorsetup.sh
including device/moto/shamu/vendorsetup.sh
including sdk/bash_completion/adb.bash
以device/asus/deb/vendorsetup.sh为例,里面有一行内容如下:
add_lunch_combo aosp_deb-userdebug
上面的代码调用了add_lunch_combo函数,参数为aosp_deb-userdebug,意思是添加一个产品,产品名为aosp_deb,编译条件为userdebug,其实在envsetup.sh脚本内部也添加了几个默认的产品:
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
sdk/bash_completion/adb.bash定义了许多adb相关的函数,它们都以_adb开头,实现Tab键自动补全命令的功能,关键代码如下:
if [[ $(type -t compopt) = "builtin" ]]; then
complete -F _adb adb
else
complete -o nospace -F _adb adb
fi
complete是bash的内建命令,-F表示执行adb命令时的自动补全功能由_adb函数提供,nospace表示自动补全命令的最后没有空格,默认是有空格的。
lunch是envsheup.sh中定义的一个函数,参数是前面介绍的通过add_lunch_combo函数添加的某个值,由两部分组成,产品名和编译条件,后者包括eng、userdebug、user,这三个选项在envsetup.sh中保存在了一个数组(或列表)中:
VARIANT_CHOICES=(user userdebug eng)
下面来看一下lunch函数的具体实现,它由两部分组成,第一部分是lunch函数本身,它的主要任务就是检查函数参数的正确性,即产品名是否正确、编译条件是否正确。首先,如果我们给lunch命令传入了参数它就开始检查这个参数,否则调用print_lunch_menu函数告诉我们lunch命令的参数有哪些候选项,通过read命令读入,这时我们可以输入某个具体的参数或者参数前面的数字序列号,两者都可以被正确处理。然后调用check_product检查产品名是否正确,调用check_variant检查编译条件是否正确。最后就是一些系统环境变量的设置,供makefile使用。
function lunch()
{
local answer
if [ "$1" ] ; then
answer=$1
else
print_lunch_menu
echo -n "Which would you like? [aosp_arm-eng] "
read answer
fi
local selection=
if [ -z "$answer" ]
then
selection=aosp_arm-eng
elif (echo -n $answer | grep -q -e "^[0-9][0-9]*$")
then
if [ $answer -le ${#LUNCH_MENU_CHOICES[@]} ]
then
selection=${LUNCH_MENU_CHOICES[$(($answer-1))]}
fi
elif (echo -n $answer | grep -q -e "^[^\-][^\-]*-[^\-][^\-]*$")
then
selection=$answer
fi
if [ -z "$selection" ]
then
echo
echo "Invalid lunch combo: $answer"
return 1
fi
export TARGET_BUILD_APPS=
local product=$(echo -n $selection | sed -e "s/-.*$//")
check_product $product
if [ $? -ne 0 ]
then
echo
echo "** Don't have a product spec for: '$product'"
echo "** Do you have the right repo manifest?"
product=
fi
local variant=$(echo -n $selection | sed -e "s/^[^\-]*-//")
check_variant $variant
if [ $? -ne 0 ]
then
echo
echo "** Invalid variant: '$variant'"
echo "** Must be one of ${VARIANT_CHOICES[@]}"
variant=
fi
if [ -z "$product" -o -z "$variant" ]
then
echo
return 1
fi
export TARGET_PRODUCT=$product
export TARGET_BUILD_VARIANT=$variant
export TARGET_BUILD_TYPE=release
echo
set_stuff_for_environment
printconfig
}
第二部分是_lunch函数,它的作用是实现lunch命令的候选参数的自动补全功能,通过Tab键完成,熟悉shell的话都知道通过Tab键实现命令自动补全,这个函数使用了一些相关的系统变量,如COMPREPLY、COMP_WORDS、COMP_CWORD,以及bash内建的命令completion和compgen,它们有其固定的用法。
function _lunch()
{
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
COMPREPLY=( $(compgen -W "${LUNCH_MENU_CHOICES[*]}" -- ${cur}) )
return 0
}
complete -F _lunch lunch
下面是lunch命令后跟着回车键的结果:
You're building on Linux
Lunch menu... pick a combo:
1. aosp_arm-eng
2. aosp_arm64-eng
3. aosp_mips-eng
4. aosp_mips64-eng
5. aosp_x86-eng
6. aosp_x86_64-eng
7. aosp_deb-userdebug
8. aosp_flo-userdebug
9. full_fugu-userdebug
10. aosp_fugu-userdebug
11. mini_emulator_arm64-userdebug
12. m_e_arm-userdebug
13. mini_emulator_mips-userdebug
14. mini_emulator_x86_64-userdebug
15. mini_emulator_x86-userdebug
16. aosp_flounder-userdebug
17. aosp_angler-userdebug
18. aosp_bullhead-userdebug
19. aosp_hammerhead-userdebug
20. aosp_hammerhead_fp-userdebug
21. aosp_shamu-userdebug
Which would you like? [aosp_arm-eng]
然后选择对应的combo就可以了,比如说选择“5”或者“aosp_x86-eng”,两者的结果都是一样的,如下:
============================================
PLATFORM_VERSION_CODENAME=REL
PLATFORM_VERSION=6.0.1
TARGET_PRODUCT=aosp_x86
TARGET_BUILD_VARIANT=eng
TARGET_BUILD_TYPE=release
TARGET_BUILD_APPS=
TARGET_ARCH=x86
TARGET_ARCH_VARIANT=x86
TARGET_CPU_VARIANT=
TARGET_2ND_ARCH=
TARGET_2ND_ARCH_VARIANT=
TARGET_2ND_CPU_VARIANT=
HOST_ARCH=x86_64
HOST_OS=linux
HOST_OS_EXTRA=Linux-3.13.0-74-generic-x86_64-with-Ubuntu-14.04-trusty
HOST_CROSS_OS=windows
HOST_BUILD_TYPE=release
BUILD_ID=MASTER
OUT_DIR=out
============================================
上面的结果与选择的“aosp_x86-eng”相匹配。
make很简单了,就是执行makefile,Android源码根目录下只有一个简单的Makefile,只读文件:
### DO NOT EDIT THIS FILE ###a
include build/core/main.mk
### DO NOT EDIT THIS FILE ###
这个文件是必须的,一般不要修改,真正的东西在build/core/main.mk中。
有一点很重要,执行make命令时,真正的编译过程开始于envsetup.sh中的make函数,源码如下:
function make()
{
local start_time=$(date +"%s")
$(get_make_command) "$@"
local ret=$?
local end_time=$(date +"%s")
local tdiff=$(($end_time-$start_time))
local hours=$(($tdiff / 3600 ))
local mins=$((($tdiff % 3600) / 60))
local secs=$(($tdiff % 60))
local ncolors=$(tput colors 2>/dev/null)
if [ -n "$ncolors" ] && [ $ncolors -ge 8 ]; then
color_failed=$'\E'"[0;31m"
color_success=$'\E'"[0;32m"
color_reset=$'\E'"[00m"
else
color_failed=""
color_success=""
color_reset=""
fi
echo
if [ $ret -eq 0 ] ; then
echo -n "${color_success}#### make completed successfully "
else
echo -n "${color_failed}#### make failed to build some targets "
fi
if [ $hours -gt 0 ] ; then
printf "(%02g:%02g:%02g (hh:mm:ss))" $hours $mins $secs
elif [ $mins -gt 0 ] ; then
printf "(%02g:%02g (mm:ss))" $mins $secs
elif [ $secs -gt 0 ] ; then
printf "(%s seconds)" $secs
fi
echo " ####${color_reset}"
echo
return $ret
}
关键代码在于这一行:
$(get_make_command) "$@"
执行get_make_command命令的返回值为“command make”,“$@”的意思是make命令后面的参数。make函数其它代码的作用就是根据编译结果计算整个编译过程所花的时间,编译结果的输出还有个简单的配色方案。