#升级包的制作与升级的流程

一、升级包的制作

1、Makefile

make otapackage是一个.PHONY伪目标,查看\build\core\Makefile:

.PHONY: otapackage
otapackage:droidcore dist_files $(INTERNAL_OTA_PACKAGE_TARGET)

可以看到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: $@"
    @echo ./build/tools/releasetools/ota_from_target_files -v \
       $(amlogic_flag) \
       $(omit_prereq_flag) \
       $(UPDATE_DTB) \
       -n \
       -p $(HOST_OUT) \
       $(wipeopt) \
       $(baksupport) \  
       -k $(KEY_CERT_PAIR) \
       $(recovery_not_patch) \
       $(dm_verity) \
       $(BUILT_TARGET_FILES_PACKAGE) $@

可以看到,$(INTERNAL_OTA_PACKAGE_TARGET)的生成依赖于$(BUILT_TARGET_FILES_PACKAGE)和$(DISTTOOLS)等生成

$(DISTTOOLS)的编译规则如下:

DISTTOOLS :=  $(HOST_OUT_EXECUTABLES)/minigzip \
      $(HOST_OUT_EXECUTABLES)/mkbootfs \
      $(HOST_OUT_EXECUTABLES)/mkbootimg \
      $(HOST_OUT_EXECUTABLES)/fs_config \
      $(HOST_OUT_EXECUTABLES)/mkyaffs2image \
      $(HOST_OUT_EXECUTABLES)/zipalign \
      $(HOST_OUT_EXECUTABLES)/bsdiff \
      $(HOST_OUT_EXECUTABLES)/imgdiff \
      $(HOST_OUT_JAVA_LIBRARIES)/dumpkey.jar \
      $(HOST_OUT_JAVA_LIBRARIES)/signapk.jar \
      $(HOST_OUT_EXECUTABLES)/mkuserimg.sh \
      $(HOST_OUT_EXECUTABLES)/make_ext4fs \
      $(HOST_OUT_EXECUTABLES)/simg2img \
      $(HOST_OUT_EXECUTABLES)/e2fsck

OTATOOLS := $(DISTTOOLS) \
      $(HOST_OUT_EXECUTABLES)/aapt

.PHONY: otatools
otatools: $(OTATOOLS)

变量$(HOST_OUT_EXECUTABLES)指代的是out/host/linux-x86/bin目录,而变量$(HOST_OUT_JAVA_LIBRARIES)/表示的是out/host/linux-x86/framework目录。可以看出,$(OTATOOLS)用于指定命令执行所需要的工具,例如:minigzip:用于gzip文件;make_ext4fs:将文件转换为ext4类型;mkyaffs2image:用于yaffs文件系统;bsdiff,imgdiff用于差分;signapk.jar用于签名等功能。

$(BUILT_TARGET_FILES_PACKAGE)主要是完成了两件事:重新打包system.img文件和生成差分资源包。$(BUILT_TARGET_FILES_PACKAGE)的编译规则如下所示:

# -----------------------------------------------------------------
# A zip of the directories that map to the target filesystem.
# This zip can be used to create an OTA package or filesystem image
# as a post-build step.
#
name := $(TARGET_PRODUCT)
ifeq ($(TARGET_BUILD_TYPE),debug)
  name := $(name)_debug
endif
name := $(name)-target_files-$(FILE_NAME_TAG)

intermediates := $(call intermediates-dir-for,PACKAGING,target_files)
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); \
  fi
endef

built_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)

