Android Build系统结构梳理(上)

前言

狗仔我自从入职新公司大家庭以来,面对新环境、新同事、新开发平台,这个适应时间有些略长,到这儿才发现自己之前的学习跟这里对上口的不多,毕竟是以产品为主的公司,对硬件和驱动的需求远远大于上层应用及framework层,所以新的一轮学习要来了。相信做好自己熟悉的领域模块,总会有用武之地!吐槽的话不多说,上菜!
这次梳理了一下Android整个系统编译的相关知识,之前一直是使用source、lunch、make等指令去编译代码,并没有深入了解这些指令的含义,对于如何编译这么庞大的Android代码也是知之甚少,正好最近公司有专业组让去学习了解这些知识,所以借此机会学习梳理一下。

概述

小编参考的环境是高通835平台Android O系统代码,使用ubuntu进行编译,Android整个系统的编译使用如下指令:

  1. 进Andoid系统代码根目录
  2. 在Terminal中执行source build/envsetup.sh
  3. 执行lunch,选择需要编译的单板配置
  4. 执行m -j8(注:-j*的数字是跟处理器核数相关,一般最大是核数的两倍)

接下来我们就一步一步分析。

Android编译代码目录

Android build系统主要是依靠Makefile和相关shell脚本、python脚本去搜寻相关依赖并执行编译过程的,代码目录主要在下面:
build系统代码目录
各个厂商设备方自定义产品信息会在系统代码根目录的device目录下去创建相关配置文件,一般命名方式为device/厂商名称/产品名称,例如device/xiaomi/Mi6,然后在Mi6目录下添加AndroidProducts.mk、BoardConfig.mk、mi6.mk、vendorsetup.sh这四个文件即可自定义编译产品,后续会出一篇博客专门介绍如何添加自定义编译模块,届时会介绍这四个文件的编写方式。

source执行过程

source执行主要是载入device目录和vendor目录下3级目录内所有的vendorsetup.sh文件,我们先看执行source命令时后面跟的sh脚本主要内容:

// 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.
- 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.

Environment 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 "/^[[:blank:]]*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
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)
}

# 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

