Path: blob/main/python/cpython/src/cowasm_bundler.py
1067 views
"""1Creates a "CoWasm bundle", which is a PyZipFile (so has pyc files),2plus we also include .so files and possibly some other extra files3on a case-by-case basis. There is no version information, since that's4going to be in the npm package.json file, and obviously no architecture5since there is only one architecture.67The idea of making Python modules only available in maximally compiled8zip archive form is very inspired by Javascript web bundlers. It is9is antithetical to how wheels, and Python packaging generally works!10But that is because the constraints are very, very different.11"""1213import io, os, sys, tarfile, time, zipfile141516class CoWasmBundle(zipfile.PyZipFile):1718def __init__(self, *args, **kwds):19zipfile.PyZipFile.__init__(self, *args, **kwds)20self.debug = 121self.compresslevel = 92223def write_so(self, pathname, basename="", filterfunc=None):24pathname = os.fspath(pathname)25if filterfunc and not filterfunc(pathname):26if self.debug:27label = 'path' if os.path.isdir(pathname) else 'file'28print('%s %r skipped by filterfunc' % (label, pathname))29return30dir, name = os.path.split(pathname)31if os.path.isdir(pathname):32initname = os.path.join(pathname, "__init__.py")33if os.path.isfile(initname):34# This is a package directory, add it35if basename:36basename = "%s/%s" % (basename, name)37else:38basename = name39if self.debug:40print("Adding package in", pathname, "as", basename)41dirlist = sorted(os.listdir(pathname))42dirlist.remove("__init__.py")43# Add all *.so files and package subdirectories44for filename in dirlist:45path = os.path.join(pathname, filename)46root, ext = os.path.splitext(filename)47if os.path.isdir(path):48if os.path.isfile(os.path.join(path, "__init__.py")):49# This is a package directory, recurse:50self.write_so(51path, basename,52filterfunc=filterfunc) # Recursive call53elif ext == ".so":54if filterfunc and not filterfunc(path):55if self.debug:56print('file %r skipped by filterfunc' % path)57continue58arcname = self.get_archive_name(path, basename)59if self.debug:60print("Adding", arcname)61self.write(path, arcname)62else:63pass64elif os.path.splitext(pathname)[1] == '.so':65arcname = self.get_archive_name(pathname, basename)66if self.debug:67print("Adding file", arcname)68self.write(pathname, arcname)6970def write_all(self, pathname, basename=""):71pathname = os.fspath(pathname)72if os.path.isdir(pathname):73# This is a directory; add it74_, name = os.path.split(pathname)75if basename:76basename = "%s/%s" % (basename, name)77else:78basename = pathname79if self.debug:80print("Adding package in", pathname, "as", basename)81dirlist = sorted(os.listdir(pathname))82# Add everything83for filename in dirlist:84path = os.path.join(pathname, filename)85root, ext = os.path.splitext(filename)86if os.path.isdir(path):87# This is a directory, recurse:88self.write_all(path, basename) # Recursive call89else:90arcname = self.get_archive_name(path, basename)91if self.debug:92print("Adding", arcname)93self.write(path, arcname)94else:95arcname = os.path.join(basename, pathname)96if self.debug:97print("Adding file", arcname)98self.write(pathname, arcname)99100def get_archive_name(self, path, basename):101arcname = os.path.split(path)[1]102if basename:103arcname = os.path.join(basename, arcname)104return arcname105106def create_bundle(name, extra_files):107108def notests(s):109fn = os.path.basename(s)110return (not ('/tests/' in s or fn.startswith('test_')))111112# Python stdlib uses the same options as below, and it's easiest113# to work with these params using possibly older versions of zip.114115# We use an in-memory buffer, since writing to disk then reading116# back triggers some WASI sync issues right now (TODO), and of course117# we don't need a file anyways since we just convert to a tarball below.118# To see this bug though, try to bundle the sympy package on macOS.119# Second we don't compress since we're just going to extract it again120# below, so it would be a waste of time.121zip = CoWasmBundle(io.BytesIO(),122'w',123compression=zipfile.ZIP_STORED)124zip.writepy(name, filterfunc=notests)125zip.write_so(name, filterfunc=notests)126for extra in extra_files:127print(f"Including extra '{extra}'")128zip.write_all(extra)129130# Create a tar.xz by *converting* the zip. We do this partly since131# there's a lot of work into making a zip with the correct contents in it,132# both above and in the cpython zipfile module, and we reuse that effort.133#134# These .tar.xz are much smaller, e.g., often 50% the size, and we135# support importing them. The recipe below to convert a zip into a tar136# is inspired by137# https://unix.stackexchange.com/questions/146264/is-there-a-way-to-convert-a-zip-to-a-tar-without-extracting-it-to-the-filesystem138# Note that npm also uses tarballs rather than zip for its packages.139140tar = tarfile.open(f'{name}.tar.xz', "w:xz")141now = time.time()142for filename in zip.namelist():143if filename.endswith('/'): continue144print(f"Adding '{filename}'")145data = zip.read(filename)146tarinfo = tarfile.TarInfo()147tarinfo.name = filename148tarinfo.size = len(data)149tarinfo.mtime = now150tar.addfile(tarinfo, io.BytesIO(data))151152153if __name__ == '__main__':154create_bundle(sys.argv[1], sys.argv[2:])155156157