From c7441e59071b0548ae85d4973364442e28a788a6 Mon Sep 17 00:00:00 2001 From: Kelvin Zhang Date: Tue, 22 Aug 2023 08:56:30 -0700 Subject: [PATCH] Add a tool to generate OTA from images During build, we will need to generate an OTA for boot partition using a 16K boot image. Typically, OTA is generated from target_files.zip . To avoid relying on target_files.zip as a dependency for 16K OTA, add a tool to generate OTA directly from a raw image. Test: th, ota_from_raw_img --partition_name boot --output ota.zip $OUT/boot_16k.img Bug: 293313353 Change-Id: I2076332faf2a8dc573450597efd481e285a49545 --- tools/releasetools/Android.bp | 22 +++++ tools/releasetools/merge_ota.py | 29 ++----- tools/releasetools/ota_from_raw_img.py | 109 ++++++++++++++++++++++++ tools/releasetools/ota_signing_utils.py | 38 +++++++++ tools/releasetools/ota_utils.py | 6 +- 5 files changed, 179 insertions(+), 25 deletions(-) create mode 100644 tools/releasetools/ota_from_raw_img.py create mode 100644 tools/releasetools/ota_signing_utils.py diff --git a/tools/releasetools/Android.bp b/tools/releasetools/Android.bp index 5a7cc76bbc..c785c8eda4 100644 --- a/tools/releasetools/Android.bp +++ b/tools/releasetools/Android.bp @@ -344,6 +344,7 @@ python_binary_host { }, srcs: [ "merge_ota.py", + "ota_signing_utils.py", ], libs: [ "ota_metadata_proto", @@ -492,6 +493,26 @@ python_binary_host { ], } +python_binary_host { + name: "ota_from_raw_img", + srcs: [ + "ota_from_raw_img.py", + "ota_signing_utils.py", + ], + main: "ota_from_raw_img.py", + defaults: [ + "releasetools_binary_defaults", + ], + required: [ + "delta_generator", + ], + libs: [ + "ota_metadata_proto", + "releasetools_common", + "ota_utils_lib", + ], +} + python_binary_host { name: "ota_package_parser", defaults: ["releasetools_binary_defaults"], @@ -590,6 +611,7 @@ python_defaults { "sign_target_files_apks.py", "validate_target_files.py", "merge_ota.py", + "ota_signing_utils.py", ":releasetools_merge_sources", ":releasetools_merge_tests", diff --git a/tools/releasetools/merge_ota.py b/tools/releasetools/merge_ota.py index 441312cf25..24d9ea9cf0 100644 --- a/tools/releasetools/merge_ota.py +++ b/tools/releasetools/merge_ota.py @@ -14,7 +14,6 @@ import argparse import logging -import shlex import struct import sys import update_payload @@ -31,6 +30,7 @@ from update_payload import Payload from payload_signer import PayloadSigner from ota_utils import PayloadGenerator, METADATA_PROTO_NAME, FinalizeMetadata +from ota_signing_utils import AddSigningArgumentParse logger = logging.getLogger(__name__) @@ -126,7 +126,7 @@ def MergeManifests(payloads: List[Payload]) -> DeltaArchiveManifest: ExtendPartitionUpdates(output_manifest.partitions, manifest.partitions) try: MergeDynamicPartitionMetadata( - output_manifest.dynamic_partition_metadata, manifest.dynamic_partition_metadata) + output_manifest.dynamic_partition_metadata, manifest.dynamic_partition_metadata) except DuplicatePartitionError: logger.error( "OTA %s has duplicate partition with some of the previous OTAs", payload.name) @@ -190,6 +190,7 @@ def CheckDuplicatePartitions(payloads: List[Payload]): f"OTA {partition_to_ota[part].name} and {payload.name} have duplicating partition {part}") partition_to_ota[part] = payload + def ApexInfo(file_paths): if len(file_paths) > 1: logger.info("More than one target file specified, will ignore " @@ -201,33 +202,19 @@ def ApexInfo(file_paths): return apex_info_bytes return None -def ParseSignerArgs(args): - if args is None: - return None - return shlex.split(args) def main(argv): parser = argparse.ArgumentParser(description='Merge multiple partial OTAs') parser.add_argument('packages', type=str, nargs='+', help='Paths to OTA packages to merge') - parser.add_argument('--package_key', type=str, - help='Paths to private key for signing payload') - parser.add_argument('--search_path', type=str, - help='Search path for framework/signapk.jar') - parser.add_argument('--payload_signer', type=str, - help='Path to custom payload signer') - parser.add_argument('--payload_signer_args', type=ParseSignerArgs, - help='Arguments for payload signer if necessary') - parser.add_argument('--payload_signer_maximum_signature_size', type=str, - help='Maximum signature size (in bytes) that would be ' - 'generated by the given payload signer') parser.add_argument('--output', type=str, help='Paths to output merged ota', required=True) parser.add_argument('--metadata_ota', type=str, help='Output zip will use build metadata from this OTA package, if unspecified, use the last OTA package in merge list') - parser.add_argument('--private_key_suffix', type=str, - help='Suffix to be appended to package_key path', default=".pk8") - parser.add_argument('-v', action="store_true", help="Enable verbose logging", dest="verbose") + parser.add_argument('-v', action="store_true", + help="Enable verbose logging", dest="verbose") + AddSigningArgumentParse(parser) + parser.epilog = ('This tool can also be used to resign a regular OTA. For a single regular OTA, ' 'apex_info.pb will be written to output. When merging multiple OTAs, ' 'apex_info.pb will not be written.') @@ -301,8 +288,6 @@ def main(argv): return 0 - - if __name__ == '__main__': logging.basicConfig() sys.exit(main(sys.argv)) diff --git a/tools/releasetools/ota_from_raw_img.py b/tools/releasetools/ota_from_raw_img.py new file mode 100644 index 0000000000..ac4f9fb8c7 --- /dev/null +++ b/tools/releasetools/ota_from_raw_img.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python +# +# Copyright (C) 2008 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. + +""" +Given a series of .img files, produces an OTA package that installs thoese images +""" + +import sys +import os +import argparse +import subprocess +import tempfile +import logging +import zipfile + +import common +from payload_signer import PayloadSigner +from ota_utils import PayloadGenerator +from ota_signing_utils import AddSigningArgumentParse + + +logger = logging.getLogger(__name__) + + +def ResolveBinaryPath(filename, search_path): + if not search_path: + return filename + if not os.path.exists(search_path): + return filename + path = os.path.join(search_path, "bin", filename) + if os.path.exists(path): + return path + path = os.path.join(search_path, filename) + if os.path.exists(path): + return path + return path + + +def main(argv): + parser = argparse.ArgumentParser( + prog=argv[0], description="Given a series of .img files, produces a full OTA package that installs thoese images") + parser.add_argument("images", nargs="+", type=str, + help="List of images to generate OTA") + parser.add_argument("--partition_names", nargs='+', type=str, + help="Partition names to install the images, default to basename of the image(no file name extension)") + parser.add_argument('--output', type=str, + help='Paths to output merged ota', required=True) + parser.add_argument("-v", action="store_true", + help="Enable verbose logging", dest="verbose") + AddSigningArgumentParse(parser) + + args = parser.parse_args(argv[1:]) + if args.verbose: + logger.setLevel(logging.INFO) + logger.info(args) + if not args.partition_names: + args.partition_names = [os.path.os.path.splitext(os.path.basename(path))[ + 0] for path in args.images] + with tempfile.NamedTemporaryFile() as unsigned_payload: + cmd = [ResolveBinaryPath("delta_generator", args.search_path)] + cmd.append("--partition_names=" + ",".join(args.partition_names)) + cmd.append("--new_partitions=" + ",".join(args.images)) + cmd.append("--out_file=" + unsigned_payload.name) + logger.info("Running %s", cmd) + + subprocess.run(cmd) + generator = PayloadGenerator() + generator.payload_file = unsigned_payload.name + logger.info("Payload size: %d", os.path.getsize(generator.payload_file)) + + # Get signing keys + key_passwords = common.GetKeyPasswords([args.package_key]) + + if args.package_key: + logger.info("Signing payload...") + # TODO: remove OPTIONS when no longer used as fallback in payload_signer + common.OPTIONS.payload_signer_args = None + common.OPTIONS.payload_signer_maximum_signature_size = None + signer = PayloadSigner(args.package_key, args.private_key_suffix, + key_passwords[args.package_key], + payload_signer=args.payload_signer, + payload_signer_args=args.payload_signer_args, + payload_signer_maximum_signature_size=args.payload_signer_maximum_signature_size) + generator.payload_file = unsigned_payload.name + generator.Sign(signer) + + logger.info("Payload size: %d", os.path.getsize(generator.payload_file)) + + logger.info("Writing to %s", args.output) + with zipfile.ZipFile(args.output, "w") as zfp: + generator.WriteToZip(zfp) + + +if __name__ == "__main__": + logging.basicConfig() + main(sys.argv) diff --git a/tools/releasetools/ota_signing_utils.py b/tools/releasetools/ota_signing_utils.py new file mode 100644 index 0000000000..60c8c94f91 --- /dev/null +++ b/tools/releasetools/ota_signing_utils.py @@ -0,0 +1,38 @@ +# Copyright (C) 2022 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. + +import argparse +import shlex + + +def ParseSignerArgs(args): + if args is None: + return None + return shlex.split(args) + + +def AddSigningArgumentParse(parser: argparse.ArgumentParser): + parser.add_argument('--package_key', type=str, + help='Paths to private key for signing payload') + parser.add_argument('--search_path', '--path', type=str, + help='Search path for framework/signapk.jar') + parser.add_argument('--payload_signer', type=str, + help='Path to custom payload signer') + parser.add_argument('--payload_signer_args', type=ParseSignerArgs, + help='Arguments for payload signer if necessary') + parser.add_argument('--payload_signer_maximum_signature_size', type=str, + help='Maximum signature size (in bytes) that would be ' + 'generated by the given payload signer') + parser.add_argument('--private_key_suffix', type=str, + help='Suffix to be appended to package_key path', default=".pk8") diff --git a/tools/releasetools/ota_utils.py b/tools/releasetools/ota_utils.py index f288a9c362..8993da9eb7 100644 --- a/tools/releasetools/ota_utils.py +++ b/tools/releasetools/ota_utils.py @@ -934,9 +934,9 @@ class PayloadGenerator(object): # 4. Dump the signed payload properties. properties_file = common.MakeTempFile(prefix="payload-properties-", suffix=".txt") - cmd = ["brillo_update_payload", "properties", - "--payload", self.payload_file, - "--properties_file", properties_file] + cmd = ["delta_generator", + "--in_file=" + self.payload_file, + "--properties_file=" + properties_file] self._Run(cmd) if self.secondary: