One of the most common drawbacks when deploying a Python application is the need to install a set of dependencies. First you need to install a suitable version of Python interpreter, then all libraries which you have learned for development must be installed. Finally, you deploy the application itself, a set of text files containing all of the source code.
Performing all of these steps manually, besides being a tedious job, often results in errors being made.
For the reasons explained above, this post will look at the PyInstaller tool, which enables the automation of all deployment tasks generating an executable file, containing our application and everything necessary, ready to be executed.
The operation of PyInstaller is very simple:
- Reads the Python script
- Analyzes the code to discover modules and libraries needed for the execution
- Collects a copy of all those files, including the Python interpreter
- Puts it all together in an executable file (packaging)
In the following step-by-step example, it is possible to see the operation in greater detail.
First we create a simple program, that prints the phrase “Hello world!” on the screen, as follows:
#! /usr/bin/python
print 'Hello world!'
We test if it works:
$ python hello.py
Hello world!
Then we invoke PyInstaller, passing our script as an argument:
$ pyinstaller --onefile hello.py
As a result of the above step, PyInstaller generates:
- A spec file hello.spec, that looks like:
# -*- mode: python -*- a = Analysis(['hello.py'], pathex=['/tmp/examples'], hiddenimports=[], hookspath=None, runtime_hooks=None) pyz = PYZ(a.pure) exe = EXE(pyz, a.scripts, a.binaries, a.zipfiles, a.datas, name='hello', debug=False, strip=None, upx=True, console=True )
- A directory “build” containing some log and metadata files:
build/ └── hello ├── logdict2.7.5.final.0-1.log ├── out00-Analysis.toc ├── out00-EXE.toc ├── out00-PKG.pkg ├── out00-PKG.toc ├── out00-PYZ.pyz ├── out00-PYZ.toc └── warnhello.txt
- A directory “dist” containing the executable file:
dist/ └── hello
The generated spec file contains information about how to package the application. By default it includes only source code and dependencies, but it is possible to edit it to include other files, like images, configuration files or anything else you want. Once the spec file has been edited, PyInstaller should be invoked, passing the spec file as an argument.
The tool stores all of the information from the source code analysis in the “build” directory. All this information is useful when you re-invoke the tool, since it does not need to analyze the code all over again.
Finally, in the directory “dist” we locate the executable file of the application. Testing it works when we get the following output:
$ ./dist/hello Hello world!
To understand the executable internals, we proceed to add a sentence to the source code so that the program remains asleep for a while, thus giving us time to inspect what it is doing. What you can see is that a directory named “_MEIfG97bR” is created in “/tmp” and all dynamic libraries are extracted from the executable and stored there:
/tmp/_MEIfG97bR/ ├── _codecs_cn.so ├── _codecs_hk.so ├── _codecs_iso2022.so ├── _codecs_jp.so ├── _codecs_kr.so ├── _codecs_tw.so ├── _collections.so ├── _functools.so ├── _hashlib.so ├── _heapq.so ├── _io.so ├── _locale.so ├── _multibytecodec.so ├── _random.so ├── _socket.so ├── _ssl.so ├── _struct.so ├── array.so ├── audioop.so ├── binascii.so ├── bz2.so ├── cPickle.so ├── cStringIO.so ├── datetime.so ├── fcntl.so ├── itertools.so ├── libbz2.so.1 ├── libcom_err.so.2 ├── libcrypto.so.10 ├── libgssapi_krb5.so.2 ├── libk5crypto.so.3 ├── libkeyutils.so.1 ├── libkrb5.so.3 ├── libkrb5support.so.0 ├── liblzma.so.5 ├── libpcre.so.1 ├── libpython2.7.so.1.0 ├── libreadline.so.6 ├── libselinux.so.1 ├── libssl.so.10 ├── libtinfo.so.5 ├── libz.so.1 ├── math.so ├── operator.so ├── readline.so ├── select.so ├── strop.so ├── termios.so ├── time.so ├── unicodedata.so └── zlib.so
Once the execution of the application is completed, the directory is deleted. This operation can cause problems, for example, when the directory “/tmp” filesystem is mounted with “noexec” flags:
/dev/mapper/vg-tmp /tmp ext4 defaults,nodev,noexec,nosuid 1 2
In this case, the application shows an “Operation not permited” error:
$ ./dist/hello ./dist/hello: error while loading shared libraries: libz.so.1: failed to map segment from shared object: Operation not permitted
A simple way to solve this issue is to set, in the environment variable TMPDIR, a path to a directory in a filesystem mounted without “noexec” flags:
$ export TMPDIR=/var/tmp/
After reviewing the main aspects of PyInstaller, we can highlight some advantages:
- Generates executable standards that work on computers without Python installed.
- Is multi-platform; works with Linux, Windows, Mac OS and Solaris.
- Has different packaging modes: Single file, Single directory, and Custom.
- Supports compression.
- Supports options to hide Python code (Obfuscation/Compiling with Cython).
Pyinstaller has a number of other interesting features – for further reading check out this site https://pythonhosted.org/PyInstaller/, where you can find the user manual for this tool.