Android编译系统分析之lunch分析

Android 编译系统解析系列文档

编译系统入口envsetup.sh解析

解析lunch的执行过程以及make执行过程中include文件的顺序

关注一些make执行过程中的几个关键点

对一些独特的语法结构进行解析


编译一个android Project,我们需要使用到makefile文件,通过makefile文件的规则我们来构建整个Project的编译过程,那么在make之前,首先我们会执行以下命令:

1
2
3
source build/envsetup.sh
lunch project_name
make -j32 ( SHOW_COMMANDS=true )

envsetup.sh脚本

我们先来看一下source build/envsetup.sh做了什么?

定义函数

envsetup.sh中定义了很多函数,函数列表大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function hmm()
function get_abs_build_var()
function get_build_var()
function check_product()
function check_variant()
function printconfig()

function choosecombo()
function add_lunch_combo()
function print_lunch_menu()
function lunch()

function gettop
function m()
function findmakefile()
function mm()
function mmm()
function mma()
function mmma()
function croot()

function ggrep()
function jgrep()
function cgrep()
function resgrep()
function mangrep()
function sepgrep()
function getprebuilt

function smoketest()
function runtest()
function godir()

function make()

这些中有我们熟悉的m,mm,mmm,lunch,add_lunch_combo,mma,croot,print_lunch_menu,findmakefile,jgrep等等,

envsetup.sh做的第一个工作就是定义了许多函数,方便开发者在编译的过程中调用

添加编译参数

定义这些函数之后,在脚本的最后,envsetup.sh会遍历源码下device以及vendor目录,查找vendorsetup.sh,找到之后将这些文件include(source vendorsetup.sh) 到当前位置,而这些vendorsetup.sh中都是使用函数add_lunch_combo定义了一些需要编译的Project

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function axxxxxx()
function bxxxxxx()

......

# 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` \
`test -d vendor && find -L vendor -maxdepth 4 -name 'vendorsetup.sh' 2> /dev/null`
do
echo "including $f"
. $f
done


LUNCH_MENU_CHOICES[@]

add_lunch_combo实现很简单,就是将其所带的参数添加到LUNCH_MENU_CHOICES数组中

因为前边使用for循环遍历了devicevendor下的vendorsetup.sh文件,所以各个project的编译选项最后都通过add_lunch_combo被添加进了编译系统中

因此如果要想让你新加的project被android编译系统检测到,你需要在vendorsetup.sh中使用add_lunch_combo()添加编译参数,至于编译参数的详细格式,我们后边会有讲到

至此,source build/envsetup.sh的工作就完成了,我们接下来看看lunch

详解lunch

lunch的使用方法

lunch的使用方法

  • 带参数使用方法
    lunch 后边可以直接带参,例如lunch meizu_m76-eng,这个meizu_m76-eng参数就是之前add_lunch_combo添加的参数,像这样的在vendorsetup.sh中定义的类似的参数我们可以直接指定

  • 不带参数使用方法
    如果后边不带参数,直接使用lunch,会在屏幕上打印出一个列表,列出了之前扫描到的所有vendorsetup.sh中设置的project,然后选择即可

  

不管带不带参数,lunch都会读取project的名称,然后检查project的命名是否符合xxxx-xxx_xxxx这样的格式
然后提取出其中的product 和 varient 检查是否合法

检查project的编译参数格式是否符合xxxx-xxxx_xxx,是通过一个正则表达式来验证的

1
(echo -n $answer | grep -q -e "^[^\-][^\-]*-[^\-][^\-]*$")

包括后边对product 以及varient的提取也是使用sed的正则表达,检查提取出来的变量值是否合法,使用函数check_product()check_varient()

检查product是否合法

我们先来看check_product

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function check_product()
{
T=$(gettop)
if [ ! "$T" ]; then
echo "Couldn't locate the top of the tree. Try setting TOP." >&2
return
fi
TARGET_PRODUCT=$1 \
TARGET_BUILD_VARIANT= \
TARGET_BUILD_TYPE= \
TARGET_BUILD_APPS= \
get_build_var TARGET_DEVICE > dev/null
# hide successful answers, but allow the errors to show
}

在解析envsetup.sh时,经常会遇到gettop()函数,这个函数的主要作用就是取得源码的根目录的位置,具体实现内容就是依次向上查找包含 build/core/envsetup.mk 文件的目录,然后使用pwd获取当前目录即可

