diff options
author | Rob Austein <sra@hactrn.net> | 2019-03-10 13:46:08 -0400 |
---|---|---|
committer | Rob Austein <sra@hactrn.net> | 2019-03-10 13:46:08 -0400 |
commit | fa9a04b06a7c6f62979c506b9999abf6b2eb8208 (patch) | |
tree | 887062e0d0c0ff844eb5fd05d603aad897c77d7e |
First public version.
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Makefile | 21 | ||||
-rw-r--r-- | README.md | 27 | ||||
-rw-r--r-- | demo1/__init__.py | 4 | ||||
-rw-r--r-- | demo1/bar.py | 7 | ||||
-rw-r--r-- | demo1/main.py | 8 | ||||
-rw-r--r-- | demo1/stuff.json | 6 | ||||
-rw-r--r-- | demo2/main.py | 1 | ||||
-rwxr-xr-x | pyzipper | 44 |
9 files changed, 120 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4170a87 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +demo[12].zip diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dea4629 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +SRC := $(wildcard demo[0-9]) +BIN := $(addsuffix .zip,${SRC}) + +all: ${BIN} + +%.zip: pyzipper Makefile + ${PYTHON} ./pyzipper $(if $(PYTHON),-e $(PYTHON)) -o $@ $* + ./$@ + +clean: + git clean -dfx + +.PHONY: all clean + +GIT_LS_TREE := $(shell git ls-tree --name-only -r HEAD) + +define DEPENDENCIES +$(1).zip : $$(filter $(1)/%,$${GIT_LS_TREE}) +endef + +$(foreach S,${SRC},$(eval $(call DEPENDENCIES,$(S)))) diff --git a/README.md b/README.md new file mode 100644 index 0000000..d248b1d --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +pyzipper +======== + +A small tool to package up a Python program and associated data files +as a single executable zip file. + +All the facilities for this are already built into Python, this script +just ties it all together with a bow. + +`demo1` and `demo2` are two trivial sample packages. + +Tested with both Python 2 and Python 3, should work with either. + +How it works +------------ + +This relies on two somewhat obscure Python features: + +* The `zipfile.PyZipFile` class's ability to construct an executable + ZIP file and populate it with Python modules, packages, and data; + and + +* Python's ability to treat a zip file as equivalent to a directory + tree on `import`. + +`demo1` takes advantage of a third obscure feature: the `pkgutil` +library's ability to load data files via the `import` system. diff --git a/demo1/__init__.py b/demo1/__init__.py new file mode 100644 index 0000000..7d1822d --- /dev/null +++ b/demo1/__init__.py @@ -0,0 +1,4 @@ +import pkgutil + +def get_resource(name): + return pkgutil.get_data(__name__, name) diff --git a/demo1/bar.py b/demo1/bar.py new file mode 100644 index 0000000..3a0992e --- /dev/null +++ b/demo1/bar.py @@ -0,0 +1,7 @@ +from . import get_resource +import json + +def run(): + j = json.loads(get_resource("stuff.json")) + print(repr(j)) + print(j["foo"]) diff --git a/demo1/main.py b/demo1/main.py new file mode 100644 index 0000000..1856d0b --- /dev/null +++ b/demo1/main.py @@ -0,0 +1,8 @@ +from . import bar + +def main(): + print("Hello, pyzipper") + bar.run() + +if __name__ == "__main__": + main() diff --git a/demo1/stuff.json b/demo1/stuff.json new file mode 100644 index 0000000..3b8448b --- /dev/null +++ b/demo1/stuff.json @@ -0,0 +1,6 @@ +{ + "foo": "FOO", + "bar": "BAR", + "wibble": 3.14159, + "wobble": [0,1,2,3,4,5,6] +} diff --git a/demo2/main.py b/demo2/main.py new file mode 100644 index 0000000..7dc46d6 --- /dev/null +++ b/demo2/main.py @@ -0,0 +1 @@ +print("Hello, world") diff --git a/pyzipper b/pyzipper new file mode 100755 index 0000000..9e50835 --- /dev/null +++ b/pyzipper @@ -0,0 +1,44 @@ +#!/usr/bin/env python + +""" +Generate an executable Python zip package. +""" + +import os, zipfile, argparse + +ap = argparse.ArgumentParser(description = __doc__, + formatter_class = argparse.ArgumentDefaultsHelpFormatter) +ap.add_argument("-o", "--output", required = True, type = argparse.FileType("wb+")) +ap.add_argument("-m", "--module", default = "main", help = "module within package to run") +ap.add_argument("-e", "--executable", default = "python", help = "python executable to run") +ap.add_argument("source") +args = ap.parse_args() + +# PyZipFile.writepy()'s behavior changes when it sees __init__.py +if os.path.exists(os.path.join(args.source, "__init__.py")): + module = "{0.source}.{0.module}".format(args) +else: + module = args.module + +# Write executable shim +args.output.write('''\ +#/bin/sh - +PYTHONPATH="$0"${{PYTHONPATH+":$PYTHONPATH"}} exec {e} -m {m} ${{1+"$@"}} +'''.format(m = module, e = args.executable).encode("ascii")) + +# Make output executable +os.fchmod(args.output.fileno(), 0o755) + +# Create the zip file and populate it with the code +z = zipfile.PyZipFile(args.output, "a", zipfile.ZIP_DEFLATED) +z.writepy(args.source) + +# Add data files, if any +if os.path.isdir(args.source): + for root, dirs, files in os.walk(args.source): + for fn in files: + if not any(fn.endswith(fn2) for fn2 in (".py", ".pyc", ".pyo")): + z.write(os.path.join(root, fn)) + +# Test the zip file we just generated out of paranoia +z.testzip() |