If you actually read my blogs, I don’t know who does because I don’t normally advertise them. I switched over to the helix editor as my main IDE

Well, like all good things, they must come to an end, and I’m trying out VSCode again.

Just one problem though, I use Fedora Kinoite, an immutable operating system.

Immutable operating systems treat the /usr/ directory as a vendor supplied system, and disallow users to modify files in that tree. This means that I cannot install development tools like rust, and all the random development libraries that inevitably show up when building random repos (as I do).

There are a few options to remedy this, each with their own level of jank.

  1. I could create a toolbox, and install vscode within that toolbox. The problem is that the level of host-system integration is minimal, so while I will at least get a graphical window, I would have to deal with issues like the mouse cursor looking all adwaita-y.

  2. I could use the vscode flatpak package, which is much better integrated with the host system; HOWEVER, I’m not able to use development libraries from the system (not a problem in an immutable system), and more importantly, I can’t install development libraries at all within a flatpak.

  3. I could use the flatpak package, but utilize vscode dev containers for all my development. This has the advantage of being integrated with the host system on the application level, BUT suffers from having the repos I clone fully contained within the dev containers. Not to mention the incredibly complicated setup for remote development in flatpak vscode

I really like flatpaks. I think for most apps they are really the best choice for a linux user. For development tools, ehhhh not so much. That doesn’t mean we can’t make it work much better.

I set out after work on Friday to experiment with how we could make the UX better. What I really wanted to see were a few things:

  • Integration with the host’s /home/ directory (no building within devcontainers)
  • Dev dependencies are installed somewhere in the host’s /home/

The solution I first thought about was “What if there was a way to have a package manager inside of a flatpak?” and of course, everyone knows that the first solution one thinks of is the best solution, so I began my downward spiral into flatpak manifests.

Building RPM inside flatpak-builder

My first objective was to get RPM built and installed (the package manager that sits below DNF). This turned out to be quite easy. The Freedesktop SDK already had most of the dependencies needed to build RPM. I only needed to add one extra dependency. This is what the modules part of my manifest looked like at this point:

{
    "name": "rpm",
    "build-options": {
        "prefix": "/usr/lib/sdk/dnf",
        "env": {
            "PKG_CONFIG_PATH": "/app/lib/pkgconfig:/app/share/pkgconfig:/usr/lib/sdk/dnf/lib/pkgconfig:/usr/lib/sdk/dnf/share/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig"
        }
    },
    "buildsystem": "simple",
    "sources": [
        {
            "type": "archive",
            "dest": ".",
            "url": "https://ftp.osuosl.org/pub/rpm/releases/rpm-4.20.x/rpm-4.20.1.tar.bz2",
            "archive-type": "tar-bzip2",
            "sha256": "52647e12638364533ab671cbc8e485c96f9f08889d93fe0ed104a6632661124f"
        }
    ],
    "build-commands": [
        "mkdir _build",
        "cd _build && cmake .. -DCMAKE_INSTALL_PREFIX=/usr/lib/sdk/dnf -DCMAKE_INSTALL_LIBDIR=lib -DWITH_SEQUOIA=OFF -DWITH_AUDIT=OFF -DWITH_DBUS=OFF -DENABLE_PYTHON=OFF && make && make install"
    ],
    "modules": [
        "shared-modules/lua5.4/lua-5.4.json",
        {
            "name": "popt",
            "buildsystem": "simple",
            "sources": [
                {
                    "type": "archive",
                    "dest": ".",
                    "url": "http://ftp.rpm.org/popt/releases/popt-1.x/popt-1.19.tar.gz",
                    "archive-type": "tar-gzip",
                    "sha256": "c25a4838fc8e4c1c8aacb8bd620edb3084a3d63bf8987fdad3ca2758c63240f9"
                }
            ],
            "build-commands": [
                "./configure --prefix=/usr/lib/sdk/dnf",
                "make",
                "make install",
                "ln -sf /usr/lib/sdk/dnf/lib/pkgconfig/popt.pc /usr/lib/sdk/dnf/lib/pkgconfig/popt1.19.pc",
                "ln -sf /usr/lib/sdk/dnf/lib/pkgconfig/popt.pc /usr/lib/sdk/dnf/lib/pkgconfig/popt119.pc"
            ]
        }
    ]
},

Not so bad, eh?

All I needed to add was a module already present in the Flathub shared modules, and something called popt.

I figured after this, DNF would be a piece of cake.

Sadly, I was wrong, DNF is a much more complex beast.

Building DNF inside flatpak-builder

Firstly, there are two versions of DNF currently available, a Python-based DNF and a much newer C++ based DNF5. I chose (for future proofing) DNF5 to build inside flatpak.

My naive process was to try including DNF and building it immediately (knowing I didn’t have any of the dependencies). Then, one-by-one, I added dependencies until I could get it to start building. This process also involved learning how CMake works, and how to adjust compile options to make things work for what I wanted to build. I realize now how similar the compile options are to the cargo features functionality in Rust (I’m a rust programmer, don’t make fun of me plz).

I feel like I got pretty close, but after a long night and a good half of Saturday working on it, I tapped out in defeat. You can find my progress here, if you’re either curious or wanting to contribute: https://github.com/ryanabx/org.freedesktop.Sdk.Extension.dnf . Otherwise, I think I am putting a pin in dnf integration for now.

