Android OTA升级详细流程分析(non-AB)

一、.ota_from_target_files.py分析
 

if __name__ == '__main__':
  try:
    # common.CloseInheritedPipes()是用于在macOS环境下关闭文件描述符,
    # 通过platform.system() != "Darwin"判断是否是MacOS
    common.CloseInheritedPipes()
    main(sys.argv[1:])
  except common.ExternalError:
    logger.exception("\n   ERROR:\n")
    sys.exit(1)
  finally:
    common.Cleanup()

common.CloseInheritedPipes()是用于在macOS环境下关闭文件描述符,之后执行ota_from_target_files的主体部分代码,最后common.Cleanup()清理垃圾文件。

以下先说明common.CloseInheritedPipes()和common.Cleanup()的代码,因为它们非常简单。

# 即使关闭失败也不会中断执行
def CloseInheritedPipes():
  """ Gmake in MAC OS has file descriptor (PIPE) leak. We close those fds
  before doing other work."""
  # 判断系统内核是否为Darwin,Darwin是macOS的内核
  if platform.system() != "Darwin":
    return
  # 关闭3~1025有打开状态的文件描述符
  for d in range(3, 1025):
    try:
      stat = os.fstat(d)
      if stat is not None:
        pipebit = stat[0] & 0x1000
        if pipebit != 0:
          os.close(d)
    except OSError:
      pass


# 发生错误后会中断程序执行
def Cleanup():
  # 从OPTIONS.tempfiles中获取生成OTA包过程中产生的临时文件,如果是文件直接删除,否则遍历删除
  # 无法删除链接文件!有缺陷,error #1669
  for i in OPTIONS.tempfiles:
    if os.path.isdir(i):
      shutil.rmtree(i, ignore_errors=True)
    else:
      os.remove(i)
  del OPTIONS.tempfiles[:]

