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