Standalone / Single File Applications with Static Linking

This document describes how to produce standalone, single file application binaries embedding Python using static linking.

See also Working with Python Extension Modules for extensive documentation about extension modules, which are often a pain point when it comes to static linking.

Building Fully Statically Linked Binaries on Linux

It is possible to produce a fully statically linked executable embedding Python on Linux. The produced binary will have no external library dependencies nor will it even support loading dynamic libraries. In theory, the executable can be copied between Linux machines and it will just work.

Building such binaries requires using the x86_64-unknown-linux-musl Rust toolchain target. Using pyoxidizer:

$ pyoxidizer build --target x86_64-unknown-linux-musl

Specifying --target x86_64-unknown-linux-musl will cause PyOxidizer to use a Python distribution built against musl libc as well as tell Rust to target musl on Linux.

Targeting musl requires that Rust have the musl target installed. Standard Rust on Linux installs typically do not have this installed! To install it:

$ rustup target add x86_64-unknown-linux-musl
info: downloading component 'rust-std' for 'x86_64-unknown-linux-musl'
info: installing component 'rust-std' for 'x86_64-unknown-linux-musl'

If you don’t have the musl target installed, you get a build time error similar to the following:

error[E0463]: can't find crate for `std`
  |
  = note: the `x86_64-unknown-linux-musl` target may not be installed

But even installing the target may not be sufficient! The standalone Python builds are using a modern version of musl and the Rust musl target must also be using this newer version or else you will see linking errors due to missing symbols. For example:

/build/Python-3.7.3/Python/bootstrap_hash.c:132: undefined reference to `getrandom'
/usr/bin/ld: /build/Python-3.7.3/Python/bootstrap_hash.c:132: undefined reference to `getrandom'
/usr/bin/ld: /build/Python-3.7.3/Python/bootstrap_hash.c:136: undefined reference to `getrandom'
/usr/bin/ld: /build/Python-3.7.3/Python/bootstrap_hash.c:136: undefined reference to `getrandom'

Rust 1.37 or newer is required for the modern musl version compatibility. And newer versions of Rust may change which version of musl they use, introducing failures similar to above. If you run into problems with a modern version of Rust, consider reporting an issue against PyOxidizer!

Once Rust’s musl target is installed, you can build away:

$ pyoxidizer build --target x86_64-unknown-linux-musl
$ ldd build/apps/myapp/x86_64-unknown-linux-musl/debug/myapp
     not a dynamic executable

Congratulations, you’ve produced a fully statically linked executable containing a Python application!

Important

There are reported performance problems with Python linked against musl libc. Application maintainers are therefore highly encouraged to evaluate potential performance issues before distributing binaries linked against musl libc.

It’s worth noting that in the default configuration PyOxidizer binaries will use jemalloc for memory allocations, bypassing musl’s apparently slower memory allocator implementation. This may help mitigate reported performance issues.

Building Statically Linked Binaries on Windows

It is possibly to produce a mostly self-contained .exe on Windows. We say mostly self-contained here because currently the built binary has some external .dll dependencies. However, these DLLs are core Windows / system DLLs and should be present on any Windows installation supported by the Python distribution being used.

The main trick to build a statically linked Windows binary is to switch the Python distribution from the default standalone_dynamic flavor to standalone_static. This can be done via the following in your config file:

dist = default_python_distribution(flavor = "standalone_static")

Important

The standalone_static Windows distributions build Python in a way that is incompatible with compiled Python extensions (.pyd files). So if you use this distribution flavor, you will need to compile all Python extensions from source and cannot use pre-built wheels packages. This can make building applications with many dependencies difficult, as many Python packages don’t compile on Windows without installing many dependencies first.

See also Windows Static Distributions Only Support Built-in Extension Modules.

See also Understanding Python Distributions for more details on the differences between standalone_dynamic and standalone_static Python distributions.

Implications of Static Linking

Most Python distributions rely heavily on dynamic linking. In addition to python frequently loading a dynamic libpython, many C extensions are compiled as standalone shared libraries. This includes the modules _ctypes, _json, _sqlite3, _ssl, and _uuid, which provide the native code interfaces for the respective non-_ prefixed modules which you may be familiar with.

These C extensions frequently link to other libraries, such as libffi, libsqlite3, libssl, and libcrypto. And more often than not, that linking is dynamic. And the libraries being linked to are provided by the system/environment Python runs in. As a concrete example, on Linux, the _ssl module can be provided by _ssl.cpython-37m-x86_64-linux-gnu.so, which can have a shared library dependency against libssl.so.1.1 and libcrypto.so.1.1, which can be located in /usr/lib/x86_64-linux-gnu or a similar location under /usr.

When Python extensions are statically linked into a binary, the Python extension code is part of the binary instead of in a standalone file.

If the extension code is linked against a static library, then the code for that dependency library is part of the extension/binary instead of dynamically loaded from a standalone file.

When PyOxidizer produces a fully statically linked binary, the code for these 3rd party libraries is part of the produced binary and not loaded from external files at load/import time.

There are a few important implications to this.

One is related to security and bug fixes. When 3rd party libraries are provided by an external source (typically the operating system) and are dynamically loaded, once the external library is updated, your binary can use the latest version of the code. When that external library is statically linked, you need to rebuild your binary to pick up the latest version of that 3rd party library. So if e.g. there is an important security update to OpenSSL, you would need to ship a new version of your application with the new OpenSSL in order for users of your application to be secure. This shifts the security onus from e.g. your operating system vendor to you. This is less than ideal because security updates are one of those problems that tend to benefit from greater centralization, not less.

It’s worth noting that PyOxidizer’s library security story is very similar to that of containers (e.g. Docker images). If you are OK distributing and running Docker images, you should be OK with distributing executables built with PyOxidizer.

Another implication of static linking is licensing considerations. Static linking can trigger stronger licensing protections and requirements. Read more at Licensing Considerations.