# Depending on the various images guarantees that the underlying
# directories are up-to-date.
#开始构建中间zip包
$(BUILT_TARGET_FILES_PACKAGE): \
        $(INSTALLED_BOOTIMAGE_TARGET) \
        $(INSTALLED_RADIOIMAGE_TARGET) \
        $(INSTALLED_RECOVERYIMAGE_TARGET) \
        $(INSTALLED_AMLOGIC_RECOVERY_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) \
        $(INSTALLED_AML_LOGO) \
        $(TARGET_AMLOGIC_KERNEL)
    @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) $(ACP) $(INSTALLED_KERNEL_TARGET) $(zip_root)/RECOVERY/kernel
    endif
    ifdef TARGET_AMLOGIC_RECOVERY_KERNEL
        $(hide) $(ACP) $(TARGET_AMLOGIC_RECOVERY_KERNEL) $(zip_root)/RECOVERY/kernel
    endif
    ifdef INSTALLED_2NDBOOTLOADER_TARGET
        $(hide) $(ACP) \
            $(INSTALLED_2NDBOOTLOADER_TARGET) $(zip_root)/RECOVERY/second
    endif
    ifdef INSTALLED_BOARDDTB_TARGET
        $(hide) $(ACP) $(INSTALLED_BOARDDTB_TARGET) $(zip_root)/RECOVERY/second
    endif
    ifdef BOARD_KERNEL_CMDLINE
        $(hide) echo "$(BOARD_KERNEL_CMDLINE)" > $(zip_root)/RECOVERY/cmdline
    endif
    ifdef BOARD_KERNEL_BASE
        $(hide) echo "$(BOARD_KERNEL_BASE)" > $(zip_root)/RECOVERY/base
    endif
    ifdef BOARD_KERNEL_PAGESIZE
        $(hide) echo "$(BOARD_KERNEL_PAGESIZE)" > $(zip_root)/RECOVERY/pagesize
    endif
    ifdef BOARD_KERNEL_OFFSET
        $(hide) echo "$(BOARD_KERNEL_OFFSET)" > $(zip_root)/RECOVERY/kernel_offset
    endif

        @# Components of the boot image
        $(hide) mkdir -p $(zip_root)/BOOT
        $(hide) $(call package_files-copy-root, \
            $(TARGET_ROOT_OUT),$(zip_root)/BOOT/RAMDISK)
    ifdef INSTALLED_KERNEL_TARGET
        $(hide) $(ACP) $(INSTALLED_KERNEL_TARGET) $(zip_root)/BOOT/kernel
    endif
    ifdef TARGET_AMLOGIC_KERNEL
        $(hide) $(ACP) $(TARGET_AMLOGIC_KERNEL) $(zip_root)/BOOT/kernel
    endif

    ......

    # 打包zip包
    #ifeq ($(PRODUCT_BUILD_SECURE_BOOT_IMAGE_DIRECTLY),true)
    $(hide) (cd $(zip_root) && zip -qry ../$(notdir $@) .)
    @# Run fs_config on all the system, boot ramdisk, and recovery ramdisk files in the zip, and save the output
    $(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-package
target-files-package: $(BUILT_TARGET_FILES_PACKAGE)

这块target-files-package目标主要实现的功能:

● 创建$(zip_root)目录,$(zip_root)即out/target/product//obj/PACKAGING/target_files_from_intermedias/-target_files-

● 创建/$(zip_root)/RECOVERY目录并将COPY相关文件,包括:kernel镜像文件、RAMDISK目录。此目录最终用来生成recovery.img

● 创建/$(zip_root)/BOOT目录并将COPY相关文件,包括:kernel镜像文件、RAMDISK目录、ramdisk镜像。此目录最终用来生成boot.img

● 创建其他目录并COPY文件,包括SYSTEM目录、OTA/bin目录、META目录

● 将$(zip_root)目录压缩为资源差分包

完成这些工作后调用ota_from_target_files做进一步的处理:

$(INTERNAL_OTA_PACKAGE_TARGET): $(BUILT_TARGET_FILES_PACKAGE) $(DISTTOOLS)
    @echo "Package OTA: $@"
    @echo ./build/tools/releasetools/ota_from_target_files -v \
       $(amlogic_flag) \
       $(omit_prereq_flag) \
       $(UPDATE_DTB) \
       -n \
       -p $(HOST_OUT) \
       $(wipeopt) \
       $(baksupport) \
       -k $(KEY_CERT_PAIR) \
       $(recovery_not_patch) \
       $(dm_verity) \
       $(BUILT_TARGET_FILES_PACKAGE) $@

2、ota_from_target_files

/build/tools/releasetools/ota_from_target_files.py

Given a target-files zipfile, produces an OTA package that installs
that build.  An incremental OTA is produced if -i is given, otherwise
a full OTA is produced.

Usage:  ota_from_target_files [flags] input_target_files output_ota_package

  -b  (--board_config)  
      Deprecated.

  -k (--package_key)  Key to use to sign the package (default is
      the value of default_system_dev_certificate from the input
      target-files's META/misc_info.txt, or
      "build/target/product/security/testkey" if that value is not
      specified).
      For incremental OTAs, the default value is based on the source
      target-file, not the target build.

  -i  (--incremental_from)  
      Generate an incremental OTA using the given target-files zip as
      the starting build.

  -w  (--wipe_user_data)
      Generate an OTA package that will wipe the user data partition
      when installed.

  -n  (--no_prereq)
      Omit the timestamp prereq check normally included at the top of
      the build scripts (used for developer OTA packages which
      legitimately need to go back and forth).

  -e  (--extra_script)  
      Insert the contents of file at the end of the update script.

  -a  (--aslr_mode)  
      Specify whether to turn on ASLR for the package (on by default).

用法:ota_from_target_files [flags] input_target_files output_ota_package

-i:生成增量OTA包时使用此选项

-k:签名所使用的密钥

-w:是否清除userdata分区

-e:是否有额外运行的脚本

-p:定义脚本用到的一些可执行文件的路径

具体来看下这个脚本的执行过程:

def main(argv):

  #将用户设定的option存入到一个OPTIONS类中
  def option_handler(o, a):
    if o in ("-b", "--board_config"):
      pass   # deprecated
    elif o in ("-k", "--package_key"):
      OPTIONS.package_key = a
    elif o in ("-i", "--incremental_from"):
      OPTIONS.incremental_source = a
    elif o in ("-w", "--wipe_user_data"):
      OPTIONS.wipe_user_data = True
    elif o in ("-s", "--no_wipe_system"):
      OPTIONS.no_wipe_system = True
    elif o in ("-n", "--no_prereq"):
      OPTIONS.omit_prereq = True
    elif o in ("-e", "--extra_script"):
      OPTIONS.extra_script = a
    elif o in ("-c", "--wipe_cache"):
      OPTIONS.wipe_cache_enable = True
    elif o in ("-a", "--aslr_mode"):
      if a in ("on", "On", "true", "True", "yes", "Yes"):
        OPTIONS.aslr_mode = True
      else:
        OPTIONS.aslr_mode = False
    elif o in ("--worker_threads"):
      OPTIONS.worker_threads = int(a)
    elif o in ("--backup_support"):
      OPTIONS.backup_support = True
    elif o in ("--recovery_not_patch"):
      OPTIONS.recovery_not_patch = True
    elif o in ("--dm_verity"):
      OPTIONS.dm_verity_enable = True
    elif o in ("--progress_iptv"):
      OPTIONS.progress_iptv = True
    elif o in ("--dtb"):
      OPTIONS.update_dtb = True
    else:
      return False
    return True

    #解析参数,将得到的参数和参数值传回给args
    args = common.ParseOptions(argv, __doc__,
                             extra_opts="b:k:i:wsneca:",
                             extra_long_opts=["board_config=",
                                              "package_key=",
                                              "incremental_from=",
                                              "wipe_user_data",
                                              "no_wipe_system",
                                              "no_prereq",
                                              "extra_script=",
                                              "wipe_cache",
                                              "dm_verity",
                                              "progress_iptv",
                                              "dtb",
                                              "worker_threads=",
                                              "aslr_mode=",
                          "backup_support",
                          "recovery_not_patch",
                                              ],
                             extra_option_handler=option_handler)

common.py中的ParseOptions,主要是调用python中的getopt模块中的getopt函数来解析参数

def ParseOptions(argv,
                 docstring,
                 extra_opts="", extra_long_opts=(),
                 extra_option_handler=None):
  """Parse the options in argv and return any arguments that aren't
  flags.  docstring is the calling module's docstring, to be displayed
  for errors and -h.  extra_opts and extra_long_opts are for flags
  defined by the caller, which are processed by passing them to
  extra_option_handler."""

  try:
    opts, args = getopt.getopt(
        argv, "ahvp:s:x:" + extra_opts,
        ["amlogic", "help", "verbose", "path=", "signapk_path=", "extra_signapk_args=",
         "java_path=", "public_key_suffix=", "private_key_suffix=",
         "device_specific=", "extra="] +
        list(extra_long_opts))
  except getopt.GetoptError, err:
    Usage(docstring)
    print "**", str(err), "**"
    sys.exit(2)

  path_specified = False

  for o, a in opts:
    if o in ("-h", "--help"):
      Usage(docstring)
      sys.exit()
    elif o in ("-a", "--amlogic"):
      OPTIONS.amlogic = True
    elif o in ("-v", "--verbose"):
      OPTIONS.verbose = True
    elif o in ("-p", "--path"):
      OPTIONS.search_path = a
    elif o in ("--signapk_path",):
      OPTIONS.signapk_path = a
    elif o in ("--extra_signapk_args",):
      OPTIONS.extra_signapk_args = shlex.split(a)
    elif o in ("--java_path",):
      OPTIONS.java_path = a
    elif o in ("--public_key_suffix",):
      OPTIONS.public_key_suffix = a
    elif o in ("--private_key_suffix",):
      OPTIONS.private_key_suffix = a
    elif o in ("-s", "--device_specific"):
      OPTIONS.device_specific = a
    elif o in ("-x", "--extra"):
      key, value = a.split("=", 1)
      OPTIONS.extras[key] = value
    else:
      if extra_option_handler is None or not extra_option_handler(o, a):
        assert False, "unknown option \"%s\"" % (o,)

  os.environ["PATH"] = (os.path.join(OPTIONS.search_path, "bin") +
                        os.pathsep + os.environ["PATH"])

  return args

然后解压目标文件包

print "unzipping target target-files..."
OPTIONS.input_tmp, input_zip = common.UnzipTemp(args[0])

#保存解压目录名称
OPTIONS.target_tmp = OPTIONS.input_tmp
#根据target-files-package生成的zipfile对象
OPTIONS.info_dict = common.LoadInfoDict(input_zip)

其中LoadInfoDict主要是解析了三个文件:META/misc_info.txt、SYSTEM/build.prop、RECOVERY/RAMDISK/etc/recovery.fstab,并将文件内容以(k,v)键值对形式保存到OPTIONS.info_dict中。

随后根据incremental_source的值,也就是之前的-i参数来判断制作全量包还是增量包:

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")
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 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)
WriteIncrementalOTAPackage(input_zip, source_zip, output_zip)

