​javascript:void(0)​


虽然已经有很多人分析过​​Android​​的编译系统的代码了,我也看过他们的博客,也学到了不少知识,但单纯的看别人分析,终究还是理解的不深入,所以,我还是要自己再认真的分析一遍。

想想我们编译android系统的过程:

首先:source build/envsetup.sh

其次:lunch    ---选择一个特定的类型

最后:make

按着这个顺序,追踪这看似简单的几步,到底有哪些背后的秘密?   

1. source build/envsetup.sh

这个文件虽然很大,但暂且不需要统统看一遍。它里面定义了很多函数,这些函数在使用的时候再具体详细学习,现在主要看看这个脚本做的事情。即便如此,打开这个脚本后第一个函数还是非常吸引人的,因为它里面介绍了这个脚本主要要做的事情:

[html]​ view plain​​​ ​​ copy​​​ ​​ print​​​​?​​​​​

  1. function hmm() {
  2. cat <<EOF
  3. Invoke ". build/envsetup.sh" from your shell to add the following functions to your environment:
  4. - lunch:   lunch <product_name>-<build_variant>
  5. - tapas:   tapas [<App1> <App2> ...] [arm|x86|mips|armv5|arm64|x86_64|mips64] [eng|userdebug|user]
  6. - croot:   Changes directory to the top of the tree.
  7. - m:       Makes from the top of the tree.
  8. - mm:      Builds all of the modules in the current directory, but not their dependencies.
  9. - mmm:     Builds all of the modules in the supplied directories, but not their dependencies.
  10. To limit the modules being built use the syntax: mmm dir/:target1,target2.
  11. - mma:     Builds all of the modules in the current directory, and their dependencies.
  12. - mmma:    Builds all of the modules in the supplied directories, and their dependencies.
  13. - cgrep:   Greps on all local C/C++ files.
  14. - ggrep:   Greps on all local Gradle files.
  15. - jgrep:   Greps on all local Java files.
  16. - resgrep: Greps on all local res/*.xml files.
  17. - mangrep: Greps on all local AndroidManifest.xml files.
  18. - sepgrep: Greps on all local sepolicy files.
  19. - sgrep:   Greps on all local source files.
  20. - godir:   Go to the directory containing a file.
  21. Environemnt options:
  22. - SANITIZE_HOST: Set to 'true' to use ASAN for all host modules. Note that
  23. ASAN_OPTIONS=detect_leaks=0 will be set by default until the
  24. build is leak-check clean.
  25. Look at the source to view more functions. The complete list is:
  26. EOF
  27. T=$(gettop)
  28. local A
  29. A=""
  30. for i in `cat $T/build/envsetup.sh | sed -n "/^[ \t]*function /s/function [a−z]∗.*/\1/p" | sort | uniq`; do
  31. A="$A $i"
  32. done
  33. echo $A
  34. }

这个函数列出来主要是它介绍了这个脚本的一些功能,第一行cat <<EOP是一个HERE文档,意思就是把EOF后面到下一个EOF前面的内容当做一个文件,然后cat 会接收这个文件的内容,而cat默认的输出是标准输出,也就是这个文件的内容会被打印到屏幕上来。这些内容介绍了这个脚本的用法和功能,用法就是“. build/envsetup.sh”注意.后面有个空格,这个.命令就是source命令,也就是说你也可以执行“source build/envsetup.sh”。此外,这个函数介绍了很多函数的功能,比如lunch,m,mm,mmm,cgrep等。

函数只有在调用到它的时候才会执行,所以暂时统统不看,现在只看函数外面的内容:

[plain]​ view plain​​​ ​​ copy​​​ ​​ print​​​​?​​​​​

  1. # Clear this variable.  It will be built up again when the vendorsetup.sh
  2. # files are included at the end of this file.
  3. unset LUNCH_MENU_CHOICES

这里调用了unset命令,unset是一个bash命令,它会删除给定的变量。也就是说它会删除LUNCH_MENU_CHOICES变量。既然这里刻意把它删除了,那么它肯定要立刻重新构造这个变量了,果然:

