整个分析过程中,机型名以xxxx为例
主要可分为
一 firmware如何添加进target-files.zip
二 编译ota升级包时如何从target-files.zip取出firmware并添加到ota升级包
三 如何向升级脚本updater-script中加入控制firmware升级的语句
四 增量升级包相比全量包不同的步骤
五 结论及修复方案
INSTALLED_RADIOIMAGE_TARGET:
device/******/xxxx/下的AndroidBoard.mk
1 android编译系统如何将device/******/xxxx/下的AndroidBoard.mk包含进来
在 build/core/main.mk中,会根据用户编译条件,选择Android源码中不同的目录,将编译目录中的所有Android.mk文件包含进来,
|
在build/target/board/Android.mk中有:
|
TARGET_DEVICE_DIR的获取:
TARGET_DEVICE_DIR的取值最终是device/******/xxxx
******/xxxx/下的AndroidBoard.mk包含进来,AndroidBoard.mk中包含了target INSTALLED_RADIOIMAGE_TARGET,将升级需要的所有firmware 作为了它的object file。
一 firmware如何添加进target-files.zip
生成target-files.zip过程中如何添加firmware
1 命令make target-files-package
build/core/Makefile
|
FILE_NAME_TAG取值为编译时的选项 ,如user,eng,userdebug,再加上USER环境变量,因此以编译机型xxx的eng版本为例,name = xxxx-target_files-eng.username
intermediates通过调用 intermediates-dir-for,返回值为out/target/product/xxxx/obj/PACKAGING/target_files_intermediates/
因此 zip_root取值为 out/target/product/xxxx/obj/PACKAGING/target_files_intermediates/xxxx-target_files-eng.username
接下来有:
|
在foreach循环中,将 INSTALLED_RADIOIMAGE_TARGET依赖的所有firmware文件都拷贝到(zip_root)/RADIO路径下。
最后将所有文件拷贝完成之后有:
|
这样就将xxxx-target_files-eng.username下的所有文件压缩成了ota升级包文件xxxx-target_files-eng.username.zip,其中所有的firmware就在压缩文件的RADIO目录下。
二 编译ota升级包时如何从target-files.zip取出firmware并添加到ota升级包
1 前期准备操作
编译全量包的命令为make otapackage
在builld/core/Makefile中有:
|
说明$(INTERNAL_OTA_PACKAGE_TARGET)依赖于$(BUILT_TARGET_FILES_PACKAGE)。
根据之前的分析,目标$(BUILT_TARGET_FILES_PACKAGE)的生成依赖的规则中有$(INSTALLED_RADIOIMAGE_TARGET),因此线刷包的生成依赖的object file中有代表firmware的INSTALLED_RADIOIMAGE_TARGET,因此在编译升级包之前会生成AndroidBoard.mk中定义的INSTALLED_RADIOIMAGE_TARGET
在build/core/Makefile中添加调试语句
|
执行时会输出make otapackage,make otapackage go here,同时@echo "Package OTA: $@"还会输出Package OTA: out/target/product/xxxx/xxxx-ota-eng.username.zip,说明当前的target($@)就是out/target/product/xxxx/xxxx-ota-eng.username.zip 验证了之前的推断
为了产生target依赖的object file $(INTERNAL_OTA_PACKAGE_TARGET),接下来会通过call调用函数build-ota-package,经过查找,认为这个函数是定义在xxxx/build下的definitions.mk中,为了验证,
在xxxx/build下的definitions.mk中修改函数build-ota-package:
|
发现在编译全两包时,输出log中依次有:
make otapackage go here
PATH=out/host/linux-x86/bin/:$PATH MKBOOTIMG=out/host/linux-x86/bin/mkbootimg \
@echo "call build-ota-package in Makefile go here!"
其中PATH=out/host/linux-x86/bin/:$PATH MKBOOTIMG=out/host/linux-x86/bin/mkbootimg \对应于PATH=$(foreach p,$(INTERNAL_USERIMAGES_BINARY_PATHS),$(p):)$$PATH MKBOOTIMG=$(MKBOOTIMG) \
因此可以肯定,接下来会执行xxxx/build/definitions.mk下的函数build-ota-package
在build-ota-package中添加调试语句,有
|
因此在函数最后会执行shell脚本ota_from_target_files.sh,其中输入参数$(1)通过在build/core/Makefile中调用build-ota-package时传入,可以在build-ota-package中通过echo "$(1)"输出,
-v --block -p out/host/linux-x86 -k build/target/product/security/testkey
因此接下来就会执行:
mxxx/build/ota_from_target_files.sh -v --block -p out/host/linux-x86 -k build/target/product/security/testkey
shell脚本ota_from_target_files.sh:
在shell脚本ota_from_target_files.sh中,首先执行print_global_variables打印一些变量
|
然后执行check_global_variables,检查ORIGINAL_OTA_FROM_TARGET_FILES_TOOL、TARGET_FILES_PACKAGE、TARGET_OTA_FILE这个三变量是否为空,任何一个为空就中断编译。
在ota_from_target_files.sh脚本的最后,会根据INCREMENTAL_TARGET_FILES_PACKAGE值是否为空,来选择执行build/tools/releasetools/ota_from_target_files时是否添加参数-i,也就是说会根据INCREMENTAL_TARGET_FILES_PACKAGE的值来决定生成增量包还是全量包。
现在INCREMENTAL_TARGET_FILES_PACKAGE为空,打印出接下来脚本执行的命令为:
build/tools/releasetools/ota_from_target_files -v --block -p out/host/linux-x86 -k build/target/product/security/testkey /tmp/tmp.mxxx.target_files_package.ota.eOL/new-target-files.zip out/target/product/xxxx/xxxx-ota-eng.username.zip
在 ota_from_target_files执行完后会判断它的返回值,如果返回值为0则认为生成失败,输出Failed to genrate otapackage并退出
3 ota_from_target_files.py
ota_from_target_files.py负责产生升级包的升级脚本,这里主要分析与添加firmware有关的必要操作。
在ota_from_target_files.py的main中,首先将传入的第一个参数,也就是target-files.zip文件解压:
OPTIONS.input_tmp, input_zip = common.UnzipTemp(args[0])
根据common.UnzipTemp的返回值,OPTIONS.input_tmp就代表target-files.zip解压的临时文件夹,input_zip 代表用zipfile.ZipFile(filename, "r")打开的target-files.zip文件
接下来:
|
这里首先看调用脚本时是否制定了--device_specific,如果制定了--device_specific的值,则这个值就会被赋予 OPTIONS.device_specific
因为调用参数中没有 device_specific,因此接下来会在target_files.zip解压的临时文件夹下的META/中寻找releasetools.py,如果找到了这个文件,就将这个文件的完整路径传给 OPTIONS.device_specific,如/tmp/targetfiles-hJ2BmE/META/releasetools.py(META前面的路径就是target_files.zip解压的临时文件夹的路径)
如果没有找到就从 OPTIONS.info_dict中寻找,OPTIONS.info_dict是python中的字典映射,这里就是在字典中查找key为 tool_extensions的这个映射的value,打印出OPTIONS.info_dict可知tool_extensions这个key对应的值为device/qcom/common,因此如果在target_files.zip解压的临时文件夹下找不到 releasetools.py,那么 OPTIONS.device_specific的值就为device/qcom/common。OPTIONS.info_dict的tool_extensions对应的value的值的获取参考子页面《OPTIONS.info_dict的tool_extensions取值过程分析》
output_zip:
|
然后将target-files.zip和 output_zip作为参数,调用
WriteFullOTAPackage(input_zip, output_zip)
4 如果是生成全两包,接下来会执行WriteFullOTAPackage(),在其中有
device_specific = common.DeviceSpecificParams(
input_zip = input_zip,
input_version = OPTIONS.info_dict[ "recovery_api_version" ],
output_zip = output_zip,
script = script,
input_tmp = OPTIONS.input_tmp,
metadata = metadata,
info_dict = OPTIONS.info_dict)
这里创建了一个 common.py中的class DeviceSpecificParams的实例,因此会执行 DeviceSpecificParams的__init__方法
|
class DeviceSpecificParams对应的同名成员变量
_init_mxxx_module中执行一些mxxx添加的功能
ota_from_target_files中获得的OPTIONS.device_specific的值传给path,并判断path是否为目录
如果之前OPTIONS.device_specific取值是target-files.zip解压的临时文件夹下的releasetools.py,如/tmp/targetfiles-hJ2BmE/META/releasetools.py,那么这里分割出releasetools.py的路径,文件名,扩展名。如果OPTIONS.device_specific取值为device/x
*****/mxxx的完整路径,那么
os.path.isdir(path)取值为true。然后在device/x*****/mxxx下查找releasetools文件。
最后调用python内置的imp的 find_module方法,将releasetools文件作为一个名为device_specific的模块加载,并传给self.module,也就是device_specific.module
5 device_specific.FullOTA_InstallEnd()
在WriteFullOTAPackage中接下来执行:
device_specific.FullOTA_InstallEnd()
这最终会调用class DeviceSpecificParams的_DoCall,输入参数中只有function_name为“FullOTA_InstallEnd”,args与kwargs均为空。在 FullOTA_InstallEnd中:
|
因为self.module是从releasetools文件加载的名为device_specific模块,打开target-files.zip的META/releasetools.py文件,可以发现里面有名为 FullOTA_InstallEnd的函数。由于args与kwargs均为空,因此接下来执行 ret = getattr(self.module, function_name)(*((self,) + args), **kwargs)
就是调用releasetools.py这个文件中的 FullOTA_InstallEnd,并且将ota_from_target_files中定义的device-specific这个class DeviceSpecificParams类型的object传入。最后 FullOTA_InstallEnd执行的返回值传给ret
releasetools中的 FullOTA_InstallEnd:
|
首先调用 OTA_VerifyEnd,输出参数中info就是ota_from_target_files中定义的device-specific这个class DeviceSpecificParams类型的object,在初始化object后,info.input_version 为3,input_zip取值为zipfile.ZipFile,就代表的是target_files.zip这个zip文件。
在 OTA_VerifyEnd中,先调用LoadFilesMap
target_files.zip中读取 RADIO/filesmap文件,判断文件的每一行,去掉其中的空行和以#开头的注释,剩余的行就是有效行,将其中第一列作为key,第二列作为value保存到字典d中,如果某一行分割后列数不为2,就使用raise抛出 ValueError异常,最后将获得的字典d返回给OTA_VerifyEnd。
GetRadioFiles中读取target-files.zip内RADIO下除了filesmap的所有文件并保存到数组中返回给OTA_VerifyEnd中的tgt_files。
然后返回OTA_VerifyEnd,在一个for循环中,依次从tgt_files中取出每一个firmware,调用GetFileDestination,GetFileDestination会取出每个文件在filesmap中对应的分区并返回给dest,destBak。
GetFileDestination 执行完返回到OTA_VerifyEnd中,在for循环继续调用
|
common.ZipWriteStr(info.output_zip, f, tf.data),其中tf就是从tgt_files中取出的每一个firmware文件, 这里调用了build/tools/releasetools/common.py下的ZipWriteStr函数,common.ZipWriteStr会调用python的ZipFile模块的writestr函数,这个函数支持将二进制数据直接写入到压缩文档。
因此最终是在这里将每个firmware文件写入到 info.output_zip的 firmware-update/文件夹中,也就是最初在ota_from_target_files的main中创建的最终输出的升级包的firmware-update/下。
然后声明3个全局变量bootImages,binImages,fwImages,分别作为函数SplitFwTypes的返回值,
在SplitFwTypes中,将filesmap中第一列的所有文件中,去掉后缀名为.p或者.enc的文件, 剩下的分为3类,bootImages代表后缀名为.mbn或者.enc的文件, binImages代表后缀名为.bin的文件。
三 如何向升级脚本updater-script中加入控制firmware升级的语句
OTA_VerifyEnd执行完后,接下来返回FullOTA_InstallEnd_MMC,执行OTA_InstallEnd,在OTA_InstallEnd中,分别执行InstallBootImages,InstallBinImages,InstallFwImages,这三个函数中最终都通过script.AppendExtra,也就是edify_generator.EdifyGenerator的 AppendExtra函数,向控制升级过程的updater-script脚本中输出了形如
package_extract_file("firmware-update/rpm.mbn", "/dev/block/bootdevice/by-name/rpm");
等在升级过程中升级firewater的语句。
四 增量升级包相比全量包不同的步骤
target-files.zip 在编译增量包之前就已经生成,因此第一步firmware如何添加进target-files.zip和全量包的过程相同。
1 以手动编译增量包为例,因为手动编译增量包通过直接调用build/tools/releasetools/ota_from_target_files.py,因此首先从这里分析
build/tools/releasetools/ota_from_target_files.py
def main(argv):
def option_handler(o, a):
if o = = "--board_config" :
pass # deprecated
elif o in ( "-k" , "--package_key" ):
OPTIONS.package_key = a
elif o in ( "-i" , "--incremental_from" ):
OPTIONS.incremental_source = a
因为生成增量包时调用ota_from_target_files.py的参数中肯定包含 -i,因此OPTIONS.incremental_source = a
2 假设前后两次生成的target-files.zip分别称为source target-files.zip,target target-files.zip,对target target-files.zip的解压与全量包完全相同:
build/tools/releasetools/ota_from_target_files.py
print "unzipping target target-files..."
OPTIONS.input_tmp, input_zip = common.UnzipTemp(args[ 0 ])
OPTIONS.target_tmp = OPTIONS.input_tmp
OPTIONS.info_dict = common.LoadInfoDict(input_zip)
不论全量包还是增量包,对要生成的升级包文件output_zip的创建方法相同:
build/tools/releasetools/ota_from_target_files.py
if OPTIONS.no_signing:
if os.path.exists(args[ 1 ]):
os.unlink(args[ 1 ])
output_zip = zipfile.ZipFile(args[ 1 ], "w" ,
compression = zipfile.ZIP_DEFLATED)
else :
temp_zip_file = tempfile.NamedTemporaryFile()
output_zip = zipfile.ZipFile(temp_zip_file, "w" ,
compression = zipfile.ZIP_DEFLATED)
之后根据OPTIONS.incremental_source来决定执行WriteFullOTAPackage还是WriteIncrementalOTAPackage,
build/tools/releasetools/ota_from_target_files.py
if OPTIONS.incremental_source is None :
WriteFullOTAPackage(input_zip, output_zip)
if OPTIONS.package_key is None :
OPTIONS.package_key = OPTIONS.info_dict.get(
"default_system_dev_certificate" ,
"build/target/product/security/testkey" )
common.ZipClose(output_zip)
break
else :
print "unzipping source target-files..."
OPTIONS.source_tmp, source_zip = common.UnzipTemp(
OPTIONS.incremental_source)
OPTIONS.target_info_dict = OPTIONS.info_dict
OPTIONS.source_info_dict = common.LoadInfoDict(source_zip)
if "selinux_fc" in OPTIONS.source_info_dict:
OPTIONS.source_info_dict[ "selinux_fc" ] = os.path.join(
OPTIONS.source_tmp, "BOOT" , "RAMDISK" , "file_contexts" )
if OPTIONS.package_key is None :
OPTIONS.package_key = OPTIONS.source_info_dict.get(
"default_system_dev_certificate" ,
"build/target/product/security/testkey" )
if OPTIONS.verbose:
print "--- source info ---"
common.DumpInfoDict(OPTIONS.source_info_dict)
try :
WriteIncrementalOTAPackage(input_zip, source_zip, output_zip)
common.ZipClose(output_zip)
break
except ValueError:
if not OPTIONS.fallback_to_full:
raise
print "--- failed to build incremental; falling back to full ---"
OPTIONS.incremental_source = None
common.ZipClose(output_zip)
对于增量包:
用同样的方法先后读取两个target-files.zip中的META/misc_info.txt,分别保存为OPTIONS.source_info_dict,OPTIONS.target_info_dict,如果调用脚本时有-v参数,就会打印出OPTIONS.source_info_dict
要生成的升级包文件
3 在WriteIncrementalOTAPackage中,
build/tools/releasetools/ota_from_target_files.py
def WriteIncrementalOTAPackage(target_zip, source_zip, output_zip):
target_has_recovery_patch = HasRecoveryPatch(target_zip)
source_has_recovery_patch = HasRecoveryPatch(source_zip)
if (OPTIONS.block_based and
target_has_recovery_patch and
source_has_recovery_patch):
return WriteBlockIncrementalOTAPackage(target_zip, source_zip, output_zip)
如果生成增量包时的参数中有--block,并且两个target-files.zip都包含SYSTEM/recovery-from-boot.p,那么实际调用WriteBlockIncrementalOTAPackage(target_zip, source_zip, output_zip)。如果条件不满足,执行WriteIncrementalOTAPackage(target_zip, source_zip, output_zip)
下来以WriteBlockIncrementalOTAPackage为例分析。
4 对于增量包,在WriteBlockIncrementalOTAPackage中对class DeviceSpecificParams类型的device_specific变量的初始化与全量包一致,都是在common.py的__init__中加载对应路径下的releasetools.py
5 接下来,执行device_specific.IncrementalOTA_VerifyEnd(),与增量包相同,还是通过DeviceSpecificParams的_DoCall跳转到releasetools.py的IncrementalOTA_VerifyEnd,在IncrementalOTA_VerifyEnd同样通过调用OTA_VerifyEnd来实现将firmware添加进增量包中。有所区别的是,全量包是OTA_VerifyEnd(info, info.input_version, info.input_zip),增量包调用OTA_VerifyEnd是OTA_VerifyEnd(info, info.target_version, info.target_zip, info.source_zip),相比全量包多一个参数,导致在OTA_VerifyEnd内部的执行流程稍有不同:
releasetools.py的OTA_VerifyEnd
src_files = None
if source_zip is not None :
print "Loading radio source..."
src_files = GetRadioFiles(source_zip)
update_list = {}
largest_source_size = 0
print "Preparing radio-update files..."
for fn in tgt_files:
dest, destBak = GetFileDestination(fn, filesmap)
if dest is None :
continue
tf = tgt_files[fn]
sf = None
if src_files is not None :
sf = src_files.get(fn, None )
full = sf is None or fn.endswith( '.enc' )
if not full:
# no difference - skip this file
if tf.sha1 = = sf.sha1:
continue
d = common.Difference(tf, sf)
_, _, d = d.ComputePatch()
# no difference - skip this file
if d is None :
continue
# if patch is almost as big as the file - don't bother patching
full = len (d) > tf.size * common.OPTIONS.patch_threshold
if not full:
f = "patch/firmware-update/" + fn + ".p"
common.ZipWriteStr(info.output_zip, f, d)
update_list[f] = (dest, destBak, tf, sf)
largest_source_size = max (largest_source_size, sf.size)
if full:
f = "firmware-update/" + fn
common.ZipWriteStr(info.output_zip, f, tf.data)
update_list[f] = (dest, destBak, None , None )
对于全量包,source_zip不再取默认值none,因此会同样取出source target_files.zip中RADIO下对应的文件。
将从两个target_files.zip中取出的文件分别作为sf,tf,因此full这时为flase,然后在for循环中,先比较它们的sha1,相同就跳过这个文件,否则对比sf和tf的差异并生成补丁d。
如果补丁d的大小与文件大小接近,也就是full = len(d) > tf.size * common.OPTIONS.patch_threshold 这个判断让full重置为true,因此这种情况下之后的流程和全量包的处理完全一样。
如果补丁d的大小没有超过阈值,这种情况下,会将产生的补丁文件写入到升级包的patch/firmware-update目录下。
6 接下来,执行device_specific.IncrementalOTA_InstallEnd(),与增量包相同,还是通过DeviceSpecificParams的_DoCall跳转到releasetools.py的IncrementalOTA_InstallEnd,IncrementalOTA_InstallEnd中继续调用OTA_InstallEnd,由于调用时并没有参数的区别,因此在OTA_InstallEnd中的处理过程与全量包没有什么大的差别,增量包的updater-script中控制firmware升级的语句也是在InstallBootImages,InstallBinImages,InstallFwImages中生成。
五 结论及修复方案
综上所述,如果我们现在要将firmware添加到ota升级包中,要做的主要有两步:
一 在编译系统中建立代表所有firmware的target INSTALLED_RADIOIMAGE_TARGET,它依赖于所有升级需要的firmware,同时编译target-files.zip依赖的object也包含它。在Makefile中编写生成这个target的规则,因为在releasetools.py中是通过读取target-files.zip的RADIO/filesmap文件来获取要升级哪些fimware的,所以具体需要的firmware要参考filesmap文件。
二 让编译系统能够找到 真正完成将firmware写入到升级包和在updater-script中输出升级语句的脚本releasetools.py。
以mxxx为例,一种具体可行的解决方案是:
1 在device/x*****/{机型名}/AndroidBoard.mk中添加如下语句:
ifeq ($(ADD_RADIO_FILES), true)
radio_dir := $(LOCAL_PATH)/radio
RADIO_FILES := $(shell cd $(radio_dir) ; ls)
$(foreach f, $(RADIO_FILES), \
$(call add-radio-file,radio/$(f)))
endif
首先调用函数add-radio-file将当前路径下radio下的filesmap文件作为INSTALLED_RADIOIMAGE_TARGET的依赖,这样在生成target-files.zip时,在将INSTALLED_RADIOIMAGE_TARGET依赖的所有文件拷贝到 $(zip_root)/RADIO,也就是out/target/product/mxxx/obj/PACKAGING/target_files_intermediates/mxxx-target_files-eng.username/RADIO下时,不光会拷贝firmware,还会同时将filesmap文件也拷贝过去,这样才能保证以后在releasetools.py中读取target-files.zip的RADIO/filesmap不会出错。
TARGET_BOOTLOADER_EMMC_INTERNAL := $(PRODUCT_OUT)/emmc_appsboot.mbn
$(TARGET_BOOTLOADER_EMMC_INTERNAL): $(INSTALLED_BOOTLOADER_MODULE)
INSTALLED_RADIOIMAGE_TARGET += $(TARGET_BOOTLOADER_EMMC_INTERNAL)
$(call add-radio-file,images/NON-HLOS.bin)
$(call add-radio-file,images/sbl1.mbn)
$(call add-radio-file,images/rpm.mbn)
$(call add-radio-file,images/tz.mbn)
$(call add-radio-file,images/devcfg.mbn)
$(call add-radio-file,images/adspso.bin)
$(call add-radio-file,images/sec.dat)
$(call add-radio-file,images/splash.img)
$(call add-radio-file,images/lksecapp.mbn)
$(call add-radio-file,images/cmnlib.mbn)
$(call add-radio-file,images/cmnlib64.mbn)
然后将当前路径下images下的相关文件,以及 $(PRODUCT_OUT)/emmc_appsboot.mbn 作为INSTALLED_RADIOIMAGE_TARGET的依赖。
2 因为在生成target-files.zip中的规则中有:
build/core/Makefile
|
所以target-files.zip中的releasetools.py其实也来自于$(tool_extensions)下的releasetools.py。因此,如果TARGET_RELEASETOOLS_EXTENSIONS没有定义,那么我们就要检查 $(TARGET_DEVICE_DIR)/../common这个路径是否存在,且这个路径下是否存在正确的releasetools.py。否则我们可以自定义TARGET_RELEASETOOLS_EXTENSIONS的值,将它设为正确的releasetools.py所在的目录。
根据在 OPTIONS.info_dict的tool_extensions取值过程分析 中的分析,我们可以在device/x*****/mxxx下的BoardConfig.mk中设置它的值,
如 TARGET_RELEASETOOLS_EXTENSIONS := device/x*****/mxxx 或者 TARGET_RELEASETOOLS_EXTENSIONS := device/qcom/common
标签:files,target,zip,ota,firmware,升级包,OPTIONS,TARGET From: https://blog.51cto.com/u_16248677/7385150