check_product()函数中,TARGET_PRODUCT将被赋值为之前提取出的product ,例如meizu_m76
接着通过 get_build_var() 来获取一些变量的值

注意这里传入的是 TARGET_DEVICE 这个变量,这个时候变量的内容为空

接下来是get_build_var函数

1
2
3
4
5
6
7
8
9
10
function get_build_var()
{
T=$(gettop)
if [ ! "$T" ]; then
echo "Couldn't locate the top of the tree. Try setting TOP." >&2
return
fi
(\cd $T; CALLED_FROM_SETUP=true BUILD_SYSTEM=build/core \
command make --no-print-directory -f build/core/config.mk dumpvar-$1)
}

定义了两个变量的值,

1
2
CALLED_FROM_SETUP=true  
BUILD_SYSTEM=build/core

然后使用了command make命令传入一个指定的文件 build/core/config.mk ,带了参数 dumpvar-TARGET_DEVICE
这里需要注意一下,command命令后边跟着的是指内置的shell命令,而不是系统命令,你可以在envsetup.sh脚本的最后找到android封装过的make函数,这里调用的其实就是那个封装过的函数,函数中增加了编译校验以及时间戳的标记方便用户查看

另外,这里也是bash脚本与makefile文件交互的位置,从这一函数开始,我们接下来的主要工作就需要用到makefile文件了,所以我们暂时先跳过这部分,接着来看bash脚本相关的内容

检查variant是否合法

检查完product 之后,需要check_variant,这个比较简单,只需要查找是否在已经预定义好的VARIANT_CHOICES数组中即可

1
2
3
4
5
6
7
8
9
10
11
function check_variant()
{
for v in ${VARIANT_CHOICES[@]}
do
if [ "$v" = "$1" ]
then
return 0
fi
done
return 1
}

设置环境变量

然后还有最后的几步工作要做

1
2
3
4
5
6
export TARGET_PRODUCT=$product
export TARGET_BUILD_VARIANT=$variant
export TARGET_BUILD_TYPE=release

set_stuff_for_environment
printconfig

将刚刚获取并且检查过的变量export,然后设置环境变量,并打印一些字段的值

我们注意到这里也使用到了get_build_var()函数,而这次传入的参数是report_config,这个具体执行过程我们在最后一起分析,至此,我们导入环境变量,并lunch product_name 的整个过程就结束了

其他注意事项

另外还有一点需要注意,envsetup.sh中导出了一些环境变量,因为envsetup.sh大多数都是local,所以为了之后操作的方便,使用export导出了如下这些环境变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ANDROID_BUILD_PATHS=$(get_build_var ANDROID_BUILD_PATHS):$ANDROID_TOOLCHAIN:$ANDROID_TOOLCHAIN_2ND_ARCH:$ANDROID_KERNEL_TOOLCHAIN_PATH$ANDROID_DEV_SCRIPTS:
ANDROID_BUILD_TOP=$(gettop)
ANDROID_DEV_SCRIPTS=$T/core/envdevelopment/core/envscripts:$T/core/envprebuilts/core/envdevtools/core/envtools
ANDROID_EMULATOR_PREBUILTS
ANDROID_HOST_OUT=$(get_abs_build_var HOST_OUT)
ANDROID_JAVA_TOOLCHAIN=$JAVA_HOME/core/envbin
ANDROID_PRE_BUILD_PATHS=$ANDROID_JAVA_TOOLCHAIN:
ANDROID_PRODUCT_OUT=$(get_abs_build_var PRODUCT_OUT)
ANDROID_SET_JAVA_HOME=true
ANDROID_TOOLCHAIN_2ND_ARCH=$gccprebuiltdir/core/env$toolchaindir2
ANDROID_TOOLCHAIN=$gccprebuiltdir/core/env$toolchaindir
ARM_EABI_TOOLCHAIN="$gccprebuiltdir/core/env$toolchaindir"
BUILD_ENV_SEQUENCE_NUMBER=10
GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01'
HOST_EXTRACFLAGS="-I "$T/core/envsystem/core/envkernel_headers/core/envhost_include
JAVA_HOME=/core/envusr/core/envlib/core/envjvm/core/envjava-7-openjdk-amd64
OUT=$ANDROID_PRODUCT_OUT
PATH=$ANDROID_PRE_BUILD_PATHS$PATH
PROMPT_COMMAND="echo -ne \"\033]0;[${arch}-${product}-${variant}] ${USER}@${HOSTNAME}: ${PWD}\007\""
TARGET_BUILD_APPS=$apps
TARGET_BUILD_DENSITY=$density
TARGET_BUILD_TYPE=
TARGET_BUILD_TYPE=release
TARGET_BUILD_VARIANT=
TARGET_GCC_VERSION=$targetgccversion
TARGET_PRODUCT=