[plain]​ view plain​​​ ​​ copy​​​ ​​ print​​​​?​​​​​

  1. # add the default one here
  2. add_lunch_combo aosp_arm-eng
  3. add_lunch_combo aosp_arm64-eng
  4. add_lunch_combo aosp_mips-eng
  5. add_lunch_combo aosp_mips64-eng
  6. add_lunch_combo aosp_x86-eng
  7. add_lunch_combo aosp_x86_64-eng

这几行调用了add_lunch_combo函数,并且传入了一些参数,这使得我们执行lunch函数的时候,多了这几条可以选择的选项。

而这个函数,就是就是重新构造LUNCH_MENU_CHOICES变量:

[html]​ view plain​​​ ​​ copy​​​ ​​ print​​​​?​​​​​

  1. function add_lunch_combo()
  2. {
  3. local new_combo=$1
  4. local c
  5. for c in ${LUNCH_MENU_CHOICES[@]} ; do
  6. if [ "$new_combo" = "$c" ] ; then
  7. return
  8. fi
  9. done
  10. LUNCH_MENU_CHOICES=(${LUNCH_MENU_CHOICES[@]} $new_combo)
  11. }


了解过shell编程就知道,$1表示传入函数的第一个参数,然后又定义了一个变量c。

从for循环中可以看出,LUNCH_MENU_CHOICES是一个数组,在shell中,可以通过 "变量名[@]"或者“变量名[*]”的方式过得数组的所有项。然后注意比较数组中的每一项,如果数组中已经有传入的参数项,就继续返回,否则,就把新的传入的参数加入到LUNCH_MENU_CHOIES数组中。

这个脚本虽然很长,但是正真执行的代码没有多少,就是说当执行source build/envsetup.sh的时候执行的代码没有多少,它里面大多数内容都是函数的定义。在该文件的最后,又执行了一点代码:

[plain]​ view plain​​​ ​​ copy​​​ ​​ print​​​​?​​​​​

  1. if [ "x$SHELL" != "x/bin/bash" ]; then
  2. case `ps -o command -p $$` in
  3. *bash*)
  4. ;;
  5. *)
  6. echo "WARNING: Only bash is supported, use of other shell would lead to erroneous results"
  7. ;;
  8. esac
  9. fi
  10. # Execute the contents of any vendorsetup.sh files we can find.
  11. for f in `test -d device && find -L device -maxdepth 4 -name 'vendorsetup.sh' 2> /dev/null | sort` \
  12. `test -d vendor && find -L vendor -maxdepth 4 -name 'vendorsetup.sh' 2> /dev/null | sort`
  13. do
  14. echo "including $f"
  15. . $f
  16. done
  17. 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,会打印一下内容:

[html]​ view plain​​​ ​​ copy​​​ ​​ print​​​​?​​​​​

  1. including device/generic/mini-emulator-arm64/vendorsetup.sh
  2. including device/generic/mini-emulator-armv7-a-neon/vendorsetup.sh
  3. including device/generic/mini-emulator-mips/vendorsetup.sh
  4. including device/generic/mini-emulator-x86_64/vendorsetup.sh
  5. including device/generic/mini-emulator-x86/vendorsetup.sh

这里只列出了一部分,那以第一条为例,看看它的内容:

[html]​ view plain​​​ ​​ copy​​​ ​​ print​​​​?​​​​​

  1. add_lunch_combo mini_emulator_arm64-userdebug

这个函数我们已经分析过了,而且这个文件也只有这么一行,这里列出的这几个vendorsetup.sh脚本都是只有这么一行,他就是在lunch的菜单中添加一项。当然也不都是如此,在有些厂家的vendorsetup.sh也会做一些其他的工作,但是,不管做多少其他的事情,第一件事情似乎都是一定的,就是调用add_lunch_combo 添加一项。

因此,总结来说,envsetup.sh脚本做了这样的事情:

android编译系统分析(一)source build/envsetup.sh与lunch_html

3.lunch

编译android的时候,执行完souce build/envsetup.sh,我们还需要执行lunch,选择一个特定的单板。

