Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/wapython
Path: blob/main/python/cpython/src/cowasm_bundler.py
1067 views
1
"""
2
Creates a "CoWasm bundle", which is a PyZipFile (so has pyc files),
3
plus we also include .so files and possibly some other extra files
4
on a case-by-case basis. There is no version information, since that's
5
going to be in the npm package.json file, and obviously no architecture
6
since there is only one architecture.
7
8
The idea of making Python modules only available in maximally compiled
9
zip archive form is very inspired by Javascript web bundlers. It is
10
is antithetical to how wheels, and Python packaging generally works!
11
But that is because the constraints are very, very different.
12
"""
13
14
import io, os, sys, tarfile, time, zipfile
15
16
17
class CoWasmBundle(zipfile.PyZipFile):
18
19
def __init__(self, *args, **kwds):
20
zipfile.PyZipFile.__init__(self, *args, **kwds)
21
self.debug = 1
22
self.compresslevel = 9
23
24
def write_so(self, pathname, basename="", filterfunc=None):
25
pathname = os.fspath(pathname)
26
if filterfunc and not filterfunc(pathname):
27
if self.debug:
28
label = 'path' if os.path.isdir(pathname) else 'file'
29
print('%s %r skipped by filterfunc' % (label, pathname))
30
return
31
dir, name = os.path.split(pathname)
32
if os.path.isdir(pathname):
33
initname = os.path.join(pathname, "__init__.py")
34
if os.path.isfile(initname):
35
# This is a package directory, add it
36
if basename:
37
basename = "%s/%s" % (basename, name)
38
else:
39
basename = name
40
if self.debug:
41
print("Adding package in", pathname, "as", basename)
42
dirlist = sorted(os.listdir(pathname))
43
dirlist.remove("__init__.py")
44
# Add all *.so files and package subdirectories
45
for filename in dirlist:
46
path = os.path.join(pathname, filename)
47
root, ext = os.path.splitext(filename)
48
if os.path.isdir(path):
49
if os.path.isfile(os.path.join(path, "__init__.py")):
50
# This is a package directory, recurse:
51
self.write_so(
52
path, basename,
53
filterfunc=filterfunc) # Recursive call
54
elif ext == ".so":
55
if filterfunc and not filterfunc(path):
56
if self.debug:
57
print('file %r skipped by filterfunc' % path)
58
continue
59
arcname = self.get_archive_name(path, basename)
60
if self.debug:
61
print("Adding", arcname)
62
self.write(path, arcname)
63
else:
64
pass
65
elif os.path.splitext(pathname)[1] == '.so':
66
arcname = self.get_archive_name(pathname, basename)
67
if self.debug:
68
print("Adding file", arcname)
69
self.write(pathname, arcname)
70
71
def write_all(self, pathname, basename=""):
72
pathname = os.fspath(pathname)
73
if os.path.isdir(pathname):
74
# This is a directory; add it
75
_, name = os.path.split(pathname)
76
if basename:
77
basename = "%s/%s" % (basename, name)
78
else:
79
basename = pathname
80
if self.debug:
81
print("Adding package in", pathname, "as", basename)
82
dirlist = sorted(os.listdir(pathname))
83
# Add everything
84
for filename in dirlist:
85
path = os.path.join(pathname, filename)
86
root, ext = os.path.splitext(filename)
87
if os.path.isdir(path):
88
# This is a directory, recurse:
89
self.write_all(path, basename) # Recursive call
90
else:
91
arcname = self.get_archive_name(path, basename)
92
if self.debug:
93
print("Adding", arcname)
94
self.write(path, arcname)
95
else:
96
arcname = os.path.join(basename, pathname)
97
if self.debug:
98
print("Adding file", arcname)
99
self.write(pathname, arcname)
100
101
def get_archive_name(self, path, basename):
102
arcname = os.path.split(path)[1]
103
if basename:
104
arcname = os.path.join(basename, arcname)
105
return arcname
106
107
def create_bundle(name, extra_files):
108
109
def notests(s):
110
fn = os.path.basename(s)
111
return (not ('/tests/' in s or fn.startswith('test_')))
112
113
# Python stdlib uses the same options as below, and it's easiest
114
# to work with these params using possibly older versions of zip.
115
116
# We use an in-memory buffer, since writing to disk then reading
117
# back triggers some WASI sync issues right now (TODO), and of course
118
# we don't need a file anyways since we just convert to a tarball below.
119
# To see this bug though, try to bundle the sympy package on macOS.
120
# Second we don't compress since we're just going to extract it again
121
# below, so it would be a waste of time.
122
zip = CoWasmBundle(io.BytesIO(),
123
'w',
124
compression=zipfile.ZIP_STORED)
125
zip.writepy(name, filterfunc=notests)
126
zip.write_so(name, filterfunc=notests)
127
for extra in extra_files:
128
print(f"Including extra '{extra}'")
129
zip.write_all(extra)
130
131
# Create a tar.xz by *converting* the zip. We do this partly since
132
# there's a lot of work into making a zip with the correct contents in it,
133
# both above and in the cpython zipfile module, and we reuse that effort.
134
#
135
# These .tar.xz are much smaller, e.g., often 50% the size, and we
136
# support importing them. The recipe below to convert a zip into a tar
137
# is inspired by
138
# https://unix.stackexchange.com/questions/146264/is-there-a-way-to-convert-a-zip-to-a-tar-without-extracting-it-to-the-filesystem
139
# Note that npm also uses tarballs rather than zip for its packages.
140
141
tar = tarfile.open(f'{name}.tar.xz', "w:xz")
142
now = time.time()
143
for filename in zip.namelist():
144
if filename.endswith('/'): continue
145
print(f"Adding '{filename}'")
146
data = zip.read(filename)
147
tarinfo = tarfile.TarInfo()
148
tarinfo.name = filename
149
tarinfo.size = len(data)
150
tarinfo.mtime = now
151
tar.addfile(tarinfo, io.BytesIO(data))
152
153
154
if __name__ == '__main__':
155
create_bundle(sys.argv[1], sys.argv[2:])
156
157