Patrick McCarthy dev blog

Making Executable Binaries With Python and cx_Freeze

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.

Getting Started

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, jinja2, jinja2.ext, and email did not get automatically resolved for me and were causing my bundled application to crash.

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 bin_includes directive.

My 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 ./sample-app!