-
Notifications
You must be signed in to change notification settings - Fork 0
/
make-zipapp.py
197 lines (181 loc) · 7.35 KB
/
make-zipapp.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#!/usr/bin/env python
# This is a standalone git server implementation in python
import argparse
from argparse import RawTextHelpFormatter
import hashlib
import logging
import os
import re
import shutil
import sys
import zipfile
if sys.version_info[0] == 2:
print('Not supported on Python 2.x')
sys.exit(1)
# Private variables
__author__ = '[email protected]'
__version__ = '1.0.0'
# Globals
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(project_root + '/lib')
def get_md5_digest(file_path, block_size=128):
md5 = hashlib.md5()
# f = open(file_path).read()
# return hashlib.md5(f).hexdigest()
with open(file_path, "rb") as f:
data = f.read(block_size)
while len(data) > 0:
md5.update(data)
data = f.read(block_size)
return md5.hexdigest()
def make_executable(path):
mode = os.stat(path).st_mode
mode |= (mode & 0o444) >> 2 # copy R bits to X
os.chmod(path, mode)
def make_zip(zipapp_file, dependencies, folder_exclusions=[], file_exclusions=[]):
"""
Creates a zip-application of itself
Result is a single executable file
"""
# Exclusions
folder_exclusion_list = folder_exclusions
file_exclusion_list = file_exclusions
working_directory = os.getcwd()
zipapp_name = os.path.basename(zipapp_file).split(".")[0]
zip_main_file_name = '__main__.py'
zip_main_file_path = '{d}/{f}'.format(d=working_directory, f=zip_main_file_name)
# Remove old zip application file
if os.path.exists(zipapp_name):
try:
print("Removing old zip application %s" % zipapp_name)
os.remove(zipapp_name)
except OSError:
print("Could not remove existing zip file {0}".format(zipapp_name))
pass
# Create __main__.py script from calling script
print("Creating {m} from copy of {a}".format(m=zip_main_file_path, a=zipapp_file))
shutil.copy(zipapp_file, zip_main_file_path)
# Create zip archive
print("Creating zip archive %s" % zipapp_name)
zipf = zipfile.ZipFile(zipapp_name, 'w', zipfile.ZIP_DEFLATED)
zipf.write(zip_main_file_name)
for dependency_obj in dependencies:
if os.path.isfile(dependency_obj):
zipf.write(dependency_obj)
elif os.path.isdir(dependency_obj):
source_dir = os.path.join(working_directory, dependency_obj)
for folder,subfolder,file in os.walk(source_dir,topdown=True):
path_element=os.path.relpath(folder,source_dir)
nextiter=False
if any([path_element.endswith(X) for X in folder_exclusion_list]):
print('Exluding folder %s' % path_element)
nextiter=True
if any([path_element.endswith(x) for x in file_exclusion_list]):
print('Exluding file %s' % path_element)
nextiter=True
if nextiter==True:
continue
zip_arcname_mod_len = len(dependency_obj) + 1
# print(subfolder+file)
for each in subfolder+file:
source = os.path.join(folder,each)
# Remove the absolute path to compose arcname
# Also handles the remaining leading path separator with lstrip
arcname = source[len(working_directory) + zip_arcname_mod_len:].lstrip(os.sep)
exclude_folder = os.path.isdir(source) and any([source.endswith(X) for X in folder_exclusion_list])
exclude_file = os.path.isfile(source) and any([source.endswith(x) for x in file_exclusion_list])
if exclude_folder or exclude_file:
print('Exluding %s' % source)
else:
# Write the file under a different name in the archive
zipf.write(source, arcname=arcname)
else:
print("Couldn't determine whether %s is a file or directory. Skipping")
continue
zipf.close()
# Write header for zip-application
print("Writing header for zip application")
try:
zip_file_content = open(zipapp_name).read()
zip_file_exe_header = '#!/usr/bin/env python\n'
except UnicodeDecodeError:
zip_file_content = open(zipapp_name, 'rb').read()
zip_file_exe_header = bytes( '#!/usr/bin/env python\n', 'Utf-8')
zip_file_content_with_headers = zip_file_exe_header + zip_file_content
try:
with open(zipapp_name, 'w') as z:
z.write(zip_file_content_with_headers)
except TypeError:
with open(zipapp_name, 'wb') as z:
z.write(zip_file_content_with_headers)
# Cleanup
try:
print("Cleaning up")
os.remove(zip_main_file_path)
except OSError:
print("Could not remove %s" % self.zip_main_file_pat)
pass
# Make zip-application executable
if sys.platform != "win32":
make_executable(zipapp_name)
md5_hex_digest = get_md5_digest(zipapp_name)
print("md5 hash for this executable is:%s" % md5_hex_digest)
# Declare the main() function
def main(args, loglevel):
logging.basicConfig(format="%(asctime)s %(levelname)s: %(message)s", level=loglevel)
logging.debug("You specified the following file: {f}".format(f=args.file))
print('Making a zipapp using {f}'.format(f=args.file))
make_zip(args.file, args.dependencies, args.folder_exclusions, args.file_exclusions)
return
# Call the main() function to begin
# the program.
if __name__ == '__main__':
# Define commandline parameters
parser = argparse.ArgumentParser(
description="Creates a zip application from the specified python script",
formatter_class=RawTextHelpFormatter,
epilog="""
Usage Examples:
Suppose you want to bundle in your python script dependencies
Assume you store these modules under a folder named 'lib'
You can easily create your zip app as follows:
mkdir lib
pip install -t lib -r requirements.txt
make-zipapp -f myscript.py
By default, the program will bundle in anything located in the local 'lib' folder
You can also specify multiple dependencies, folders or files, as follows:
make-zipapp -f myscript.py -d lib conf config.ini
""",
fromfile_prefix_chars='@')
parser.add_argument(
"-f",
"--file",
help="Specify the target python script",
metavar="ARG", required=True)
parser.add_argument(
"-x",
"--file-exclusions",
help="Specify files to exclude",
metavar="ARG", nargs='+', default=[], required=False)
parser.add_argument(
"-X",
"--folder-exclusions",
help="Specify folders to exclude",
metavar="ARG", nargs='+', default=[], required=False)
parser.add_argument(
"-d",
"--dependencies",
help="Specify a list of dependencies to bundle in with the specified python script",
required=False, nargs='*', default=['lib'])
parser.add_argument(
"-v",
"--verbose",
help="increase output verbosity",
action="store_true")
args = parser.parse_args()
# Setup logging
if args.verbose:
loglevel = logging.DEBUG
else:
loglevel = logging.INFO
main(args, loglevel)