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
diff --git a/tools/releasetools/Android.bp b/tools/releasetools/Android.bp
index 5a7cc76..c785c8e 100644
--- a/tools/releasetools/Android.bp
+++ b/tools/releasetools/Android.bp
@@ -344,6 +344,7 @@
     },
     srcs: [
         "merge_ota.py",
+        "ota_signing_utils.py",
     ],
     libs: [
         "ota_metadata_proto",
@@ -493,6 +494,26 @@
 }
 
 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"],
     srcs: [
@@ -590,6 +611,7 @@
         "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 441312c..24d9ea9 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 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 @@
     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 @@
               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 @@
       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 @@
   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 0000000..ac4f9fb
--- /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 0000000..60c8c94
--- /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 f288a9c..8993da9 100644
--- a/tools/releasetools/ota_utils.py
+++ b/tools/releasetools/ota_utils.py
@@ -934,9 +934,9 @@
     # 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: