Building a flatpak app without the flatpak cli
12 minutes read. Published:
Table of contents
- Introduction
- Starting from scratch
- Getting access to flathub
- Understanding repositories
- Downloading the required branches
- Working with the downloaded branches
- Creating the build environment
- Entering the build environment
- Building the app
- Finishing the build
- Exporting the app to a repository
- Installing the app and running it
- Conclusions
Introduction
I believe building a flatpak package is the easiest way to distribute an app targeting 90% of linux distros.
Preparing a flatpak package mostly means writing a manifest.json
describing all the needed dependencies of the app and calling flatpak-builder
with the manifest file.
Today we are going deeper. We won't write a manifest file, we won't use flatpak-builder
and not even flatpak
.
We are going to replicate the build process manually, in the shell, to see how the technologies behind flatpak
are cooperating.
This article was written to improve my knowledge on flatpaks and to evaluate potential changes/improvements to the flatpak build process
Starting from scratch
We are going to start from a clean system, without flatpak installed.
To do so, I'm going to work inside a fedora
virtual machine.
vagrant init fedora/37-cloud-base \
--box-version 37.20221105.0
vagrant up
vagrant ssh
# I've also edited the generated Vagrantfile to grant more memory to the vm...
Getting access to flathub
We will need to download some data from flathub, that is, the biggest flatpak repository.
Normally, if we had flatpak
installed, we would follow the guide on https://flatpak.org/setup/, but we can't do that.
Let's try following the steps manually. We are interested in replicating the command
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
For now we will just download the .flatpakrepo file using curl:
curl -L "https://flathub.org/repo/flathub.flatpakrepo" > flatpakrepo
Understanding repositories
Flatpaks must work on every distro. That means the app we are going to distribute needs to bring with it all the needed dependencies.
Let's say we want to build a python program. Python may not be installed on the target system, so we need to bring python with us. The same goes for all the dependencies of our app.
If we keep this line of reasoning, installing 10 flatpaks will require downloading a copy of python 10 times. That's completely wasteful and unnecessary. To eliminate this problem, flatpaks underneath are built using ostree
repositories.
OSTree
OSTree offers a way to build a filesystem, track its changes overtime, and distribute it. The changes are stored in a "repository", similarly to how git works.
When we download 10 flatpaks which use the same python version, we aren't downloading python 10 times.
We're downloading an ostree repository containing python, one time.
When we run a program using flatpak run org.myorg.Myapp
, the flatpak
command takes the job of creating a "container" with the files of the ostree repository containing python and the files of our program.
In reality, ostree repositories often are a big bundle of dependencies, and they don't contain only python. Most apps are probably going to reuse the org.gnome.Runtime
branch, which contains python, but also gjs
(a javascript runtime) and many other commands and libraries needed to run a GTK app. There are also branches containing build tools, like org.gnome.Sdk
, which are only used during build-time and aren't required by the end user installing our app.
Downloading the required branches
We are going to download the branches org.gnome.Runtime
and org.gnome.Sdk
from the repository flathub.
First, we need to init a local repository to store the contents of org.gnome.Runtime
and org.gnome.Sdk
.
Oh, and let's install ostree
.
dnf install ostree
ostree init --repo myrepository --mode bare-user-only
The flag bare-user-only
is required so that the downloaded files can be used by an unprivileged user. The same mode is used by flatpak on the user folder located at ~/.local/share/flatpak
.
If you have already used flatpak on your system, you can in fact check that the file ~/.local/share/flatpak/repo/config
contains
[core]
...
mode=bare-user-only
...
To retrieve the download-url of the branches, we need to see the contents of the flatpakrepo
file we have downloaded earlier:
[Flatpak Repo]
Title=Flathub
Url=https://dl.flathub.org/repo/
Homepage=https://flathub.org/
Comment=Central repository of Flatpak applications
Description=Central repository of Flatpak applications
Icon=https://dl.flathub.org/repo/logo.svg
GPGKey=mQINBFlD2sABEADsiUZUOYBg1UdDaWkEdJYkTSZD68214m8Q1fbrP5AptaUfCl8KYKFMNoAJRBXn9FbE6q6VBzghHXj...
We can see it contains a user friendly description of the repository, a url which we will use to download our data, and even an Icon which can be displayed by software stores, like KDE's Discover or the gnome software center.
Now we can add the Url
we just found in the file flatpakrepo
as a remote of our local repository, in a similar way to what we would do with git.
ostree remote add --repo myrepository flathub https://dl.flathub.org/repo/
We can't use the remote yet, because ostree
wants to check the signature of everything we download from it. We need to import the GPGKey of the repository. The key can be found inside the file flatpakrepo
, but it's in base64, so we need to decode it. To ease the process, let's copy-paste the GPGKey field to a file base64key
. Then:
base64 --wrap 0 -d base64key > repokey
ostree remote gpg-import --repo myrepository flathub --stdin < repokey
We can now download the branches we need from flathub
ostree pull --repo myrepository --depth 1 flathub runtime/org.gnome.Sdk/x86_64/43
ostree pull --repo myrepository --depth 1 flathub runtime/org.gnome.Platform/x86_64/43
Working with the downloaded branches
If everything went well, we should be able to list the files inside one of the branches we just downloaded
ostree ls --repo myrepository runtime/org.gnome.Sdk/x86_64/43
The files are now stored inside the repository in a content-addressed-object store. To access those files "normally", we need to checkout the branches to a folder.
# create folders to store the checkouts
mkdir -p ./runtime/org.gnome.Sdk/x86_64/
mkdir -p ./runtime/org.gnome.Platform/x86_64/
# checkout the branches to their respective folders
ostree checkout --repo myrepository runtime/org.gnome.Sdk/x86_64/43 ./runtime/org.gnome.Sdk/x86_64/43
ostree checkout --repo myrepository runtime/org.gnome.Platform/x86_64/43 ./runtime/org.gnome.Platform/x86_64/43
# check that everything went ok
ls -la ./runtime/org.gnome.Platform/x86_64/43/
This process is similar to flatpak install
:
when you install a branch as an end user, using flatpak install org.gnome.Sdk
, flatpak will checkout the files of that branch to a folder as /var/lib/flatpak/runtime/org.gnome.Sdk/x86_64/43/
.
Creating the build environment
We now need to replicate flatpak build-init
. This command creates a build folder containing some files, as follows:
mkdir -p build/files
mkdir -p build/var
touch build/metadata
The folder build/files
will contain the files of our app. metadata
is a description of the flatpak we are building, containing the icon path of our app, the required permissions, and other metadata...
We are mostly interested in filling build/files
with something executable.
Entering the build environment
We are now replicating the command flatpak build
.
We will build a small GTK4 app, in C, so that we are sure the build environment is working correctly. I haven't installed any gtk library in the virtual machine, so if the build succeeds, it means we are using the libraries and the compiler provided by org.gnome.Sdk
.
flatpak build ./build command
would do the following:
- create a container using the files inside
org.gnome.Sdk
as the filesystem (this step is a bit more complicated, we will see how). - bind
./build/files
to/app
- bind
./build/var
to/var
(useless for us) - execute
command
inside the container
We can do the same thing using bubblewrap
, a tool to create a sandbox, or in the words, a container, on the fly. bubblewrap
is heavily used by flatpak
to achieve sandboxing.
Let's enter the build environment
dnf install bubblewrap
bwrap \
--unshare-all \
--clearenv \
--dev /dev \
--proc /proc \
--bind /home /home \
--bind ./runtime/org.gnome.Sdk/x86_64/43/files /usr \
--symlink usr/lib /lib \
--symlink usr/lib64 /lib64 \
--symlink usr/bin /bin \
--symlink usr/sbin /sbin \
--bind ./build/files/ /app \
--bind ./build/var/ /var \
/usr/bin/bash
Using --unshare-all
we are telling bubblewrap to run the process bash
with a different ipc
, net
, pid
and user
namespace. That means the process won't be able to use the network, it won't be able to see the other processes (except its children) and it will be assigned to a different uid.
You can see we are binding the sdk folder ./runtime/org.gnome.Sdk/x86_64/43/files
to /usr
.
Then we have created some symlinks to /usr
, to ensure that the binaries and the loader is accessible. The loader path is expected to be on /lib64/...
.
/app
will contain our executable file.
We are now inside the container, running bash. We can check that we have access to every file by wandering around:
ls -la /
file /bin/bash
Before continuing, we should export the PATH
env variable, else a lot of things will behave extremely weirdly.
Apparently bash
has a default PATH
variable, but it's not exported.
export PATH
# check that everything works...
which gcc
We have access to gcc
, so we can finally build our app.
Building the app
I'm going to take the GTK4 example app from gtk.org and copy it to a file inside /app/main.c
.
I'm adding a printf
line, to see some output once I'll run the program from my virtual machine, which doesn't have a graphics stack.
cat > /app/main.c <<EOF
// Include gtk
#include <gtk/gtk.h>
static void on_activate (GtkApplication *app) {
// Create a new window
GtkWidget *window = gtk_application_window_new (app);
// Create a new button
GtkWidget *button = gtk_button_new_with_label ("Hello, World!");
// When the button is clicked, close the window passed as an argument
g_signal_connect_swapped (button, "clicked", G_CALLBACK (gtk_window_close), window);
gtk_window_set_child (GTK_WINDOW (window), button);
gtk_window_present (GTK_WINDOW (window));
}
int main (int argc, char *argv[]) {
printf("HELLO WORLD!");
// Create a new application
GtkApplication *app = gtk_application_new ("com.example.GtkApplication",
G_APPLICATION_FLAGS_NONE);
g_signal_connect (app, "activate", G_CALLBACK (on_activate), NULL);
return g_application_run (G_APPLICATION (app), argc, argv);
}
EOF
Now we need to build it. I'm now copying the build command from gtk.org, adapting it to our use case:
mkdir /app/bin
gcc $(pkg-config --cflags gtk4) -o /app/bin/main /app/main.c $(pkg-config --libs gtk4)
# check that everything went well
ls -la /app/bin/main
# remove the source file, so that we don't waste bytes in the final export
rm /app/main.c
We are done compiling.
Let's exit the build environment with exit
.
Finishing the build
At this point we would call flatpak build-finish ./build --command main
.
Let's replicate the command manually.
We would need to export the icon of our app from ./build/files
to ./build/export
, but we don't have an icon, so we'll skip that.
The most important thing to do is to fill the metadata
file with something useful. We need to tell flatpak
what's the entry point of our app. We also need to give it a name: my.handcrafted.App
.
cat > ./build/metadata <<EOF
[Application]
name=my.handcrafted.App
runtime=org.gnome.Platform/x86_64/43
sdk=org.gnome.Sdk/x86_64/43
command=main
EOF
Exporting the app to a repository
flatpak
installs apps from a repository, like flathub.
Our app isn't inside a repository, so we can't install it yet.
To export the app to a local repo, we would use flatpak build-export
.
Let's create another ostree repo, manually.
Then we commit our changes to a branch with the syntax app/APPNAME/ARCH/VERSION
, so that it's
discoverable by flatpak install
.
ostree init --repo outrepo --mode bare-user-only
ostree commit --repo outrepo \
--branch app/my.handcrafted.App/x86_64/master \
--add-metadata-string xa.metadata="$(cat ./build/metadata)"$'\n' \
--tree=dir=./build
ostree summary -u --repo outrepo
Apparently the metadata file must be added also inside the metadata of the commit. 🤷
Else, we can't add the repository using flatpak remote-add
.
Some info on this issue.
Notice also the trick "..."$'\n'
in the parameter --add-metadata-string
.
I'm using that to add a newline, which is present inside ./build/metadata
, but seems to be discarded by the process substitution
"$(cat ./build/metadata)"
. 🤷
Installing the app and running it
This is the last step. The app is already built and ready to be used.
We can finally use flatpak
. Yay!
It's the same process as if we were installing the app from flathub.
- We add the repository with flatpak
- We install the app from that repo
dnf install flatpak
# install the usual flathub repo, that's needed to download org.gnome.Sdk and org.gnome.Platform, as with any other app
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install --user org.gnome.Sdk
flatpak install --user org.gnome.Platform
# Add our local repository containing the app
flatpak remote-add --user --no-gpg-verify outrepo file://`pwd`/outrepo
# Finally install and run it
flatpak install --user outrepo my.handcrafted.App
flatpak run --user my.handcrafted.App
And then we get...
[vagrant@localhost ~]$ flatpak run --user my.handcrafted.App
Failed to register: GDBus.Error:org.freedesktop.DBus.Error.ServiceUnknown: org.freedesktop.DBus.Error.ServiceUnknown
HELLO WORLD![vagrant@localhost ~]$
OH YEAH!
(we don't care about that dbus error, it's nothing important, I guess it's not even related to our build process).
WE'VE DONE IT
Conclusions
That was a long ride. We've seen how ostree
is an essential component of flatpak
. It enables distribution of apps, runtimes and sdks using repositories, and it enables keeping track
of changes in a git-like fashion. The flatpak
command also relies heavily on namespaces provided by the kernel, in a similar way to docker
and podman
.
In a certain sense, we could say docker
/podman
is used for running contanerized services on servers, and flatpak
is used for running containerized apps on personal computers.
Bundling the most common dependencies inside a runtime, as org.gnome.Platform/x86_64/43
, is certainly a smart move, because it enables security updates by updating only the runtime, without
needing to rebuild every single app. ostree pull
is also smart enough to download only the changed files, so doing a security update won't require a giant update.
The build process isn't easy, nor small, but I can see that the complexity is mostly inevitable. The tool flatpak-builder
tries to hide most of the complexity by making the build process declarative,
but it expects users to declare everything inside a static manifest.json
file, which could be limiting for some people. Also, it requires learning the syntax and rules of the file manifest.json
, which
is quite an effort itself. But flatpak-builder
isn't the scope of this article, so I'm going to stop here.
Thanks for reading. If you liked this post, you can buy me a coffee on patreon or github.