先来分析全量包的制作WriteFullOTAPackage:

def WriteFullOTAPackage(input_zip, output_zip):
  # TODO: how to determine this?  We don't know what version it will
  # be installed on top of.  For now, we expect the API just won't
  # change very often.
  script = edify_generator.EdifyGenerator(3, OPTIONS.info_dict)

新建了updater-script文件,这是一个Android内置的Edify可扩展语言脚本,通过内建函数命令来执行操作,是整个升级包的核心。

随后开始向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)

AppendAssertions(script, OPTIONS.info_dict)
device_specific.FullOTA_Assertions()
device_specific.FullOTA_InstallBegin()

if OPTIONS.progress_iptv:
script.ShowProgress(0.8, 40)
else:
script.ShowProgress(0.9, 95)

platform = GetBuildProp("ro.board.platform", OPTIONS.info_dict)
print "ro.board.platform: %s" % (platform)
if "meson3" in platform:
script.SetBootloaderEnv("upgrade_step", "0")
elif "meson6" in platform:
script.SetBootloaderEnv("upgrade_step", "0")
else:
script.SetBootloaderEnv("upgrade_step", "3")

if OPTIONS.wipe_user_data:
script.FormatPartition("/data")

if "selinux_fc" in OPTIONS.info_dict:
WritePolicyConfig(OPTIONS.info_dict["selinux_fc"], output_zip)

