blob: c9f56cb2a686da941ff6987053cf0f4bd8f8e39a [file] [log] [blame] [edit]
#!/usr/bin/env python3
#
# 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)