.. py:currentmodule:: starlark_pyoxidizer .. _packaging_static_linking: ========================================================= 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 :ref:`packaging_extension_modules` for extensive documentation about extension modules, which are often a pain point when it comes to static linking. .. _statically_linked_linux: 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. .. _statically_linked_windows: 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: .. code-block:: python 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 :ref:`packaging_extension_modules_windows_static`. See also :ref:`packaging_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 :ref:`licensing_considerations`.