if OPTIONS.dm_verity_enable:
script.FormatPartition("/system")
if not OPTIONS.recovery_not_patch:
  OPTIONS.recovery_not_patch = True
  print 'enable write recovery.img in dm-verity'
else:
if not OPTIONS.no_wipe_system:
  script.FormatPartition("/system")
  if OPTIONS.backup_support:
    script.FormatPartition("/backup")
script.Mount("/system")

if not OPTIONS.dm_verity_enable:
script.UnpackPackageDir("recovery", "/system")
script.UnpackPackageDir("system", "/system")

symlinks = CopySystemFiles(input_zip, output_zip)
script.MakeSymlinks(symlinks)

然后执行:

# 将updater-script和update-binary写入到output_zip
script.AddToZip(input_zip, output_zip)
#将metadata信息写入到output_zip的META-INF/com/android/metadata文件中
WriteMetadata(metadata, output_zip)

这里的output_zip是tmp目录下的临时zip文件,最后回到main函数执行SignOutput:

SignOutput(temp_zip_file.name, args[1])

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_name, output_zip_name, OPTIONS.package_key, pw,
                  whole_file=True)

temp_zip_file.name就是output_zip对象对应的文件名称,args[1]就是最终的OTA zip包的名称。实际执行的就是我们平时手动签名的命令。

到这里完成了全量包的制作,接下来对于差分包的制作WriteIncrementalOTAPackage进行一下分析:

