blob: c9f56cb2a686da941ff6987053cf0f4bd8f8e39a [file] [log] [blame]
Cole Faust547ca202021-12-29 13:54:10 -08001#!/usr/bin/env python3
Mitchell Wills1c790ca2019-07-29 10:29:32 -07002#
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
Mitchell Wills032de672019-12-06 16:47:37 -080041_HEADER_TEMPLATE = """#!/bin/bash
Mitchell Wills1c790ca2019-07-29 10:29:32 -070042#
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
Mitchell Wills855bf6a2019-11-08 15:08:59 -080094_MAX_OFFSET_WIDTH = 20
Mitchell Willsa428b092019-12-06 17:22:43 -080095def _generate_extract_command(start, size, extract_name):
Mitchell Wills1c790ca2019-07-29 10:29:32 -070096 """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
Mitchell Willsa428b092019-12-06 17:22:43 -0800104 size: size in bytes of the wrapped file
Mitchell Wills1c790ca2019-07-29 10:29:32 -0700105 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
Mitchell Willsa428b092019-12-06 17:22:43 -0800114 size_str = ('%d' % size).rjust(_MAX_OFFSET_WIDTH)
115 if len(size_str) != _MAX_OFFSET_WIDTH:
116 raise Exception('Size too large (%d)' % size)
Mitchell Wills1c790ca2019-07-29 10:29:32 -0700117
Mitchell Willsa428b092019-12-06 17:22:43 -0800118 return "tail -c %s $0 | head -c %s > %s\n" % (start_str, size_str, extract_name)
Mitchell Wills1c790ca2019-07-29 10:29:32 -0700119
120
121def main(argv):
Mitchell Wills855bf6a2019-11-08 15:08:59 -0800122 if len(argv) != 5:
Cole Faust547ca202021-12-29 13:54:10 -0800123 print('generate-self-extracting-archive.py expects exactly 4 arguments')
Mitchell Wills855bf6a2019-11-08 15:08:59 -0800124 sys.exit(1)
125
Mitchell Wills1c790ca2019-07-29 10:29:32 -0700126 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
Mitchell Wills855bf6a2019-11-08 15:08:59 -0800136 if not license:
Cole Faust547ca202021-12-29 13:54:10 -0800137 print('License file was empty')
Mitchell Wills855bf6a2019-11-08 15:08:59 -0800138 sys.exit(1)
139
140 if 'SOFTWARE LICENSE AGREEMENT' not in license:
Cole Faust547ca202021-12-29 13:54:10 -0800141 print('License does not look like a license')
Mitchell Wills855bf6a2019-11-08 15:08:59 -0800142 sys.exit(1)
143
Mitchell Wills1c790ca2019-07-29 10:29:32 -0700144 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
Mitchell Wills855bf6a2019-11-08 15:08:59 -0800176 umask = os.umask(0)
177 os.umask(umask)
178 os.chmod(output_filename, 0o777 & ~umask)
179
Mitchell Wills1c790ca2019-07-29 10:29:32 -0700180if __name__ == "__main__":
181 main(sys.argv)