Add build script to create a self extracting archive with a click through license

Bug: 130257221
Test: generated an archive using the script and ran the result
Change-Id: If1147cd41fa939b0d9958196e627042b5731dc14
diff --git a/tools/generate-self-extracting-archive.py b/tools/generate-self-extracting-archive.py
new file mode 100755
index 0000000..f0b7568
--- /dev/null
+++ b/tools/generate-self-extracting-archive.py
@@ -0,0 +1,165 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2019 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.
+
+"""
+Generates a self extracting archive with a license click through.
+
+Usage:
+  generate-self-extracting-archive.py $OUTPUT_FILE $INPUT_ARCHIVE $COMMENT $LICENSE_FILE
+
+  The comment will be included at the beginning of the output archive file.
+
+Output:
+  The output of the script is a single executable file that when run will
+  display the provided license and if the user accepts extract the wrapped
+  archive.
+
+  The layout of the output file is roughly:
+   * Executable shell script that extracts the archive
+   * Actual archive contents
+   * Zip file containing the license
+"""
+
+import tempfile
+import sys
+import os
+import zipfile
+
+_HEADER_TEMPLATE = """#!/bin/sh
+#
+{comment_line}
+#
+# Usage is subject to the enclosed license agreement
+
+echo
+echo The license for this software will now be displayed.
+echo You must agree to this license before using this software.
+echo
+echo -n Press Enter to view the license
+read dummy
+echo
+more << EndOfLicense
+{license}
+EndOfLicense
+
+if test $? != 0
+then
+  echo "ERROR: Couldn't display license file" 1>&2
+  exit 1
+fi
+echo
+echo -n 'Type "I ACCEPT" if you agree to the terms of the license: '
+read typed
+if test "$typed" != "I ACCEPT"
+then
+  echo
+  echo "You didn't accept the license. Extraction aborted."
+  exit 2
+fi
+echo
+{extract_command}
+if test $? != 0
+then
+  echo
+  echo "ERROR: Couldn't extract files." 1>&2
+  exit 3
+else
+  echo
+  echo "Files extracted successfully."
+fi
+exit 0
+"""
+
+_PIPE_CHUNK_SIZE = 1048576
+def _pipe_bytes(src, dst):
+  while True:
+    b = src.read(_PIPE_CHUNK_SIZE)
+    if not b:
+      break
+    dst.write(b)
+
+_MAX_OFFSET_WIDTH = 8
+def _generate_extract_command(start, end, extract_name):
+  """Generate the extract command.
+
+  The length of this string must be constant no matter what the start and end
+  offsets are so that its length can be computed before the actual command is
+  generated.
+
+  Args:
+    start: offset in bytes of the start of the wrapped file
+    end: offset in bytes of the end of the wrapped file
+    extract_name: of the file to create when extracted
+
+  """
+  # start gets an extra character for the '+'
+  # for tail +1 is the start of the file, not +0
+  start_str = ('+%d' % (start + 1)).rjust(_MAX_OFFSET_WIDTH + 1)
+  if len(start_str) != _MAX_OFFSET_WIDTH + 1:
+    raise Exception('Start offset too large (%d)' % start)
+
+  end_str = ('%d' % end).rjust(_MAX_OFFSET_WIDTH)
+  if len(end_str) != _MAX_OFFSET_WIDTH:
+    raise Exception('End offset too large (%d)' % end)
+
+  return "tail -c %s $0 | head -c %s > %s\n" % (start_str, end_str, extract_name)
+
+
+def main(argv):
+  output_filename = argv[1]
+  input_archive_filename = argv[2]
+  comment = argv[3]
+  license_filename = argv[4]
+
+  input_archive_size = os.stat(input_archive_filename).st_size
+
+  with open(license_filename, 'r') as license_file:
+    license = license_file.read()
+
+  comment_line = '# %s\n' % comment
+  extract_name = os.path.basename(input_archive_filename)
+
+  # Compute the size of the header before writing the file out. This is required
+  # so that the extract command, which uses the contents offset, can be created
+  # and included inside the header.
+  header_for_size = _HEADER_TEMPLATE.format(
+      comment_line=comment_line,
+      license=license,
+      extract_command=_generate_extract_command(0, 0, extract_name),
+  )
+  header_size = len(header_for_size.encode('utf-8'))
+
+  # write the final output
+  with open(output_filename, 'wb') as output:
+    output.write(_HEADER_TEMPLATE.format(
+        comment_line=comment_line,
+        license=license,
+        extract_command=_generate_extract_command(header_size, input_archive_size, extract_name),
+    ).encode('utf-8'))
+
+    with open(input_archive_filename, 'rb') as input_file:
+      _pipe_bytes(input_file, output)
+
+    with tempfile.TemporaryFile() as trailing_zip:
+      with zipfile.ZipFile(trailing_zip, 'w') as myzip:
+        myzip.writestr('license.txt', license, compress_type=zipfile.ZIP_STORED)
+
+      # append the trailing zip to the end of the file
+      trailing_zip.seek(0)
+      _pipe_bytes(trailing_zip, output)
+
+if __name__ == "__main__":
+  main(sys.argv)