def WriteIncrementalOTAPackage(target_zip, source_zip, output_zip):

    script = edify_generator.EdifyGenerator(source_version,
                                          OPTIONS.target_info_dict)

    #加载源、目标zip文件中的SYSTEM目录文件
    print "Loading target..."
    target_data = LoadSystemFiles(target_zip)
    print "Loading source..."
    source_data = LoadSystemFiles(source_zip)

    matching_file_cache = {}
    for fn in source_data.keys():
    sf = source_data[fn]
    assert fn == sf.name
    matching_file_cache["path:" + fn] = sf
    # Only allow eligability for filename/sha matching
    # if there isn't a perfect path match.
    if target_data.get(sf.name) is None:
      matching_file_cache["file:" + fn.split("/")[-1]] = sf
      matching_file_cache["sha:" + sf.sha1] = sf

    for fn in sorted(target_data.keys()):
    tf = target_data[fn]
    assert fn == tf.name
    #在ClosestFileMatch构建源、目标文件对应关系
    sf = ClosestFileMatch(tf, matching_file_cache, renames)
    if sf is not None and sf.name != tf.name:
      print "File has moved from " + sf.name + " to " + tf.name
      renames[sf.name] = tf

    if sf is None or fn in OPTIONS.require_verbatim:
      # This file should be included verbatim
      if fn in OPTIONS.prohibit_verbatim:
        raise common.ExternalError("\"%s\" must be sent verbatim" % (fn,))
      print "send", fn, "verbatim"
      #写入目标文件或者增量文件到输出zip文件
      tf.AddToZip(output_zip)
      verbatim_targets.append((fn, tf.size))
    elif tf.sha1 != sf.sha1:
      # File is different; consider sending as a patch
      diffs.append(common.Difference(tf, sf))
    else:
      # Target file data identical to source (may still be renamed)
      pass

    common.ComputeDifferences(diffs)

    #检查Fingerprint是否一致以及挂载/system分区(写入脚本)
    source_fp = GetBuildProp("ro.build.fingerprint", OPTIONS.source_info_dict)
    target_fp = GetBuildProp("ro.build.fingerprint", OPTIONS.target_info_dict)
    metadata["pre-build"] = source_fp
    metadata["post-build"] = target_fp

    script.Mount("/system")
    script.AssertSomeFingerprint(source_fp, target_fp)

    #从源zip、目标zip中分别获取boot.img、recovery.img
    if boot_img_exists:
    source_boot = common.GetBootableImage(
        "/tmp/boot.img", "boot.img", OPTIONS.source_tmp, "BOOT",
        OPTIONS.source_info_dict)
    target_boot = common.GetBootableImage(
        "/tmp/boot.img", "boot.img", OPTIONS.target_tmp, "BOOT")
    updating_boot = (source_boot.data != target_boot.data)

    if recovery_img_exists:
    source_recovery = common.GetBootableImage(
        "/tmp/recovery.img", "recovery.img", OPTIONS.source_tmp, "RECOVERY",
        OPTIONS.source_info_dict)
    target_recovery = common.GetBootableImage(
        "/tmp/recovery.img", "recovery.img", OPTIONS.target_tmp, "RECOVERY")
    updating_recovery = (source_recovery.data != target_recovery.data)

    #校验所有源文件的SHA1(写入脚本)
    script.Print("Verifying current system...")

    device_specific.IncrementalOTA_VerifyBegin()

    script.ShowProgress(0.1, 0)
    total_verify_size = float(sum([i[2].size for i in patch_list]) + 1)
    if updating_boot:
    total_verify_size += source_boot.size
    if updating_bootloader:
    total_verify_size += target_bootloader.size
    if updating_dtb:
    total_verify_size += target_dtb.size
    so_far = 0

    for fn, tf, sf, size, patch_sha in patch_list:
    script.PatchCheck("/"+fn, tf.sha1, sf.sha1)
    so_far += sf.size
    script.SetProgress(so_far / total_verify_size)

    if updating_bootloader:
    so_far += target_bootloader.size
    script.SetProgress(so_far / total_verify_size)

    if updating_logo:
    so_far += target_logo.size
    script.SetProgress(so_far / total_verify_size)

    if updating_dtb:
    so_far += target_dtb.size
    script.SetProgress(so_far / total_verify_size)

    #如果boot有更新,校验boot的SHA1(写入脚本)
    if updating_boot:

    boot_type, boot_device = common.GetTypeAndDevice("/boot", OPTIONS.info_dict)

    #script.PatchCheck("%s:%s:%d:%s:%d:%s" %
    print("update boot_img %s:%s:from %d:%s:to %d:%s" %
                      (boot_type, boot_device,
                       source_boot.size, source_boot.sha1,
                       target_boot.size, target_boot.sha1))
    so_far += target_boot.size
    script.SetProgress(so_far / total_verify_size)

    #检验cache剩余空间是否足够(写入脚本)
    if patch_list or updating_recovery or updating_boot or updating_bootloader or updating_logo or updating_dtb:
        script.CacheFreeSpaceCheck(largest_source_size)

    #删除无需修改的文件(写入脚本)
    script.Print("Removing unneeded files...")
    script.DeleteFiles(["/"+i[0] for i in verbatim_targets] +
                     ["/"+i for i in sorted(source_data)
                            if i not in target_data and
                            i not in renames] +
                     ["/system/recovery.img"])

    #应用patch(写入脚本)
    script.Print("Patching system files...")
    deferred_patch_list = []
    for item in patch_list:
    fn, tf, sf, size, _ = item
    if tf.name == "system/build.prop":
      deferred_patch_list.append(item)
      continue
    script.ApplyPatch("/"+fn, "-", tf.size, tf.sha1, sf.sha1, "patch/"+fn+".p")
    so_far += tf.size

    if updating_boot:
    #如果boot有更新,写入img(写入脚本)
    script.Print("install boot image...");
    common.ZipWriteStr(output_zip, "boot.img", target_boot.data)
    script.WriteRawImage("/boot", "boot.img")
    so_far += target_boot.size
    script.SetProgress(so_far / total_patch_size)
    print "boot image changed; including."
    else:
    print "boot image unchanged; skipping."

    if updating_recovery:
    #如果recovery有更新,写入img(写入脚本)      
    print("recovery_Image from:%d:%s: to %d:%s" %
                      (source_recovery.size, source_recovery.sha1,
                       target_recovery.size, target_recovery.sha1))
    script.Print("install recovery image...");
    common.ZipWriteStr(output_zip, "recovery.img", target_recovery.data)
    script.WriteRawImage("/recovery", "recovery.img")
    so_far += target_recovery.size
    script.SetProgress(so_far / total_patch_size)
    print "recovery image changed; including."
    else:
    print "recovery image unchanged; skipping."

    ......

    #将升级脚本写入到输出zip
    script.AddToZip(target_zip, output_zip)
    #将metadata写入到输出zip
    WriteMetadata(metadata, output_zip)

至此,我们所需的差分包就制作完成了。

Unpacking new recovery...
Renaming files...
script aborted: Rename of system/wplayerplugins/com.pptv.gstplayer.apk() to system/app/com.pptv.gstplayer.apk() failed, error No such file or directory()
Rename of system/wplayerplugins/com.pptv.gstplayer.apk() to system/app/com.pptv.gstplayer.apk() failed, error No such file or directory()
E:the child process end code is 7
E:Error in /cache/update.zip
(Status 7)
Installation aborted

ui_print("Renaming files...");
rename("system/wplayerplugins/com.pptv.gstplayer.apk", "system/app/com.pptv.gstplayer.apk");

二、升级流程

recovery.cpp头部的注释:

