Creating executables for a Python CLI project

Before going into more details about how to develop an executable for a Python project, get some background information on our CLI tool Gefyra, a tool for local application development directly with Kubernetes. This is an Open Source Python project, that we are trying to wrap into convenient executables in this blog post.

gefyra-python

The aim was to develop an executable with (almost) the startup performance of kubectl. kubectl is the executable to control a Kubernetes cluster. That means, fast startup times and ideally just one file (which is statically-linked) are crucial for an easy distribution. In addition, executables for Windows, MacOS and Linux shall be provided. For those requirements people would usually opt for Go . However we built a prototype that was written in Python and it evolved over time. Therefore a solution for Python should be developed.

PyInstaller

PyInstaller was quite easy to set up. However, the resulting executable was complained about by Virustotal because of PyInstaller's bootloader. Somehow the code signature was also found in viruses. The compilation of a bootloader removed the virus issues.

Facing startup times of more than 10 seconds with internet connection and about 3 seconds without internet connection showed that the concept of PyInstaller will potentially always be a problem for fast startup times. Mac users complained about this issue before in the context of the former docker-compose command being created from PyInstaller.

This makes it unsuitable for CLI applications.

Nuitka

Using Nuitka very large binaries of about 150 Mb were generated. The startup performance was already much better than PyInstaller for Mac and Linux. However, very long compile times (about 10 min) left room for improvement.

PyOxidizer

PyOxidizer turned out to be the best approach. This well-crafted toolkit compiles Python to Rust code and also includes all dependencies into one handy binary executable. With no special optimizations startup times of about 700 ms were possible. Those times being almost acceptable this was the basis for further development.

The examination of the output of python -X importtime -m gefyra 2> import.log was the starting point to check the imports. There is an awesome tool to analyze the Python imports: tuna. tuna allows analyzing the import times from the log. Run it like this tuna import.log. It opens a browser window and visualizes the import times.

Thus it is possible to manually move all imports to the functions in which they are needed (and bring in some other optimizations). This greatly violates PEP 8 but leads to very fast startup times.

These are the startup values finally reached with gefyra under average modern Ubuntu:

Pretty neat, isn’t it?

In comparison the kubectl executable:

In addition, GitHub actions were created to run the PyOxidizer builds once a new version is released. Only Windows is missing at the moment.

Although PyInstaller and Nuitka did not deliver the best startup times, the intent of this article is not to speak them ill.They probably shine at other aspects.