def main(argv):

  # 此处代码在common.ParseOptions传入
  def option_handler(o, a):
    if o in ("-k", "--package_key"):
      OPTIONS.package_key = a
    # 制作差量包
    elif o in ("-i", "--incremental_from"):
      OPTIONS.incremental_source = a
    elif o == "--full_radio":
      OPTIONS.full_radio = True
    elif o == "--full_bootloader":
      OPTIONS.full_bootloader = True
    # 清除用户数据
    elif o == "--wipe_user_data":
      OPTIONS.wipe_user_data = True
    elif o in ("-n", "--no_prereq"):
      OPTIONS.omit_prereq = True
    elif o == "--downgrade":
      OPTIONS.downgrade = True
      OPTIONS.wipe_user_data = True
    elif o == "--override_timestamp":
      OPTIONS.downgrade = True
    elif o in ("-o", "--oem_settings"):
      OPTIONS.oem_source = a.split(',')
    elif o == "--oem_no_mount":
      OPTIONS.oem_no_mount = True
    elif o in ("-e", "--extra_script"):
      OPTIONS.extra_script = a
    elif o in ("-t", "--worker_threads"):
      if a.isdigit():
        OPTIONS.worker_threads = int(a)
      else:
        raise ValueError("Cannot parse value %r for option %r - only "
                         "integers are allowed." % (a, o))
    elif o in ("-2", "--two_step"):
      OPTIONS.two_step = True
    elif o == "--include_secondary":
      OPTIONS.include_secondary = True
    elif o == "--no_signing":
      OPTIONS.no_signing = True
    elif o == "--verify":
      OPTIONS.verify = True
    elif o == "--block":
      OPTIONS.block_based = True
    elif o in ("-b", "--binary"):
      OPTIONS.updater_binary = a
    elif o == "--stash_threshold":
      try:
        OPTIONS.stash_threshold = float(a)
      except ValueError:
        raise ValueError("Cannot parse value %r for option %r - expecting "
                         "a float" % (a, o))
    elif o == "--log_diff":
      OPTIONS.log_diff = a
    elif o == "--payload_signer":
      OPTIONS.payload_signer = a
    elif o == "--payload_signer_args":
      OPTIONS.payload_signer_args = shlex.split(a)
    elif o == "--payload_signer_key_size":
      OPTIONS.payload_signer_key_size = a
    elif o == "--extracted_input_target_files":
      OPTIONS.extracted_input = a
    elif o == "--skip_postinstall":
      OPTIONS.skip_postinstall = True
    elif o == "--retrofit_dynamic_partitions":
      OPTIONS.retrofit_dynamic_partitions = True
    elif o == "--skip_compatibility_check":
      OPTIONS.skip_compatibility_check = True
    elif o == "--output_metadata_path":
      OPTIONS.output_metadata_path = a
    else:
      return False
    return True

  # 解析argv(传入的)参数,并返回所有不是标记的参数。__doc__指需要调用的模块,extra_opts和extra_long_opts都是标记,由传入者定义,它们将会交给option_handler进行处理。
  # 这一步完成之后,生成OTA脚本的所有配置应当都准备就绪了,存储在common.OPTIONS中,当前脚本有一个指向common.OPTIONS的引用,名为common.OPTIONS
  args = common.ParseOptions(argv, __doc__,
                             extra_opts="b:k:i:d:ne:t:a:2o:",
                             extra_long_opts=[
                                 "package_key=",
                                 "incremental_from=",
                                 "full_radio",
                                 "full_bootloader",
                                 "wipe_user_data",
                                 "no_prereq",
                                 "downgrade",
                                 "override_timestamp",
                                 "extra_script=",
                                 "worker_threads=",
                                 "two_step",
                                 "include_secondary",
                                 "no_signing",
                                 "block",
                                 "binary=",
                                 "oem_settings=",
                                 "oem_no_mount",
                                 "verify",
                                 "stash_threshold=",
                                 "log_diff=",
                                 "payload_signer=",
                                 "payload_signer_args=",
                                 "payload_signer_key_size=",
                                 "extracted_input_target_files=",
                                 "skip_postinstall",
                                 "retrofit_dynamic_partitions",
                                 "skip_compatibility_check",
                                 "output_metadata_path=",
                             ], extra_option_handler=option_handler)

  # 如果之前处理后获得的参数不是两个,输出本脚本使用说明并退出程序
  if len(args) != 2:
    common.Usage(__doc__)
    sys.exit(1)
  
  # 初始化日志模块
  common.InitLogging()

  # 降级增量(如果需要使用OTA包降级的话,应当打增量包而非全量包)
  if OPTIONS.downgrade:
    # We should only allow downgrading incrementals (as opposed to full).
    # Otherwise the device may go back from arbitrary build with this full
    # OTA package.
    if OPTIONS.incremental_source is None:
      raise ValueError("Cannot generate downgradable full OTAs - consider"
                       "using --omit_prereq?")

  # Load the build info dicts from the zip directly or the extracted input
  # directory. We don't need to unzip the entire target-files zips, because they
  # won't be needed for A/B OTAs (brillo_update_payload does that on its own).
  # When loading the info dicts, we don't need to provide the second parameter
  # to common.LoadInfoDict(). Specifying the second parameter allows replacing
  # some properties with their actual paths, such as 'selinux_fc',
  # 'ramdisk_dir', which won't be used during OTA generation.
  if OPTIONS.extracted_input is not None:
    OPTIONS.info_dict = common.LoadInfoDict(OPTIONS.extracted_input)
  else:
    with zipfile.ZipFile(args[0], 'r') as input_zip:
      OPTIONS.info_dict = common.LoadInfoDict(input_zip)

  logger.info("--- target info ---")
  common.DumpInfoDict(OPTIONS.info_dict)

  # Load the source build dict if applicable.
  if OPTIONS.incremental_source is not None:
    OPTIONS.target_info_dict = OPTIONS.info_dict
    with zipfile.ZipFile(OPTIONS.incremental_source, 'r') as source_zip:
      OPTIONS.source_info_dict = common.LoadInfoDict(source_zip)

    logger.info("--- source info ---")
    common.DumpInfoDict(OPTIONS.source_info_dict)

  # Load OEM dicts if provided.
  OPTIONS.oem_dicts = _LoadOemDicts(OPTIONS.oem_source)

  # Assume retrofitting dynamic partitions when base build does not set
  # use_dynamic_partitions but target build does.
  if (OPTIONS.source_info_dict and
      OPTIONS.source_info_dict.get("use_dynamic_partitions") != "true" and
      OPTIONS.target_info_dict.get("use_dynamic_partitions") == "true"):
    if OPTIONS.target_info_dict.get("dynamic_partition_retrofit") != "true":
      raise common.ExternalError(
          "Expect to generate incremental OTA for retrofitting dynamic "
          "partitions, but dynamic_partition_retrofit is not set in target "
          "build.")
    logger.info("Implicitly generating retrofit incremental OTA.")
    OPTIONS.retrofit_dynamic_partitions = True

  # Skip postinstall for retrofitting dynamic partitions.
  if OPTIONS.retrofit_dynamic_partitions:
    OPTIONS.skip_postinstall = True

  ab_update = OPTIONS.info_dict.get("ab_update") == "true"

  # Use the default key to sign the package if not specified with package_key.
  # package_keys are needed on ab_updates, so always define them if an
  # ab_update is getting created.
  if not OPTIONS.no_signing or ab_update:
    if OPTIONS.package_key is None:
      OPTIONS.package_key = OPTIONS.info_dict.get(
          "default_system_dev_certificate",
          "build/target/product/security/testkey")
    # Get signing keys
    OPTIONS.key_passwords = common.GetKeyPasswords([OPTIONS.package_key])

  if ab_update:
    WriteABOTAPackageWithBrilloScript(
        target_file=args[0],
        output_file=args[1],
        source_file=OPTIONS.incremental_source)

    logger.info("done.")
    return

  # Sanity check the loaded info dicts first.
  if OPTIONS.info_dict.get("no_recovery") == "true":
    raise common.ExternalError(
        "--- target build has specified no recovery ---")

  # Non-A/B OTAs rely on /cache partition to store temporary files.
  cache_size = OPTIONS.info_dict.get("cache_size")
  if cache_size is None:
    logger.warning("--- can't determine the cache partition size ---")
  OPTIONS.cache_size = cache_size

  if OPTIONS.extra_script is not None:
    OPTIONS.extra_script = open(OPTIONS.extra_script).read()

  if OPTIONS.extracted_input is not None:
    OPTIONS.input_tmp = OPTIONS.extracted_input
  else:
    logger.info("unzipping target target-files...")
    OPTIONS.input_tmp = common.UnzipTemp(args[0], UNZIP_PATTERN)
  OPTIONS.target_tmp = OPTIONS.input_tmp

  # If the caller explicitly specified the device-specific extensions path via
  # -s / --device_specific, use that. Otherwise, use META/releasetools.py if it
  # is present in the target target_files. Otherwise, take the path of the file
  # from 'tool_extensions' in the info dict and look for that in the local
  # filesystem, relative to the current directory.
  if OPTIONS.device_specific is None:
    from_input = os.path.join(OPTIONS.input_tmp, "META", "releasetools.py")
    if os.path.exists(from_input):
      logger.info("(using device-specific extensions from target_files)")
      OPTIONS.device_specific = from_input
    else:
      OPTIONS.device_specific = OPTIONS.info_dict.get("tool_extensions")

  if OPTIONS.device_specific is not None:
    OPTIONS.device_specific = os.path.abspath(OPTIONS.device_specific)

  # Generate a full OTA.
  if OPTIONS.incremental_source is None:
    with zipfile.ZipFile(args[0], 'r') as input_zip:
      WriteFullOTAPackage(
          input_zip,
          output_file=args[1])

  # Generate an incremental OTA.
  else:
    logger.info("unzipping source target-files...")
    OPTIONS.source_tmp = common.UnzipTemp(
        OPTIONS.incremental_source, UNZIP_PATTERN)
    with zipfile.ZipFile(args[0], 'r') as input_zip, \
        zipfile.ZipFile(OPTIONS.incremental_source, 'r') as source_zip:
      WriteBlockIncrementalOTAPackage(
          input_zip,
          source_zip,
          output_file=args[1])

    if OPTIONS.log_diff:
      with open(OPTIONS.log_diff, 'w') as out_file:
        import target_files_diff
        target_files_diff.recursiveDiff(
            '', OPTIONS.source_tmp, OPTIONS.input_tmp, out_file)

  logger.info("done.")