进入makefile的世界

之前我们在分析envsetup.sh脚本的时候遇到了get_build_var()函数,这个函数指定了一个makefile文件,并传入dumpvar-$1参数,我们可以很轻易的找到对于dumpvar-$1类似参数出现的地方只有一个,就是dumpvar.mk文件,而我们在config.mk的尾行找到这样一句
include dumpvar.mk
所以我们直接来关注dumpvar.mk文件

dumpvar.mk文件的作用是打印出一些基本的变量

我们来看看具体干了什么

1
2
dumpvar_goals := \
$(strip $(patsubst dumpvar-%,%,$(filter dumpvar-%,$(MAKECMDGOALS))))

这个dumpvar_goals的目标就是前边的传进来的,经过替换

dumpvar_goals := TARGET_DEVICE

1
2
3
4
5
6
7
8
9
10
11
12
13
absolute_dumpvar := $(strip $(filter abs-%,$(dumpvar_goals)))
ifdef absolute_dumpvar
dumpvar_goals := $(patsubst abs-%,%,$(dumpvar_goals))
ifneq ($(filter /core/env%,$($(dumpvar_goals))),)
DUMPVAR_VALUE := $($(dumpvar_goals))
else
DUMPVAR_VALUE := $(PWD)/core/env$($(dumpvar_goals))
endif
dumpvar_target := dumpvar-abs-$(dumpvar_goals)
else
DUMPVAR_VALUE := $($(dumpvar_goals))
dumpvar_target := dumpvar-$(dumpvar_goals)
endif

如果之前追加的变量是dumpvar-abs-VARNAME, 那就返回一个参数,我们直接lunch的时候,传入一个dumpvar-VARNAME,因此直接来看11,12行的代码
那么

DUMPVAR_VALUE := $(TARGET_DEVICE)   
dumpvar_target := dumpvar-TARGET_DEVICE

那么这个TARGET_DEVICE的值是从哪里来的呢? 我们前边只是知道了product的值(meizu_m76),它和device的值有什么关系吗?  

我们来看看相关makefile的include的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
config.mk (
......
151:include envsetup.mk (
......
0:include version_defaults.mk
138:include product_config.mk(
......
178:include node_fns.mk
179:include product.mk
180:include device.mk
......
)
......
)
......
$:include dumpvar.mk(
)
)

通过以上这个include的顺序图,我们可以看到lunch的过程中,make构建规则依赖的就是include 这些文件而组成的一个大的Makefile文件

而AndroidProduct.mk的相关的定义是在product_config.mk中的这句:

all_product_configs := $(get-all-product-makefiles)

取所有的AndroidProduct.mk中定义的product_makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
define _find-android-products-files
$(shell test -d device && find device -maxdepth 6 -name AndroidProducts.mk) \
$(shell test -d vendor && find vendor -maxdepth 6 -name AndroidProducts.mk) \
$(SRC_TARGET_DIR)/core/product/core/AndroidProducts.mk
endef

......

define get-product-makefiles
$(sort \
$(foreach f,$(1), \
$(eval PRODUCT_MAKEFILES :=) \
# 去掉 f 定义的路径,然后include 文件到这里,取出PRODUCT_MAKEFILES的值,将其排序
$(eval LOCAL_DIR := $(patsubst %/core/env,%,$(dir $(f)))) \
$(eval include $(f)) \
$(PRODUCT_MAKEFILES) \
) \
$(eval PRODUCT_MAKEFILES :=) \
$(eval LOCAL_DIR :=) \
)
endef

#
# Returns the sorted concatenation of all PRODUCT_MAKEFILES
# variables set in all AndroidProducts.mk files.
# $(call ) isn't necessary.
#
define get-all-product-makefiles
$(call get-product-makefiles,$(_find-android-products-files))
endef

其中AndroidProduct.mk查找的范围为 device 和 vendor 目录下6级深度以内以及 SRC_TARGET_DIR 下的所有文件

