Add a host tool to create an APEX bundle

Even though the soong build system generates a bundle module for an
apex, the bundle module itself should be further processed to be an
AppBundle (.aab) file which can be uploaded to Play.

This script fills the gap by invoking bundletool to create an
AppBundle (.aab) file out of soong-built bundle module for an apex.
(Note: uploading APEX bundle (.aab) to Play is not supported yet.)

You can create an .aab file by:
- TARGET_BUILD_APPS={apex name} m dist
- m build-apex-bundle
- build-apex-bundle --output out.aab out/dist/{apex name}-base.zip

For now it creates a single-ABI APEX bundle. In the future it can be
extended to support multiple-ABI APEX bundles.

Bug: 236673372
Test: m build-apex-bundle
Test: TARGET_BUILD_APPS=com.google.cf.bt m dist
Test: build-apex-bundle --output bt.aab out/dist/com.google.cf.bt-base.zip
Change-Id: Id321efcd42c0fe60294a8348047c9ebbf7acf391
diff --git a/scripts/Android.bp b/scripts/Android.bp
index b5b588b..5dd45cd 100644
--- a/scripts/Android.bp
+++ b/scripts/Android.bp
@@ -199,6 +199,17 @@
     ],
 }
 
+python_binary_host {
+    name: "build-apex-bundle",
+    main: "build-apex-bundle.py",
+    srcs: [
+        "build-apex-bundle.py",
+    ],
+    required: [
+        "bundletool",
+    ],
+}
+
 sh_binary_host {
     name: "list_image",
     src: "list_image.sh",
diff --git a/scripts/build-apex-bundle.py b/scripts/build-apex-bundle.py
new file mode 100644
index 0000000..dcdd9ef
--- /dev/null
+++ b/scripts/build-apex-bundle.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python
+#
+# 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.
+#
+"""A tool to create an APEX bundle out of Soong-built base.zip"""
+
+from __future__ import print_function
+
+import argparse
+import sys
+import tempfile
+import zipfile
+import os
+import json
+import subprocess
+
+
+def parse_args():
+  """Parse commandline arguments."""
+  parser = argparse.ArgumentParser()
+  parser.add_argument(
+      '--overwrite',
+      action='store_true',
+      help='If set, any previous existing output will be overwritten')
+  parser.add_argument('--output', help='specify the output .aab file')
+  parser.add_argument(
+      'input', help='specify the input <apex name>-base.zip file')
+  return parser.parse_args()
+
+
+def build_bundle(input, output, overwrite):
+  base_zip = zipfile.ZipFile(input)
+
+  tmpdir = tempfile.mkdtemp()
+  tmp_base_zip = os.path.join(tmpdir, 'base.zip')
+  tmp_bundle_config = os.path.join(tmpdir, 'bundle_config.json')
+
+  bundle_config = None
+  abi = []
+
+  # This block performs three tasks
+  # - extract/load bundle_config.json from input => bundle_config
+  # - get ABI from input => abi
+  # - discard bundle_config.json from input => tmp/base.zip
+  with zipfile.ZipFile(tmp_base_zip, 'a') as out:
+    for info in base_zip.infolist():
+
+      # discard bundle_config.json
+      if info.filename == 'bundle_config.json':
+        bundle_config = json.load(base_zip.open(info.filename))
+        continue
+
+      # get ABI from apex/{abi}.img
+      dir, basename = os.path.split(info.filename)
+      name, ext = os.path.splitext(basename)
+      if dir == 'apex' and ext == '.img':
+        abi.append(name)
+
+      # copy entries to tmp/base.zip
+      out.writestr(info, base_zip.open(info.filename).read())
+
+  base_zip.close()
+
+  if not bundle_config:
+    raise ValueError(f'bundle_config.json not found in {input}')
+  if len(abi) != 1:
+    raise ValueError(f'{input} should have only a single apex/*.img file')
+
+  # add ABI to tmp/bundle_config.json
+  apex_config = bundle_config['apex_config']
+  if 'supported_abi_set' not in apex_config:
+    apex_config['supported_abi_set'] = []
+  supported_abi_set = apex_config['supported_abi_set']
+  supported_abi_set.append({'abi': abi})
+
+  with open(tmp_bundle_config, 'w') as out:
+    json.dump(bundle_config, out)
+
+  # invoke bundletool
+  cmd = [
+      'bundletool', 'build-bundle', '--config', tmp_bundle_config, '--modules',
+      tmp_base_zip, '--output', output
+  ]
+  if overwrite:
+    cmd.append('--overwrite')
+  subprocess.check_call(cmd)
+
+
+def main():
+  """Program entry point."""
+  try:
+    args = parse_args()
+    build_bundle(args.input, args.output, args.overwrite)
+
+  # pylint: disable=broad-except
+  except Exception as err:
+    print('error: ' + str(err), file=sys.stderr)
+    sys.exit(-1)
+
+
+if __name__ == '__main__':
+  main()