分析如下:

主函数main是python的入口函数,我们从main函数开始看,大概看一下main函数(脚本最后)里的流程就能知道脚本的执行过程了。

    ① 在main函数的开头,首先将用户设定的option选项存入OPTIONS变量中,它是一个python中的类。紧接着判断有没有额外的脚本,如果有就读入到OPTIONS变量中。
    ② 解压缩输入的zip包,即我们在上文生成的原始zip包。然后判断是否用到device-specific extensions(设备扩展)如果用到,随即读入到OPTIONS变量中。
    ③ 判断是否签名,然后判断是否有新内容的增量源,有的话就解压该增量源包放入一个临时变量中(source_zip)。自此,所有的准备工作已完毕,随即会调用该 脚本中最主要的函数WriteFullOTAPackage(input_zip,output_zip)
    ④ WriteFullOTAPackage函数的处理过程是先获得脚本的生成器。默认格式是edify。然后获得metadata元数据,此数据来至于Android的一些环境变量。然后获得设备配置参数比如api函数的版本。然后判断是否忽略时间戳。
    ⑤ WriteFullOTAPackage函数做完准备工作后就开始生成升级用的脚本文件(updater-script)了。生成脚本文件后将上一步获得的metadata元数据写入到输出包out_zip。
    ⑥至此一个完整的update.zip升级包就生成了。生成位置在:out/target/product/tcc8800/trinket.zip。将升级包拷贝到SD卡中就可以用来升级了。
四、 Android OTA增量包update.zip的生成

1.2代码中的方法分析
显示进度条 (写入update_script) script.ShowProgress(增加百分比,持续时间)edify_generator.py

显示文字之类的 script.Print -> ui_print

显示进度条 script.ShowProgress -> show_progress

解压(写入)到指定区域 script.WriteRawImage -> package_extract_file

卸载所有分区 script.UnmountAll -> unmount

将另一个脚本的内容附加到当前脚本 script.AppendScript

二、update.zip升级包的制作


