在下载完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时你看到的哪些产品
如下图所示
接下来查看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 @"代表编译的相关参数
整体流程如下图所示
到这里我们算是知道了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目录生成相关的文件
整体流程如下图所示