android 编译流程

在下载完android源码后,大家都会先尝试编译

source build/envsetup.sh
lunch sdk_phone_x86_64
make -j$(nproc --all)

执行这三步。就可以编译android源码,生成.img文件

这里我们主要聊聊为什么通过这三步我们可以进行编译

首先是build/envsetup.sh

在 Android 开发中,envsetup.sh 是一个重要的脚本文件,用于设置和初始化 Android 开发环境。该脚本位于 Android 源代码树的根目录下,通常是在执行编译、构建和调试等开发任务之前运行。

envsetup.sh 脚本主要完成以下几个任务:

设置环境变量:envsetup.sh 会设置一些必要的环境变量,包括指向 Android 源代码树的路径、构建工具链的路径、输出路径等。这些环境变量对于后续的编译、构建和调试操作是必需的。

导入函数和工具:envsetup.sh 中定义了一些有用的函数和工具,可以方便地执行各种常见的开发任务。通过执行 source envsetup.sh 命令,可以将这些函数和工具导入当前的命令行环境,从而可以在命令行中直接使用它们。

设置编译选项:envsetup.sh 提供了一些用于设置编译选项的函数。通过调用这些函数,可以选择要构建的目标平台、编译模式(例如调试模式或发布模式)、构建类型(完整构建或增量构建)等。

需要注意的是,envsetup.sh 是一个 Shell 脚本,它需要在命令行终端中运行。在运行之前,你需要先进入到 Android 源代码树的根目录,并确保已经安装了必要的环境依赖和工具链。

下面我们可以看下envsetup.sh脚本文件

