blob: f0b756811629668f5e59f85296c3758377d55833 [file] [log] [blame]
Mitchell Wills1c790ca2019-07-29 10:29:32 -07001#!/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/sh
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 = 8
95def _generate_extract_command(start, end, 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 end: offset in bytes of the end 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 end_str = ('%d' % end).rjust(_MAX_OFFSET_WIDTH)
115 if len(end_str) != _MAX_OFFSET_WIDTH:
116 raise Exception('End offset too large (%d)' % end)
117
118 return "tail -c %s $0 | head -c %s > %s\n" % (start_str, end_str, extract_name)
119
120
121def main(argv):
122 output_filename = argv[1]
123 input_archive_filename = argv[2]
124 comment = argv[3]
125 license_filename = argv[4]
126
127 input_archive_size = os.stat(input_archive_filename).st_size
128
129 with open(license_filename, 'r') as license_file:
130 license = license_file.read()
131
132 comment_line = '# %s\n' % comment
133 extract_name = os.path.basename(input_archive_filename)
134
135 # Compute the size of the header before writing the file out. This is required
136 # so that the extract command, which uses the contents offset, can be created
137 # and included inside the header.
138 header_for_size = _HEADER_TEMPLATE.format(
139 comment_line=comment_line,
140 license=license,
141 extract_command=_generate_extract_command(0, 0, extract_name),
142 )
143 header_size = len(header_for_size.encode('utf-8'))
144
145 # write the final output
146 with open(output_filename, 'wb') as output:
147 output.write(_HEADER_TEMPLATE.format(
148 comment_line=comment_line,
149 license=license,
150 extract_command=_generate_extract_command(header_size, input_archive_size, extract_name),
151 ).encode('utf-8'))
152
153 with open(input_archive_filename, 'rb') as input_file:
154 _pipe_bytes(input_file, output)
155
156 with tempfile.TemporaryFile() as trailing_zip:
157 with zipfile.ZipFile(trailing_zip, 'w') as myzip:
158 myzip.writestr('license.txt', license, compress_type=zipfile.ZIP_STORED)
159
160 # append the trailing zip to the end of the file
161 trailing_zip.seek(0)
162 _pipe_bytes(trailing_zip, output)
163
164if __name__ == "__main__":
165 main(sys.argv)