http://blog.csdn.net/u011913612/article/details/51878356
虽然已经有很多人分析过Android的编译系统的代码了,我也看过他们的博客,也学到了不少知识,但单纯的看别人分析,终究还是理解的不深入,所以,我还是要自己再认真的分析一遍。
想想我们编译android系统的过程:
首先:source build/envsetup.sh
其次:lunch ---选择一个特定的类型
最后:make
按着这个顺序,追踪这看似简单的几步,到底有哪些背后的秘密?
1. source build/envsetup.sh
这个文件虽然很大,但暂且不需要统统看一遍。它里面定义了很多函数,这些函数在使用的时候再具体详细学习,现在主要看看这个脚本做的事情。即便如此,打开这个脚本后第一个函数还是非常吸引人的,因为它里面介绍了这个脚本主要要做的事情:
- function hmm() {
- cat <<EOF
- 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.
- - 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 "/^[ ]*function /s/function [a−z]∗.*/1/p" | sort | uniq`; do
- A="$A $i"
- done
- echo $A
- }
函数只有在调用到它的时候才会执行,所以暂时统统不看,现在只看函数外面的内容:
- # 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
- # 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
而这个函数,就是就是重新构造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)
- }
了解过shell编程就知道,$1表示传入函数的第一个参数,然后又定义了一个变量c。
从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
前面的if语句块就是检查支持的shell解析器。后面的for语句块比较关键。
这里使用了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
因此,总结来说,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
- }
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
- }
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)
- }
这里就是执行make命令,-f给它指定了makefile,同时还传入了一个目标。然后,config.mk就会执行。
这个函数,我们不妨感性认识一下先:在命令行中执行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
假定一切正常,继续执行代码:
- 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
然后调用了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 命令所做的事情。