SRC_TARGET_DIR := $(TOPDIR)build/core/target

翻译一下也就是build/core/target/目录中的AndroidProduct.mk
那么最后all_product_configs的值就是所有xxxxx_product.mk的集合

注意:这里有一个关键点TARGET_BUILD_APPS如果构建的是APP的话,我们可以只需要加载核心product makefiles就可以,在这里我们是编译系统,所以这个值为空

1
2
3
4
5
6
7
8
9
ifneq ($(strip $(TARGET_BUILD_APPS)),)
# An unbundled app build needs only the core product makefiles.
all_product_configs := $(call get-product-makefiles,\
$(SRC_TARGET_DIR)/core/product/core/AndroidProducts.mk)
else
# Read in all of the product definitions specified by the AndroidProducts.mk
# files in the tree.
all_product_configs := $(get-all-product-makefiles)
endif

这里有对PRODUCT_MAKEFILES的格式的定义,如果product_name 和文件名称一样,那么可以省略
例如:product_namemeizu_m76,如果product的makefile文件为meizu_m76.mk,
那么product_name这个前缀就可以省略不写

1
2
3
4
5
Format of PRODUCT_MAKEFILES:
# <product_name>:<path_to_the_product_makefile>
# If the <product_name> is the same as the base file name (without dir
# and the .mk suffix) of the product makefile, "<product_name>:" can be
# omitted.

搞清楚了这一点,我们来看一下如何找到current_product_makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
current_product_makefile :=
all_product_makefiles :=
$(foreach f, $(all_product_configs),\
$(eval _cpm_words := $(subst :,$(space),$(f)))\
$(eval _cpm_word1 := $(word 1,$(_cpm_words)))\
$(eval _cpm_word2 := $(word 2,$(_cpm_words)))\
$(if $(_cpm_word2),\
$(eval all_product_makefiles += $(_cpm_word2))\
$(if $(filter $(TARGET_PRODUCT),$(_cpm_word1)),\
$(eval current_product_makefile += $(_cpm_word2)),),\
$(eval all_product_makefiles += $(f))\
$(if $(filter $(TARGET_PRODUCT),$(basename $(notdir $(f)))),\
$(eval current_product_makefile += $(f)),)))
_cpm_words :=
_cpm_word1 :=
_cpm_word2 :=
current_product_makefile := $(strip $(current_product_makefile))
all_product_makefiles := $(strip $(all_product_makefiles))

根据定义,我们可以使用简写,那么_cpm_word2为空,我们只需将与TARGET_PRODUCT相等的makefile加进去就可以了,
这里使用的是+=, 表示可以定义多个product_makefile,
current_product_makefile 则是我们定义的TARGET_PRODUCT过滤出来的文件,
all_product_makefiles 则是之前找到的全部的product文件

product_config.mk文件还定义了我们之前一直苦苦寻找的TARGET_DEVICE

1
2
# Find the device that this product maps to.
TARGET_DEVICE := $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_DEVICE)

这个INTERNAL_PRODUCT值通过在product.mk中定义的resolve-short-product-name函数,返回product所对应的makefile的路径

INTERNAL_PRODUCT := $(call resolve-short-product-name, $(TARGET_PRODUCT))

google使用INTERNAL_PRODUCT这个变量的作用是为了区分例如PRODUCT_COPY_FILES这些变量在不同地方定义无法区分的问题,通过使用这样的定义就可以区分不同文件的相同的定义了
这里TARGET_DEVICE的值是product makefile中的PRODUCT_DEVICE字段,如果是meizu_m76 我们定义的就是m76

知道了TARGET_DEVICE的值,我们继续回到dumpvar.mk中看看之后做了什么

1
2
3
.PHONY: $(dumpvar_target)
$(dumpvar_target):
@echo $(DUMPVAR_VALUE)

定义了一个target 就是dumpvar-TARGET_DEVICE,然后打印了TARGET_DEVICE变量的值
这样就check_product完毕(打印了这个值就是执行成功,如果执行失败就会直接报错,也就是检查失败)

在dumpvar.mk文件的末尾,打印解析出来的一些product的字段

作者

0xforee

发布于

2015-12-01

更新于

2015-12-02

许可协议


欢迎关注我的公众号 0xforee,第一时间获取更多有价值的思考

评论