Android编译机制学习
1、Makefile去哪儿了
学习了 GNU makefile 中文手册部分内容之后,觉得对 AOSP 系统的编译机制掌握了原理,只要找到根目录的 Makefile ,然后一步一步跟着流程就能够对整个源码编译了如指掌, so easy !!!But ,在最近的 Android 13源码上看,根目录下没有了 Makefile ,那这个 make 命令是怎么找到目标的呢,难道 make 还有什么隐含的默认规则没有找到吗,又去看了下 GNU Makefile 中文手册,然而并没有,指定 makefile 文件就那么几种方式,在 Android 13上都不适用。
2、一探究竟
执行编译前,都有一个初始化当前 shell 环境变量的过程,熟悉无比
source build/envsetup.sh
lunch or choosecombo
make -j8 2>&1 | tee build.log
在 build/envsetup.sh 脚本中发现了端倪所在,原来在当前 shell 下执行的 make 命令并不是直接使用的 GNU make 命令,而是在该脚本中定义的一个 shell 函数而已。。。而该 make 函数实现在不同的 AOSP 版本上有着巨大的差异:
#AOSP7.1.2_r39 build/envsetup.sh 部分源码如下:
function get_make_command()
{
echo command make
}
#AOSP8.1.0_r81 build/envsetup.sh 部分源码如下:
#后面的AOSP版本此函数实现大同小异
function get_make_command()
{
# If we're in the top of an Android tree, use soong_ui.bash instead of make
if [ -f build/soong/soong_ui.bash ]; then
echo build/soong/soong_ui.bash --make-mode
else
echo command make
fi
}
#make函数实现没有太多变化
function make()
{
_wrap_build $(get_make_command) "$@"
}
在 AOSP 7 及以前的版本,make 函数仅仅是把 GNU make 命令又封装了一次而已,而在 AOSP 8 开始,make 函数判断是否存在 soong_ui.bash 文件,将其实现替换为了 soong_ui.bash 进行编译。
执行 make -j8 2>&1 | tee build.log 就相当于
执行 build/soong/soong_ui.bash --make-mode -j8 2>&1 | tee build.log
3、以 AOSP 13 的代码来进行研究
http://aospxref.com/android-13.0.0_r3
soong_ui.bash 也是一个 shell 脚本,其核心内容如下:
export ORIGINAL_PWD=${PWD}
export TOP=$(gettop)
source ${TOP}/build/soong/scripts/microfactory.bash
soong_build_go soong_ui android/soong/cmd/soong_ui
soong_build_go mk2rbc android/soong/mk2rbc/cmd
soong_build_go rbcrun rbcrun/cmd
cd ${TOP}
exec "$(getoutdir)/soong_ui" "$@"
首先是source另一bash脚本build/soong/scripts/microfactory.bash,其内容如下:
# 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 -pkg-path prebuilts/bazel/common/proto=${TOP}/prebuilts/bazel/common/proto -pkg-path rbcrun=${TOP}/build/make/tools/rbcrun -pkg-path google.golang.org/protobuf=${TOP}/external/golang-protobuf -pkg-path go.starlark.net=${TOP}/external/starlark-go" \
build_go $@
}
source ${TOP}/build/blueprint/microfactory/microfactory.bash
该脚本主要是引入函数 soong_build_go 然后再引入另一脚本build/blueprint/microfactory/microfactory.bash,用于引入函数build_go
# Set of utility functions to build and run go code with microfactory
#
# Inputs:
# ${GOROOT}
# ${BUILDDIR}
# ${BLUEPRINTDIR}
# ${SRCDIR}
# Bootstrap microfactory from source if necessary and use it to build the
# requested binary.
#
# Arguments:
# $1: name of the requested binary
# $2: package name
# ${EXTRA_ARGS}: extra arguments to pass to microfactory (-pkg-path, etc)
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
if [ -f "${mf_bin}" ] && [ -f "${mf_version_file}" ]; then
if [ "${mf_version}" -eq "$(cat "${mf_version_file}")" ]; then
from_src=0
fi
fi
local mf_cmd
if [ $from_src -eq 1 ]; then
# `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"
printf "\n//for use with go run\nfunc main() { Main() }\n" >>"${gen_src_dir}/microfactory.go"
mf_cmd="${GOROOT}/bin/go run ${gen_src_dir}/microfactory.go"
else
mf_cmd="${mf_bin}"
fi
rm -f "${BUILDDIR}/.$1.trace"
# GOROOT must be absolute because `go run` changes the local directory
GOROOT=$(cd $GOROOT; pwd) ${mf_cmd} -b "${mf_bin}" \
-pkg-path "github.com/google/blueprint=${BLUEPRINTDIR}" \
-trimpath "${SRCDIR}" \
${EXTRA_ARGS} \
-o "${built_bin}" $2
if [ $? -eq 0 ] && [ $from_src -eq 1 ]; then
echo "${mf_version}" >"${mf_version_file}"
fi
}
soong_build_go soong_ui android/soong/cmd/soong_ui
soong_build_go mk2rbc android/soong/mk2rbc/cmd
soong_build_go rbcrun rbcrun/cmd
这3行代码主要是执行 build_go 函数用于生成3个可执行文件,均是由go语言编写,这3个模块具体的编译生成流程暂未深究。。。。
1. soong_ui 用于后面执行 aosp 编译
模块位置:build/soong/cmd/soong_ui/
入口源文件:build/soong/cmd/soong_build/main.go
2. mk2rbc 通过--help查看,该命令应该是用于转换makefile的
模块位置:build/soong/mk2rbc/
入口源文件:build/soong/mk2rbc/cmd/mk2rbc.go
3. rbcrun 该命令不太清楚作用
模块位置:build/make/tools/rbcrun/
入口源文件:build/make/tools/rbcrun/cmd/rbcrun.go
备注:Go程序入口均为 main 函数
接下来就是通过 out_sys/soong_ui --make-mode -j8 2>&1 | tee build.log 执行编译了
soong_ui 是由 build/soong/cmd/soong_build/main.go 编译生成,首先会执行其 main 函数
在该函数中判断是 --make-mode 会继续执行另一个runMake函数
import (
......
"android/soong/ui/build"
......
)
// list of supported commands (flags) supported by soong ui
var commands = []command{
{
flag: "--make-mode",
description: "build the modules by the target name (i.e. soong_docs)",
config: build.NewConfig,
stdio: stdio,
run: runMake,
},
...
}
func runMake(ctx build.Context, config build.Config, _ []string) {
......
build.Build(ctx, config)
}
而 runMake 函数会继续调用另一个build模块的Build函数,该build模块源码位置:build/soong/ui/build/build.go
build.Build 函数实现
// Build the tree. Various flags in `config` govern which components of
// the build to run.
func Build(ctx Context, config Config) {
......
if what&RunKati != 0 {
genKatiSuffix(ctx, config)
runKatiCleanSpec(ctx, config)
runKatiBuild(ctx, config)
runKatiPackage(ctx, config)
ioutil.WriteFile(config.LastKatiSuffixFile(), []byte(config.KatiSuffix()), 0666) // a+rw
}
......
createCombinedBuildNinjaFile(ctx, config)
......
if what&RunNinja != 0 {
......
runNinjaForBuild(ctx, config)
}
}
Go语言写的,流程看不太懂,上述代码流程是根据网络大神的流程图而来
soong.jpg
1. 首先调用 runSoong 函数,具体实现:/build/soong/ui/build/soong.go
该函数主要作用是加载所有的Android bp文件,生成 /out/soong/build.ninja 文件
2. 然后调用 runKatiBuild 函数,具体实现:build/soong/ui/build/kati.go
该函数有个非常重要的步骤,就是加载build/make/core/main.mk,该文件是Android Build 系统的主控文件。从main.mk开始,将通过include命令将其所有需要的.mk文件包含进来,最终在内存中形成一个包括所有编译脚本的集合,这个相当于一个巨大Makefile文件。Makefile文件看上去很庞大,其实主要由三种内容构成: 变量定义、函数定义和目标依赖规则,此外mk文件之间的包含也很重要。所有的mk文件生成一个out/build-aosp_arm.ninja 文件。
3.然后调用runKatiPackage函数,具体实现:build/soong/ui/build/kati.go
主要作用是生成out/build-aosp_arm-package.ninja
4.然后调用createCombinedBuildNinjaFile函数,具体实现:/build/soong/ui/build/soong.go
主要作用是将上述3个步骤生成的ninja文件包含到一个最终的out/combined-aosp_arm.ninja 文件
5.执行 runNinja 函数,具体实现:build/soong/ui/build/ninja.go
Android 10 是 runNinja 函数,在 Android 13 该函数改名为 runNinjaForBuild,该函数开始使用 ninja 进行完整的编译过程。。
相关总结:
Blueprint:Blueprint是生成、解析Android.bp的工具,是Soong的一部分。Soong负责Android编译而设计的工具,而Blueprint只是解析文件格式,Soong解析内容的具体含义。Blueprint和Soong都是由Golang写的项目,从Android 7.0,prebuilts/go/目录下新增Golang所需的运行环境,在编译时使用。而在Android 6.0及以前的版本中 prebuilds 目录下没有 go 目录。