aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRob Austein <sra@hactrn.net>2019-03-10 13:46:08 -0400
committerRob Austein <sra@hactrn.net>2019-03-10 13:46:08 -0400
commitfa9a04b06a7c6f62979c506b9999abf6b2eb8208 (patch)
tree887062e0d0c0ff844eb5fd05d603aad897c77d7e
First public version.
-rw-r--r--.gitignore2
-rw-r--r--Makefile21
-rw-r--r--README.md27
-rw-r--r--demo1/__init__.py4
-rw-r--r--demo1/bar.py7
-rw-r--r--demo1/main.py8
-rw-r--r--demo1/stuff.json6
-rw-r--r--demo2/main.py1
-rwxr-xr-xpyzipper44
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()