已开通新的博客,后续文字都会发到新博客
https://www.0xforee.top
Android 编译系统解析系列文档
编译系统入口envsetup.sh解析
解析lunch的执行过程以及make执行过程中include文件的顺序
关注一些make执行过程中的几个关键点
对一些独特的语法结构进行解析
编译一个android Project,我们需要使用到makefile文件,通过makefile文件的规则我们来构建整个Project的编译过程,那么在make之前,首先我们会执行以下命令:
source build/envsetup.sh
lunch project_name
make -j32 ( SHOW_COMMANDS=true )
我们先来看一下source build/envsetup.sh
做了什么?
在envsetup.sh
中定义了很多函数,函数列表大致如下:
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
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
循环遍历了device
和vendor
下的vendorsetup.sh
文件,所以各个project的编译选项最后都通过add_lunch_combo被添加进了编译系统中
因此如果要想让你新加的project被android编译系统检测到,你需要在vendorsetup.sh
中使用add_lunch_combo()
添加编译参数,至于编译参数的详细格式,我们后边会有讲到
至此,source build/envsetup.sh
的工作就完成了,我们接下来看看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,是通过一个正则表达式来验证的
(echo -n $answer | grep -q -e "^[^\-][^\-]*-[^\-][^\-]*$")
包括后边对product 以及varient的提取也是使用sed的正则表达,检查提取出来的变量值是否合法,使用函数check_product()
和check_varient()
我们先来看check_product
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函数
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)
}
定义了两个变量的值,
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脚本相关的内容
检查完product 之后,需要check_variant,这个比较简单,只需要查找是否在已经预定义好的VARIANT_CHOICES
数组中即可
function check_variant()
{
for v in ${VARIANT_CHOICES[@]}
do
if [ "$v" = "$1" ]
then
return 0
fi
done
return 1
}
然后还有最后的几步工作要做
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导出了如下这些环境变量
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=
之前我们在分析envsetup.sh脚本的时候遇到了get_build_var()
函数,这个函数指定了一个makefile文件,并传入dumpvar-$1
参数,我们可以很轻易的找到对于dumpvar-$1
类似参数出现的地方只有一个,就是dumpvar.mk文件,而我们在config.mk的尾行找到这样一句
include dumpvar.mk
所以我们直接来关注dumpvar.mk文件
dumpvar.mk文件的作用是打印出一些基本的变量
我们来看看具体干了什么
dumpvar_goals := \
$(strip $(patsubst dumpvar-%,%,$(filter dumpvar-%,$(MAKECMDGOALS))))
这个dumpvar_goals的目标就是前边的传进来的,经过替换
dumpvar_goals := TARGET_DEVICE
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的关系
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
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就可以,在这里我们是编译系统,所以这个值为空
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_name
为meizu_m76
,如果product的makefile文件为meizu_m76.mk,
那么product_name这个前缀就可以省略不写
Format of PRODUCT_MAKEFILES:
# :
# If the is the same as the base file name (without dir
# and the .mk suffix) of the product makefile, ":" can be
# omitted.
搞清楚了这一点,我们来看一下如何找到current_product_makefile
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
# 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中看看之后做了什么
.PHONY: $(dumpvar_target)
$(dumpvar_target):
@echo $(DUMPVAR_VALUE)
定义了一个target 就是dumpvar-TARGET_DEVICE
,然后打印了TARGET_DEVICE
变量的值
这样就check_product完毕(打印了这个值就是执行成功,如果执行失败就会直接报错,也就是检查失败)
在dumpvar.mk文件的末尾,打印解析出来的一些product的字段