#!/usr/bin/env python3 # # Copyright 2020 - The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """This script is for test infrastructure to mix images in a super image.""" import argparse import os import shutil import stat import subprocess import tempfile import zipfile # The file extension of the unpacked images. IMG_FILE_EXT = ".img" # The directory containing executable files in OTA tools zip. BIN_DIR_NAME = "bin" def existing_abs_path(path): """Validates that a path exists and returns the absolute path.""" abs_path = os.path.abspath(path) if not os.path.exists(abs_path): raise ValueError(path + " does not exist.") return abs_path def partition_image(part_img): """Splits a string into a pair of strings by "=".""" part, sep, img = part_img.partition("=") if not part or not sep: raise ValueError(part_img + " is not in the format of " "PARITITON_NAME=IMAGE_PATH.") return part, (existing_abs_path(img) if img else "") def unzip_ota_tools(ota_tools_zip, output_dir): """Unzips OTA tools and sets the files in bin/ to be executable.""" ota_tools_zip.extractall(output_dir) permissions = (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH | stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH) for root_dir, dir_names, file_names in os.walk( os.path.join(output_dir, BIN_DIR_NAME)): for file_name in file_names: file_path = os.path.join(root_dir, file_name) file_stat = os.stat(file_path) os.chmod(file_path, file_stat.st_mode | permissions) def is_sparse_image(image_path): """Checks whether a file is a sparse image.""" with open(image_path, "rb") as image_file: return image_file.read(4) == b"\x3a\xff\x26\xed" def rewrite_misc_info(args_part_imgs, unpacked_part_imgs, lpmake_path, input_file, output_file): """Changes the lpmake path and image paths in a misc info file. Args: args_part_imgs: A dict of {partition_name: image_path} that the user intends to substitute. The partition_names must not have slot suffixes. unpacked_part_imgs: A dict of {partition_name: image_path} unpacked from the input super image. The partition_names must have slot suffixes if the misc info enables virtual_ab. lpmake_path: The path to the lpmake binary. input_file: The input misc info file object. output_file: The output misc info file object. Returns: The list of the partition names without slot suffixes. """ virtual_ab = False partition_names = () for line in input_file: split_line = line.strip().split("=", 1) if len(split_line) < 2: split_line = (split_line[0], "") if split_line[0] == "dynamic_partition_list": partition_names = split_line[1].split() elif split_line[0] == "lpmake": output_file.write("lpmake=%s\n" % lpmake_path) continue elif split_line[0].endswith("_image"): continue elif split_line[0] == "virtual_ab" and split_line[1] == "true": virtual_ab = True output_file.write(line) for partition_name in partition_names: img_path = args_part_imgs.get(partition_name) if img_path is None: # _a is the active slot for the super images built from source. img_path = unpacked_part_imgs.get(partition_name + "_a" if virtual_ab else partition_name) if img_path is None: raise KeyError("No image for " + partition_name + " partition.") if img_path != "": output_file.write("%s_image=%s\n" % (partition_name, img_path)) return partition_names def main(): parser = argparse.ArgumentParser( description="This script is for test infrastructure to mix images in a " "super image.") parser.add_argument("--temp-dir", default=tempfile.gettempdir(), type=existing_abs_path, help="The directory where this script creates " "temporary files.") parser.add_argument("--ota-tools", required=True, type=existing_abs_path, help="The path to the zip or directory containing OTA " "tools.") parser.add_argument("--misc-info", required=True, type=existing_abs_path, help="The path to the misc info file.") parser.add_argument("super_img", metavar="SUPER_IMG", type=existing_abs_path, help="The path to the super image to be repacked.") parser.add_argument("part_imgs", metavar="PART_IMG", nargs="*", type=partition_image, help="The partition and the image that will be added " "to the super image. The format is " "PARITITON_NAME=IMAGE_PATH. PARTITION_NAME must " "not have slot suffix. If IMAGE_PATH is empty, the " "partition will be resized to 0.") args = parser.parse_args() # Convert the args.part_imgs to a dictionary. args_part_imgs = dict() for part, img in args.part_imgs: if part in args_part_imgs: raise ValueError(part + " partition is repeated.") args_part_imgs[part] = img if args.temp_dir: tempfile.tempdir = args.temp_dir temp_dirs = [] temp_files = [] try: if os.path.isdir(args.ota_tools): ota_tools_dir = args.ota_tools else: print("Unzip OTA tools.") temp_ota_tools_dir = tempfile.mkdtemp(prefix="ota_tools") temp_dirs.append(temp_ota_tools_dir) with zipfile.ZipFile(args.ota_tools) as ota_tools_zip: unzip_ota_tools(ota_tools_zip, temp_ota_tools_dir) ota_tools_dir = temp_ota_tools_dir if not is_sparse_image(args.super_img): super_img_path = args.super_img else: print("Convert to unsparsed super image.") simg2img_path = os.path.join(ota_tools_dir, BIN_DIR_NAME, "simg2img") with tempfile.NamedTemporaryFile( mode="wb", prefix="super", suffix=".img", delete=False) as raw_super_img: temp_files.append(raw_super_img.name) super_img_path = raw_super_img.name subprocess.check_call([simg2img_path, args.super_img, super_img_path]) print("Unpack super image.") unpacked_part_imgs = dict() lpunpack_path = os.path.join(ota_tools_dir, BIN_DIR_NAME, "lpunpack") unpack_dir = tempfile.mkdtemp(prefix="lpunpack") temp_dirs.append(unpack_dir) subprocess.check_call([lpunpack_path, super_img_path, unpack_dir]) for file_name in os.listdir(unpack_dir): if file_name.endswith(IMG_FILE_EXT): part = file_name[:-len(IMG_FILE_EXT)] unpacked_part_imgs[part] = os.path.join(unpack_dir, file_name) print("Create temporary misc info.") lpmake_path = os.path.join(ota_tools_dir, BIN_DIR_NAME, "lpmake") with tempfile.NamedTemporaryFile( mode="w", prefix="misc_info", suffix=".txt", delete=False) as misc_info_file: temp_files.append(misc_info_file.name) misc_info_file_path = misc_info_file.name with open(args.misc_info, "r") as misc_info: part_list = rewrite_misc_info(args_part_imgs, unpacked_part_imgs, lpmake_path, misc_info, misc_info_file) # Check that all input partitions are in the partition list. parts_not_found = args_part_imgs.keys() - set(part_list) if parts_not_found: raise ValueError("Cannot find partitions in misc info: " + " ".join(parts_not_found)) print("Build super image.") build_super_image_path = os.path.join(ota_tools_dir, BIN_DIR_NAME, "build_super_image") subprocess.check_call([build_super_image_path, misc_info_file_path, args.super_img]) finally: for temp_dir in temp_dirs: shutil.rmtree(temp_dir, ignore_errors=True) for temp_file in temp_files: os.remove(temp_file) if __name__ == "__main__": main()