Maintaining a flatpak repository

So, you’ve built a flatpak, using flatpak-builder, and now you have a directory called repo. How do you go from here to something that your users can install the  application from?

To start with, lets make a minimal sample application, just so that we have a repo to look at:

$ echo $'#!/bin/sh\necho hello world' > hello.sh
$ flatpak build-init appdir com.example.App \
          org.freedesktop.Sdk \
          org.freedesktop.Platform 1.4
$ flatpak build appdir mkdir /app/bin
$ flatpak build appdir install --mode=750 hello.sh /app/bin
$ flatpak build-finish --command=hello.sh appdir

This produces the following appdir directory:

├── export
├── files/bin/hello.sh
└── metadata

We can now export this to the repo directory:

flatpak build-export repo appdir stable

If we copy this directory to a webserver, so that it is accessible via http, lets say as http://example.com/repo, then anyone can install your application like this:

flatpak remote-add --no-gpg-verify example \
        http://example.com/repo
flatpak install example com.example.App

Note: You can of course add a local remote using the path of the repo instead of a uri if you just want to test your build.

The anatomy of a repo

So, how does this work? Lets look a bit deeper at the repository. It
looks something like this:

├── config
├── objects
│ ├── 44/6a0ef11b7cc167f3b...0b6c5488.dirmeta
│ ├── 6e/340b9cffb37a989ca...617afa01d.dirtree
│ ├── 74/cfc6e8dd69905a525...64fc0d5e5.dirtree
│ ├── 75/af15131ec214e5074...41858318a.commit
│ ├── c4/083227ca305c44cd6...ceeecd27c.dirtree
│ ├── d8/fdc0a16351fa8bcd8...ac90e63e2.filez
│ ├── e4/2aa65aae7c7b61539...5557cad40.filez
│ └── e9/c16c731270e0a8718...cc8be2601.dirtree
├── refs/heads/app/com.example.App/x86_64/stable
└── summary

If you’ve ever seen a git repository, this looks very similar. The application is referred to by what is called a ref, in this case app/com.example.App/x86_64/stable. If we look at the file with that name in the refs/heads directory we get latest commit id:

$ cat repo/refs/heads/app/com.example.App/x86_64/stable
75af15131ec214e5074...41858318a

When flatpak installs the app it starts by pulling from the repository, which means we download into a local repository the corresponding commit object (i.e. objects/75/af15131ec214e5074...41858318a.commit) and then follow that until all required objects are downloaded.

Another important feature is the summary file. This is a single file that contains information from all the refs files in the repository. It is regenerated from the refs files each time you export a new app or run flatpak build-update-repo. This file is needed because when the repository is accessed via http there is no standard way to list all the refs files, for instance when listing available apps.

This is basically all you have to do to get something running, just build it and copy the results to a webserver. However, there are some details that get important when you’re maintaining a production repository.

GPG signatures

For an official build you want to have a GPG key to sign the repository. In ostree (which is what flatpak uses) every commit is signed, as well as the summary file. When we pull from the repository the summary and the commit signatures are verified, to make sure nothing was modified. This is important because it allows us to use unencrypted http for faster download, and it allows secure use of mirrored repos.

The example repo above did not have signatures, so lets create some. If you have a GPG key already you can use it, although it is recommended that you create a custom key for the repository. That way you can transfer maintainership without having to give anyone else your private key.

So, lets create a custom key:

$ mkdir gpg
$ gpg2 --homedir gpg --quick-gen-key user@example.com

This eventually create a keyring in the gpg directory and print it out:

pub rsa2048 2017-02-09 [S]
 770B194227ED91BF6C9038B83F4BF10E09A00F3B
 uid [ultimate] user@example.com
 sub rsa2048 2017-02-09 []

We can then sign the repo after the fact (you can also sign it at build-export time):

$ flatpak build-sign repo \
  --gpg-sign=770B194227ED91BF6C9038B83F4BF10E09A00F3B \
  --gpg-homedir=gpg

This will create the signature of the commit:

repo/objects/75/af15131ec214e5074...41858318a.commitmeta

You also need to sign the summary file:

$ flatpak build-update-repo repo                     \
 --gpg-sign=770B194227ED91BF6C9038B83F4BF10E09A00F3B \
 --gpg-homedir=gpg

Which will recreate the summary file, now with a summary.sig file next to it.

If we export the public key for the repo we can give it to users:

$ gpg2 --homedir=gpg \
  --export 770B194227ED91BF6C9038B83F4BF10E09A00F3B > example.gpg

And you can then use this instead of --no-gpg-verify when adding the remote:

$ flatpak remote-add --gpg-import=example.gpg \
         example http://example.com/repo

Flatpakref files

Having to both add a remote (with the right gpg key file) and then install the application is not a great user experience. To make this easier for users, flatpak has something called flatpakref files. These contain both the information about the repository, including the GPG key, and the application id. The result is that you can just point flatpak at one file and it will install the application.

Lets create a example-app.flatpakref file for our app:

[Flatpak Ref]
Title=Example App
Name=com.example.App
Branch=stable
Url=http://example.com/repo
GPGKey=mQENBFicjMMBCAC...
IsRuntime=false
RuntimeRepo=https://sdk.gnome.org/gnome.flatpakrepo

The GPGKey field is just a base64 version of the key, which you can get like this:

$ base64 example.gpg | tr -d '\n'

Most of the fields are obvious, but the last one need a special explanation. It specifies a file that describes the repository containing the runtimes that the application uses. If this repository is not configured on the users system it will be automatically added and the runtimes will be installed from it as needed.

