Flatpak: Instructions Not Included
There is a very peculiar thing I have found in software development, it is incredibly difficult to actually get software on other people’s machines. Even compiled languages can have difficulty.
I am elbow deep in a very large personal project at the moment (all will be revealed soon) that has so far taken a month. The project is getting to a stage now where deployment methods have to be evaluated. From the beginning I wanted the project to be fully cross platform across Windows, Linux, Mac with a singular codebase. Because of these specific requirements, I went with the Qt GUI framework, it is cross platform, runs natively on each platform, and has bindings for Python (my preferred language).
Because nothing is ever easy, there are actually two different python bindings for Qt. There is PySide which was created by the Qt Company themselves. It has a less restrictive license, but seems to have less of a community around it due to it being the newer of the two bindings. The other binding is called PyQt and came first, even being called
python3-qt in the fedora package manager. When it comes down to coding, there is pretty much no difference between them since they are all just pythonic fronts for the same C++ back-end. For the whole project up until recently I was using PySide since it is the same in every way except for a better license.
Python, as it is shipped as CPython, does not (by default) compile to machine code. You’ll notice a parenthetical and a subordinate clause in the previous sentence, that’s how windy this road goes.
There are systems that will turn your python code into binary executables, I was using PyInstaller to get the job done for me. I’d run it on Linux, Windows, MacOS and all my code would be packaged in to binary form; there was the issue of non-python dependencies I had but I could find ways around that. So there I was with all my binaries in a row and feeling confident about how I’m going to deploy my application.
Linux Binary Woes
My current main machine runs Fedora Linux, this is a distribution that is kept reasonably up to date and, at the time of writing, has a new desktop environment and newer kernel than other non-Arch distros. All of this meant that when I tried to run my Linux compiled program on Ubuntu (A major distro I want to support), I get thrown an error that Ubuntu doesn’t have the correct version of glibc (the dynamically linked C libraries). Sticking with the standard glibc versions that come with each distro, GNU/Linux is backwards compatible but not forwards compatible. That is to say that a program compiled on an older distro will run on a newer distro but not vice-versa. The solution to this seems straightforward doesn’t it? Compile the program on an older version. So I loaded up an Ubuntu LTS VM, compile the program, works like a charm. I try to run that same executable on my main Fedora machine and I get an error message:
The program does not run. This is without doubt a bug in Gnome 40. But it is a bug that happens 100% of the time, it may get fixed, it may not. My application is either incompatible with anything other than my own machine, or compatible with everything except my own machine. Of course I could have two separate binaries, but that would be a bit of a hassle to maintain. In comes what I thought would be my saviour, Flatpak.
Linux has 3 main methods of deploying applications. Shipping binaries, making per distribution packages, and containers (of which flatpak is one). Binaries are, as we’ve seen, version dependent, packages are distribution dependent, and containers run across all machines but have extra bulk since they package there own dependencies rather than relying on system libraries.
Throughout my exploration of deployment options, I have made all three. I have documented my results of binaries, and I also went on to make an RPM package (the packaging system that Fedora uses).
NOTICE: The above is a script used for testing, it is mostly functional but don’t use it as a guide for how to do things properly
The above is a script you feed into
rpmbuild and it will generate a .rpm file which Fedora users can use to install your software. You can see that all it really is is some metadata listing app information, dependencies (these dependencies are ones that are inside the fedora packaging system), and a shell script to say what gets installed and how. Because all the dependencies are listed, and are all from the the distros package manager, the file size of an rpm only consists of the files that you supply. In my case it was some python source files and some vector icons, totalling 1.2MB. Of course the full size of the app still needs to be downloaded but the other 150MB or so come in the form of system libraries that other apps can also use. This is the classic Linux method of software deployment, it is lean, and ensures compatibility with a particular distro. The problem comes when looking at other distributions. Some distributions also use RPM (CentOS, RHEL, OpenSUSE) but have different packages, and so need special cases in the file. Other distributions use a different package management system, Debian and derivatives use .deb files for instance.
Flatpak and it’s rival Snap aim to solve the dependency hell by shipping everything together in one big package. You avoid incompatibility at the cost of redundancy. The cover image of this post is my flatpak spec file that gets fed into
flatpak-builder. And you may notice that it works in a similar way to a distribution’s own package manager.
We have metadata along with base dependencies
Because everything is in a sandbox, we have to declare what parts of the host system we have to use
And then comes the rest of our dependencies which have to be files we supply, along with the build script we use to actually do what we want with the files.
The Trouble with Python, Qt and Flatpak
All was going swimmingly until I had to get my Qt app in flatpak. 2 of my app dependencies are installed by effectively copying the prebuild binaries into the flatpak sandbox.
One of my dependencies is actually built and compiled within the flatpak container. (Given no
buildsystem option on archives with makefiles in them, flatpak will automatically compile and install them)
So we are back to the dilemma of using source files or prebuilt binaries, you’ll see later that the solution is a mix of both.
One option is just shipping a compiled version of my program in a flatpak. The issue is that flatpak’s glibc is once again older than my main machine so all flatpak builds would have to be done in an Ubuntu VM, not ideal. Next up comes building from source in the flatpak. My plan for the process was this:
- Get python and resource files in the flatpak
pip installall dependencies
Sounds simple enough, but flatpak didn’t like me communicating with the internet while installing (I tried
share=network on individual modules and it didn’t work)
So, each python library had to be down as it’s own python module with a link to the tar balls on pypi. Flatpak has a tool for such an occasion called flatpak-pip-generator
so the line in the flatpak yml
is a pointer to a file actually containing all pip dependencies
You’ll notice that there is no mention of PySide or Qt. The key module that the app depends so heavily is not there, why? Well because both Qt python dependencies are not available as source files from pip. Pip hosts source files and wheels. All the above are source files. These are tar balls that pip will compile and build from source. Wheels are, once again, pre-built binaries. Flatpak doesn’t include wheels by default since it wants everything to be as cross platform and cross architecture as possible.
I also wanted to be as platform agnostic as possible so I attempted to build my python bindings from source in the flatpak. Flatpak did not like this. Neither PySide nor PyQt would build and each threw different error during the compilation process. My best guess would by the sandbox does not contain some required system libraries but I am at a loss to know which ones. And C compilers are not the most helpful thing as your only point of reference for a system error.
Lasers to the rescue
At this stage, I didn’t actually know what python wheels were, I just assumed everything had to be built from source. I was desperate for answers after 3 days of debugging and staring at config files. I did something I very rarely do, I used the GitHub search function. To my amazement I found a solution.
The Optical Metrology Group in the Physics department of Humboldt University Berlin created a Linux application for locking onto spectroscopy lines. Guess what, they made a flatpak!
I looked at there yaml, and I knew then and there that they had found a solution.
They had obviously also faced similar Qt-based problems. Their YAML led to me finding out about python wheels, how I could get PyQt in a flatpak, and how I could tell pip the right directory for a python installation (I had no idea what
--target would do before this).
Thanks to some German physicists, my next project is back on track. Beady eyed people may notice some information in some of the config files I’ve posted that hint to what it could be. I did try and use PySide using the same wheel method but I got into some strange cyclical versioning thing since it required PySide an Shiboken2 to be installed at the same time. All that means is that my application will be open source out of a legal obligation, as well as an civil one.
SIDE: kerberos is also a requirement for PyQt to work in a flatpak, I have no idea why but it may have something to do with certain networking modules.
Here are some useful sources of information I turned to while interacting with packaging systems on Linux
- An excellent and succinct guide that covers making non-qt python apps work in a flatpak
- Information on what a flatpak is and what its parts consist of.
- A good reference for what commands can be used in flatpak YAML
- The Humboldt University Berlin YAML
- A straightforward guide on how to make RPM packages