Any other options?

I wasn’t ready to completely give up on the idea of a package manager inside flatpak, so what other options do I have?

apt, pacman, and plenty of other package managers will probably have similar issues to dnf in terms of bootstrapping into the flatpak. I may try them long in the future, but as a Fedora user, I don’t have as much experience with them.

There is a package manager more oriented towards installing things in /home/ that I hadn’t considered until now…

Building Homebrew inside flatpak-builder

That’s right, I’m building Homebrew. Well, building is not as correct. Homebrew comes ready to be used out of the box. In fact, the homepage of homebrew has a very easy way to install it over the network:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

We are not going to do that though. We need to do a more custom installation to get it working inside a flatpak.

This is actually quite easy, as the source for homebrew is the released build (it must be using some sort of scripting language). This means we don’t actually have any other dependencies to install; thus, our manifest looks like this:

{
    "$schema": "https://raw.githubusercontent.com/flatpak/flatpak-builder/refs/heads/main/data/flatpak-manifest.schema.json",
    "id": "org.freedesktop.Sdk.Extension.Homebrew",
    "branch": "24.08",
    "runtime": "org.freedesktop.Sdk",
    "build-extension": true,
    "sdk": "org.freedesktop.Sdk",
    "runtime-version": "24.08",
    "sdk-extensions": [],
    "modules": [
        {
            "name": "Homebrew",
            "buildsystem": "simple",
            "sources": [
                {
                    "type": "git",
                    "tag": "4.6.0",
                    "url": "https://github.com/Homebrew/brew.git",
                    "dest": ".linuxbrew"
                }
            ],
            "build-commands": [
                "mv .linuxbrew /usr/lib/sdk/Homebrew"
            ]
        },
        {
            "name": "scripts",
            "sources": [
                {
                    "type": "script",
                    "commands": [
                        "mkdir -p $XDG_DATA_HOME/linuxbrew/.linuxbrew/",
                        "cp -rf /usr/lib/sdk/Homebrew/.linuxbrew/* $XDG_DATA_HOME/linuxbrew/.linuxbrew/",
                        "eval \"$($XDG_DATA_HOME/linuxbrew/.linuxbrew/bin/brew shellenv)\""
                    ],
                    "dest-filename": "enable.sh"
                }
            ],
            "buildsystem": "simple",
            "build-commands": [
                "cp enable.sh /usr/lib/sdk/Homebrew/"
            ]
        },
        {
            "name": "appdata",
            "buildsystem": "simple",
            "build-commands": [
                "install -Dm0644 org.freedesktop.Sdk.Extension.Homebrew.appdata.xml -t /usr/lib/sdk/Homebrew/share/metainfo"
            ],
            "sources": [
                {
                    "type": "file",
                    "path": "org.freedesktop.Sdk.Extension.Homebrew.appdata.xml"
                }
            ]
        }
    ]
}

I’ll explain how homebrew works a little bit in the next section.

The important bits here is that we clone the repo, put it in the SDK extension’s directory, and then I create an enable.sh script that sits at the SDK extension’s root. This is a standard script as far as I can tell, as the other SDK extensions have one too. Basically, the script is responsible for initializing the extension. In our case, it copies the homebrew source to a mutable directory (the flatpak’s data directory), and runs the initialization script that homebrew provides.

How does homebrew work?

Homebrew works by installing itself into /home/linuxbrew/.linuxbrew/, and all packages that you install through homebrew are localized into this directory. We cannot use the default installation because some flatpaks bind mount the host’s /home/ directory into the flatpak, and making a new folder in the /home/ directory is a privileged operation. Even if we could do this, we wouldn’t want to populate the home folder with flatpak-specific things anyways.

There is an alternative installation method though, defined here:

Untar anywhere (unsupported)

Technically, you can just extract (or git clone) Homebrew wherever you want. However, you shouldn’t install outside the default, supported, best prefix. Many things will need to be built from source outside the default prefix. Building from source is slow, energy-inefficient, buggy and unsupported. The main reason Homebrew just works is because we use bottles (binary packages) and most of these require using the default prefix. If you decide to use another prefix: don’t open any issues, even if you think they are unrelated to your prefix choice. They will be closed without response.

TL;DR: pick another prefix at your peril!

It does what it says on the tin though, most things that are installed have to be built from source! This sucks, obviously, and honestly I kinda expected there to be a more elegant solution for “bottles” - basically prebuilt packages. For some reason, they HAVE to match your prefix path, or else they won’t work. I don’t know why they couldn’t have just used the prefix path, well, as a prefix, and just push the results to the proper subdirectories of the prefix, but I guess either there’s something I don’t know about it that makes it impossible to do so, or I’m a goddamn genius. In any case, read this GitHub discussion I found for more info. They don’t explain super well why it can’t happen, but I assume there’s a reason.

Otherwise, we have a working Homebrew installation in a Flatpak environment! This was a lot of fun to do, as opposed to the dnf attempt (mainly because it succeeded).

You can find the Homebrew extension here: https://github.com/ryanabx/org.freedesktop.Sdk.Extension.Homebrew

And you can find the DNF extension attempt here: https://github.com/ryanabx/org.freedesktop.Sdk.Extension.dnf

That’s all for now!