Android编译系统分析系列文章:
android编译系统分析一
虽然已经有很多人分析过android的编译系统的代码了,我也看过他们的博客,也学到了不少知识,但单纯的看别人分析,终究还是理解的不深入,所以,我还是要自己再认真的分析一遍。
想想我们编译android系统的过程:
首先:source build/envsetup.sh
其次:lunch ---选择一个特定的类型
最后:make
按着这个顺序,追踪这看似简单的几步,到底有哪些背后的秘密?
1. source build/envsetup.sh
这个文件虽然很大,但暂且不需要统统看一遍。它里面定义了很多函数,这些函数在使用的时候再具体详细学习,现在主要看看这个脚本做的事情。即便如此,打开这个脚本后第一个函数还是非常吸引人的,因为它里面介绍了这个脚本主要要做的事情:
function hmm() {
cat <-
- tapas: tapas [ ...] [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.
- 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.
- sepgrep: Greps on all local sepolicy files.
- sgrep: Greps on all local source files.
- godir: Go to the directory containing a file.
Environemnt options:
- SANITIZE_HOST: Set to 'true' to use ASAN for all host modules. Note that
ASAN_OPTIONS=detect_leaks=0 will be set by default until the
build is leak-check clean.
Look at the source to view more functions. The complete list is:
EOF
T=$(gettop)
local A
A=""
for i in `cat $T/build/envsetup.sh | sed -n "/^[ \t]*function /s/function \([a-z_]*\).*/\1/p" | sort | uniq`; do
A="$A $i"
done
echo $A
}
这个函数列出来主要是它介绍了这个脚本的一些功能,第一行cat <函数只有在调用到它的时候才会执行,所以暂时统统不看,现在只看函数外面的内容:
# Clear this variable. It will be built up again when the vendorsetup.sh
# files are included at the end of this file.
unset LUNCH_MENU_CHOICES
这里调用了unset命令,unset是一个bash命令,它会删除给定的变量。也就是说它会删除LUNCH_MENU_CHOICES变量。既然这里刻意把它删除了,那么它肯定要立刻重新构造这个变量了,果然:
# add the default one here
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
这几行调用了add_lunch_combo函数,并且传入了一些参数,这使得我们执行lunch函数的时候,多了这几条可以选择的选项。
而这个函数,就是就是重新构造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)
}
从for循环中可以看出,LUNCH_MENU_CHOICES是一个数组,在shell中,可以通过 "变量名[@]"或者“变量名[*]”的方式过得数组的所有项。然后注意比较数组中的每一项,如果数组中已经有传入的参数项,就继续返回,否则,就把新的传入的参数加入到LUNCH_MENU_CHOIES数组中。
这个脚本虽然很长,但是正真执行的代码没有多少,就是说当执行source build/envsetup.sh的时候执行的代码没有多少,它里面大多数内容都是函数的定义。在该文件的最后,又执行了一点代码:
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
# Execute the contents of any vendorsetup.sh files we can find.
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
unset f
这里使用了shell的反引号的用法,它的作用就是把反引号的内容当做shell命令来执行。然后把执行结果使用echo输出到屏幕。反引号中,首先使用test命令测试 device目录是否存在,存在的话就查找vendorsetup.sh脚本,并且设定了查找深度为4层目录。2> /dev/null 是shell中的重定向语法,这里把标准错误重定向到/dev/null中,也就是,把错误统统删掉,左后把找到的vendorsetup.sh脚本使用sort命令进行排序。
. $f相当于source $f,也就是source /device和/vendor下找到的所有vendorsetup.sh脚本。然后,这个脚本就分析结束了,接下来,自然是要去分析/device和/vendor下的vendorsetup.sh脚本了。
2./device/vendor下的vendorsetup.sh
执行一下source build/envsetup.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
这里只列出了一部分,那以第一条为例,看看它的内容:
add_lunch_combo mini_emulator_arm64-userdebug
这个函数我们已经分析过了,而且这个文件也只有这么一行,这里列出的这几个vendorsetup.sh脚本都是只有这么一行,他就是在lunch的菜单中添加一项。当然也不都是如此,在有些厂家的vendorsetup.sh也会做一些其他的工作,但是,不管做多少其他的事情,第一件事情似乎都是一定的,就是调用add_lunch_combo 添加一项。
因此,总结来说,envsetup.sh脚本做了这样的事情:
3.lunch
编译android的时候,执行完souce build/envsetup.sh,我们还需要执行lunch,选择一个特定的单板。
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
}
这个函数首先判断有没有传入参数,如果传入了就把值赋给answer这个变量,如果没有传参数,也就是说知识执行了lunch,那么就会调用fprint_lunch_menu函数显示一份菜单出来,显示出来后,接受用户的输入。
print_lunch_menu函数如下:
function print_lunch_menu()
{
local uname=$(uname)
echo
echo "You're building on" $uname
echo
echo "Lunch menu... pick a combo:"
local i=1
local choice
for choice in ${LUNCH_MENU_CHOICES[@]}
do
echo " $i. $choice"
i=$(($i+1))
done
echo
}
可以看到,这个函数输出了一些头信息以后,就输出LUNCH_MENU_CHOICES数组,这里通过for遍历整个这个数组,打印每一项。这个数组在之前就已经分析过,它的每一项是通过调用add_lunch_combo函数添加进来的。
1.不管执行lunch的时候有没有传入参数,最终都会将选择的结果存入到answer变量中,那么接下来,当然是检查输入的参数的合法性了。这里首先判断answer变量是不是为0,如果为零的话,selection变量就会赋值为aosp_arm-eng,但是如果不为零的话,首先会输出answer的值,使用echo -n $answer,-n选项是的输出的时候不输出换行符,answer变量的值并没有输出到屏幕上,而是通过管道传给了后面一条命令:grep -q -e "^[0-9][0-9]*$",这条命令从answer变量中搜寻以两位数字开头的字符串,如果找到,就认为是输入的是数字。然后进一步对这个数字做有没有越界的检查。如果这个数字小于LUNCH_MENU_CHOICES的大小,就会把LUNCH_MENU_CHOICES的地$answer-1项复制给selection变量。
2.如果传入的不是数字,就会尝试匹配这样的字符串grep -q -e "^[^\-][^\-]*-[^\-][^\-]*$",-q标示quiet模式,也就是不打印信息,-e表示后面跟的是一个用于匹配的模式,这里,^标示匹配开始,[]中的^则标示排除,\是转译字符,因为-可能是表示范围的符号。所以,这里匹配的是不以连续两个--开始,后面跟任意字符,然后必须有个-,最后又不以--结束的字符串。其实,这里就是期望得到product-varient的形式。也就是我们之前使用add_lunch_combo添加的那些字符串的格式。比如:aosp_arm-eng,product就是aosp_arm,varient就是eng.中间的-是必须的。如果发现符合要求的格式的话selection变量就会被$answer赋值,也就是说,selection其实就是一个product-varient模式的字符串。
3.如果既不是数字,又不是合法的字符串,或者是数字,这个时候,selection应该就没有被赋值过,这个时候-z就成立了,那么就会提示你输入的东西不对。并且直接返回。
如果输入合法,通过检测后, export TARGET_BUILD_APPS= 这行导出了一个变量,但是它的值是空的。而之后的 local product=$(echo -n $selection | sed -e "s/-.*$//")这句则是得到了product部分,也就是把varient部分砍掉了。这里使用了sed编辑器,-e 表示执行多条命令,但这里只有一条,双引号的s表示替换,这里就是把-后面接任意字符,然后以任意字符结尾的部分替换为空,也就是砍掉-后面的了。得到produce后就开始检查product的合法性。 check_product $produc ,这里使用了check_product函数:
# check to see if the supplied product is one we can build
function check_product()
{
T=$(gettop)
if [ ! "$T" ]; then
echo "Couldn't locate the top of the tree. Try setting TOP." >&2
return
fi
TARGET_PRODUCT=$1 \
TARGET_BUILD_VARIANT= \
TARGET_BUILD_TYPE= \
TARGET_BUILD_APPS= \
get_build_var TARGET_DEVICE > /dev/null
# hide successful answers, but allow the errors to show
}
函数的一开始就调用了gettop函数,所以,我们得先弄明白这个函数的功能:
function gettop
{
local TOPFILE=build/core/envsetup.mk
if [ -n "$TOP" -a -f "$TOP/$TOPFILE" ] ; then
# The following circumlocution ensures we remove symlinks from TOP.
(cd $TOP; PWD= /bin/pwd)
else
if [ -f $TOPFILE ] ; then
# The following circumlocution (repeated below as well) ensures
# that we record the true directory name and not one that is
# faked up with symlink names.
PWD= /bin/pwd
else
local HERE=$PWD
T=
while [ \( ! \( -f $TOPFILE \) \) -a \( $PWD != "/" \) ]; do
\cd ..
T=`PWD= /bin/pwd -P`
done
\cd $HERE
if [ -f "$T/$TOPFILE" ]; then
echo $T
fi
fi
fi
}
gettop函数一开始定义了一个局部变量TOPFILE,并且给他赋了值,然后是一个测试语句:if [ -n "$TOP" -a -f "$TOP/$TOPFILE" ] ; then,这里-n 是判断 $TOP是否不为空, -a 就是and的意思,和C语言中的&&相同, -f是判断给定的变量是不是文件,那么,这个测试语句就是如果 $TOP不为空 切同时 $TOP/$TOPFILE文件存在,就执行下面的代码:
(cd $TOP; PWD= /bin/pwd)
也就是进入到$TOP目录下,并且给PWD变量赋一个pwd命令的返回值,也就是当前目录的路劲。我试着在这个脚本中搜索TOP变量,发现它并没有出现并且赋值,所以,这里应该执行else部分。else中,首先判断build/core/envsetup.mk这个文件是否存在,当在源码顶层目录下的时候,这个文件是存在的,那么这里为真,然后PWD变量就是android代码的根目录。所以如果souce build/envsetup.sh的时候,如果处于android源码的顶级目录,那么这个函数就返回了。关于shell函数的返回值问题,还需要留意一下,当一个函数没有返回任何内容的时候,默认返回的是最后一条命令的执行结果,也就是这里的/bin/pwd的结果。那当然就是android源码的顶级目录了。这个时候如果不在顶级目录,build/core/envsetup.mk应该不存在,这个时候就会while循环不断的进入道上层目录,然后判断$TOPFILE是否存在,并且判断是否到达根目录了,如果这个文件不存在且没有到达根目录,那么就会一个往上一级目录查找。最终如果找到了这个文件,就意味着找到了android源码的顶层目录,并把这个路劲返回。前面的两次判断如果都成立的话也没有返回任何东西,是因为,当前目录肯定就是源码的顶级目录了。也就是说,这个函数就是找到源码的顶级目录,如果当前目录就是顶级目录,就什么也不返回,如果当前目录不是顶级目录,就返回顶级目录的路劲。
再回过头来看check_product函数,可以看到在获取到android源码的顶级目录以后,就会判断这个T是不是空值,空的的话就说明没有获取到顶级目录,这个时候这个函数就直接返回了。如果一切正常,那么就会定义几个变量。
TARGET_PRODUCT=$1 \
TARGET_BUILD_VARIANT= \
TARGET_BUILD_TYPE= \
TARGET_BUILD_APPS= \
这几个变量只有一个变量都是全局变量,因为没有加local关键字修饰,它们中,只有第一个变量赋值为$1,也就是这个函数的第一个参数。目前,这几个变量的作用还不得而知,所以,我们继续向下分析。
这个函数还有最后一件事情要做:
get_build_var TARGET_DEVICE > /dev/null
这里调用了get_build_var这个函数,这个函数如下:
# Get the exact value of a build variable.
function get_build_var()
{
T=$(gettop)
if [ ! "$T" ]; then
echo "Couldn't locate the top of the tree. Try setting TOP." >&2
return
fi
(\cd $T; CALLED_FROM_SETUP=true BUILD_SYSTEM=build/core \
command make --no-print-directory -f build/core/config.mk dumpvar-$1)
}
乍看之下,这个函数非常简单,一开始,就是获得android源码的顶层目录,然后检查是否获得成功,这在之前已经分析过了。做完这些以后,迎来了一个看着很奇怪的语句。这个语句先是进入到android源码的顶层目录,然后然后定义了两个变量,之后使用command执行了一条命令。command是一个shell中的命令,它的功能是执行指定的命令,
这个函数,我们不妨感性认识一下先:在命令行中执行get_build_var TARGET_DEVICE
可以看到打印了generic这个值。这个函数虽然分析起来比较复杂,但是它做的非常简单。config.mk会include dumpvar.mk,这个文件中会提取我们传入的dumpvar-TARGET_DEVICE变量中的TARGET_DEVICE,然后打印$(TARGET_DEVICE)。所以它做的事情很简单。
这个函数执行完以后,有返回到lunch函数继续执行:
if [ $? -ne 0 ]
then
echo
echo "** Don't have a product spec for: '$product'"
echo "** Do you have the right repo manifest?"
product=
fi
这里就是判断这个函数的返回值,-ne是不等于的意思,如果返回值不等于0,那么就出问题了。
假定一切正常,继续执行代码:
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
这段代码时提取variant并且检查是否合法,不要忘了前面说过,add_lunch_combo添加的字符串就是product-variant格式的字符串,这两部分代码非常相似,就不具体分析这段代码了。
下面的代码是:
if [ -z "$product" -o -z "$variant" ]
then
echo
return 1
fi
检查得到的product和variant是不是为空,空就不可以 了。
export TARGET_PRODUCT=$product
export TARGET_BUILD_VARIANT=$variant
export TARGET_BUILD_TYPE=release
然后把辛辛苦苦得到的product和variant变量复制给全局变量并且导出为环境变量。以后看到这几个变量,知道它们的值就可以了。
然后调用了set_stuff_for_environment函数,这个函数内容如下:
function set_stuff_for_environment()
{
settitle
set_java_home
setpaths
set_sequence_number
export ANDROID_BUILD_TOP=$(gettop)
# With this environment variable new GCC can apply colors to warnings/errors
export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01'
export ASAN_OPTIONS=detect_leaks=0
}
这个函数会继续补充一些变量。其中set_java_home会检查导出JAVA_HOME这个环境变量,这个环境变量就是JDK坐在的路劲,setpaths函数会给PATH环境变量补充编译Android需要的一些路径。
最后lunch调用了 printconfig函数,这个函数打印出了配置信息。
至此,source build/envsetup.sh 和 lunch就分析完了。接下来将分析make 命令所做的事情。
小结:source build/envsetup.sh会调用add_lunch_combo函数添加很多单板信息进来,同时还会查找/device和/vendor下的vendorsetup.sh文件,查找深度为4级目录,找到后就执行它,它里面至少会有这么一行:add_lunch_combo xxxx,继续添加单板信息。lunch函数则会打印出所有的单板信息供你选择,你输入选择后,luch命令会对你的选择做一系列检测,并从中提取出product和varient,并最终导出这些信息,供正式编译的时候使用。