Create repack_gki, a standalone tool for repacking GKI images.

Bug: 191162695
Test: repack_gki --kernel_version 5.10 \
        --ramdisk_build_id <build id> --kernel_build_id <build id>
Test: unpack_bootimg for each individual boot.img.
      Ensure kernel images are replaced, and adhere to the following:
	- allsyms boot images use the debug (allsyms) kernel Image
	- gz boot images use the gz kernel Image
	- lz4 boot images use the lz4 kernel Image
Test: Repeat for boot images inside the img.zip.
Test: Ensure BOOT/kernel-* inside target_files.zip match kernel build.
Change-Id: I0a3287ca83cb6e7b9739bfece4cffa1081272748
diff --git a/gki/Android.bp b/gki/Android.bp
new file mode 100644
index 0000000..d5b886d
--- /dev/null
+++ b/gki/Android.bp
@@ -0,0 +1,50 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "tools_treble_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["tools_treble_license"],
+}
+
+python_defaults {
+  name: "repack_gki_defaults",
+  version: {
+      py2: {
+          enabled: false,
+          embedded_launcher: false,
+      },
+      py3: {
+          enabled: true,
+          embedded_launcher: false,
+      },
+  },
+}
+
+python_library_host {
+  name: "repack_gki_lib",
+  defaults: ["repack_gki_defaults"],
+  srcs: [
+    "repack_gki_lib.py",
+  ],
+  libs: [
+    "fetcher-lib",
+  ],
+  pkg_path: "treble/gki",
+}
+
+python_binary_host {
+    name: "repack_gki",
+    main: "repack_gki.py",
+    defaults: ["repack_gki_defaults"],
+    srcs: [
+        "repack_gki.py",
+    ],
+    libs: [
+        "repack_gki_lib",
+    ],
+    required: [
+        "mkbootimg",
+        "unpack_bootimg",
+    ],
+}
diff --git a/gki/repack_gki.py b/gki/repack_gki.py
new file mode 100644
index 0000000..38e50fd
--- /dev/null
+++ b/gki/repack_gki.py
@@ -0,0 +1,137 @@
+"""Repacks GKI boot images with the given kernel images."""
+import argparse
+import json
+import os
+import shutil
+import tempfile
+
+from treble.fetcher import fetcher_lib
+from treble.gki import repack_gki_lib
+
+
+def main():
+  parser = argparse.ArgumentParser(
+      description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+  parser.add_argument(
+      '--json_keyfile',
+      help='JSON keyfile containing credentials. '
+      '(Default: Use default credential file)')
+  parser.add_argument(
+      '--ramdisk_build_id',
+      required=True,
+      help='Download from the specified build.')
+  parser.add_argument(
+      '--ramdisk_target',
+      required=True,
+      help='Name of the ramdisk target from the ramdisk branch.')
+  parser.add_argument(
+      '--kernel_build_id',
+      required=True,
+      help='Download from the specified build.')
+  parser.add_argument(
+      '--kernel_target',
+      required=True,
+      help='Name of the kernel target from the kernel branch.')
+  parser.add_argument(
+      '--kernel_debug_target',
+      required=True,
+      help='Name of the kernel debug target from the kernel branch.')
+  parser.add_argument(
+      '--kernel_version',
+      required=True,
+      help='The Kernel version to use when repacking.')
+  parser.add_argument(
+      '--out_dir', required=True, help='Save output to this directory.')
+
+  args = parser.parse_args()
+  client = fetcher_lib.create_client_from_json_keyfile(
+      json_keyfile_name=args.json_keyfile)
+
+  if not os.path.exists(args.out_dir):
+    os.makedirs(args.out_dir)
+
+  with tempfile.TemporaryDirectory() as tmp_bootimg_dir, \
+      tempfile.TemporaryDirectory() as tmp_kernel_dir:
+    # Fetch boot images.
+    repack_gki_lib.fetch_bootimg(
+        client=client,
+        out_dir=tmp_bootimg_dir,
+        build_id=args.ramdisk_build_id,
+        kernel_version=args.kernel_version,
+        target=args.ramdisk_target,
+    )
+
+    # Fetch kernel artifacts.
+    kernel_dir, kernel_debug_dir = repack_gki_lib.fetch_kernel(
+        client=client,
+        out_dir=tmp_kernel_dir,
+        build_id=args.kernel_build_id,
+        kernel_target=args.kernel_target,
+        kernel_debug_target=args.kernel_debug_target,
+    )
+
+    # Save kernel artifacts to the out dir.
+    kernel_out_dir = os.path.join(args.out_dir, 'kernel', args.kernel_version)
+    if not os.path.exists(kernel_out_dir):
+      os.makedirs(kernel_out_dir)
+
+    def copy_kernel_file(in_dir, filename, outname=None):
+      if not outname:
+        outname = filename
+      shutil.copy(
+          os.path.join(in_dir, filename), os.path.join(kernel_out_dir, outname))
+
+    copy_kernel_file(kernel_dir, 'System.map')
+    copy_kernel_file(kernel_dir, 'abi.xml')
+    copy_kernel_file(kernel_dir, 'abi_symbollist')
+    copy_kernel_file(kernel_dir, 'Image',
+                     'kernel-{}'.format(args.kernel_version))
+    copy_kernel_file(kernel_dir, 'Image.lz4',
+                     'kernel-{}-lz4'.format(args.kernel_version))
+    copy_kernel_file(kernel_dir, 'Image.gz',
+                     'kernel-{}-gz'.format(args.kernel_version))
+    copy_kernel_file(kernel_debug_dir, 'System.map', 'System.map-allsyms')
+    copy_kernel_file(kernel_debug_dir, 'Image',
+                     'kernel-{}-allsyms'.format(args.kernel_version))
+    copy_kernel_file(kernel_debug_dir, 'Image.lz4',
+                     'kernel-{}-lz4-allsyms'.format(args.kernel_version))
+    copy_kernel_file(kernel_debug_dir, 'Image.gz',
+                     'kernel-{}-gz-allsyms'.format(args.kernel_version))
+
+    # Repack individual boot images using the fetched kernel artifacts,
+    # then save to the out dir.
+    repack_gki_lib.repack_bootimgs(tmp_bootimg_dir, kernel_dir,
+                                   kernel_debug_dir)
+    shutil.copytree(tmp_bootimg_dir, args.out_dir, dirs_exist_ok=True)
+
+    # Repack boot images inside the img.zip and save to the out dir.
+    img_zip_name = [f for f in os.listdir(tmp_bootimg_dir) if '-img-' in f][0]
+    img_zip_path = os.path.join(tmp_bootimg_dir, img_zip_name)
+    repack_gki_lib.repack_img_zip(img_zip_path, kernel_dir, kernel_debug_dir,
+                                  args.kernel_version)
+    shutil.copy(img_zip_path, args.out_dir)
+
+    # Replace kernels within the target_files.zip and save to the out dir.
+    target_files_zip_name = [
+        f for f in os.listdir(tmp_bootimg_dir) if '-target_files-' in f
+    ][0]
+    target_files_zip_path = os.path.join(tmp_bootimg_dir, target_files_zip_name)
+    repack_gki_lib.replace_target_files_zip_kernels(target_files_zip_path,
+                                                    kernel_out_dir,
+                                                    args.kernel_version)
+    shutil.copy(target_files_zip_path, args.out_dir)
+
+    # Copy otatools.zip from the ramdisk build, used for GKI signing.
+    shutil.copy(os.path.join(tmp_bootimg_dir, 'otatools.zip'), args.out_dir)
+
+    # Write prebuilt-info.txt using the prebuilt artifact build IDs.
+    data = {
+        'ramdisk-build-id': int(args.ramdisk_build_id),
+        'kernel-build-id': int(args.kernel_build_id),
+    }
+    with open(os.path.join(kernel_out_dir, 'prebuilt-info.txt'), 'w') as f:
+      json.dump(data, f, indent=4)
+
+
+if __name__ == '__main__':
+  main()
diff --git a/gki/repack_gki_lib.py b/gki/repack_gki_lib.py
new file mode 100644
index 0000000..6df90d1
--- /dev/null
+++ b/gki/repack_gki_lib.py
@@ -0,0 +1,160 @@
+"""Helper library for repacking GKI boot images."""
+import os
+import shutil
+import subprocess
+import tempfile
+
+from treble.fetcher import fetcher_lib
+
+
+def fetch_bootimg(client, out_dir, build_id, kernel_version, target):
+  """Fetches boot.img artifacts from a given build ID."""
+  fetcher_lib.fetch_artifacts(
+      client=client,
+      build_id=build_id,
+      target=target,
+      pattern=r'(.*-img-.*\.zip|.*-target_files-.*\.zip|boot-debug-{version}.*\.img|boot-test-harness-{version}.*\.img|otatools.zip)'
+      .format(version=kernel_version),
+      out_dir=out_dir)
+
+
+def fetch_kernel(client, out_dir, build_id, kernel_target, kernel_debug_target):
+  """Fetches kernel artifacts from a given build ID."""
+  kernel_dir = os.path.join(out_dir, 'kernel')
+  kernel_debug_dir = os.path.join(out_dir, 'kernel_debug')
+  os.makedirs(kernel_dir)
+  os.makedirs(kernel_debug_dir)
+
+  fetcher_lib.fetch_artifacts(
+      client=client,
+      build_id=build_id,
+      target=kernel_target,
+      pattern=r'(Image|Image.lz4|System\.map|abi.xml|abi_symbollist)',
+      out_dir=kernel_dir)
+  fetcher_lib.fetch_artifacts(
+      client=client,
+      build_id=build_id,
+      target=kernel_debug_target,
+      pattern=r'(Image|Image.lz4|System\.map)',
+      out_dir=kernel_debug_dir)
+
+  print('Compressing kernels')
+
+  def compress_kernel(kernel_path):
+    zipped_kernel_path = os.path.join(os.path.dirname(kernel_path), 'Image.gz')
+    with open(zipped_kernel_path, 'wb') as zipped_kernel:
+      cmd = [
+          'gzip',
+          '-nc',
+          kernel_path,
+      ]
+      print(' '.join(cmd))
+      subprocess.check_call(cmd, stdout=zipped_kernel)
+
+  compress_kernel(os.path.join(kernel_dir, 'Image'))
+  compress_kernel(os.path.join(kernel_debug_dir, 'Image'))
+
+  return kernel_dir, kernel_debug_dir
+
+
+def _replace_kernel(bootimg_path, kernel_path):
+  """Unpacks a boot.img, replaces the kernel, then repacks."""
+  with tempfile.TemporaryDirectory() as unpack_dir:
+    print('Unpacking bootimg %s' % bootimg_path)
+    cmd = [
+        'out/host/linux-x86/bin/unpack_bootimg',
+        '--boot_img',
+        bootimg_path,
+        '--out',
+        unpack_dir,
+        '--format',
+        'mkbootimg',
+    ]
+    print(' '.join(cmd))
+    mkbootimg_args = subprocess.check_output(cmd).decode('utf-8').split(' ')
+    print('Copying kernel %s' % kernel_path)
+    shutil.copy(kernel_path, os.path.join(unpack_dir, 'kernel'))
+    print('Repacking with mkbootimg')
+    cmd = [
+        'out/host/linux-x86/bin/mkbootimg',
+        '--output',
+        bootimg_path,
+    ] + mkbootimg_args
+    print(' '.join(cmd))
+    subprocess.check_call(cmd)
+
+
+def repack_bootimgs(bootimg_dir, kernel_dir, kernel_debug_dir):
+  """Repacks all boot images in a given dir using the provided kernels."""
+  for bootimg_path in os.listdir(bootimg_dir):
+    bootimg_path = os.path.join(bootimg_dir, bootimg_path)
+    if not bootimg_path.endswith('.img'):
+      continue
+
+    kernel_name = 'Image'
+    if '-gz' in bootimg_path:
+      kernel_name = 'Image.gz'
+    elif '-lz4' in bootimg_path:
+      kernel_name = 'Image.lz4'
+
+    kernel_path = os.path.join(kernel_dir, kernel_name)
+    if bootimg_path.endswith('-allsyms.img'):
+      kernel_path = os.path.join(kernel_debug_dir, kernel_name)
+
+    _replace_kernel(bootimg_path, kernel_path)
+
+
+def repack_img_zip(img_zip_path, kernel_dir, kernel_debug_dir, kernel_version):
+  """Repacks boot images within an img.zip archive."""
+  with tempfile.TemporaryDirectory() as unzip_dir:
+    pattern = 'boot-{}*'.format(kernel_version)
+    print('Unzipping %s to repack bootimgs' % img_zip_path)
+    cmd = [
+        'unzip',
+        '-d',
+        unzip_dir,
+        img_zip_path,
+        pattern,
+    ]
+    print(' '.join(cmd))
+    subprocess.check_call(cmd)
+    repack_bootimgs(unzip_dir, kernel_dir, kernel_debug_dir)
+    cmd = [
+        'zip',
+        img_zip_path,
+        pattern,
+    ]
+    print(' '.join(cmd))
+    subprocess.check_call(cmd, cwd=unzip_dir)
+
+
+def replace_target_files_zip_kernels(target_files_zip_path, kernel_out_dir,
+                                     kernel_version):
+  """Replaces the BOOT/kernel-* kernels within a target_files.zip archive."""
+  with tempfile.TemporaryDirectory() as unzip_dir:
+    pattern = 'BOOT/kernel-{}*'.format(kernel_version)
+    print(
+        'Unzipping %s to replace kernels in preparation for signing' %
+        target_files_zip_path,)
+    cmd = [
+        'unzip',
+        '-d',
+        unzip_dir,
+        target_files_zip_path,
+        pattern,
+    ]
+    print(' '.join(cmd))
+    subprocess.check_call(cmd)
+    for kernel in os.listdir(kernel_out_dir):
+      if kernel.startswith('kernel-{}'.format(kernel_version)):
+        print('Copying %s' % kernel)
+        shutil.copy(
+            os.path.join(kernel_out_dir, kernel),
+            os.path.join(unzip_dir, 'BOOT'))
+    cmd = [
+        'zip',
+        target_files_zip_path,
+        pattern,
+    ]
+    print(' '.join(cmd))
+    subprocess.check_call(cmd, cwd=unzip_dir)