1#!/usr/bin/env python 2# 3# Copyright (C) 2019 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17""" 18Generates a self extracting archive with a license click through. 19 20Usage: 21 generate-self-extracting-archive.py $OUTPUT_FILE $INPUT_ARCHIVE $COMMENT $LICENSE_FILE 22 23 The comment will be included at the beginning of the output archive file. 24 25Output: 26 The output of the script is a single executable file that when run will 27 display the provided license and if the user accepts extract the wrapped 28 archive. 29 30 The layout of the output file is roughly: 31 * Executable shell script that extracts the archive 32 * Actual archive contents 33 * Zip file containing the license 34""" 35 36import tempfile 37import sys 38import os 39import zipfile 40 41_HEADER_TEMPLATE = """#!/bin/bash 42# 43{comment_line} 44# 45# Usage is subject to the enclosed license agreement 46 47echo 48echo The license for this software will now be displayed. 49echo You must agree to this license before using this software. 50echo 51echo -n Press Enter to view the license 52read dummy 53echo 54more << EndOfLicense 55{license} 56EndOfLicense 57 58if test $? != 0 59then 60 echo "ERROR: Couldn't display license file" 1>&2 61 exit 1 62fi 63echo 64echo -n 'Type "I ACCEPT" if you agree to the terms of the license: ' 65read typed 66if test "$typed" != "I ACCEPT" 67then 68 echo 69 echo "You didn't accept the license. Extraction aborted." 70 exit 2 71fi 72echo 73{extract_command} 74if test $? != 0 75then 76 echo 77 echo "ERROR: Couldn't extract files." 1>&2 78 exit 3 79else 80 echo 81 echo "Files extracted successfully." 82fi 83exit 0 84""" 85 86_PIPE_CHUNK_SIZE = 1048576 87def _pipe_bytes(src, dst): 88 while True: 89 b = src.read(_PIPE_CHUNK_SIZE) 90 if not b: 91 break 92 dst.write(b) 93 94_MAX_OFFSET_WIDTH = 20 95def _generate_extract_command(start, size, extract_name): 96 """Generate the extract command. 97 98 The length of this string must be constant no matter what the start and end 99 offsets are so that its length can be computed before the actual command is 100 generated. 101 102 Args: 103 start: offset in bytes of the start of the wrapped file 104 size: size in bytes of the wrapped file 105 extract_name: of the file to create when extracted 106 107 """ 108 # start gets an extra character for the '+' 109 # for tail +1 is the start of the file, not +0 110 start_str = ('+%d' % (start + 1)).rjust(_MAX_OFFSET_WIDTH + 1) 111 if len(start_str) != _MAX_OFFSET_WIDTH + 1: 112 raise Exception('Start offset too large (%d)' % start) 113 114 size_str = ('%d' % size).rjust(_MAX_OFFSET_WIDTH) 115 if len(size_str) != _MAX_OFFSET_WIDTH: 116 raise Exception('Size too large (%d)' % size) 117 118 return "tail -c %s $0 | head -c %s > %s\n" % (start_str, size_str, extract_name) 119 120 121def main(argv): 122 if len(argv) != 5: 123 print 'generate-self-extracting-archive.py expects exactly 4 arguments' 124 sys.exit(1) 125 126 output_filename = argv[1] 127 input_archive_filename = argv[2] 128 comment = argv[3] 129 license_filename = argv[4] 130 131 input_archive_size = os.stat(input_archive_filename).st_size 132 133 with open(license_filename, 'r') as license_file: 134 license = license_file.read() 135 136 if not license: 137 print 'License file was empty' 138 sys.exit(1) 139 140 if 'SOFTWARE LICENSE AGREEMENT' not in license: 141 print 'License does not look like a license' 142 sys.exit(1) 143 144 comment_line = '# %s\n' % comment 145 extract_name = os.path.basename(input_archive_filename) 146 147 # Compute the size of the header before writing the file out. This is required 148 # so that the extract command, which uses the contents offset, can be created 149 # and included inside the header. 150 header_for_size = _HEADER_TEMPLATE.format( 151 comment_line=comment_line, 152 license=license, 153 extract_command=_generate_extract_command(0, 0, extract_name), 154 ) 155 header_size = len(header_for_size.encode('utf-8')) 156 157 # write the final output 158 with open(output_filename, 'wb') as output: 159 output.write(_HEADER_TEMPLATE.format( 160 comment_line=comment_line, 161 license=license, 162 extract_command=_generate_extract_command(header_size, input_archive_size, extract_name), 163 ).encode('utf-8')) 164 165 with open(input_archive_filename, 'rb') as input_file: 166 _pipe_bytes(input_file, output) 167 168 with tempfile.TemporaryFile() as trailing_zip: 169 with zipfile.ZipFile(trailing_zip, 'w') as myzip: 170 myzip.writestr('license.txt', license, compress_type=zipfile.ZIP_STORED) 171 172 # append the trailing zip to the end of the file 173 trailing_zip.seek(0) 174 _pipe_bytes(trailing_zip, output) 175 176 umask = os.umask(0) 177 os.umask(umask) 178 os.chmod(output_filename, 0o777 & ~umask) 179 180if __name__ == "__main__": 181 main(sys.argv) 182