Once the flatpakref file is put on the webpage your application can be installed in a single command:

flatpak install http://example.com/example-app.flatpakref

The user can also click on the flatpakref link, and it will open in the default application for software installation (like gnome-software). There are also some additional fields that will be shown in gnome-software: Homepage, Comment, Description and Icon.

Static deltas

When downloading an application, flatpak will do individual requests for each file in the application. If you have a lot of small files this can be slow, so you want to make sure the HTTP keep-alive is enabled on the web server. But even then it can be slow. Additionally, as objects are whole files, we still have to download the entire file even if just one byte in the file changed.

In order to solve this flatpak supports something called static deltas. These are pre-generated delta files (using bsdiff) that contain all the data needed to go from one version to another (or from nothing to a version). If these files are available they are used instead of the individual objects, which allows pulls to be much more efficient.

You can generate static deltas for the latest versions of all apps by passing --generate-static-deltas to flatpak build-update-repo, and I recommend everyone do this for production repositories.

Configuration

There are a couple of configurations in a flatpak repo that you can set via build-update-repo. One is the title (--title=TITLE). This will be used by default as the title when listing the configured remotes.

Another is the default branch (--default-branch=BRANCH). If this is set, then this will be used as the default branch name (the last part of the ref) when it is not specified. This is useful when you have multiple versions of the same app (for instance a nightly build and a stable build) in the same repository.

AppStream branches

In order to support nice graphical frontends like gnome-software, flatpak uses a metadata format called AppStream. The way this works is that each application ships an AppData xml file, and an icon. Then when you run flatpak build-update-repo, each such xml file and icon are extracted and put in a separate per-arch appdata branch (called e.g. appstream/x86_64). Flatpak then mirrors this branch locally for each remote, and this data is used by gnome-software.

So, make sure your application ships an appdata file, and make sure you run build-update-repo to update the branches whenever an application changes.

Syncing updates

If you build a new version of your app on you will get an updated repository locally that has both the old build and the new build. You then need then to copy this updated repo to the webserver. Generally if you just copy the files over with e.g. rsync then things will work fine.

However, there are some race conditions if the new repo files are written at the same time as someone is pulling from the repository. For instance, if you write the summary file before all the objects are copied, then there is a short period where a new pull will not see a complete version of the commit. Another race is if you delete objects from an older version before the new commit is fully uploaded.

All these races are easily avoided by ordering the sync so that the summary file is uploaded after all the objects, and any deletes are done at the end. The ostree-releng-scripts repo has a script to do this.

Hosting

Many developers don’t run their own server to host things on, relying instead on services like github or gitlab to host releases. Unfortunately these are generally designed to host single-file releases (such as tarballs), rather than a multi-file repository.

Since flatpak repositories are just a bunch of static files using Amazon S3, or similar services is very effective. I personally use S3 for the Skype and Spotify flatpak repos, and the cost is about $0.01 a month so far. So, in practice this is essentially free. However, you still need to register with Amazon and have a credit card.

I haven’t found any perfect completely free alternative, but there are some workarounds:

On github, you can use the github pages feature. To do this you create a new git repo and then you commit the flatpak repository into the git repo. Then you enable github pages for the repo and point it at the master branch.

Here is an example of using this: https://github.com/alexlarsson/test-releases
With the flatpak repo itself available as: https://alexlarsson.github.io/test-releases/repo

The maximum size of a github pages size is 1GB, and there is a soft bandwidth limit at 100GB per month, so depending on the size of your application and how popular it is, this could be a useful approach.

On gitlab you can use the gitlab pages feature to do something similar.

10 thoughts on “Maintaining a flatpak repository”

  1. Very nice blog.
    For hosting you should check JFrog’s bintray.com, you can use a free oss generic repository for hosting the flatpack repo, or pay a little if you want the advanced features.

  2. I have some questions:

    1. What happens if we delete our local repo, create a new one and upload that? I suppose users won’t have problems updating the apps, it’ll just re-download more stuff.

    2. If I understand correctly, when we push a new version, the old version is still present in the repo, and it’ll generate new static deltas. So the repo size will grow and grow. Isn’t that a problem? Is it possible to remove old versions from a repo? Or does it replace the old version by the new one?

    With an old repo, when a user installs an app for the first time, does it download all static deltas (from the first version through all subsequent versions, i.e. the all history)? Or static deltas are generated differently? Does it download only the required data for the latest version, like a shallow git clone with the –depth argument?

    1. Sébastien:

      1) Creating a new repo should be fine, assuming you used the same gpg key. It will just never use deltas. Compare it to a git pull where you re-set the branch to some other commit that is independent on the original commit.

      2) flatpak build-update-repo will delete old static deltas, and if you add –prune-depth it allows you to prune old commits (and their corresponding static deltas). If you use “ostree prune” directly, it has some additional features.

    2. 3) Flatpak (and by default ostree) always pulls with –depth=0. Which is the same as a shallow git clone. Also, it only ever downloads one static delta, the one named “$mylocalcommit-$latestremotecommit” (where $mylocalcommit is optional if you’re pulling from scratch).

      1. Ok, I better understand how it works now. I was maybe a little confused by knowing that OSTree is the equivalent of git but for OS binaries. But the “push/pull” works differently by default.

  3. What if I have to change the GPG key an a production repo?
    Can I add more than one GPG Key for a successful transition or will it handle that automaticly?

Leave a Reply to alexl Cancel reply

Your email address will not be published. Required fields are marked *