|  | #!/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/bash | 
|  | # | 
|  | {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 = 20 | 
|  | def _generate_extract_command(start, size, 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 | 
|  | size: size in bytes 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) | 
|  |  | 
|  | size_str = ('%d' % size).rjust(_MAX_OFFSET_WIDTH) | 
|  | if len(size_str) != _MAX_OFFSET_WIDTH: | 
|  | raise Exception('Size too large (%d)' % size) | 
|  |  | 
|  | return "tail -c %s $0 | head -c %s > %s\n" % (start_str, size_str, extract_name) | 
|  |  | 
|  |  | 
|  | def main(argv): | 
|  | if len(argv) != 5: | 
|  | print 'generate-self-extracting-archive.py expects exactly 4 arguments' | 
|  | sys.exit(1) | 
|  |  | 
|  | 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() | 
|  |  | 
|  | if not license: | 
|  | print 'License file was empty' | 
|  | sys.exit(1) | 
|  |  | 
|  | if 'SOFTWARE LICENSE AGREEMENT' not in license: | 
|  | print 'License does not look like a license' | 
|  | sys.exit(1) | 
|  |  | 
|  | 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) | 
|  |  | 
|  | umask = os.umask(0) | 
|  | os.umask(umask) | 
|  | os.chmod(output_filename, 0o777 & ~umask) | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | main(sys.argv) |