目录
- 目录
- 概述
- make otapackage
- BUILT_TARGET_FILES_PACKAGE
- ota_from_target_files
- WriteFullOTAPackage
- SignOutput
- 总结
概述
make otapackage是Android Build系统支持的命令,用来生成Recovery系统能够进行升级的zip包。因此,想要了解Android的OTA升级机制,我们首先需要学习make otapackage命令的执行过程。
为了防止泄密,以下源码内容都是基于Android4.4.2_r1分支进行分析。
make otapackage
make otapackage是一个.PHONY伪目标。make系统中,伪目标并不是一个文件,只是一个标签,由于伪目标不是文件,所以make无法生成它的依赖关系和决定它是否要执行。我们只能通过显示的指明这个“目标”才能让其生效。需要注意的是,伪目标的取名不能和文件名重名,不然就失去伪目标的意义了。
了解了.PHONY之后,我们来看一下make otapackage的make源码:
.PHONY: otapackageotapackage: $(INTERNAL_OTA_PACKAGE_TARGET)
通过make代码,我们看到otapackage这个伪目标是依赖于$(INTERNAL_OTA_PACKAGE_TARGET)的,接下来,我会分析一下依赖文件的生成源码:
INTERNAL_OTA_PACKAGE_TARGET := $(PRODUCT_OUT)/$(name).zip$(INTERNAL_OTA_PACKAGE_TARGET): KEY_CERT_PAIR := $(DEFAULT_KEY_CERT_PAIR)$(INTERNAL_OTA_PACKAGE_TARGET): $(BUILT_TARGET_FILES_PACKAGE) $(DISTTOOLS) @echo "Package OTA: $@" $(hide) ./build/tools/releasetools/ota_from_target_files -v \ -p $(HOST_OUT) \ -k $(KEY_CERT_PAIR) \ $(BUILT_TARGET_FILES_PACKAGE) $@
可以看到,$(INTERNAL_OTA_PACKAGE_TARGET)依赖于$(KEY_CERT_PAIR),$(HOST_OUT),$(BUILT_TARGET_FILES_PACKAGE)这三个文件和$(DISTTOOLS)所代表的jar包。
而这些所依赖的文件,最后都会作为参数传递给ota_from_target_files这个python脚本,这个python脚本会进一步生成最终的recovery升级包。示例参数如下:
['-v', '-p', 'out/host/linux-x86', '-k', 'build/target/product/security/testkey', 'out/target/product/xxx/obj/PACKAGING/target_files_intermediates/product-target_files-wangzhengyi.zip', 'out/target/product/xxx/product_20150724.1246-ota.zip']
从参数中可以看出,out/target/product/xxx/product_20150724.1246-ota.zip是最终的recovery升级包,而out/target/product/xxx/obj/PACKAGING/target_files_intermediates/product-target_files-wangzhengyi.zip是中间的临时zip包,它其实就是$(BUILT_TARGET_FILES_PACKAGE),接下来我们看一下$(BUILT_TARGET_FILES_PACKAGE)的生成代码。
BUILT_TARGET_FILES_PACKAGE
BUILT_TARGET_FILES_PACKAGE的make源码如下:
BUILT_TARGET_FILES_PACKAGE:= $(intermediates)/$(name).zip$(BUILT_TARGET_FILES_PACKAGE): intermediates := $(intermediates)$(BUILT_TARGET_FILES_PACKAGE): zip_root := $(intermediates)/$(name)# $(1): Directory to copy# $(2): Location to copy it to# The "ls -A" is to prevent "acp s/* d" from failing if s is empty.define package_files-copy-root if [ -d "$(strip $(1))" -a "$$(ls -A $(1))" ]; then \ mkdir -p $(2) && \ $(ACP) -rd $(strip $(1))/* $(2); \ fiendifbuilt_ota_tools := \ $(call intermediates-dir-for,EXECUTABLES,applypatch)/applypatch \ $(call intermediates-dir-for,EXECUTABLES,applypatch_static)/applypatch_static \ $(call intermediates-dir-for,EXECUTABLES,check_prereq)/check_prereq \ $(call intermediates-dir-for,EXECUTABLES,sqlite3)/sqlite3 \ $(call intermediates-dir-for,EXECUTABLES,updater)/updater$(BUILT_TARGET_FILES_PACKAGE): PRIVATE_OTA_TOOLS := $(built_ota_tools)$(BUILT_TARGET_FILES_PACKAGE): PRIVATE_RECOVERY_API_VERSION := $(RECOVERY_API_VERSION)$(BUILT_TARGET_FILES_PACKAGE): PRIVATE_RECOVERY_FSTAB_VERSION := $(RECOVERY_FSTAB_VERSION)# 开始构建中间zip包$(BUILT_TARGET_FILES_PACKAGE): \ $(INSTALLED_BOOTIMAGE_TARGET) \ $(INSTALLED_RADIOIMAGE_TARGET) \ $(INSTALLED_RECOVERYIMAGE_TARGET) \ $(INSTALLED_SYSTEMIMAGE) \ $(INSTALLED_USERDATAIMAGE_TARGET) \ $(INSTALLED_CACHEIMAGE_TARGET) \ $(INSTALLED_VENDORIMAGE_TARGET) \ $(INSTALLED_ANDROID_INFO_TXT_TARGET) \ $(SELINUX_FC) \ $(built_ota_tools) \ $(APKCERTS_FILE) \ $(HOST_OUT_EXECUTABLES)/fs_config \ | $(ACP) @echo "Package target files: $@" # 删除之前的zip文件 $(hide) rm -rf $@ $(zip_root) $(hide) mkdir -p $(dir $@) $(zip_root) @# Components of the recovery image $(hide) mkdir -p $(zip_root)/RECOVERY $(hide) $(call package_files-copy-root, \ $(TARGET_RECOVERY_ROOT_OUT),$(zip_root)/RECOVERY/RAMDISK)ifdef INSTALLED_KERNEL_TARGET $(hide) $(INSTALLED_KERNEL_TARGET) $(zip_root)/RECOVERY/kernelendififdef INSTALLED_2NDBOOTLOADER_TARGET $(hide) $(ACP) \ $(INSTALLED_2NDBOOTLOADER_TARGET) $(zip_root)/RECOVERY/secondendififdef BOARD_KERNEL_CMDLINE $(hide) echo "$(BOARD_KERNEL_CMDLINE)" > $(zip_root)/RECOVERY/cmdlineendififdef BOARD_KERNEL_PAGESIZE $(hide) echo "$(BOARD_KERNEL_PAGESIZE)" > $(zip_root)/RECOVERY/pagesizeendif $(hide) $(foreach t, $(INSTALLED_RADIOIMAGE_TARGET), \ mkdir -p $(zip_root)/RADIO; \ $(ACP) $(t) $(zip_root)/RADIO/$(notdir $(t));) @# Contents of the system image $(hide) $(call package_files-copy-root, \ $(SYSTEMIMAGE_SOURCE_DIR),$(zip_root)/SYSTEM) @# Contents of the data image $(hide) $(call package_files-copy-root, \ $(TARGET_OUT_DATA),$(zip_root)/DATA)ifdef BOARD_VENDORIMAGE_FILE_SYSTEM_TYPE @# Contents of the vendor image $(hide) $(call package_files-copy-root, \ $(TARGET_OUT_VENDOR), $(zip_root)/VENDOR)endif @# Extra contents of the OTA package $(hide) mkdir -p $(zip_root)/OTA/bin $(hide) $(ACP) $(INSTALLED_ANDROID_INFO_TXT_TARGET) $(zip_root)/OTA/ $(hide) $(ACP) $(PRIVATE_OTA_TOOLS) $(zip_root)/OTA/bin/ @# Files that do not end up in any images, but are necessary to build them. $(hide) mkdir -p $(zip_root)/META $(hide) $(ACP) $(APKCERTS_FILE) $(zip_root)/META/apkcerts.txt # 向otakeys.txt文件中写入各种信息 $(hide) echo "$(PRODUCT_OTA_PUBLIC_KEYS)" > $(zip_root)/META/otakeys.txt $(hide) echo "recovery_api_version=$(PRIVATE_RECOVERY_API_VERSION)" > $(zip_root)/META/misc_info.txt $(hide) echo "fstab_version=$(PRIVATE_RECOVERY_FSTAB_VERSION)" >> $(zip_root)/META/misc_info.txtifdef BOARD_FLASH_BLOCK_SIZE $(hide) echo "blocksize=$(BOARD_FLASH_BLOCK_SIZE)" >> $(zip_root)/META/misc_info.txtendififdef BOARD_BOOTIMAGE_PARTITION_SIZE $(hide) echo "boot_size=$(BOARD_BOOTIMAGE_PARTITION_SIZE)" >> $(zip_root)/META/misc_info.txtendififdef BOARD_RECOVERYIMAGE_PARTITION_SIZE $(hide) echo "recovery_size=$(BOARD_RECOVERYIMAGE_PARTITION_SIZE)" >> $(zip_root)/META/misc_info.txtendif $(hide) echo "tool_extensions=$(tool_extensions)" >> $(zip_root)/META/misc_info.txt $(hide) echo "default_system_dev_certificate=$(DEFAULT_SYSTEM_DEV_CERTIFICATE)" >> $(zip_root)/META/misc_info.txtifdef PRODUCT_EXTRA_RECOVERY_KEYS $(hide) echo "extra_recovery_keys=$(PRODUCT_EXTRA_RECOVERY_KEYS)" >> $(zip_root)/META/misc_info.txtendif $(hide) echo "mkbootimg_args=$(BOARD_MKBOOTIMG_ARGS)" >> $(zip_root)/META/misc_info.txt $(hide) echo "use_set_metadata=1" >> $(zip_root)/META/misc_info.txt $(hide) echo "update_rename_support=1" >> $(zip_root)/META/misc_info.txt $(call generate-userimage-prop-dictionary, $(zip_root)/META/misc_info.txt) # 打包zip包 $(hide) (cd $(zip_root) && zip -qry ../$(notdir $@) .) $(hide) zipinfo -1 $@ | awk 'BEGIN { FS="SYSTEM/" } /^SYSTEM\// {print "system/" $$2}' | $(HOST_OUT_EXECUTABLES)/fs_config -C -S $(SELINUX_FC) > $(zip_root)/META/filesystem_config.txt $(hide) zipinfo -1 $@ | awk 'BEGIN { FS="BOOT/RAMDISK/" } /^BOOT\/RAMDISK\// {print $$2}' | $(HOST_OUT_EXECUTABLES)/fs_config -C -S $(SELINUX_FC) > $(zip_root)/META/boot_filesystem_config.txt $(hide) zipinfo -1 $@ | awk 'BEGIN { FS="RECOVERY/RAMDISK/" } /^RECOVERY\/RAMDISK\// {print $$2}' | $(HOST_OUT_EXECUTABLES)/fs_config -C -S $(SELINUX_FC) > $(zip_root)/META/recovery_filesystem_config.txt $(hide) (cd $(zip_root) && zip -q ../$(notdir $@) META/*filesystem_config.txt).PHONY: target-files-packagetarget-files-package: $(BUILT_TARGET_FILES_PACKAGE)
不知道大家第一次看到这段代码,第一感觉如何,会不会觉得很难理解?
反正,我学习Android Build系统的makefile源码时候,都感觉很晕、很绕、很难入手,毕竟我之前是写解释型语言的。这里给大家介绍两个经验:
- 坚持的去看,坚持总是没错的。
- 用warning和error去打log,去跟踪。
接下来,我进一步解释一下上面这块目标(target-files-package)主要实现了哪些功能:
- 创建$(zip_root)根目录,接下来都是基于zip_root目录进行其它目录的创建。
- 创建并填充RECOVERY目录,包括:kernel镜像文件、RAMDISK目录。此目录最终用来生成recovery.img。
- 创建并填充BOOT目录,包括:kernel镜像文件、RAMDISK目录、ramdisk镜像。此目录最终用来生成boot.img。
- 填充SYSTEM目录。
- 创建并填充OTA/bin目录。
- 创建META目录并向该目录下添加一些文本文件。
- 最后将目录打包成zip包。
ota_from_target_files
上面的make系统只是生成了target-files-package这个临时目标文件,而最终Recovery系统能使用的zip包还需要经过ota_from_target_files脚本处理。
ota_from_target_files脚本还是很强大的,可以生成:
- OTA全量包。
- OTA增量包。
这里我们只介绍OTA全量包的生成过程。
在最初的otapackage的make源码中,我们已经看到,最终是会调用ota_from_target_files脚本的,并且还有必要的参数传递,这里给出示例的参数如下:
['-v', '-p', 'out/host/linux-x86', '-k', 'build/target/product/security/testkey', 'out/target/product/xxx/obj/PACKAGING/target_files_intermediates/product-target_files-wangzhengyi.zip', 'out/target/product/xxx/product_20150724.1246-ota.zip']
参数作用:
- -v : verbose标识,有这个标识在ota生成过程中会打印出更多的执行信息。
- -p : 定义脚本中用到的可执行文件的路径。
- -k : 签名时所用的密钥,防止ota升级包的内容被篡改。
跟OTA增量升级相关的源码如下(为了理清逻辑关系,我的代码顺序和声明顺序正好是反过来的):
if __name__ == '__main__': try: main(sys.argv[1:]) except common.ExternalError, e: print print " ERROR: %s" % (e,) print sys.exit(1)def main(argv): # 调用getprop.getprop处理我们传入的参数,这里省略处理参数代码 # 处理参数主要是为了给OPTIONS对象赋值 # 以上面的传入参数为例,处理之后,OPTIONS对象的成员属性赋值如下: # OPTIONS.package_key = build/target/product/security/testkey # OPTIONS.verbose = True # OPTIONS.search_path = out/host/linux-x86 if len(args) != 2: # args是getprop.getprop无法处理的参数 # 这里的args是['target-files-package', 'final ota zip name']组成的list sys.exit(1) print "unzipping target target-files..." # OPTIONS.input_tmp保存解压目录名称 # input_zip为根据target-files-package生成的zipfile对象 OPTIONS.input_tmp, input_zip = common.UnzipTemp(args[0]) OPTIONS.target_tmp = OPTIONS.input_tmp # 主要是解析如下三个文件,并将文件内容以(k,v)键值对形式保存到OPTIONS.info_dict中 # 三个文件分别是: # 1. META/misc_info.txt # 2. SYSTEM/build.prop # 3. RECOVERY/RAMDISK/etc/recovery.fstab OPTIONS.info_dict = common.LoadInfoDict(input_zip) if "selinux_fc" in OPTIONS.info_dict: OPTIONS.info_dict["selinux_fc"] = os.path.join(OPTIONS.input_tmp, "BOOT", "RAMDISK", "file_contexts") if OPTIONS.device_specific is None: OPTIONS.device_specific = OPTIONS.info_dict.get("tool_extensions", None) if OPTIONS.device_specific is not None: OPTIONS.device_specific = os.path.normpath(OPTIONS.device_specific) print "using device-specific extensions in", OPTIONS.device_specific temp_zip_file = tempfile.NamedTemporaryFile() output_zip = zipfile.ZipFile(temp_zip_file, "w", compression=zipfile.ZIP_DEFLATED) # 构造全量包时OPTIONS.incremental_source的值为None 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") output_zip.close() # 对全量包进行防篡改签名,并进行重命名,稍微重点分析 SignOutput(temp_zip_file.name, args[1]) temp_zip_file.close() print "done."# common类的UnzipTemp函数源码def UnzipTemp(filename, pattern=None): # 创建/tmp目录下临时文件,用于存储target-files-package解压的内容 tmp = tempfile.mkdtemp(prefix="targetfiles-") OPTIONS.tempfiles.append(tmp) def unzip_to_dir(filename, dirname): cmd = ["unzip", "-o", "-q", filename, "-d", dirname] p = subprocess.Popen(cmd, stdout = subprocess.PIPE) p.communicate() if p.returncode != 0: raise ExternalError("failed to unzip input target-files") unzip_to_dir(filename, tmp) return tmp, zipfile.ZipFile(filename, "r")# common类的LoadInfoDict函数def LoadInfoDict(zip): d = {} try: for line in zip.read("META/misc_info.txt").split("\n"): line = line.strip() if not line or line.startswith("#"): continue k, v = line.split("=", 1) d[k] = v except KeyError: pass # 解析挂载信息,不上源码了 d["fstab"] = LoadRecoveryFSTab(zip, d["fstab_version"]) d["build.prop"] = LoadBuildProp(zip) return ddef LoadBuildProp(zip): try: data = zip.read("SYSTEM/build.prop") except KeyError: print "Warning: could not find SYSTEM/build.prop in %s" % zip data = "" d = {} for line in data.split("\n"): line = line.strip() if not line or line.startswith("#"): continue name, value = line.split("=", 1) d[name] = value return d
ota_from_target_files生成全量包的代码如上所示,一些不必要看的代码我已经省略了。但是还有两处生成全量包的关键函数我这边需要重点分析。
WriteFullOTAPackage
这是根据target-files-package生成全量包的关键函数,我们来看一下它的具体实现,有一点需要解释一下,根据我们的示例参数,input_zip和output_zip分别是:
- input_zip:out/target/product/xxx/obj/PACKAGING/target_files_intermediates/product-target_files-wangzhengyi.zip的zipfile对象
- output_zip:/tmp目录下临时文件的zipfile对象
def WriteFullOTAPackage(input_zip, output_zip): script = edify_generator.EdifyGenerator(3, OPTIONS.info_dict) # 从build.prop文件内容中获取属性构建metadata字典 metadata = {"post-build": GetBuildProp("ro.build.fingerprint", OPTIONS.info_dict), "pre-device": GetBuildProp("ro.product.device", OPTIONS.info_dict), "post-timestamp": GetBuildProp("ro.build.data.utc", OPTIONS.info_dict), } 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) # 在updater-script脚本增加时间判断,如果需要升级的版本晚于当前系统的时间, # 则不进行更新 if not OPTIONS.omit_prereq: ts = GetBuildProp("ro.build.date.utc", OPTIONS.info_dict) ts_text = GetBuildProp("ro.build.date", OPTIONS.info_dict) script.AssertOlderBuild(ts, ts_text) # 在updater-script脚本中增加产品类型判断 AppendAssertions(script, OPTIONS.info_dict) # 在updater-script增加进度显示 script.ShowProgress(0.5, 0) # 安全相关,将BOOT/RAMDISK/file_contexts写入到output_zip中 if "selinux_fc" in OPTIONS.info_dict: WritePolicyConfig(OPTIONS.info_dict["selinux_fc"], output_zip) # 在updater-script脚本中: # 1. 增加system分区格式化代码 # 2. 增加system分区挂载代码 # 3. 将recovery目录解压到system分区 # 4. 将system目录解压到system分区 script.FormatPartition("/system") script.Mount("/system") script.UnpackPackageDir("recovery", "/system") script.UnpackPackageDir("system", "/system") # 将input_zip的system目录文件拷贝到output_zip中,并返回链接文件名称 symlinks = CopySystemFiles(input_zip, output_zip) script.MakeSymlinks(symlinks) # 调用mkbootfs、minigzip、mkbootimg构造boot.img和recovery.img boot_img = common.GetBootableImage("boot.img", "boot.img", OPTIONS.input_tmp, "BOOT") recovery_img = common.GetBootableImage("recovery.img", "recovery.img", OPTIONS.input_tmp, "RECOVERY") MakeRecoveryPath(OPTIONS.input_tmp, output_zip, recovery_img, boot_img) # 在updater-script中设置system文件权限 Item.Get("system").SetPermissions(script) # 将boot.img放入output_zip中,并在updater-script中增加写入信息 common.CheckSize(boot_img.data, "boot.img", OPTIONS.info_dict) common.ZipWriteStr(output_zip, "boot.img", boot_img.data) script.ShowProgress(0.2, 0) script.ShowProgress(0.2, 10) script.WriteRawImage("/boot", "boot.img") # updater-script中增加分区卸载信息 script.UmountAll() # 将updater-script和update-binary写入到output_zip script.AddToZip(input_zip, output_zip) #将metadata信息写入到output_zip的META-INF/com/android/metadata文件中 WriteMetadata(metadata, output_zip)def CopySystemFiles(input_zip, output_zip=None, substitute=None): symlinks = [] for info in input_zip.infolist(): if info.filename.startswith("SYSTEM/"): basefilename = info.filename[7:] if IsSymlink(info): #链接文件 symlinks.append((input_zip.read(info.filename), "/system/" + basefilename)) else: info2 = copy.copy(info) fn = info2.filename = "system/" + basefilename if output_zip is not None: data = input_zip.read(info.filename) output_zip.writestr(info2, data) if fn.endswith("/"): Item.Get(fn[:-1], dir=True) else: Item.Get(fn, dir=False)i symlinks.sort() return symlinksdef AddToZip(self, input_zip, output_zip, input_path=None): common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script", "\n".join(self.script) + "\n") if input_path is None: data = input_zip.read("OTA/bin/updater") else: data = open(os.path.join(input_path, "updater")).read() common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary", data, perms=0755)
代码里注释写的很清楚了,大家有一点python基础,应该很好理解。
需要注意的是:这里的output_zip是tmp目录下的临时zip文件,那么真正的OTA包是在哪里生成的呢?
这就需要去看一下SignOutput的源码了。
SignOutput
首先,我们先看一下main函数中对SignOutput的调用:
SignOutput(temp_zip_file.name, args[1])
其中:
- temp_zip_file.name : 就是output_zip对象对应的文件名称
- args[1] : 就是最终的OTA zip包的名称
所以,我们很容易想到通过签名之后才形成的正式OTA zip包。
源码如下:
def SignOutput(temp_zip_name, output_zip_name): key_passwords = common.GetKeyPasswords([OPTIONS.package_key]) pw = key_passwords[OPTIONS.package_key] common.SignFile(temp_zip_file, output_zip_name, OPTIONS.package_key, pw, whole_file=True)def SignFile(input_name, output_name, key, password, align=None, whole_file=False): sign_name = output_name cmd = [OPTIONS.java_path, "-Xmx2048m", "-jar", os.path.join(OPTIONS.search_path, OPTIONS.signapk_path)] cmd.extend(OPTIONS.extra_signapk_args) if whole_file: cmd.append("-w") cmd.extend([key + OPTIONS.public_key_suffix, key + OPTIONS.private_key_suffix, input_name, sign_name]) p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) if password is not None: password += "\n" p.communicate(password)
真正的签名执行命令如下:
java -Xmx2048m -jar out/host/linux-x86/framework/signapk.jar -w build/target/product/security/testkey.xxx.pem build/target/product/security/testkey.xxx /tmp/tmpXZiMiN out/target/product/xxx/product_20150724.1246-ota.zip
总结
这篇博客主要是总结了执行make otapackage所需要了解的依赖关系和python生成OTA包的脚本原理。
接下来,我还会总结一篇文章,是从Recovery源码角度出发,看Recovery是如何解析OTA zip包,执行升级流程。
版权声明:本文为博主原创文章,未经博主允许不得转载。