/*
 * The recovery tool communicates with the main system through /cache files.
 *   /cache/recovery/command - INPUT - command line for tool, one arg per line
 *   /cache/recovery/log - OUTPUT - combined log file from recovery run(s)
 *   /cache/recovery/intent - OUTPUT - intent that was passed in
 *
 * The arguments which may be supplied in the recovery.command file:
 *   --send_intent=anystring - write the text out to recovery.intent
 *   --update_package=path - verify install an OTA package file
 *   --wipe_data - erase user data (and cache), then reboot
 *   --wipe_cache - wipe cache (but not user data), then reboot
 *   --set_encrypted_filesystem=on|off - enables / diasables encrypted fs
 *   --just_exit - do nothing; exit and reboot
 *
 * After completing, we remove /cache/recovery/command and reboot.
 * Arguments may also be supplied in the bootloader control block (BCB).
 * These important scenarios must be safely restartable at any point:
 *
 * FACTORY RESET
 * 1. user selects "factory reset"
 * 2. main system writes "--wipe_data" to /cache/recovery/command
 * 3. main system reboots into recovery
 * 4. get_args() writes BCB with "boot-recovery" and "--wipe_data"
 *    -- after this, rebooting will restart the erase --
 * 5. erase_volume() reformats /data
 * 6. erase_volume() reformats /cache
 * 7. finish_recovery() erases BCB
 *    -- after this, rebooting will restart the main system --
 * 8. main() calls reboot() to boot main system
 *
 * OTA INSTALL
 * 1. main system downloads OTA package to /cache/some-filename.zip
 * 2. main system writes "--update_package=/cache/some-filename.zip"
 * 3. main system reboots into recovery
 * 4. get_args() writes BCB with "boot-recovery" and "--update_package=..."
 *    -- after this, rebooting will attempt to reinstall the update --
 * 5. install_package() attempts to install the update
 *    NOTE: the package install must itself be restartable from any point
 * 6. finish_recovery() erases BCB
 *    -- after this, rebooting will (try to) restart the main system --
 * 7. ** if install failed **
 *    7a. prompt_and_wait() shows an error icon and waits for the user
 *    7b; the user reboots (pulling the battery, etc) into the main system
 * 8. main() calls maybe_install_firmware_update()
 *    ** if the update contained radio/hboot firmware **:
 *    8a. m_i_f_u() writes BCB with "boot-recovery" and "--wipe_cache"
 *        -- after this, rebooting will reformat cache & restart main system --
 *    8b. m_i_f_u() writes firmware image into raw cache partition
 *    8c. m_i_f_u() writes BCB with "update-radio/hboot" and "--wipe_cache"
 *        -- after this, rebooting will attempt to reinstall firmware --
 *    8d. bootloader tries to flash firmware
 *    8e. bootloader writes BCB with "boot-recovery" (keeping "--wipe_cache")
 *        -- after this, rebooting will reformat cache & restart main system --
 *    8f. erase_volume() reformats /cache
 *    8g. finish_recovery() erases BCB
 *        -- after this, rebooting will (try to) restart the main system --
 * 9. main() calls reboot() to boot main system
 */

这部分的注释详细说明了recovery的两大功能:FACTORY RESET和OTA INSTALL的流程。

这段注释告诉我们,执行恢复出厂设置或者OTA升级,我们需要先向/cache/recovery/command中写入命令:

 * The arguments which may be supplied in the recovery.command file:
 *   --send_intent=anystring - write the text out to recovery.intent
 *   --update_package=path - verify install an OTA package file
 *   --wipe_data - erase user data (and cache), then reboot
 *   --wipe_cache - wipe cache (but not user data), then reboot
 *   --set_encrypted_filesystem=on|off - enables / diasables encrypted fs

所以先来看下制作完成的升级包是如何进入到升级流程中的。

以升级U盘中的update.zip为例,插入U盘检测到升级包之后,我们进入Settings开始进入升级流程。无论上层应用如何操作升级包的来源,最终都是调用RecoverySystem.installPackage来执行升级操作。

public static void installPackage(Context context, File packageFile)
        throws IOException {
    String arg = getInstallPackageArg(context, packageFile);
    bootCommand(context, arg, true);
}

这里的getInstallPackageArg是比较关键的,它就是处理/cache/recovery/command中命令的方法:

