android编译系统分析一:source build/envsetup.sh与lunch

Android编译系统分析系列文章:

android编译系统分析一
Android编译系统<二>-mm编译单个模块
android编译系统分析(三)-make
android编译系统(四)-实战:新增一个产品
Android编译系统分析(五)-system.img的生成过程


虽然已经有很多人分析过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)
}

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

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

android编译系统分析一:source build/envsetup.sh与lunch_第1张图片

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中的命令,它的功能是执行指定的命令,
这里就是执行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
这里就是判断这个函数的返回值,-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,并最终导出这些信息,供正式编译的时候使用。


你可能感兴趣的:(Android编译系统)