2.1 update.zip包的目录结构

          |----boot.img
          |----system/
          |----recovery/
                `|----recovery-from-boot.p
                `|----etc/
                        `|----install-recovery.sh
          |---META-INF/
              `|CERT.RSA
              `|CERT.SF
              `|MANIFEST.MF
              `|----com/
                     `|----google/
                             `|----android/
                                    `|----update-binary
                                    `|----updater-script
                             `|----android/
                                    `|----metadata
2.2 update.zip包目录结构详解

    以上是我们用命令make otapackage 制作的update.zip包的标准目录结构。
        2.2.1、boot.img是更新boot分区所需要的文件。这个boot.img主要包括kernel+ramdisk。

         2.2.2、system/目录的内容在升级后会放在系统的system分区。主要用来更新系统的一些应用或则应用会用到的一些库等等。可以将Android源码编译out/target/product/trinket/system/中的所有文件拷贝到这个目录来代替。

         2.2.3、recovery/目录中的recovery-from-boot.p是boot.img和recovery.img的补丁(patch),主要用来更新recovery分区,其中etc/目录下的install-recovery.sh是更新脚本。
         2.2.4、update-binary是一个二进制文件,相当于一个脚本解释器,能够识别updater-script中描述的操作。该文件在Android源码编译后out/target/product/trinket/system bin/updater生成,可将updater重命名为update-binary得到。
               该文件在具体的更新包中的名字由源码中bootable/recovery/install.c中的宏ASSUMED_UPDATE_BINARY_NAME的值而定。
         2.2.5、updater-script:此文件是一个脚本文件,具体描述了更新过程。我们可以根据具体情况编写该脚本来适应我们的具体需求。该文件的命名由源码中bootable/recovery/updater/updater.c文件中的宏SCRIPT_NAME的值而定。
         2.2.6、 metadata文件是描述设备信息及环境变量的元数据。主要包括一些编译选项,签名公钥,时间戳以及设备型号等。
         2.2.7、我们还可以在包中添加userdata目录,来更新系统中的用户数据部分。这部分内容在更新后会存放在系统的/data目录下。

         2.2.8、update.zip包的签名:update.zip更新包在制作完成后需要对其签名,否则在升级时会出现认证失败的错误提示。而且签名要使用和目标板一致的加密公钥。加密公钥及加密需要的三个文件在Android源码编译后生成的具体路径为:

               out/host/linux-x86/framework/signapk.jar 

               build/target/product/security/releasekey.x509.pem         

               build/target/product/security/releasekey.pk8 。

              我们用命令make otapackage制作生成的update.zip包是已签过名的,如果自己做update.zip包时必须手动对其签名。

              具体的加密方法:$ java -Xmx2048m -Djava.library.path="out/host/linux-x86/lib64" -jar out/host/linux-x86/framework/signapk.jar –w build/target/product/security/releasekey.x509.pem  build/target/product/security/releasekey.pk8 update.zip update_signed.zip
              以上命令在update.zip包所在的路径下执行,其中signapk.jar releasekey.x509.pem以及releasekey.pk8文件的引用使用绝对路径。update.zip 是我们已经打好的包,update_signed.zip包是命令执行完生成的已经签过名的包。
         2.2.9、MANIFEST.MF:这个manifest文件定义了与包的组成结构相关的数据。类似Android应用的mainfest.xml文件。
        2.2.10、CERT.RSA:与签名文件相关联的签名程序块文件,它存储了用于签名JAR文件的公共签名。
        2.2.11、CERT.SF:这是JAR文件的签名文件,其中前缀CERT代表签名者。
        另外,在具体升级时,对update.zip包检查时大致会分三步:①检验SF文件与RSA文件是否匹配。②检验MANIFEST.MF与签名文件中的digest是否一致。③检验包中的文件与MANIFEST中所描述的是否一致
2.3 Android升级包的生成过程分析

在源码根目录下执行make otapackage命令生成update.zip包主要分为两步,第一步是根据Makefile执行编译生成一个update原包(zip格式)。第二步是运行一个python脚本,并以上一步准备的zip包作为输入,最终生成我们需要的升级包。下面进一步分析这两个过程。

            第一步:编译Makefile。对应的Makefile文件所在位置:build/core/Makefile。开始会生成一个zip包,这个包最后会用来制作OTA package 或者filesystem image。先将这部分的对应的Makefile贴出来如下:
 

# -----------------------------------------------------------------  
# 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,updater)/updater  
$(BUILT_TARGET_FILES_PACKAGE): PRIVATE_OTA_TOOLS := $(built_ota_tools)  
  
$(BUILT_TARGET_FILES_PACKAGE): PRIVATE_RECOVERY_API_VERSION := $(RECOVERY_API_VERSION)  
  
ifeq ($(TARGET_RELEASETOOLS_EXTENSIONS),)  
# default to common dir for device vendor  
$(BUILT_TARGET_FILES_PACKAGE): tool_extensions := $(TARGET_DEVICE_DIR)/../common  
else  
$(BUILT_TARGET_FILES_PACKAGE): tool_extensions := $(TARGET_RELEASETOOLS_EXTENSIONS)  
endif  
  
# Depending on the various images guarantees that the underlying  
# directories are up-to-date.  
$(BUILT_TARGET_FILES_PACKAGE): \  
        $(INSTALLED_BOOTIMAGE_TARGET) \  
        $(INSTALLED_RADIOIMAGE_TARGET) \  
        $(INSTALLED_RECOVERYIMAGE_TARGET) \  
        $(INSTALLED_SYSTEMIMAGE) \  
        $(INSTALLED_USERDATAIMAGE_TARGET) \  
        $(INSTALLED_ANDROID_INFO_TXT_TARGET) \  
        $(built_ota_tools) \  
        $(APKCERTS_FILE) \  
        $(HOST_OUT_EXECUTABLES)/fs_config \  
        | $(ACP)  
    @echo "Package target files: $@"  
    $(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 INSTALLED_2NDBOOTLOADER_TARGET  
    $(hide) $(ACP) \  
        $(INSTALLED_2NDBOOTLOADER_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  
    @# 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 INSTALLED_2NDBOOTLOADER_TARGET  
    $(hide) $(ACP) \  
        $(INSTALLED_2NDBOOTLOADER_TARGET) $(zip_root)/BOOT/second  
endif  
ifdef BOARD_KERNEL_CMDLINE  
    $(hide) echo "$(BOARD_KERNEL_CMDLINE)" > $(zip_root)/BOOT/cmdline  
endif  
ifdef BOARD_KERNEL_BASE  
    $(hide) echo "$(BOARD_KERNEL_BASE)" > $(zip_root)/BOOT/base  
endif  
ifdef BOARD_KERNEL_PAGESIZE  
    $(hide) echo "$(BOARD_KERNEL_PAGESIZE)" > $(zip_root)/BOOT/pagesize  
endif  
    $(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)  
    @# 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  
    $(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  
ifdef BOARD_FLASH_BLOCK_SIZE  
    $(hide) echo "blocksize=$(BOARD_FLASH_BLOCK_SIZE)" >> $(zip_root)/META/misc_info.txt  
endif  
ifdef BOARD_BOOTIMAGE_PARTITION_SIZE  
    $(hide) echo "boot_size=$(BOARD_BOOTIMAGE_PARTITION_SIZE)" >> $(zip_root)/META/misc_info.txt  
endif  
ifdef BOARD_RECOVERYIMAGE_PARTITION_SIZE  
    $(hide) echo "recovery_size=$(BOARD_RECOVERYIMAGE_PARTITION_SIZE)" >> $(zip_root)/META/misc_info.txt  
endif  
ifdef BOARD_SYSTEMIMAGE_PARTITION_SIZE  
    $(hide) echo "system_size=$(BOARD_SYSTEMIMAGE_PARTITION_SIZE)" >> $(zip_root)/META/misc_info.txt  
endif  
ifdef BOARD_USERDATAIMAGE_PARTITION_SIZE  
    $(hide) echo "userdata_size=$(BOARD_USERDATAIMAGE_PARTITION_SIZE)" >> $(zip_root)/META/misc_info.txt  
endif  
    $(hide) echo "tool_extensions=$(tool_extensions)" >> $(zip_root)/META/misc_info.txt  
ifdef mkyaffs2_extra_flags  
    $(hide) echo "mkyaffs2_extra_flags=$(mkyaffs2_extra_flags)" >> $(zip_root)/META/misc_info.txt  
endif  
    @# Zip everything up, preserving symlinks  
    $(hide) (cd $(zip_root) && zip -qry ../$(notdir $@) .)  
    @# Run fs_config on all the system files in the zip, and save the output  
    $(hide) zipinfo -1 $@ | awk -F/ 'BEGIN { OFS="/" } /^SYSTEM\// {$$1 = "system"; print}' | $(HOST_OUT_EXECUTABLES)/fs_config > $(zip_root)/META/filesystem_config.txt  
    $(hide) (cd $(zip_root) && zip -q ../$(notdir $@) META/filesystem_config.txt)  
  
  
target-files-package: $(BUILT_TARGET_FILES_PACKAGE)  
  
  
ifneq ($(TARGET_SIMULATOR),true)  
ifneq ($(TARGET_PRODUCT),sdk)  
ifneq ($(TARGET_DEVICE),generic)  
ifneq ($(TARGET_NO_KERNEL),true)  
ifneq ($(recovery_fstab),)  

第一步:创建一个root_zip根目录,并依次在此目录下创建所需要的如下其他目录

       ①创建RECOVERY目录,并填充该目录的内容,包括kernel的镜像和recovery根文件系统的镜像。此目录最终用于生成recovery.img。

        ②创建并填充BOOT目录。包含kernel和cmdline以及pagesize大小等,该目录最终用来生成boot.img。
        ③向SYSTEM目录填充system image。
        ④向DATA填充data image。
        ⑤用于生成OTA package包所需要的额外的内容。主要包括一些bin命令。
        ⑥创建META目录并向该目录下添加一些文本文件,如apkcerts.txt(描述apk文件用到的认证证书),misc_info.txt(描述Flash内存的块大小以及boot、recovery、system、userdata等分区的大小信息)。
        ⑦使用保留连接选项压缩我们在上面获得的root_zip目录。
        ⑧使用fs_config(build/tools/fs_config)配置上面的zip包内所有的系统文件(system/下各目录、文件)的权限属主等信息。fs_config包含了一个头文件#include“private/android_filesystem_config.h”。在这个头文件中以硬编码的方式设定了system目录下各文件的权限、属主。执行完配置后会将配置后的信息以文本方式输出 到META/filesystem_config.txt中。并再一次zip压缩成我们最终需要的原始包。

第二步:上面的zip包只是一个编译过程中生成的原始包。

        这个原始zip包在实际的编译过程中有两个作用,一是用来生成OTA update升级包,二是用来生成系统镜像。在编译过程中若生成OTA update升级包时会调用一个名为ota_from_target_files的python脚本,位置在/build/tools/releasetools/ota_from_target_files。这个脚本的作用是以第一步生成的zip原始包作为输入,最终生成可用的OTA升级zip包。

㈠ 首先看一下这个脚本开始部分的帮助文档。

Usage: ota_from_target_files [flags] input_target_files output_ota_package
    -b 过时的。
    -k 签名所使用的密钥
    -i 生成增量OTA包时使用此选项。后面我们会用到这个选项来生成OTA增量包。
    -w 是否清除userdata分区
    -n 在升级时是否不检查时间戳,缺省要检查,即缺省情况下只能基于旧版本升级。
    -e 是否有额外运行的脚本
    -m 执行过程中生成脚本(updater-script)所需要的格式,目前有两种即amend和edify。对应上两种版本升级时会采用不同的解释器。缺省会同时生成两种格式的脚 本。
    -p 定义脚本用到的一些可执行文件的路径。
    -s 定义额外运行脚本的路径。
    -x 定义额外运行的脚本可能用的键值对。
    -v 执行过程中打印出执行的命令。
    -h 命令帮助

三、Recovery服务的核心方法 install_package


      和Recovery服务中的wipe_data、wipe_cache不同,install_package()是升级update.zip特有的一部分,也是最核心的部分。在这一步才真正开始对我们的update.zip包进行处理。下面就开始分析这一部分。

Android系统进行升级的时候,有两种途径:

一种是通过接口传递升级包路径自动升级(Android系统SD卡升级),升级完之后系统自动重启。

另一种是手动进入recovery模式下,选择升级包进行升级,升级完成之后停留在recovery界面,需要手动选择重启。

前者多用于手机厂商的客户端在线升级,后者多用于开发和测试人员。但不管哪种,原理都是一样的,都要在recovery模式下进行升级。

下面介绍的是升级包保存在cache目录下,且升级包路径保存在/cache/recovery/command中的方式(升级包的存放路径,从BCB或者/cache/recovery/command里面解析得到的)。
 

Android  OTA升级详细流程分析(non-AB)

 

重启进入升级主要流程:

  1. 系统重启进入Recovery模式。读取BCB的command,读取到”boot-recovery”后,加载recovery.img,启动recovery。
  2. 在install.cpp进行升级操作
  3. try_update_binary执行升级脚本
  4. 调用finish_recovery方法,清除BCB信息,重启

1、系统重启进入Recovery模式

    系统重启时会判断/cache/recovery目录下是否有command文件,如果存在就进入recovery模式,否则就正常启动。

    进入到Recovery模式下,将执行recovery.cpp的main函数,下面贴出关键代码片段:

static Device::BuiltinAction prompt_and_wait(Device* device, int status) {
  for (;;) {
    finish_recovery();
    switch (status) {
      case INSTALL_SUCCESS:
      case INSTALL_NONE:
        ui->SetBackground(RecoveryUI::NO_COMMAND);
        break;

      case INSTALL_ERROR:
      case INSTALL_CORRUPT:
        ui->SetBackground(RecoveryUI::ERROR);
        break;
    }
    ui->SetProgressType(RecoveryUI::EMPTY);

    size_t chosen_item = ui->ShowMenu(
        {}, device->GetMenuItems(), 0, false,
        std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2));
    // Handle Interrupt key
    if (chosen_item == static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED)) {
      return Device::KEY_INTERRUPTED;
    }
    // Device-specific code may take some action here. It may return one of the core actions
    // handled in the switch statement below.
    Device::BuiltinAction chosen_action =
        (chosen_item == static_cast<size_t>(RecoveryUI::KeyError::TIMED_OUT))
            ? Device::REBOOT
            : device->InvokeMenuItem(chosen_item);

    switch (chosen_action) {
      case Device::NO_ACTION:
        break;

      case Device::ENTER_FASTBOOT:
      case Device::ENTER_RECOVERY:
      case Device::REBOOT:
      case Device::REBOOT_BOOTLOADER:
      case Device::REBOOT_FASTBOOT:
      case Device::REBOOT_RECOVERY:
      case Device::REBOOT_RESCUE:
      case Device::SHUTDOWN:
        return chosen_action;

      case Device::WIPE_DATA:
        save_current_log = true;
        if (ui->IsTextVisible()) {
          if (ask_to_wipe_data(device)) {
            WipeData(device, false);
          }
        } else {
          WipeData(device, false);
          return Device::NO_ACTION;
        }
        break;

      case Device::WIPE_CACHE: {
        save_current_log = true;
        std::function<bool()> confirm_func = [&device]() {
          return yes_no(device, "Wipe cache?", "  THIS CAN NOT BE UNDONE!");
        };
        WipeCache(ui, ui->IsTextVisible() ? confirm_func : nullptr);
        if (!ui->IsTextVisible()) return Device::NO_ACTION;
        break;
      }

      case Device::APPLY_ADB_SIDELOAD:
      case Device::APPLY_SDCARD:
      case Device::ENTER_RESCUE: {
        save_current_log = true;

        bool adb = true;
        Device::BuiltinAction reboot_action;
        if (chosen_action == Device::ENTER_RESCUE) {
          // Switch to graphics screen.
          ui->ShowText(false);
          status = ApplyFromAdb(device, true /* rescue_mode */, &reboot_action);
        } else if (chosen_action == Device::APPLY_ADB_SIDELOAD) {
          status = ApplyFromAdb(device, false /* rescue_mode */, &reboot_action);
        } else {
		  adb = false;
		  int required_battery_level;
		  if(is_battery_ok(&required_battery_level)){
			  status = ApplyFromSdcard(device, ui);
		  }else{
			  ui->Print("battery capacity is not enough for installing package: %d%% needed\n",
					  required_battery_level);
			  status = INSTALL_SKIPPED;
		  }
        }

        ui->Print("\nInstall from %s completed with status %d.\n", adb ? "ADB" : "SD card", status);
        if (status == INSTALL_REBOOT) {
          return reboot_action;
        }

        if (status != INSTALL_SUCCESS) {
          ui->SetBackground(RecoveryUI::ERROR);
          ui->Print("Installation aborted.\n");
          copy_logs(save_current_log, has_cache, sehandle);
        } else if (!ui->IsTextVisible()) {
          return Device::NO_ACTION;  // reboot if logs aren't visible
        }
        break;
      }

      case Device::VIEW_RECOVERY_LOGS:
        choose_recovery_file(device);
        break;

      case Device::RUN_GRAPHICS_TEST:
        run_graphics_test();
        break;

      case Device::RUN_LOCALE_TEST: {
        ScreenRecoveryUI* screen_ui = static_cast<ScreenRecoveryUI*>(ui);
        screen_ui->CheckBackgroundTextImages();
        break;
      }
      case Device::MOUNT_SYSTEM:
        // the system partition is mounted at /mnt/system
        if (ensure_path_mounted_at(get_system_root(), "/mnt/system") != -1) {
          ui->Print("Mounted /system.\n");
        }
        break;

      case Device::KEY_INTERRUPTED:
        return Device::KEY_INTERRUPTED;
    }
  }
}
对上述函数的流程分析
1.bootable/recovery/recovery.cpp ShowMenu显示recovery各种菜单显示
InvokeMenuItem 表示选中的具体哪个item
2.bootable/recovery/recoveryui/device.cpp 
    Device::BuiltinAction Device::InvokeMenuItem(size_t menu_position) {
      return g_menu_actions[menu_position].second;
    }
    static std::vector<std::pair<std::string, Device::BuiltinAction>> g_menu_actions{
      { "Reboot system now", Device::REBOOT },
      { "Reboot to bootloader", Device::REBOOT_BOOTLOADER },
      { "Enter fastboot", Device::ENTER_FASTBOOT },
      { "Apply update from ADB", Device::APPLY_ADB_SIDELOAD },
      { "Apply update from SD card", Device::APPLY_SDCARD },
      { "Wipe data/factory reset", Device::WIPE_DATA },
      { "Wipe cache partition", Device::WIPE_CACHE },
      { "Mount /system", Device::MOUNT_SYSTEM },
      { "View recovery logs", Device::VIEW_RECOVERY_LOGS },
      { "Run graphics test", Device::RUN_GRAPHICS_TEST },
      { "Run locale test", Device::RUN_LOCALE_TEST },
      { "Enter rescue", Device::ENTER_RESCUE },
      { "Power off", Device::SHUTDOWN },
    };
  我们一般选择从sdcard选择升级,故选择Apply update from SD card 对应的device type是APPLY_SDCARD
   走到ApplyFromSdcard方法,其中ApplyFromSdcard是在bootable/recovery/install/fuse_sdcard_install.cpp中定义的  代码如下:
int ApplyFromSdcard(Device* device, RecoveryUI* ui) {
  .........

    result = install_package(FUSE_SIDELOAD_HOST_PATHNAME, false, false, 0 /*retry_count*/, ui);
    break;
  }
 其中最为关键的方法就是install_package(bootable/recovery/install/install.cpp) 最终执行的是
really_install_package
  static int really_install_package(const std::string& path, bool* wipe_cache, bool needs_mount,
                                  std::vector<std::string>* log_buffer, int retry_count,
                                  int* max_temperature, RecoveryUI* ui) {
  ui->SetBackground(RecoveryUI::INSTALLING_UPDATE);
  ui->Print("Finding update package...\n");
  // Give verification half the progress bar...
  ui->SetProgressType(RecoveryUI::DETERMINATE);
  ui->ShowProgress(VERIFICATION_PROGRESS_FRACTION, VERIFICATION_PROGRESS_TIME);
  LOG(INFO) << "Update location: " << path;

  // Map the update package into memory.
  ui->Print("Opening update package...\n");

  if (needs_mount) {
    if (path[0] == '@') {
      ensure_path_mounted(path.substr(1));
    } else {
      ensure_path_mounted(path);
    }
  }

  auto package = Package::CreateMemoryPackage(
      path, std::bind(&RecoveryUI::SetProgress, ui, std::placeholders::_1));
  if (!package) {
    log_buffer->push_back(android::base::StringPrintf("error: %d", kMapFileFailure));
    return INSTALL_CORRUPT;
  }

  // Verify package.验证签名
  if (!verify_package(package.get(), ui)) {
    log_buffer->push_back(android::base::StringPrintf("error: %d", kZipVerificationFailure));
    return INSTALL_CORRUPT;
  }

  // Try to open the package.打开升级包 
  ZipArchiveHandle zip = package->GetZipArchiveHandle();
  if (!zip) {
    log_buffer->push_back(android::base::StringPrintf("error: %d", kZipOpenFailure));
    return INSTALL_CORRUPT;
  }

  // Additionally verify the compatibility of the package if it's a fresh install.
  if (retry_count == 0 && !verify_package_compatibility(zip)) {
    log_buffer->push_back(android::base::StringPrintf("error: %d", kPackageCompatibilityFailure));
    return INSTALL_CORRUPT;
  }

  // Verify and install the contents of the package.
  ui->Print("Installing update...\n");
  if (retry_count > 0) {
    ui->Print("Retry attempt: %d\n", retry_count);
  }
  ui->SetEnableReboot(false); // 执行升级脚本文件,开始升级
  int result =
      try_update_binary(path, zip, wipe_cache, log_buffer, retry_count, max_temperature, ui);
  ui->SetEnableReboot(true);
  ui->Print("\n");

  return result;
}

try_update_binary执行升级脚本

// If the package contains an update binary, extract it and run it.
static int try_update_binary(const std::string& package, ZipArchiveHandle zip, bool* wipe_cache,
                             std::vector<std::string>* log_buffer, int retry_count,
                             int* max_temperature, RecoveryUI* ui) {
  std::map<std::string, std::string> metadata;
  if (!ReadMetadataFromPackage(zip, &metadata)) {
    LOG(ERROR) << "Failed to parse metadata in the zip file";
    return INSTALL_CORRUPT;
  }

  bool is_ab = android::base::GetBoolProperty("ro.build.ab_update", false);
  // Verifies against the metadata in the package first.
  if (int check_status = is_ab ? CheckPackageMetadata(metadata, OtaType::AB) : 0;
      check_status != 0) {
    log_buffer->push_back(android::base::StringPrintf("error: %d", kUpdateBinaryCommandFailure));
    return check_status;
  }
.......

  std::vector<std::string> args;
  if (int update_status =
          is_ab ? SetUpAbUpdateCommands(package, zip, pipe_write.get(), &args)
                : SetUpNonAbUpdateCommands(package, zip, retry_count, pipe_write.get(), &args);
      
......

  return INSTALL_SUCCESS;
}
通过获取设备ro.build.ab_update信息获得  是否为AB动态分区   如果是AB分区升级(update_engine)进入CheckPackageMetadata 判断当前版本信息(版本号 系列号 fingerprint等)与目标版本信息是否匹配  否则fail
最终AB分区调用SetUpAbUpdateCommands  非AB分区升级调用SetUpNonAbUpdateCommands
 其中SetUpAbUpdateCommands  其实就是调用update_engine完成升级
*cmd = {
    "/system/bin/update_engine_sideload",
    "--payload=file://" + package,
    android::base::StringPrintf("--offset=%ld", payload_offset),
    "--headers=" + std::string(payload_properties.begin(), payload_properties.end()),
    android::base::StringPrintf("--status_fd=%d", status_fd),
  };

  

OTA升级成功,清空misc分区(BCB置零),并将保存到内存系统的升级日志/tmp/recovery.log保存到/cache/recovery/last_log。重启设备进入Main System,升级完成。


四 recovery升级过程中log调试方法
 

有客户反馈不知道如何调试recovery,在这里介绍下recovery的调试方法。

1. 如何在recovery模式使用adb

在recovery模式下,init程序加载的rc文件是bootable/recovery/etc/init.rc。

service adbd /sbin/adbd recovery
    disabled

# Always start adbd on userdebug and eng builds
on property:ro.debuggable=1
    write /sys/class/android_usb/android0/enable 1
    start adbd

这里可以看到如果是在userdebug或者eng编译模式,adbd服务是启动的。执行adb shell时提示不存在system/bin/sh,因为这个时候recovery/root/system为空,并没有sh程序可执行。

 

虽然adb shell不能执行,但adb的其他很多命令都是能够使用的。

adb devices

adb push/pull

 

2. 如何增加log

在recovery的代码中能看到有两种方式添加的打印信息:printf和UI->Print。

printf输出到stdout好理解,UI->Print调用screen_ui的print函数,将信息显示在屏幕上。

void ScreenRecoveryUI::Print(const char *fmt, ...)
{
    char buf[256];
    va_list ap;
    va_start(ap, fmt);
    vsnprintf(buf, 256, fmt, ap);
    va_end(ap);
 
    fputs(buf, stdout);

除了显示在屏幕上,也将信息输出到了stdout标准输出。

 

3. 标准输出信息在哪

int
main(int argc, char **argv) {
    
    // If these fail, there's not really anywhere to complain...
    freopen(TEMPORARY_LOG_FILE, "a", stdout); setbuf(stdout, NULL);
    freopen(TEMPORARY_LOG_FILE, "a", stderr); setbuf(stderr, NULL);

recovery.cpp的main函数中,最开始就将stdout的输出信息保存在了/tmp/recovery.log文件中。也就是说我们需要查看的log信息都在这里。

再看看recovery结束时做了什么

static void
finish_recovery(const char *send_intent) {
    // Copy logs to cache so the system can find out what happened.
    copy_log_file(TEMPORARY_LOG_FILE, LOG_FILE, true);
    copy_log_file(TEMPORARY_LOG_FILE, LAST_LOG_FILE, false);
    copy_log_file(TEMPORARY_INSTALL_FILE, LAST_INSTALL_FILE, false);
    chmod(LOG_FILE, 0600);
    chown(LOG_FILE, 1000, 1000);   // system user
    chmod(LAST_LOG_FILE, 0640);
    chmod(LAST_INSTALL_FILE, 0644);

将"/tmp/recovery.log"拷贝到了"/cache/recovery/last_log"。

4. 如何查看log

 

1)如果能使用adb,在recovery模式下就能使用adb pull出log信息

adb pull /tmp/recovery.log .\

2)即使adb服务不能使用,前一次recovery的log也会保存到cache/recovery目录下,在reboot正常进入系统后pull 出来查看。

adb pull /cache/recovery/last_log .\

cache/recovery/last_install保存的是最后一次更新的OTA包。
 

eng 版本 如何在recovery mode下抓取LOG

[SOLUTION]

1、在recovery mode下,升级动作之后 adb pull /tmp/recovery.log

如果是KK之前版本:

2、在nomal mode下 adb pull /cache/recovery/last_log

如果是KK版本:

2、在nomal mode下 adb pull /cache/recovery/last_log_r

此两种方法均可 

如果是user版本:

In recovery mode

    目前没有办法在user版本也看到recovery.log,目前的办法是

直接用eng版本的recovery.img替换user版本的recovery.img,然后抓取log。

Reboot to normal mode

    在user版本也会产生/cache/recovery/last_log,但是可能会不能用adb pull出来!目前的办法是做完recovery,reboot到normal mode后,重新烧boot.img,用eng版本的boot.img替换user 版本的boot.img,然后将log pull出来!

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

上一篇:golang channel 源码剖析


下一篇:一个关于VTS VtsKernelCheckpointTest、VtsKernelCheckpointTest测项失败的代码分析