在前文中,我们分析了Android编译环境的初始化过程。Android编译环境初始化完成后,我们就可以用m/mm/mmm/make命令编译源代码了。当然,这要求每一个模块都有一个Android.mk文件。Android.mk实际上是一个Makefile脚本,用来描述模块编译信息。Android编译系统通过整合Android.mk文件完成编译过程。本文就对Android源代码的编译过程进行详细分析。
从前面Android编译系统环境初始化过程分析这篇文章可以知道,lunch命令其实是定义在build/envsetup.sh文件中的函数lunch提供的。与lunch命令一样,m、mm和mmm命令也分别是由定义在build/envsetup.sh文件中的函数m、mm和mmm提供的,而这三个函数又都是通过make命令来对源代码进行编译的。事实上,命令m就是对make命令的简单封装,并且是用来对整个Android源代码进行编译,而命令mm和mmm都是通过make命令来对Android源码中的指定模块进行编译。接下来我们就先分别介绍一下函数m、mm和mmm的实现,然后进一步分析它们是如何通过make命令来编译代码的。
函数m的实现如下所示:
function m()
{
T=$(gettop)
if [ "$T" ]; then
make -C $T $@
else
echo "Couldn't locate the top of the tree. Try setting TOP."
fi
}
函数m调用函数gettop得到的是Android源代码根目录T。在执行make命令的时候,先通过-C选项指定工作目录为T,即Android源代码根目录,接着又将执行命令m指定的参数$@作为命令make的参数。从这里就可以看出,命令m实际上就是对命令make的简单封装。
函数mm的实现如下所示:
function mm()
{
# If we're sitting in the root of the build tree, just do a
# normal make.
if [ -f build/core/envsetup.mk -a -f Makefile ]; then
make $@
else
# Find the closest Android.mk file.
T=$(gettop)
local M=$(findmakefile)
# Remove the path to top as the makefilepath needs to be relative
local M=`echo $M|sed 's:'$T'/::'`
if [ ! "$T" ]; then
echo "Couldn't locate the top of the tree. Try setting TOP."
elif [ ! "$M" ]; then
echo "Couldn't locate a makefile from the current directory."
else
ONE_SHOT_MAKEFILE=$M make -C $T all_modules $@
fi
fi
}
函数mm首先是判断当前目录是否就是Android源码根目录,即当前目录下是否存在一个build/core/envsetup.mk文件和一个Makefile文件。如果是的话,就将命令mm当作是一个普通的make命令来执行。否则的话,就调用函数findmakefile从当前目录开始一直往上寻找是否存在一个Android.mk文件。如果在寻找的过程中,发现了一个Android.mk文件,那么就获得它的绝对路径,并且停止上述寻找过程。
由于接下来执行make命令时,我们需要指定的是要编译的Android.mk文件的相对于Android源码根目录路径,因此函数mm需要将刚才找到的Android.mk绝对文件路径M中与Android源码根目录T相同的那部分路径去掉。这是通过sed命令来实现的,也就是将字符串M前面与字符串T相同的子串删掉。
最后,将找到的Android.mk文件的相对路径设置给环境变量ONE_SHOT_MAKE,表示接下来要对它进行编译。另外,函数mm还将make命令目标设置为all_modules。这是什么意思呢?我们知道,一个Android.mk文件同时可以定义多个模块,因此,all_modules就表示要对前面指定的Android.mk文件中定义的所有模块进行编译。
函数mmm的实现如下所示:
function mmm()
{
T=$(gettop)
if [ "$T" ]; then
local MAKEFILE=
local MODULES=
local ARGS=
local DIR TO_CHOP
local DASH_ARGS=$(echo "$@" | awk -v RS=" " -v ORS=" " '/^-.*$/')
local DIRS=$(echo "$@" | awk -v RS=" " -v ORS=" " '/^[^-].*$/')
for DIR in $DIRS ; do
MODULES=`echo $DIR | sed -n -e 's/.*:.∗$/\1/p' | sed 's/,/ /'`
if [ "$MODULES" = "" ]; then
MODULES=all_modules
fi
DIR=`echo $DIR | sed -e 's/:.*//' -e 's:/$::'`
if [ -f $DIR/Android.mk ]; then
TO_CHOP=`(cd -P -- $T && pwd -P) | wc -c | tr -d ' '`
TO_CHOP=`expr $TO_CHOP + 1`
START=`PWD= /bin/pwd`
MFILE=`echo $START | cut -c${TO_CHOP}-`
if [ "$MFILE" = "" ] ; then
MFILE=$DIR/Android.mk
else
MFILE=$MFILE/$DIR/Android.mk
fi
MAKEFILE="$MAKEFILE $MFILE"
else
if [ "$DIR" = snod ]; then
ARGS="$ARGS snod"
elif [ "$DIR" = showcommands ]; then
ARGS="$ARGS showcommands"
elif [ "$DIR" = dist ]; then
ARGS="$ARGS dist"
elif [ "$DIR" = incrementaljavac ]; then
ARGS="$ARGS incrementaljavac"
else
echo "No Android.mk in $DIR."
return 1
fi
fi
done
ONE_SHOT_MAKEFILE="$MAKEFILE" make -C $T $DASH_ARGS $MODULES $ARGS
else
echo "Couldn't locate the top of the tree. Try setting TOP."
fi
}
函数mmm的实现就稍微复杂一点,我们详细解释一下。
首先,命令mmm可以这样执行:
$ mmm <dir-1> <dir-2> ... <dir-N>[:module-1,module-2,...,module-M]
其中,dir-1、dir-2、dir-N都是包含有Android.mk文件的目录。在最后一个目录dir-N的后面可以带一个冒号,冒号后面可以通过逗号分隔一系列的模块名称module-1、module-2和module-M,用来表示要编译前面指定的Android.mk中的哪些模块。
知道了命令mmm的使用方法之后 ,我们就可以分析函数mmm的执行逻辑了:
1. 调用函数gettop获得Android源码根目录。
2. 通过命令awk将执行命令mmm时指定的选项参数提取出来,也就是将以横线“-”开头的字符串提取出来,并且保存在变量DASH_ARGS中。
3. 通过命令awk将执行命令mmm时指定的非选项参数提取出来,也就是将非以横线“-”开头的字符串提取出来,并且保存在变量DIRS中。这里得到的实际上就是跟在命令mmm后面的字符串“<dir-1> <dir-2> ... <dir-N>[:module-1,module-2,...,module-M]”。
4. 变量DIRS保存的字符串可以看成是一系以空格分隔的子字符串,因此,就可以通过一个for循环来对这些子字府串进行遍历。每一个子字符串DIR描述的都是一个包含有Android.mk文件的目录。对每一个目录DIR执行以下操作:
4.1 由于目录DIR后面可能会通过冒号指定有模块名称,因此就先通过两个sed命令来获得这些模块名称。第一个sed命令获得的是一系列以逗号分隔的模块名称列表,第二个sed命令用来将前面获得的以逗号分隔的模块名称列表转化为以空格分隔的模块名称列表。最后,获得的以空格分隔的模块名称列表保存在变量MODULES中。由于目录DIR后面也可能不指定有模块名称,因此前面得到的变量MODULES的值就会为空。在这种情况下,需要将变量MODULES的值设置为“all_modules”,表示要编译的是所有模块。
4.2 通过两个sed命令获得真正的目录DIR。第一个sed命令将原来DIR字符串后面的冒号以及冒号后面的模块列表字符串删掉。第二个sed命令将执行前面一个sed命令获得的目录后面的"/"斜线去掉,最后就得到一个末尾不带有斜线“/”的路径,并且保存在变量DIR中。
4.3 如果变量DIR描述的是一个真正的路径,也就是在该路径下存在一个Android.mk文件,那么就进行以下处理:
4.3.1 统计Android源码根目录T包含的字符数,并且将这个字符数加1,得到的值保存在变量TO_CHOP中。
4.3.2 通过执行/bin/pwd命令获得当前执行命令mmm的目录START。
4.3.3 通过cut命令获得当前目录START相对于Android源码根目录T的路径,并且保存在变量MFILE中。
4.3.4 如果变量MFILE的值等于空,就表明是在Android源码根目录T中执行mmm命令,这时候就表明变量DIR描述的就是相对Android源码根目录T的一个目录,这时候指定的Android.mk文件相对于Android源码根目录T的路径就为$DIR/Android.mk。
4.3.5 如果变量MFILE的值不等于空,就表明是在Android源码根目录T的某一个子目录中执行mmm命令,这时候$MFILE/$DIR/Android.mk表示的Android.mk文件路径才是相对于Android源码根目录T的。
4.3.6 将获得的Android.mk路径MFILE附加在变量MAKEFILE描述的字符串的后面,并且以空格分隔。
4.4 如果变量DIR描述的不是一个真正的路径,并且它的值等于"snod"、"showcomands"、“dist”或者“incrementaljavac”,那么它描述的其实是make修饰命令。这四个修饰命令的含义分别如下所示:
4.4.1 snod是“systemimage with no dependencies”的意思,表示忽略依赖性地重新打包system.img。
4.4.2 showcommands表示显示编译过程中执行的命令。
4.4.3 dist表示将编译后产生的发布文件拷贝到out/dist目录中。
4.4.4 incrementaljavac表示对Java源文件采用增量式编译,也就是如果一个Java文件如果没有修改过,那么就不要重新生成对应的class文件。
5. 上面的for循环执行完毕,变量MAKEFILE保存的是要编译的Android.mk文件列表,它们都是相对于Android源码根目录的路径,变量DASH_ARGS保存的是原来执行mmm命令时带的选项参数,变量MODULES保存的是指定要编译的模块名称,变量ARGS保存的是修饰命令。其中,变量MAKEFILE的内容通过环境变量ONE_SHOT_MAKEFILE传递给make命令,而其余变量都是通过参数的形式传递给make命令,并且变量MODULES作为make命令的目标。
明白了函数m、mm和mmm的实现之后,我们就可以知道:
1. mm和mmm命令是类似的,它们都是用来编译某些模块。
2. m命令用来编译所有模块。
如果我们理解了mm或者mmm命令的编译过程,那么自然也会明白m命令的编译过程,因为所有模块的编译过程就等于把每一个模块的编译都编译出来,因此,接下来我们就选择具有代表性的、常用的编译命令mmm来分析Android源码的编译过程,如图1所示:
图1 mmm命令的编译过程
函数mmm在Android源码根目录执行make命令的时候,没有通过-f指定Makefile文件,因此默认就使用Android源码根目录下的Makefile文件,它的内容如下所示:
### DO NOT EDIT THIS FILE ###
include build/core/main.mk
### DO NOT EDIT THIS FILE ###
它仅仅是将build/core/main.mk文件加载进来。build/core/main.mk是Android编译系统的入口文件,它通过加载其它的mk文件来对Android源码中的各个模块进行编译,以及将编译出来的文件打包成各种镜像文件。以下就是build/core/main.mk文件的主要内容:
......
# This is the default target. It must be the first declared target.
.PHONY: droid
DEFAULT_GOAL := droid
$(DEFAULT_GOAL):
......
# Set up various standard variables based on configuration
# and host information.
include $(BUILD_SYSTEM)/config.mk
......
# Bring in standard build system definitions.
include $(BUILD_SYSTEM)/definitions.mk
......
# These targets are going to delete stuff, don't bother including
# the whole directory tree if that's all we're going to do
ifeq ($(MAKECMDGOALS),clean)
dont_bother := true
endif
ifeq ($(MAKECMDGOALS),clobber)
dont_bother := true
endif
ifeq ($(MAKECMDGOALS),dataclean)
dont_bother := true
endif
ifeq ($(MAKECMDGOALS),installclean)
dont_bother := true
endif
# Bring in all modules that need to be built.
ifneq ($(dont_bother),true)
......
ifneq ($(ONE_SHOT_MAKEFILE),)
# We've probably been invoked by the "mm" shell function
# with a subdirectory's makefile.
include $(ONE_SHOT_MAKEFILE)
......
else # ONE_SHOT_MAKEFILE
#
# Include all of the makefiles in the system
#
# Can't use first-makefiles-under here because
# --mindepth=2 makes the prunes not work.
subdir_makefiles := \
$(shell build/tools/findleaves.py --prune=out --prune=.repo --prune=.git $(subdirs) Android.mk)
include $(subdir_makefiles)
endif # ONE_SHOT_MAKEFILE
......
# -------------------------------------------------------------------
# Define dependencies for modules that require other modules.
# This can only happen now, after we've read in all module makefiles.
#
# TODO: deal with the fact that a bare module name isn't
# unambiguous enough. Maybe declare short targets like
# APPS:Quake or HOST:SHARED_LIBRARIES:libutils.
# BUG: the system image won't know to depend on modules that are
# brought in as requirements of other modules.
define add-required-deps
$(1): $(2)
endef
$(foreach m,$(ALL_MODULES), \
$(eval r := $(ALL_MODULES.$(m).REQUIRED)) \
$(if $(r), \
$(eval r := $(call module-installed-files,$(r))) \
$(eval $(call add-required-deps,$(ALL_MODULES.$(m).INSTALLED),$(r))) \
) \
)
......
modules_to_install := $(sort \
$(ALL_DEFAULT_INSTALLED_MODULES) \
$(product_FILES) \
$(foreach tag,$(tags_to_install),$($(tag)_MODULES)) \
$(call get-tagged-modules, shell_$(TARGET_SHELL)) \
$(CUSTOM_MODULES) \
)
......
# build/core/Makefile contains extra stuff that we don't want to pollute this
# top-level makefile with. It expects that ALL_DEFAULT_INSTALLED_MODULES
# contains everything that'