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)