前言
狗仔我自从入职新公司大家庭以来,面对新环境、新同事、新开发平台,这个适应时间有些略长,到这儿才发现自己之前的学习跟这里对上口的不多,毕竟是以产品为主的公司,对硬件和驱动的需求远远大于上层应用及framework层,所以新的一轮学习要来了。相信做好自己熟悉的领域模块,总会有用武之地!吐槽的话不多说,上菜!
这次梳理了一下Android整个系统编译的相关知识,之前一直是使用source、lunch、make等指令去编译代码,并没有深入了解这些指令的含义,对于如何编译这么庞大的Android代码也是知之甚少,正好最近公司有专业组让去学习了解这些知识,所以借此机会学习梳理一下。
概述
小编参考的环境是高通835平台Android O系统代码,使用ubuntu进行编译,Android整个系统的编译使用如下指令:
- 进Andoid系统代码根目录
- 在Terminal中执行source build/envsetup.sh
- 执行lunch,选择需要编译的单板配置
- 执行m -j8(注:-j*的数字是跟处理器核数相关,一般最大是核数的两倍)
接下来我们就一步一步分析。
Android编译代码目录
Android build系统主要是依靠Makefile和相关shell脚本、python脚本去搜寻相关依赖并执行编译过程的,代码目录主要在下面:
各个厂商设备方自定义产品信息会在系统代码根目录的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符号“-”前后并分别赋值给product和variant,此时
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_PRODUCT和TARGET_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编译的所有文件:
下面列一下主要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,所以我们要接着找此目标的依赖,一步步延伸下去。下面是简单的主题部分:
配置产品信息
我们从main.mk文件中可以梳理出如下图的加载结构出来(主要是include模块展开):
通常会在device下AndroidProducts.mk中添加产品的配置信息,BoardConfig.mk配置主板属性,main.mk文件会经过一系列的方法会找到device目录下定义的产品信息并加载进来。
中断
由于后续篇幅较多,看代码的进度比较缓慢,故而拆为两个博文输出,敬请期待~