I’ve recently been facing a deployment challenge for a small Python and Flask project I’ve been building. It is, however, not directly a Python challenge, but a challenge of how to deploy a Python application to an environment I have effectively no control over. The target environment is a RedHat-like Linux box that has no internet connection, no package managers, and I have effectively no ability to install additional packages. While it is not an embedded platform in the strictest sense, it shares many of the same difficulties.
What I needed to do was find a way of bundling up my entire Python project into a single deployable archive that contains a Python interpreter, all the Python module dependencies (via
pip), and even some binary C library dependencies. After much trial and error, I found success using cx_Freeze.
First things first is to install cx_Freeze. Either run
pip install cx_Freeze or add it to your project’s
requirements.txt and install from there.
cx_Freeze takes advantage of Python’s
setuptools, which requires a
setup.py script. A sample can be generated by running
cxfreeze-quickstart. The sample is a good starting point, but some additional tweaks will be necessary.
A Sample Build Script
While cx_Freeze mostly auto-resolves dependencies, Flask apps need a little bit of extra hinting to pull in everything necessary. Notably,
Additionally, there is a big caveat to the “self-contained” nature of bundlers like cx_Freeze (and others like PyInstaller, etc.): they are not actually 100% self-contained!
Turns out that these bundlers still assume a baseline of system libraries. In most cases where you are running a “normal” Linux distribution they will be available. But my case was one of the exceptions. Because of the unusual constraints of my target environment, certain expected system C libraries were not available so I had to manually include them through the use of the
setup.py that produces working bundles ultimately ended up looking like this:
from cx_Freeze import setup, Executable # Dependencies are automatically detected, but some modules need help. buildOptions = dict( packages = [ 'jinja2', 'jinja2.ext', 'email' ], excludes = , # We can list binary includes here if our target environment is missing them. bin_includes = [ 'libcrypto.so.1.0.0', 'libcrypto.so.10', 'libgssapi_krb5.so.2', 'libk5crypto.so.3', 'libkeyutils.so.1', 'libssl.so.1.0.1e', 'libssl.so.10' ] ) executables = [ Executable( 'run.py', base = None, targetName = 'sample-app', copyDependentFiles = True, compress = True ) ] setup( name='Sample Flask App', version = '0.1', description = 'Sample Flask App', options = dict(build_exe = buildOptions), executables = executables )
Running the build is as simple as
python setup.py build. I can then move the resulting build artifact directory to my target environment and run it successfully as a plain executable binary with