private static String getInstallPackageArg(Context context ,File packageFile)
        throws IOException {
    String filename = packageFile.getCanonicalPath();
    String strExt2Path = Environment.getExternalStorage2Directory().toString();
    String arg = null;

    if(filename.startsWith(strExt2Path)) {
        if(Environment.isExternalStorageBeSdcard()) {
            String newpath = filename.substring(4); 
            Log.w(TAG, "!!! REBOOTING TO INSTALL 1 " + newpath + " !!!");
            arg = "--update_package=" + newpath;

    String ibEnableStatus = SystemProperties.get("persist.sys.instaboot.enable");
    if ("enable".equals(ibEnableStatus) || "prepare".equals(ibEnableStatus)) {
        Log.w(TAG, "clean instaboot image");
        InstabootManager im = new InstabootManager(context);
        im.disable();
    }
            arg += "\n--locale=" + Locale.getDefault().toString();
        } else {
            String newpath = new String("/sdcard") + filename.substring(strExt2Path.length()); 
            Log.w(TAG, "!!! REBOOTING TO INSTALL 2 " + newpath + " !!!");
            arg = "--update_package=" + newpath;
            arg += "\n--locale=" + Locale.getDefault().toString();
        }
    } else if(filename.startsWith(Environment.getExternalStorageDirectory().toString())) {
        if(Environment.isExternalStorageBeSdcard()) {
            String absPath = packageFile.getAbsolutePath();
            if(SystemProperties.getInt("vold.fakesdcard.enable",0)==1 && absPath.startsWith("/mnt/sda1/")) {
                String newpath =new String("/udisk/")+absPath.substring(10); 
                Log.w(TAG, "!!! REBOOTING TO INSTALL 3-1 " + newpath + " !!!");
                arg = "--update_package=" + newpath;
                arg += "\n--locale=" + Locale.getDefault().toString();
            } else {
                String newpath = filename.substring(4); 
                Log.w(TAG, "!!! REBOOTING TO INSTALL 3-2 " + newpath + " !!!");
                arg = "--update_package=" + newpath;
                arg += "\n--locale=" + Locale.getDefault().toString();
            }
        } else {
            String newpath = new String("/media/" + packageFile.getName()); 
            Log.w(TAG, "!!! REBOOTING TO INSTALL 4 " + newpath + " !!!");
            arg = "--update_package=" + newpath;
            arg += "\n--locale=" + Locale.getDefault().toString();
        }
    } else if(filename.startsWith(Environment.getInternalStorageDirectory().toString())) {
        String newpath = new String("/media/"+packageFile.getName()); 
        Log.w(TAG, "!!! REBOOTING TO INSTALL 5 " + newpath + " !!!");
        arg = "--update_package=" + newpath;
        arg += "\n--locale=" + Locale.getDefault().toString();
    } else if(filename.startsWith("/udisk")) {
        String newpath =new String("/udisk/")+filename.substring(7);
        Log.w(TAG, "!!! REBOOTING TO INSTALL 6 " + newpath + " !!!");
        arg = "--update_package=" + newpath;
        arg += "\n--locale=" + Locale.getDefault().toString();
    } else {
        Log.w(TAG, "!!! REBOOTING TO INSTALL 7 " + filename + " !!!");
        arg = "--update_package=" + filename;
        arg += "\n--locale=" + Locale.getDefault().toString();
    }
    return arg;
}

继续往下看,进入了bootCommand方法中:

private static void bootCommand(Context context, String arg, Boolean update) throws IOException {
    RECOVERY_DIR.mkdirs();  // 创建/cache/recovery
    COMMAND_FILE.delete();  // 清除/cache/recovery/command
    LOG_FILE.delete();

    FileWriter command = new FileWriter(COMMAND_FILE);
    FileOutputStream fos = new FileOutputStream(COMMAND_FILE);
    try {
        command.write(arg);//写入传进来的arg
        command.write("\n");
    } finally {
        command.close();
        FileUtils.sync(fos);
    }

    ......

    // Having written the command file, go ahead and reboot
    PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
    if(update)
        pm.reboot("update");//重启升级
    else 
        pm.reboot("recovery");

    throw new IOException("Reboot failed (no permissions?)");
}

然后进入了PMS的reboot,接着shutdownOrRebootInternal,继而ShutdownThread.reboot,接着rebootOrShutdown,最后回到了PMS的lowLevelReboot:

