Python supports executable archives. It is a way to wrap up a program and a bunch of modules into a single file. For example, youtube-dl
is distributed like this.
Note that the "executable" is basically just a zip-file with a #!
-line in front. So a knowledgable person can still get at the source code.
On UNIX it is easy to make this in a Makefile
using zip
.
But on some platforms this is not as easy. So I built a Python-only solution called build.py
.
"""Create runnable archives from program files and custom modules."""
import os
import py_compile
import tempfile
import zipfile as z
def mkarchive(name, modules, main='__main__.py',
shebang=b'#!/usr/bin/env python3\n'):
"""Create a runnable archive.
Arguments:
name: Name of the archive.
modules: Module name or iterable of module names to include.
main: Name of the main file. Defaults to __main__.py
shebang: Description of the interpreter to use. Defaults to Python 3.
"""
std = '__main__.py'
if isinstance(modules, str):
modules = [modules]
if main != std:
try:
os.remove(std)
except OSError:
pass
os.link(main, std)
# Forcibly compile __main__.py lest we use an old version!
py_compile.compile(std)
tmpf = tempfile.TemporaryFile()
with z.PyZipFile(tmpf, mode='w', compression=z.ZIP_DEFLATED) as zf:
zf.writepy(std)
for m in modules:
zf.writepy(m)
if main != std:
os.remove(std)
tmpf.seek(0)
archive_data = tmpf.read()
tmpf.close()
with open(name, 'wb') as archive:
archive.write(shebang)
archive.write(archive_data)
os.chmod(name, 0o755)
if __name__ == '__main__':
pass
This is not supposed to be used unmodified.
Copy build.py
into your project. Then customize the part after in __name__ == '__main__'
, based on the example given below.
Suppose the directory src
contains several Python files, eggs.py
, ham.py
and foo.py
. It also contains a subdirectory spam
, which contains a Python module that is used by all scripts. The following code will create three executable archives, eggs
, ham
and foo
:
if __name__ == '__main__':
from shutil import copy
os.chdir('src')
programs = [f for f in os.listdir('.') if f.endswith('.py')]
for pyfile in programs:
name = pyfile[:-3]
mkarchive(name, 'spam', pyfile)
copy(name, '../'+name)
os.remove(name)
If you just want to use a single file:
if __name__ == '__main__':
mkarchive('scriptname', None, 'filename.py')