[html]​ view plain​​​ ​​ copy​​​ ​​ print​​​​?​​​​​

  1. function lunch()
  2. {
  3. local answer
  4. if [ "$1" ] ; then
  5. answer=$1
  6. else
  7. print_lunch_menu
  8. echo -n "Which would you like? [aosp_arm-eng] "
  9. read answer
  10. fi
  11. local selection=
  12. if [ -z "$answer" ]
  13. then
  14. selection=aosp_arm-eng
  15. elif (echo -n $answer | grep -q -e "^[0-9][0-9]*$")
  16. then
  17. if [ $answer -le ${#LUNCH_MENU_CHOICES[@]} ]
  18. then
  19. selection=${LUNCH_MENU_CHOICES[$(($answer-1))]}
  20. fi
  21. elif (echo -n $answer | grep -q -e "^[^\-][^\-]*-[^\-][^\-]*$")
  22. then
  23. selection=$answer
  24. fi
  25. if [ -z "$selection" ]
  26. then
  27. echo
  28. echo "Invalid lunch combo: $answer"
  29. return 1
  30. fi
  31. export TARGET_BUILD_APPS=
  32. local product=$(echo -n $selection | sed -e "s/-.*$//")
  33. check_product $product
  34. if [ $? -ne 0 ]
  35. then
  36. echo
  37. echo "** Don't have a product spec for: '$product'"
  38. echo "** Do you have the right repo manifest?"
  39. product=
  40. fi
  41. local variant=$(echo -n $selection | sed -e "s/^[^\-]*-//")
  42. check_variant $variant
  43. if [ $? -ne 0 ]
  44. then
  45. echo
  46. echo "** Invalid variant: '$variant'"
  47. echo "** Must be one of ${VARIANT_CHOICES[@]}"
  48. variant=
  49. fi
  50. if [ -z "$product" -o -z "$variant" ]
  51. then
  52. echo
  53. return 1
  54. fi
  55. export TARGET_PRODUCT=$product
  56. export TARGET_BUILD_VARIANT=$variant
  57. export TARGET_BUILD_TYPE=release
  58. echo
  59. set_stuff_for_environment
  60. printconfig
  61. }

这个函数首先判断有没有传入参数,如果传入了就把值赋给answer这个变量,如果没有传参数,也就是说知识执行了lunch,那么就会调用fprint_lunch_menu函数显示一份菜单出来,显示出来后,接受用户的输入。

print_lunch_menu函数如下:

[html]​ view plain​​​ ​​ copy​​​ ​​ print​​​​?​​​​​

  1. function print_lunch_menu()
  2. {
  3. local uname=$(uname)
  4. echo
  5. echo "You're building on" $uname
  6. echo
  7. echo "Lunch menu... pick a combo:"
  8. local i=1
  9. local choice
  10. for choice in ${LUNCH_MENU_CHOICES[@]}
  11. do
  12. echo "     $i. $choice"
  13. i=$(($i+1))
  14. done
  15. echo
  16. }

可以看到,这个函数输出了一些头信息以后,就输出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函数:

[html]​ view plain​​​ ​​ copy​​​ ​​ print​​​​?​​​​​

  1. # check to see if the supplied product is one we can build
  2. function check_product()
  3. {
  4. T=$(gettop)
  5. if [ ! "$T" ]; then
  6. echo "Couldn't locate the top of the tree.  Try setting TOP." >&2
  7. return
  8. fi
  9. TARGET_PRODUCT=$1 \
  10. TARGET_BUILD_VARIANT= \
  11. TARGET_BUILD_TYPE= \
  12. TARGET_BUILD_APPS= \
  13. get_build_var TARGET_DEVICE > /dev/null
  14. # hide successful answers, but allow the errors to show
  15. }


函数的一开始就调用了gettop函数,所以,我们得先弄明白这个函数的功能:

[java]​ view plain​​​ ​​ copy​​​ ​​ print​​​​?​​​​​

  1. function gettop
  2. {
  3. local TOPFILE=build/core/envsetup.mk
  4. if [ -n "$TOP" -a -f "$TOP/$TOPFILE" ] ; then
  5. # The following circumlocution ensures we remove symlinks from TOP.
  6. (cd $TOP; PWD= /bin/pwd)
  7. else
  8. if [ -f $TOPFILE ] ; then
  9. # The following circumlocution (repeated below as well) ensures
  10. # that we record the true directory name and not one that is
  11. # faked up with symlink names.
  12. PWD= /bin/pwd
  13. else
  14. local HERE=$PWD
  15. T=
  16. while [ !\(−f$TOPFILE \) -a \( $PWD != "/" \) ]; do
  17. \cd ..
  18. T=`PWD= /bin/pwd -P`
  19. done
  20. \cd $HERE
  21. if [ -f "$T/$TOPFILE" ]; then
  22. echo $T
  23. fi
  24. fi
  25. fi
  26. }


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这个函数,这个函数如下:

[plain]​ view plain​​​ ​​ copy​​​ ​​ print​​​​?​​​​​

  1. # Get the exact value of a build variable.
  2. function get_build_var()
  3. {
  4. T=$(gettop)
  5. if [ ! "$T" ]; then
  6. echo "Couldn't locate the top of the tree.  Try setting TOP." >&2
  7. return
  8. fi
  9. (\cd $T; CALLED_FROM_SETUP=true BUILD_SYSTEM=build/core \
  10. command make --no-print-directory -f build/core/config.mk dumpvar-$1)
  11. }

乍看之下,这个函数非常简单,一开始,就是获得android源码的顶层目录,然后检查是否获得成功,这在之前已经分析过了。做完这些以后,迎来了一个看着很奇怪的语句。这个语句先是进入到android源码的顶层目录,然后然后定义了两个变量,之后使用command执行了一条命令。command是一个shell中的命令,它的功能是执行指定的命令,

这里就是执行make命令,-f给它指定了makefile,同时还传入了一个目标。然后,config.mk就会执行。

这个函数,我们不妨感性认识一下先:在命令行中执行get_build_var TARGET_DEVICE

可以看到打印了generic这个值。这个函数虽然分析起来比较复杂,但是它做的非常简单。config.mk会include dumpvar.mk,这个文件中会提取我们传入的dumpvar-TARGET_DEVICE变量中的TARGET_DEVICE,然后打印$(TARGET_DEVICE)。所以它做的事情很简单。

这个函数执行完以后,有返回到lunch函数继续执行:

[plain]​ view plain​​​ ​​ copy​​​ ​​ print​​​​?​​​​​

  1. if [ $? -ne 0 ]
  2. then
  3. echo
  4. echo "** Don't have a product spec for: '$product'"
  5. echo "** Do you have the right repo manifest?"
  6. product=
  7. fi

这里就是判断这个函数的返回值,-ne是不等于的意思,如果返回值不等于0,那么就出问题了。

假定一切正常,继续执行代码:

[plain]​ view plain​​​ ​​ copy​​​ ​​ print​​​​?​​​​​

  1. local variant=$(echo -n $selection | sed -e "s/^[^\-]*-//")
  2. check_variant $variant
  3. if [ $? -ne 0 ]
  4. then
  5. echo
  6. echo "** Invalid variant: '$variant'"
  7. echo "** Must be one of ${VARIANT_CHOICES[@]}"
  8. variant=
  9. fi

这段代码时提取variant并且检查是否合法,不要忘了前面说过,add_lunch_combo添加的字符串就是product-variant格式的字符串,这两部分代码非常相似,就不具体分析这段代码了。

下面的代码是:

[plain]​ view plain​​​ ​​ copy​​​ ​​ print​​​​?​​​​​

  1. if [ -z "$product" -o -z "$variant" ]
  2. then
  3. echo
  4. return 1
  5. fi

检查得到的product和variant是不是为空,空就不可以 了。

[plain]​ view plain​​​ ​​ copy​​​ ​​ print​​​​?​​​​​

  1. export TARGET_PRODUCT=$product
  2. export TARGET_BUILD_VARIANT=$variant
  3. export TARGET_BUILD_TYPE=release

然后把辛辛苦苦得到的product和variant变量复制给全局变量并且导出为环境变量。以后看到这几个变量,知道它们的值就可以了。

然后调用了set_stuff_for_environment函数,这个函数内容如下:

[plain]​ view plain​​​ ​​ copy​​​ ​​ print​​​​?​​​​​

  1. function set_stuff_for_environment()
  2. {
  3. settitle
  4. set_java_home
  5. setpaths
  6. set_sequence_number
  7. export ANDROID_BUILD_TOP=$(gettop)
  8. # With this environment variable new GCC can apply colors to warnings/errors
  9. export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01'
  10. export ASAN_OPTIONS=detect_leaks=0
  11. }

这个函数会继续补充一些变量。其中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,并最终导出这些信息,供正式编译的时候使用。