function hmm() {
cat <<EOF

Run "m help" for help with the build system itself.

Invoke ". build/envsetup.sh" from your shell to add the following functions to your environment:
- lunch:      lunch <product_name>-<build_variant>
              Selects <product_name> as the product to build, and <build_variant> as the variant to
              build, and stores those selections in the environment to be read by subsequent
              invocations of 'm' etc.
- tapas:      tapas [<App1> <App2> ...] [arm|x86|arm64|x86_64] [eng|userdebug|user]
              Sets up the build environment for building unbundled apps (APKs).
- banchan:    banchan <module1> [<module2> ...] [arm|x86|arm64|x86_64] [eng|userdebug|user]
              Sets up the build environment for building unbundled modules (APEXes).
- croot:      Changes directory to the top of the tree, or a subdirectory thereof.
- m:          Makes from the top of the tree.
- mm:         Builds and installs all of the modules in the current directory, and their
              dependencies.
- mmm:        Builds and installs all of the modules in the supplied directories, and their
              dependencies.
              To limit the modules being built use the syntax: mmm dir/:target1,target2.
- mma:        Same as 'mm'
- mmma:       Same as 'mmm'
- 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.
- gogrep:     Greps on all local Go files.
- jgrep:      Greps on all local Java files.
- ktgrep:     Greps on all local Kotlin files.
- resgrep:    Greps on all local res/*.xml files.
- mangrep:    Greps on all local AndroidManifest.xml files.
- mgrep:      Greps on all local Makefiles and *.bp files.
- owngrep:    Greps on all local OWNERS files.
- rsgrep:     Greps on all local Rust files.
- sepgrep:    Greps on all local sepolicy files.
- sgrep:      Greps on all local source files.
- godir:      Go to the directory containing a file.
- allmod:     List all modules.
- gomod:      Go to the directory containing a module.
- pathmod:    Get the directory containing a module.
- outmod:     Gets the location of a module's installed outputs with a certain extension.
- dirmods:    Gets the modules defined in a given directory.
- installmod: Adb installs a module's built APK.
- refreshmod: Refresh list of modules for allmod/gomod/pathmod/outmod/installmod.
- syswrite:   Remount partitions (e.g. system.img) as writable, rebooting if necessary.

Environment options:
- SANITIZE_HOST: Set to 'address' to use ASAN for all host modules.
- ANDROID_QUIET_BUILD: set to 'true' to display only the essential messages.

Look at the source to view more functions. The complete list is:
EOF
    local T=$(gettop)
    local A=""
    local i
    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
}

因为envsetup文件中定义了太多的函数,所以我们只是挑其中一个函数简单看下,后续用到哪个函数,我们在去看对应函数

简单说下hmm函数,这个函数就是讲解在加载了envsetup脚本文件后,你可以通过它内部定义的函数都做些什么

cat << EOF
使用了 Here Document(文档内嵌)的方式,将多行文本传递给 cat 命令进行输出。在这里,文本被定义为从 <<EOF 开始,直到遇到下一个 EOF 为止的内容。

比如为什么使用lunch函数,为什么使用make或者m mm等函数构建编译的原因

这些感兴趣可以自己细看

接着往下看来到envsetup.sh文件的最底部

validate_current_shell
source_vendorsetup
addcompletions

这里调用了之前定义的函数,我们来依次分析

function validate_current_shell() {
    #获取电脑命令行环境的类型
    local current_sh="$(ps -o command -p $$)"
    #bash的话 获取类型定义check_type函数
    case "$current_sh" in
        *bash*)
            function check_type() { type -t "$1"; }
            ;;
    #zsh的话 同样
        *zsh*)
            function check_type() { type "$1"; }
            enable_zsh_completion ;;
        *)
        #除此之外的类型不支持会打印警告信息
            echo -e "WARNING: Only bash and zsh are supported.\nUse of other shell would lead to erroneous results."
            ;;
    esac
}

首先检查当前电脑的命令行环境,是否满足执行脚本文件函数

function source_vendorsetup() {
    unset VENDOR_PYTHONPATH
    local T="$(gettop)"
    #查找device vendor product目录下的allowed-vendorsetup_sh-files并将里面的内容写到allowed变量中
    #如果查找的allowed-vendorsetup_sh-files文件有多个,则直接退出并且打印警告信息
    allowed=
    for f in $(cd "$T" && find -L device vendor product -maxdepth 4 -name 'allowed-vendorsetup_sh-files' 2>/dev/null | sort); do
        if [ -n "$allowed" ]; then
            echo "More than one 'allowed_vendorsetup_sh-files' file found, not including any vendorsetup.sh files:"
            echo "  $allowed"
            echo "  $f"
            return
        fi
        allowed="$T/$f"
    done
    #根据allowed里的内容在device vendor product下找到vendorsetup.sh文件进行加载
    allowed_files=
    [ -n "$allowed" ] && allowed_files=$(cat "$allowed")
    for dir in device vendor product; do
        for f in $(cd "$T" && test -d $dir && \
            find -L $dir -maxdepth 4 -name 'vendorsetup.sh' 2>/dev/null | sort); do

            if [[ -z "$allowed" || "$allowed_files" =~ $f ]]; then
                echo "including $f"; . "$T/$f"
            else
                echo "ignoring $f, not in $allowed"
            fi
        done
    done
}
function addcompletions()
{
    local f=
    #确保函数在合适的 Shell 环境下运行,并且要求 Bash 的版本不低于 3
    # Keep us from trying to run in something that's neither bash nor zsh.
    if [ -z "$BASH_VERSION" -a -z "$ZSH_VERSION" ]; then
        return
    fi

    # Keep us from trying to run in bash that's too old.
    if [ -n "$BASH_VERSION" -a ${BASH_VERSINFO[0]} -lt 3 ]; then
        return
    fi
    #定义编译文件数组,检查这些文件是否需要添加编译
    local completion_files=(
      system/core/adb/adb.bash
      system/core/fastboot/fastboot.bash
      tools/asuite/asuite.sh
    )
    # Completion can be disabled selectively to allow users to use non-standard completion.
    # e.g.
    # ENVSETUP_NO_COMPLETION=adb # -> disable adb completion
    # ENVSETUP_NO_COMPLETION=adb:bit # -> disable adb and bit completion
    for f in ${completion_files[*]}; do
        if [ -f "$f" ] && should_add_completion "$f"; then
            . $f
        fi
    done

    if should_add_completion bit ; then
        complete -C "bit --tab" bit
    fi
    if [ -z "$ZSH_VERSION" ]; then
        # Doesn't work in zsh.
        complete -o nospace -F _croot croot
    fi
    #主要是命令补全相关
    complete -F _lunch lunch

    complete -F _complete_android_module_names pathmod
    complete -F _complete_android_module_names gomod
    complete -F _complete_android_module_names outmod
    complete -F _complete_android_module_names installmod
    complete -F _complete_android_module_names m
}

这里主要是设置相关的文件和命令自动补全,提高工作效率相关

在第二部分source_vendorsetup函数中,我们会根据allowed-vendorsetup_sh-files里的内容,找到对应路径的vendorsetup.sh脚本文件进行加载,这里还会调用脚本里的函数,也就是lunch时你看到的哪些产品

如下图所示


image.png

接下来查看lunch函数

function lunch()
{
    local answer
    #case 判断调用lunch函数传入的参数>1 直接退出 提示警告信息
    if [[ $# -gt 1 ]]; then
        echo "usage: lunch [target]" >&2
        return 1
    fi

    local used_lunch_menu=0
    #这里打印lunch可以选择的产品选项,等待用户输入
    if [ "$1" ]; then
        answer=$1
    else
        print_lunch_menu
        echo "Which would you like? [aosp_arm-eng]"
        echo -n "Pick from common choices above (e.g. 13) or specify your own (e.g. aosp_barbet-eng): "
        read answer
        used_lunch_menu=1
    fi
    #根据用户的输入确认最终lunch选择的产品
    local selection=

    if [ -z "$answer" ]
    then
        selection=aosp_arm-eng
    elif (echo -n $answer | grep -q -e "^[0-9][0-9]*$")
    then
        local choices=($(TARGET_BUILD_APPS= get_build_var COMMON_LUNCH_CHOICES))
        if [ $answer -le ${#choices[@]} ]
        then
            # array in zsh starts from 1 instead of 0.
            if [ -n "$ZSH_VERSION" ]
            then
                selection=${choices[$(($answer))]}
            else
                selection=${choices[$(($answer-1))]}
            fi
        fi
    else
        selection=$answer
    fi
    #解析用户选择的产品、变体和版本信息,并在解析过程中进行检查和处理,最终确定产品信息并进行错误处理。
    export TARGET_BUILD_APPS=

    local product variant_and_version variant version
    product=${selection%%-*} # Trim everything after first dash
    variant_and_version=${selection#*-} # Trim everything up to first dash
    if [ "$variant_and_version" != "$selection" ]; then
        variant=${variant_and_version%%-*}
        if [ "$variant" != "$variant_and_version" ]; then
            version=${variant_and_version#*-}
        fi
    fi

    if [ -z "$product" ]
    then
        echo
        echo "Invalid lunch combo: $selection"
        return 1
    fi
    #设置构建相关的环境变量,并执行一些构建前的准备工作
    TARGET_PRODUCT=$product \
    TARGET_BUILD_VARIANT=$variant \
    TARGET_PLATFORM_VERSION=$version \
    build_build_var_cache
    if [ $? -ne 0 ]
    then
        if [[ "$product" =~ .*_(eng|user|userdebug) ]]
        then
            echo "Did you mean -${product/*_/}? (dash instead of underscore)"
        fi
        return 1
    fi
    export TARGET_PRODUCT=$(get_build_var TARGET_PRODUCT)
    export TARGET_BUILD_VARIANT=$(get_build_var TARGET_BUILD_VARIANT)
    if [ -n "$version" ]; then
      export TARGET_PLATFORM_VERSION=$(get_build_var TARGET_PLATFORM_VERSION)
    else
      unset TARGET_PLATFORM_VERSION
    fi
    export TARGET_BUILD_TYPE=release

    if [ $used_lunch_menu -eq 1 ]; then
      echo
      echo "Hint: next time you can simply run 'lunch $selection'"
    fi

    [[ -n "${ANDROID_QUIET_BUILD:-}" ]] || echo

    set_stuff_for_environment
    [[ -n "${ANDROID_QUIET_BUILD:-}" ]] || printconfig
    destroy_build_var_cache

    if [[ -n "${CHECK_MU_CONFIG:-}" ]]; then
      check_mu_config
    fi
}

大致内容就是检查你lunch 选择的产品,然后根据对应产品设置环境变量,为make构建项目做好准备

接下来就mm或者make来编译android系统
android系统相较于linux系统编译耗时较长的原因

  • 1 android系统的代码量对比linux系统足够大
  • 2 android支持非依赖式的模块编译,需要将每一个模块单编在编译一些其他文件最终构成镜像文件,这无疑是耗时的

对于envsetup.sh定义的shell函数,m mm mmm make都可以进行源码编译
但是前三者都是内部也都是封装的make

先看下前三个编译的函数

function m()
{
    local T=$(gettop)
    if [ "$T" ]; then
        _wrap_build $T/build/soong/soong_ui.bash --make-mode $@
    else
        echo "Couldn't locate the top of the tree.  Try setting TOP."
        return 1
    fi
}

首先获取android根目录,如果存在执行_wrap_build 传入参数

  • 1 $T/build/soong/soong_ui.bash 我这里理解为启动soong构建系统,为了编译解析bp文件
  • 2 --make-mode $@ 编译参数用于传递给soong脚本
    可以看到在m函数中,已经只能使用soong来编译,对于传统的make编译直接舍弃
    由这一点我们也可以看出android希望soong取代make的决心
function mm()
{
    #获取android根目录
    local T=$(gettop)
    # If we're sitting in the root of the build tree, just do a
    # normal build.
    #检查是否存在soong_ui.bash文件
    if [ -f build/soong/soong_ui.bash ]; then
        #有的话执行启动soong编译
        _wrap_build $T/build/soong/soong_ui.bash --make-mode $@
    else
        #查找指定目录mk文件(没查到会去该目录的上一层继续查找)
        # Find the closest Android.mk file.
        local M=$(findmakefile)
        local MODULES=
        local GET_INSTALL_PATH=
        local ARGS=
        # Remove the path to top as the makefilepath needs to be relative
        #echo 输出M的值 也就是mk文件的路径
        #sed函数对输入文本进行内容替换 这里将mk文件的路径中包含android根目录的路径清空
        local M=`echo $M|sed 's:'$T'/::'`
        #检查android根目录是否存在
        if [ ! "$T" ]; then
            echo "Couldn't locate the top of the tree.  Try setting TOP."
            return 1
        #检查mk文件是否存在
        elif [ ! "$M" ]; then
            echo "Couldn't locate a makefile from the current directory."
            return 1
        else
            #定义变量arg用于保存编译相关参数
            local ARG
            #遍历所有参数
            for ARG in $@; do
                case $ARG in
                  GET-INSTALL-PATH) GET_INSTALL_PATH=$ARG;;
                esac
            done
            #这里检查GET_INSTALL_PATH是否存在
            if [ -n "$GET_INSTALL_PATH" ]; then
              MODULES=
              #这里将mk文件和GET-INSTALL-PATH-IN关联到一起
              ARGS=GET-INSTALL-PATH-IN-$(dirname ${M})
              #这里将/转换层-
              ARGS=${ARGS//\//-}
            else
              #这里指代编译的mk的文件的所有模块
              MODULES=MODULES-IN-$(dirname ${M})
              # Convert "/" to "-".
              MODULES=${MODULES//\//-}
              ARGS=$@
            fi
            if [ "1" = "${WITH_TIDY_ONLY}" -o "true" = "${WITH_TIDY_ONLY}" ]; then
              MODULES=tidy_only
            fi
            #这里进行编译操作 编译指定mk文件的所有模块
            ONE_SHOT_MAKEFILE=$M _wrap_build $T/build/soong/soong_ui.bash --make-mode $MODULES $ARGS
        fi
    fi
}

它同时支持了 Soong 编译和传统的 Makefile 编译。在函数中,首先检查是否存在 build/soong/soong_ui.bash 文件,如果存在则使用 Soong 构建系统进行编译,否则会查找最接近的 Android.mk 文件并使用soong系统进行操作。
到这里我们看到不管是bp文件还是mk文件都由soong系统来尝试编译

在进行 Soong 编译时,它会调用 _wrap_build 函数来启动 Soong 编译,并根据传入的参数来执行相应的编译操作。而对于传统的 Makefile 编译,它会解析传入的参数,然后调用 _wrap_build 启动 Soong 编译,但是传递的参数会稍有不同

mm函数是编译指定目录的mk/bp文件里的所有模块

分析mmm编译单个模块的命令

function mmm()
{
    #获取根目录
    local T=$(gettop)
    #检查根目录是否存在
    if [ "$T" ]; then
        local MAKEFILE=
        local MODULES=
        local MODULES_IN_PATHS=
        local ARGS=
        local DIR TO_CHOP
        local DIR_MODULES
        local GET_INSTALL_PATH=
        local GET_INSTALL_PATHS=
        local DASH_ARGS=$(echo "$@" | awk -v RS=" " -v ORS=" " '/^-.*$/')
        local DIRS=$(echo "$@" | awk -v RS=" " -v ORS=" " '/^[^-].*$/')
        #遍历目录参数
        for DIR in $DIRS ; do
            DIR_MODULES=`echo $DIR | sed -n -e 's/.*:\(.*$\)/\1/p' | sed 's/,/ /'`
            DIR=`echo $DIR | sed -e 's/:.*//' -e 's:/$::'`
            # Remove the leading ./ and trailing / if any exists.
            DIR=${DIR#./}
            DIR=${DIR%/}
            #目录下只要存在mk文件或者bp文件
            if [ -f $DIR/Android.mk -o -f $DIR/Android.bp ]; then
                local TO_CHOP=`(\cd -P -- $T && pwd -P) | wc -c | tr -d ' '`
                local TO_CHOP=`expr $TO_CHOP + 1`
                #获取当前工作目录的绝对路径
                local START=`PWD= /bin/pwd`
                #截取工作目录的绝对路径
                local MDIR=`echo $START | cut -c${TO_CHOP}-`
                #根据传入的路径信息 $T 和目标文件夹名称 $DIR,计算出目标目录 MDIR 的绝对路径
                if [ "$MDIR" = "" ] ; then
                    MDIR=$DIR
                else
                    MDIR=$MDIR/$DIR
                fi
                MDIR=${MDIR%/.}
                if [ "$DIR_MODULES" = "" ]; then
                    MODULES_IN_PATHS="$MODULES_IN_PATHS MODULES-IN-$MDIR"
                    GET_INSTALL_PATHS="$GET_INSTALL_PATHS GET-INSTALL-PATH-IN-$MDIR"
                else
                    MODULES="$MODULES $DIR_MODULES"
                fi
                MAKEFILE="$MAKEFILE $MDIR/Android.mk"
            else
                #输出没有相关配置文件信息 
                case $DIR in
                  showcommands | snod | dist | *=*) ARGS="$ARGS $DIR";;
                  GET-INSTALL-PATH) GET_INSTALL_PATH=$DIR;;
                  *) if [ -d $DIR ]; then
                         echo "No Android.mk in $DIR.";
                     else
                         echo "Couldn't locate the directory $DIR";
                     fi
                     return 1;;
                esac
            fi
        done
        #检查GET_INSTALL_PATH值,存在则将GET_INSTALL_PATH值由/变为-赋值给ARGS
        if [ -n "$GET_INSTALL_PATH" ]; then
          ARGS=${GET_INSTALL_PATHS//\//-}
          MODULES=
          MODULES_IN_PATHS=
        fi
        #获取WITH_TIDY_ONLY值为 1 或者 true 设置moudles
        if [ "1" = "${WITH_TIDY_ONLY}" -o "true" = "${WITH_TIDY_ONLY}" ]; then
          MODULES=tidy_only
          MODULES_IN_PATHS=
        fi
        # Convert "/" to "-".
        MODULES_IN_PATHS=${MODULES_IN_PATHS//\//-}
        #通过/soong_ui.bash 来编译
        ONE_SHOT_MAKEFILE="$MAKEFILE" _wrap_build $T/build/soong/soong_ui.bash --make-mode $DASH_ARGS $MODULES $MODULES_IN_PATHS $ARGS
    else
        echo "Couldn't locate the top of the tree.  Try setting TOP."
        return 1
    fi
}

mmm里根据条件定义好编译相关变量参数,通过 _wrap_build函数通过soong执行编译操作,它的特点在于可以指定目录和指定模块进行编译

在研究m mm mmm函数的时候 我们发现都涉及/build/soong/soong_ui.bash这个文件,而这个文件就是进入soong编译系统的入口
接下来查看soong_ui.bash这个文件

function gettop
{
    local TOPFILE=build/soong/root.bp
    #检查TOP变量是否为空 -a 逻辑与 -f表示该路径是否存在文件
    if [ -z "${TOP-}" -a -f "${TOP-}/${TOPFILE}" ] ; then
        #如果TOP为空,但是${TOP-}/${TOPFILE}不为空
        # The following circumlocution ensures we remove symlinks from TOP.
        #pwd指获取真实目录路径
        (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.
            #TOP不为空且TOPFILE也不为空 尝试查找目录真实路径
            PWD= /bin/pwd
        else
            local HERE=$PWD
            T=
            #只要 $TOPFILE 文件不存在且当前工作目录不是根目录 "/",则条件成立
            while [ \( ! \( -f $TOPFILE \) \) -a \( $PWD != "/" \) ]; do
            #先回到上一级目录 在尝试查找目录真实路径
                \cd ..
                T=`PWD= /bin/pwd -P`
            done
            #在这里检查TOPFILE是否存在,存在则echo输出该文件路径
            \cd $HERE
            if [ -f "$T/$TOPFILE" ]; then
                echo $T
            fi
        fi
    fi
}

# Save the current PWD for use in soong_ui
#设置ORIGINAL_PWD环境变量值
export ORIGINAL_PWD=${PWD}
#设置TOP的值
export TOP=$(gettop)
#加载/microfactory.bash文件
source ${TOP}/build/soong/scripts/microfactory.bash
#执行soong_build_go函数
soong_build_go soong_ui android/soong/cmd/soong_ui
#进入根目录
cd ${TOP}
#执行soong_ui程序
exec "$(getoutdir)/soong_ui" "$@"
  • 1 首先命令行加载gettop函数
  • 2 设置ORIGINAL_PWD环境变量值
    将当前的工作目录路径 ${PWD} 存储在环境变量 ORIGINAL_PWD 中
  • 3 设置TOP环境变量
    将TOPFILE的路径传递给TOP
  • 4 加载/build/soong/scripts/microfactory.bash文件
  • 5 执行soong_build_go函数 传入参数soong_ui android/soong/cmd/soong_ui
  • 6 进入根目录
  • 7 执行根目录下的soong_ui程序
    可以看到除了一些函数的加载,重要的操作有三步分别是4,5,6
    这里我们继续分析/build/soong/scripts/microfactory.bash脚本文件做了什么
#根据uname类型 设置GOROOT的值
#设置 GOROOT 环境变量,指向 prebuild 的 go 编译工具链
case $(uname) in
    Linux)
        export GOROOT="${TOP}/prebuilts/go/linux-x86/"
        ;;
    Darwin)
        export GOROOT="${TOP}/prebuilts/go/darwin-x86/"
        ;;
    *) echo "unknown OS:" $(uname) >&2 && exit 1;;