public static void lowLevelReboot(String reason) {
    if (reason == null) {
        reason = "";
    }
    SystemProperties.set("sys.powerctl", "reboot," + reason);
    try {
        Thread.sleep(20000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

最后带着reason=update进入kernel开始重启。

经历了重启之后,kernel加载recovery.img,起来后执行的第一个进程就是init,此进程读取init.rc启动相应的服务:

......

on init
    //设置环境变量
    sysclktz 0
    loglevel 7
    export PATH /sbin:/system/sbin:/system/bin:/system/xbin
    export LD_LIBRARY_PATH /system/lib
    export ANDROID_ROOT /system
    export ANDROID_DATA /data
    export EXTERNAL_STORAGE /sdcard

    //建立连接
    symlink /system/etc /etc
    mkdir /system/bin
    symlink /sbin/sh /system/bin/sh

    //建立目录
    mkdir /sdcard
    mkdir /system
    mkdir /data
    mkdir /cache
    //挂载/tmp为内存文件系统tmpfs
    mount /tmp /tmp tmpfs

......

//启动recovery服务  
service recovery /sbin/recovery

//启动adbd服务
service adbd /sbin/adbd recovery
    disabled
    socket adbd stream 660 system system

......

这样就回到了开头所说了recovery模式的入口:recovery.cpp,我们从main函数开始分析

//将recovery产生的log信息重定向到/tmp/recovery.log这个文件里
freopen(TEMPORARY_LOG_FILE, "a", stdout); setbuf(stdout, NULL);
freopen(TEMPORARY_LOG_FILE, "a", stderr); setbuf(stderr, NULL);

//判断是否使用adb的sideload来传入,通过参数--adbd来判断
if (argc == 2 && strcmp(argv[1], "--adbd") == 0) {
    adb_main();
    return 0;
}

//初始化并装载recovery的分区表recovery.fstab
load_volume_table();

//在recovery中挂载/cache/recovery/last_log这个文件
ensure_path_mounted(LAST_LOG_FILE);
// Rename last_log -> last_log.1 -> last_log.2 -> ... -> last_log.$max
rotate_last_logs(10);

只有挂载到了对应的分区,才能访问前面的command文件,然后才能正确打开升级包,上面这步也是非常重要的。

挂载完相应的分区以后,就需要获取命令参数:

//从/cache/recovery/command获取参数
get_args(&argc, &argv);

int arg;
while ((arg = getopt_long(argc, argv, "", OPTIONS, NULL)) != -1) {
    switch (arg) {
    case 'q': aml_update_version = optarg; break;
    case 'p': previous_runs = atoi(optarg); break;
    case 's': send_intent = optarg; break;
    case 'u': update_package = optarg; break;
    case 'x': update_patch = optarg; break;
#ifdef RECOVERY_HAS_PARAM
    case 'w': wipe_data = wipe_cache = wipe_param = 1; break;
#else
    case 'w': wipe_data = wipe_cache = 1; break;
#endif
    case 'g': restore_system = 1; break;
    case 'c': wipe_cache = 1; break;
    case 't': show_text = 1; break;
    //case 'x': just_exit = true; break;
    case 'l': locale = optarg; break;
    case 'f': reboot_to_factorymode = 1; break;
    case 'n': usb_burning = 1; break;
    case 'o': without_format = 1; break;    
case 'z': file_copy_from_partition_args = optarg; break;
#ifdef RECOVERY_HAS_MEDIA
    case 'm': wipe_media = 1; break;
#endif /* RECOVERY_HAS_MEDIA */
#ifdef RECOVERY_HAS_PARAM
    case 'P': wipe_param = 1; break;
#endif /* RECOVERY_HAS_PARAM */

#ifdef RECOVERY_HAS_EFUSE
    case 'v': set_efuse_version = 1; efuse_version = optarg; break;
    case 'd': set_efuse_ethernet_mac = 1; break;
    case 'b': set_efuse_bluetooth_mac = 1; break;
#ifdef EFUSE_LICENCE_ENABLE
    case 'a': set_efuse_audio_license = 1; break;
#endif /* EFUSE_LICENCE_ENABLE */

#endif /* RECOVERY_HAS_EFUSE */

#ifdef RECOVERY_WRITE_KEY
    case 'B': flash_write_mac = 1; break;
    case 'C': flash_write_mac_force = 1; break;
    case 'D': flash_write_mac_bt = 1; break;
    case 'E': flash_write_mac_bt_force = 1; break;
    case 'F': flash_write_mac_wifi = 1; break;
    case 'G': flash_write_mac_wifi_force = 1; break;
    case 'H': flash_write_hdcp = 1; break;
    case 'I': flash_write_hdcp_force = 1; break;
    case 'J': flash_write_usid = 1; break;
    case 'K': flash_write_usid_force = 1; break;
#endif /* RECOVERY_WRITE_KEY */

    case 'R': restore_systembackup = 1; break;
    case 'e': just_exit = true; break;
    case 'r': run_cmd = 1; cmd_args = optarg; break;
    case 'h': keep_file_path = optarg; wipe_data = wipe_cache = 1; break;
    case 'y': erase_data = 1; break; //Add for The data partition is full when OTA upgrades
    case '?':
        LOGE("Invalid command argument\n");
        continue;
    }
}

获取到对应的命令,就会执行对应的标志,后面会根据标志来执行对应的操作。做完以上的流程后,下面就是设置语言信息,创建设备,初始化recovery的UI界面,设置Selinux权限等操作:

//设置语言
if (locale == NULL) {
    load_locale_from_cache();
}

//创建设备
Device* device = make_device();
//获取UI
ui = device->GetUI();
//设置当前的UI
gCurrentUI = ui;
//UI初始化
ui->Init();

......

接下来根据上面传入的命令参数,开始真正进入到重要的执行环节:

if (update_package != NULL) {
    unlink("/data/property/persist.sys.needcheckdata");
    ui->SetBackground(RecoveryUI::INSTALLING_UPDATE);
    ui->SetProgressType(RecoveryUI::DETERMINATE);

    //执行install_package进行升级
    status = install_package(update_package, &wipe_cache, TEMPORARY_INSTALL_FILE);

    if (status != INSTALL_SUCCESS) {
        ui->Print("Installation aborted.\n");
        ui->ShowText(true);
    } else {
        ui->SetBackground(RecoveryUI::UPDATED);
        sleep(3);
    }
}

if (wipe_data) {

    ......

} else if (wipe_cache) {

    ......

}

这里就是执行OTA升级和恢复出厂设置的地方了,恢复出厂设置也就是手机双清,清除/data分区和/cache分区,代码流程还是比较清楚详细的,可以跟一下,这边先不展开了。

接下来的重点就是在install_package方法上面了:

int
install_package(const char* path, int* wipe_cache, const char* install_file)
{
    FILE* install_log = fopen_path(install_file, "w");
    if (install_log) {
        fputs(path, install_log);
        fputc('\n', install_log);
    } else {
        LOGE("failed to open last_install: %s\n", strerror(errno));
    }
    int result;

    //设置安装挂载对应的节点,也就是判断挂载的路径是/tmp或者/cache
    if (setup_install_mounts() != 0) {
        LOGE("failed to set up expected mounts for install; aborting\n");
        set_upgrade_step("2");
        result = INSTALL_ERROR;
    } else {
        result = really_install_package(path, wipe_cache);
    }
    if (install_log) {
        fputc(result == INSTALL_SUCCESS ? '1' : '0', install_log);
        fputc('\n', install_log);
        fclose(install_log);
    }
    return result;
}