/ ... /

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` \
         `test -d product && find -L product -maxdepth 4 -name 'vendorsetup.sh' 2> /dev/null | sort`
do
    echo "including $f"
    . $f
done
unset f

addcompletions

执行source指令主要是加载编译过程中使用的方法和添加的单板信息,在hmm方法中我们看到注释里提到了许多我们常用的编译指令,方法说明如下:
编译方法说明EOF关键词就是在EOF前后包括的内容被视为文档内容,就理解为打开的文档一样,hmm方法主要就是打印常用编译指令及其简单说明。
再往后看会有一个全局变量数组 LUNCH_MENU_CHOICES,执行了unset释放函数初始化数据,此数组记录了所有添加的单板信息,在 add_lunch_combo 方法会将未添加的配置信息添加到 LUNCH_MENU_CHOICES 数组中。我们看到,在envsetup.sh中已经调用了 add_lunch_combo 方法添加了默认的几个配置信息:

# 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

脚本最后会先检查shell脚本的执行环境是否合法,之后再查找vendor、device、product四级目录下单板信息并打印,就是找是否存在vendorsetup.sh,之后执行查找出的vendorsetup.sh脚本,将所有单板信息都加入到数组 LUNCH_MENU_CHOICES 中。

lunch执行过程

lunch执行的目的是过滤单板信息,导出选择的编译产品和版本,供正式编译的时候使用。首先先看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 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

    local product=$(echo -n $selection | sed -e "s/-.*$//")
    TARGET_PRODUCT=$product \
    TARGET_BUILD_VARIANT=$variant \
    build_build_var_cache
    if [ $? -ne 0 ]
    then
        echo
        echo "** Don't have a product spec for: '$product'"
        echo "** Do you have the right repo manifest?"
        product=
    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
    export LC_ALL=C

    echo

    set_stuff_for_environment
    printconfig
    destroy_build_var_cache
}

我们逐步分析,首先判断执行lunch指令时是否带入参,如果不存在,则执行方法 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 这个数组变量吗?这个在source的过程中缓存了所有单板的信息,这里一一进行打印。
之后对输入的单板信息进行合法性校验,并保存到局部变量 selection 中,比如你选择了msm8998-userdebug这个单板进行编译,那么此时

selection = msm8998-userdebug

之后通过sed指令去replace符号“-”前后并分别赋值给productvariant,此时

product = msm8998
variant = userdebug

在获取到variant值后调用check_variant对此进行校验:

VARIANT_CHOICES=(user userdebug eng)

# check to see if the supplied variant is valid
function check_variant()
{
    for v in ${VARIANT_CHOICES[@]}
    do
        if [ "$v" = "$1" ]
        then
            return 0
        fi
    done
    return 1
}

这里就是对编译版本类型进行判断,只有user、userdebug、eng三个是合法的。
之后将产品信息和编译版本类型赋值给变量TARGET_PRODUCTTARGET_BUILD_VARIANT
赋值后build_build_var_cache方法用于创建var_cache_xxx='yyy’和abs_var_cache_xxx='yyy’的键值对,用于存储环境变量。之后又赋值给全局变量。再之后就调用了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打印配置信息。
至此,lunch函数就已经结束。

m -j* 指令执行过程

Android编译系统是依赖式编译系统,编译是一步一步扩展蔓延开来,正是因为这一点,它的编译时间比一般的系统编译慢很多,但也正是因为这一点,使得配置编译条件变的有据可循。
前面的两条指令都是为执行make指令做环境变量的准备,我们先来看m函数的定义:

function m()
{
    local T=$(gettop)
    local DRV=$(getdriver $T)
    if [ "$T" ]; then
        $DRV make -C $T -f build/core/main.mk $@
    else
        echo "Couldn't locate the top of the tree.  Try setting TOP."
        return 1
    fi
}

如果我们暂且不看-j* 的带参,这个函数首先去执行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
}

之后调用getdriver,虽然不知道此方法是何用,但测试执行后返回为空,所以暂且不关注这个函数,之后执行了make指令:

make -C $T -f build/core/main.mk $@
//make -C 系统代码根目录 -f build/core/main.mk 所带所有入参
//-C 执行make之前先进入后面的目录
//-f 指定后面的文件为编译mk文件 

我们在根目录下也可以执行make -j来编译系统代码,或者 make -C 系统根目录 -j来执行系统编译,Linux执行make时会优先找当前目录下Makefile**文件,在系统根目录下有一个此文件:

### DO NOT EDIT THIS FILE ###
include build/core/main.mk
### DO NOT EDIT THIS FILE ###

文件中只有一句话,这就跟m指令进行编译相吻合了。
由于main.mk文件过于庞大,我们就分析一下这个文件的相关结构及属性。
首先我们来看它包含的mk文件结构,此入口囊括了Android编译的所有文件:
Makefile包含结构下面列一下主要mk文件的功能:

文件名作用
main.mk最主要的 Make 文件,该文件中首先将对编译环境进行检查,同时引入其他的 Make 文件
help.mk包含了名称为 help 的 Make 目标的定义,该目标将列出主要的 Make 目标及其说明
pathmap.mk将许多头文件的路径通过名值对的方式定义为映射表,并提供 include-path-for 函数来获取。例如,通过$(call include-path-for, frameworks-native)便可以获取到 framework 本地代码需要的头文件路径
envsetup.mk配置 Build 系统需要的环境变量,例如:TARGET_PRODUCT,TARGET_BUILD_VARIANT,HOST_OS,HOST_ARCH 等。当前编译的主机平台信息(例如操作系统,CPU 类型等信息)就是在这个文件中确定的。另外,该文件中还指定了各种编译结果的输出路径
combo/select.mk根据当前编译器的平台选择平台相关的 Make 文件
dumpvar.mk在 Build 开始之前,显示此次 Build 的配置信息
config.mk整个 Build 系统的配置文件,最重要的 Make 文件之一。该文件中主要包含以下内容:定义了许多的常量来负责不同类型模块的编译。定义编译器参数以及常见文件后缀,例如 .zip,.jar,.apk。根据 BoardConfig.mk 文件,配置产品相关的参数。设置一些常用工具的路径,例如 flex,e2fsck,dx
definitions.mk最重要的 Make 文件之一,在其中定义了大量的函数。这些函数都是 Build 系统的其他文件将用到的。例如:my-dir,all-subdir-makefiles,find-subdir-files,sign-package 等,关于这些函数的说明请参见每个函数的代码注释
distdir.mk针对 dist 目标的定义。dist 目标用来拷贝文件到指定路径
dex_preopt.mk针对启动 jar 包的预先优化
pdk_config.mk针对 pdk(Platform Developement Kit)的配置文件
post_clean.mk在前一次 Build 的基础上检查当前 Build 的配置,并执行必要清理工作。
host_static_library.mk定义了如何编译主机上的静态库
host_shared_library.mk定义了如何编译主机上的共享库
static_library.mk定义了如何编译设备上的静态库
shared_library.mk定义了如何编译设备上的共享库
executable.mk定义了如何编译设备上的可执行文件
host_executable.mk定义了如何编译主机上的可执行文件
package.mk定义了如何编译 APK 文件
prebuilt.mk定义了如何处理一个已经编译好的文件 ( 例如 Jar 包 )
multi_prebuilt.mk定义了如何处理一个或多个已编译文件,该文件的实现依赖 prebuilt.mk
host_prebuilt.mk处理一个或多个主机上使用的已编译文件,该文件的实现依赖 multi_prebuilt.mk
java_library.mk定义了如何编译设备上的共享 Java 库
static_java_library.mk定义了如何编译设备上的静态 Java 库
host_java_library.mk定义了如何编译主机上的共享 Java 库

可见由一个文件就逐步扩展为整个系统的编译mk文件。

一、依赖浅析

在我们执行make指令时候,如果没带指定编译模块,那会指定默认的编译目标:

// build/core/main.mk

# This is the default target.  It must be the first declared target.
.PHONY: droid
DEFAULT_GOAL := droid
$(DEFAULT_GOAL): droid_targets

.PHONY目标并非实际的文件名:只是在显式请求时执行命令的名字。使用它基本有两种原因:防止文件名字重复; 提高执行效率。具体这个关键字含义自行百度,在这里定义了一个伪执行目标droid,所以我们要接着找此目标的依赖,一步步延伸下去。下面是简单的主题部分:
droid文件展开

配置产品信息

我们从main.mk文件中可以梳理出如下图的加载结构出来(主要是include模块展开):
mk文件include流程
通常会在device下AndroidProducts.mk中添加产品的配置信息,BoardConfig.mk配置主板属性,main.mk文件会经过一系列的方法会找到device目录下定义的产品信息并加载进来。

中断

由于后续篇幅较多,看代码的进度比较缓慢,故而拆为两个博文输出,敬请期待~

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值