esac

# Find the output directory
#获取输出目录
function getoutdir
{
    local out_dir="${OUT_DIR-}"
    if [ -z "${out_dir}" ]; then
        #尝试取OUT_DIR_COMMON_BASE的值 默认为-
        if [ "${OUT_DIR_COMMON_BASE-}" ]; then
        #OUT_DIR_COMMON_BASE存在则设置out_dir 的路径
            out_dir="${OUT_DIR_COMMON_BASE}/$(basename ${TOP})"
        else
            out_dir="out"
        #不存在则设置为out
            out_dir="out"
        fi
    fi
    if [[ "${out_dir}" != /* ]]; then
    #如果out_dir不以/开头的绝对路径
    #相对路径不会以/开头 他们以父目录开头
    #设置out_dir的路径值
        out_dir="${TOP}/${out_dir}"
    fi
    #输出out_dir的路径值
    echo "${out_dir}"
}

# Bootstrap microfactory from source if necessary and use it to build the
# requested binary.
#
# Arguments:
#  $1: name of the requested binary
#  $2: package name
function soong_build_go
{
    #属性赋值
    BUILDDIR=$(getoutdir) \
      SRCDIR=${TOP} \
      BLUEPRINTDIR=${TOP}/build/blueprint \
      EXTRA_ARGS="-pkg-path android/soong=${TOP}/build/soong" \
      #执行build_go函数
      build_go $@
}
#加载/build/blueprint/microfactory/microfactory.bash文件
source ${TOP}/build/blueprint/microfactory/microfactory.bash
  • 1 根据uname设置GOROOT的值
    根据不同体系结构 后续的构建和编译过程能够正确地使用相应的 Go 工具链。
  • 2 定义getoutdir函数
  • 3 定义soong_build_go函数
  • 4 加载/build/blueprint/microfactory/microfactory.bash文件
    这里我们接着看/build/blueprint/microfactory/microfactory.bash文件
function build_go
{
    # Increment when microfactory changes enough that it cannot rebuild itself.
    # For example, if we use a new command line argument that doesn't work on older versions.
    local mf_version=3
    #设置相关属性
    local mf_src="${BLUEPRINTDIR}/microfactory"
    local mf_bin="${BUILDDIR}/microfactory_$(uname)"
    local mf_version_file="${BUILDDIR}/.microfactory_$(uname)_version"
    local built_bin="${BUILDDIR}/$1"
    local from_src=1
    #检查mf_bin和mf_version_file是否存在
    if [ -f "${mf_bin}" ] && [ -f "${mf_version_file}" ]; then
        if [ "${mf_version}" -eq "$(cat "${mf_version_file}")" ]; then
        #检查mf_version和mf_version_file里的值是否相等
            from_src=0
        fi
    fi

    local mf_cmd
    if [ $from_src -eq 1 ]; then
    #这里根据是否通过源码编译生成对应的mf_cmd命令
        # `go run` requires a single main package, so create one
        local gen_src_dir="${BUILDDIR}/.microfactory_$(uname)_intermediates/src"
        mkdir -p "${gen_src_dir}"
        sed "s/^package microfactory/package main/" "${mf_src}/microfactory.go" >"${gen_src_dir}/microfactory.go"

        mf_cmd="${GOROOT}/bin/go run ${gen_src_dir}/microfactory.go"
    else
        mf_cmd="${mf_bin}"
    fi
    #移除旧的trace文件
    rm -f "${BUILDDIR}/.$1.trace"
    # GOROOT must be absolute because `go run` changes the local directory
    #
    #构建go项目
    GOROOT=$(cd $GOROOT; pwd) ${mf_cmd} -b "${mf_bin}" \
            -pkg-path "github.com/google/blueprint=${BLUEPRINTDIR}" \
            -trimpath "${SRCDIR}" \
            ${EXTRA_ARGS} \
            -o "${built_bin}" $2
    # $?获取上一次命令执行的结果 等于0代表执行成功 且是通过源码构建的goxiangmu
    if [ $? -eq 0 ] && [ $from_src -eq 1 ]; then
    #echo > 将版本写入mf_version_file文件中
        echo "${mf_version}" >"${mf_version_file}"
    fi
}

build_go函数主要是根据变量来确认当前是否需要源码编译生成mf_cmd,再根据mf_cmd 使用GOROOT构建go项目,如果是源码编译且构建成功会更新版本

到这里我们了解了soong_ui.bash在加载时执行的source ${TOP}/build/soong/scripts/microfactory.bash文件又加载了哪些文件,以及这些文件又都做了什么

接下来我们分析soong_ui.bash的soong_build_go函数,因为前面已经对该函数做了讲解,我们这里就简单总结下主要就是构建go项目
soong_build_go soong_ui android/soong/cmd/soong_ui。其作用是调用 soong_build_go 函数。这个函数有两个参数,从第一步的分析可以知道,soong_build_go 实际上是一个对 build_go() 函数的调用封装,所以以上语句等价于 build_go soong_ui android/soong/cmd/soong_ui。第一参数 soong_ui 是指定了编译生成的可执行程序的名字, soong_ui 是一个用 go 语言写的程序,也是 Soong 的实际执行程序。在第二个参数告诉 soong_build_go 函数,soong_ui 程序的源码在哪里,这里制定了其源码路径 android/soong/cmd/soong_ui(实际对应的位置是 build/soong/cmd/soong_ui)

那么最后一步执行exec函数执行这个go程序soong_ui
这一步相当于等价替换了原来传统意义上的 make @ 现在是用out/soong_ui 来处理编译"@"代表编译的相关参数
整体流程如下图所示

未命名文件 (2).png

到这里我们算是知道了android新的编译系统soong是如何启动来开始编译的
接下来我们分析soong系统的搭建
在android8以上采用soong编译系统来编译bp和mk文件,bp采用blueprint来编译生成.ninja文件,mk采用kanti来编译生成ninja文件 最后交给ninja来处理

Soong 子系统都是用 go 语言写的,其主文件是 build/soong/cmd/soong_ui/main.go

func main() {
    log := logger.New(os.Stderr)
    defer log.Cleanup()

    if len(os.Args) < 2 || !(inList("--make-mode", os.Args) ||
        os.Args[1] == "--dumpvars-mode" ||
        os.Args[1] == "--dumpvar-mode") {

        log.Fatalln("The `soong` native UI is not yet available.")
    }

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    trace := tracer.New(log)
    defer trace.Close()

    build.SetupSignals(log, cancel, func() {
        trace.Close()
        log.Cleanup()
    })
    //创建buildcontext 它包含上下文 log,trace等等
    buildCtx := build.Context{&build.ContextImpl{
        Context:        ctx,
        Logger:         log,
        Tracer:         trace,
        StdioInterface: build.StdioImpl{},
    }}
    //构建build.Config对象
    var config build.Config
    if os.Args[1] == "--dumpvars-mode" || os.Args[1] == "--dumpvar-mode" {
        //如果是dumpvars-mode或dumpvar-mode 我们根据buildctx创建config对象
        config = build.NewConfig(buildCtx)
    } else {
        //如果不是这俩个那就是make-node 这代表当次为编译操作,构建config对象要加入相关编译参数
        config = build.NewConfig(buildCtx, os.Args[1:]...)
    }

    log.SetVerbose(config.IsVerbose())
    build.SetupOutDir(buildCtx, config)

    if config.Dist() {
        logsDir := filepath.Join(config.DistDir(), "logs")
        os.MkdirAll(logsDir, 0777)
        log.SetOutput(filepath.Join(logsDir, "soong.log"))
        trace.SetOutput(filepath.Join(logsDir, "build.trace"))
    } else {
        log.SetOutput(filepath.Join(config.OutDir(), "soong.log"))
        trace.SetOutput(filepath.Join(config.OutDir(), "build.trace"))
    }

    if start, ok := os.LookupEnv("TRACE_BEGIN_SOONG"); ok {
        if !strings.HasSuffix(start, "N") {
            if start_time, err := strconv.ParseUint(start, 10, 64); err == nil {
                log.Verbosef("Took %dms to start up.",
                    time.Since(time.Unix(0, int64(start_time))).Nanoseconds()/time.Millisecond.Nanoseconds())
                buildCtx.CompleteTrace("startup", start_time, uint64(time.Now().UnixNano()))
            }
        }

        if executable, err := os.Executable(); err == nil {
            trace.ImportMicrofactoryLog(filepath.Join(filepath.Dir(executable), "."+filepath.Base(executable)+".trace"))
        }
    }
    // build.FindSources 会创建 `out/.module_paths` 这个目录并在这个目录下产生一些
    // 特殊的文件记录。譬如我们比较关心的 `out/.module_paths/Android.bp.list` 这个
    // 文件,打开这个文件我们会看到里面记录了 AOSP 项目中所有 Android.bp 文件的路径
    // 届时后面的操作会根据这里记录的项目对这些 Android.bp 文件进行分析,进而产生
    // 最终的 build.ninja 文件。
    f := build.NewSourceFinder(buildCtx, config)
    defer f.Shutdown()
    build.FindSources(buildCtx, config, f)

    if os.Args[1] == "--dumpvar-mode" {
        dumpVar(buildCtx, config, os.Args[2:])
    } else if os.Args[1] == "--dumpvars-mode" {
        dumpVars(buildCtx, config, os.Args[2:])
    } else {
        toBuild := build.BuildAll
        if config.Checkbuild() {
            toBuild |= build.RunBuildTests
        }
    // 前面的准备工作做好后,这里开始执行实质的构造动作,我们看到这里调用了一个关键的
    // 构造函数 `build.Build()`。这里传进入三个主要参数:buildCtx 和 config(这是
    // 前面创建的上下文对象和配置信息),还有一个 toBuild 是用来控制整个 Build 流程
    // 关键步骤的,我们可以通过这个第三个参数有选择地执行某些步骤,缺省是 BuildAll,
    // 也就是走一个完整的流程)
        build.Build(buildCtx, config, toBuild)
    }
}

这里我们主要介绍函数的关键部分

  • 1 build.Context
    通常用于在构建过程中传递上下文信息、日志记录、追踪和标准输入输出接口
  • 2 build.Config
    主要是定义构建配置项,这里我们看make-mode,所以会携带相关编译参数
  • 3 执行FindSources 创建out/.module_paths文件夹且内部包含的
    Android.bp.list`记录了AOSP项目所有bp文件的路径,后续会根据这些路径找到对应bp文件编译生成ninja文件
  • 4 build.Build 构建soong系统

继续我们的soong构建旅程,到这里我们调用了build.Build函数来构建
这个函数定义在build/soong/ui/build/build.go文件下

func Build(ctx Context, config Config, what int) {
//截取相关函数
    if what&BuildSoong != 0 {
        // Run Soong
        runSoong(ctx, config)
    }

    if what&BuildKati != 0 {
        // Run ckati
        runKati(ctx, config)

        ioutil.WriteFile(config.LastKatiSuffixFile(), []byte(config.KatiSuffix()), 0777)
    } else {
        // Load last Kati Suffix if it exists
        if katiSuffix, err := ioutil.ReadFile(config.LastKatiSuffixFile()); err == nil {
            ctx.Verboseln("Loaded previous kati config:", string(katiSuffix))
            config.SetKatiSuffix(string(katiSuffix))
        }
    }

    if what&BuildNinja != 0 {
        if !config.SkipMake() {
            installCleanIfNecessary(ctx, config)
        }

        // Run ninja
        runNinja(ctx, config)
    }

}

你可以理解为runSoong是为了将bp文件解析写到build.ninja中, runKati是为了将mk解析写到build.ninja中,当ninja不为空,处理ninja文件
j接下来分析runsoong函数

func runSoong(ctx Context, config Config) {
    ctx.BeginTrace("soong")
    defer ctx.EndTrace()

    func() {
        //开启trace跟踪blueprint构建系统引导过程
        ctx.BeginTrace("blueprint bootstrap")
        //结束时取消跟踪
        defer ctx.EndTrace()
        //创建命令 在"build/blueprint/bootstrap.bash中执行blueprint bootstrap
        cmd := Command(ctx, config, "blueprint bootstrap", "build/blueprint/bootstrap.bash", "-t")
        //设置执行命令需要的相关参数变量
        cmd.Environment.Set("BLUEPRINTDIR", "./build/blueprint")
        cmd.Environment.Set("BOOTSTRAP", "./build/blueprint/bootstrap.bash")
        cmd.Environment.Set("BUILDDIR", config.SoongOutDir())
        cmd.Environment.Set("GOROOT", "./"+filepath.Join("prebuilts/go", config.HostPrebuiltTag()))
        cmd.Environment.Set("BLUEPRINT_LIST_FILE", filepath.Join(config.FileListDir(), "Android.bp.list"))
        cmd.Environment.Set("NINJA_BUILDDIR", config.OutDir())
        cmd.Environment.Set("SRCDIR", ".")
        cmd.Environment.Set("TOPNAME", "Android.bp")
        cmd.Sandbox = soongSandbox
        //输出和错误重定向
        cmd.Stdout = ctx.Stdout()
        cmd.Stderr = ctx.Stderr()
        //执行命令
        //创建 out/soong/.minibootstrap/ 目录并在这个目录下创建一系列文件,
        //其中最重要的是 out/soong/.minibootstrap/build.ninja 这个文件。
        //这个文件很关键,是构造下一个阶段 bootstrap 的 ninja build 文件。
        cmd.RunOrFatal()
    }()

    func() {
        ctx.BeginTrace("environment check")
        defer ctx.EndTrace()

        envFile := filepath.Join(config.SoongOutDir(), ".soong.environment")
        envTool := filepath.Join(config.SoongOutDir(), ".bootstrap/bin/soong_env")
        if _, err := os.Stat(envFile); err == nil {
            if _, err := os.Stat(envTool); err == nil {
                cmd := Command(ctx, config, "soong_env", envTool, envFile)
                cmd.Sandbox = soongSandbox
                cmd.Stdout = ctx.Stdout()
                cmd.Stderr = ctx.Stderr()
                if err := cmd.Run(); err != nil {
                    ctx.Verboseln("soong_env failed, forcing manifest regeneration")
                    os.Remove(envFile)
                }
            } else {
                ctx.Verboseln("Missing soong_env tool, forcing manifest regeneration")
                os.Remove(envFile)
            }
        } else if !os.IsNotExist(err) {
            ctx.Fatalf("Failed to stat %f: %v", envFile, err)
        }
    }()

    func() {
        //构建minibp
        ctx.BeginTrace("minibp")
        defer ctx.EndTrace()

        var cfg microfactory.Config
        //将github.com/google/blueprint映射到build/blueprint
        cfg.Map("github.com/google/blueprint", "build/blueprint")

        cfg.TrimPath = absPath(ctx, ".")
        //设置minibp的执行路径/out/.minibootstrap/minibp
        minibp := filepath.Join(config.SoongOutDir(), ".minibootstrap/minibp")
        //通过microfactory来构建minibp程序
        if _, err := microfactory.Build(&cfg, minibp, "github.com/google/blueprint/bootstrap/minibp"); err != nil {
            ctx.Fatalln("Failed to build minibp:", err)
        }
    }()

    ninja := func(name, file string) {
        ctx.BeginTrace(name)
        defer ctx.EndTrace()

        cmd := Command(ctx, config, "soong "+name,
            config.PrebuiltBuildTool("ninja"),
            "-d", "keepdepfile",
            "-w", "dupbuild=err",
            "-j", strconv.Itoa(config.Parallel()),
            "-f", filepath.Join(config.SoongOutDir(), file))
        if config.IsVerbose() {
            cmd.Args = append(cmd.Args, "-v")
        }
        cmd.Sandbox = soongSandbox
        cmd.Stdin = ctx.Stdin()
        cmd.Stdout = ctx.Stdout()
        cmd.Stderr = ctx.Stderr()

        defer ctx.ImportNinjaLog(filepath.Join(config.OutDir(), ".ninja_log"), time.Now())
        cmd.RunOrFatal()
    }
    //这里根据".minibootstrap/build.ninja来生成.bootstrap/build.ninja文件
    ninja("minibootstrap", ".minibootstrap/build.ninja")
    //这里根据.bootstrap/build.ninja,bootstrap 阶段中首先会在 bin 目录下生成了本阶段需要的一些工具程序
    //使用 out/soong/.bootstrap/bin/soong_build 
    //会逐个扫描前期记录在 out/.module_paths/Android.bp.list 中的 Android.bp 文件,
    //所以这包含了针对整个 AOSP 项目的所有模块的编译步骤描述,
    //所以这里生成的 out/soong/build.ninja 这个文件超级巨大,谨慎打开!
    ninja("bootstrap", ".bootstrap/build.ninja")
}

在这里每一阶段做相应的操作

  • 1 在build/blueprint/bootstrap.bash文件,执行blueprint bootstrap命令
    结果在out/soong/.minibootstrap/ 目录并在这个目录下创建一系列文件,
    其中最重要的是 out/soong/.minibootstrap/build.ninja 这个文件。
    这个文件很关键,是构造下一个阶段 bootstrap 的 ninja build 文件。
  • 2 通过microfactory构建minibp程序
  • 3 执行ninja函数根据.minibootstrap/build.ninja文件创建.bootstrap/build.ninja文件
  • 4 执行ninja函数根据.bootstrap/build.ninja文件会生成可执行程序集
$ ls ./out/soong/.bootstrap/bin
gotestmain  gotestrunner  loadplugins  soong_build  soong_env

通过soong_build,会逐个扫描前期记录在 out/.module_paths/Android.bp.list 中的 Android.bp 文件,然后解析将相关模块构建规则,构建生成.soong/build.ninja文件
所以这个文件会很大,最后将该ninja文件交给ninja工具集处理,out目录下才会产生那么多的模块文件(apk/lib/jar/img 等等)
对于kati来说也是分流程解析最后通过工具去挨个解析mk文件生成build.ninja文件,交给ninja去处理,最后在out目录生成相关的文件
整体流程如下图所示


未命名文件 (4).png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容