Flatpak packaging is done by flatpak-builder and needs some preparation. Since the final build process happens inside a sandbox with no network access we need to provide lists with all dependencies pnpm-sources and rust-sources.
flatpak-builder pre-fetches these declared sources so they trigger no network calls in the build process. Some direct links to dependencies have no entries in package.json and would not be prefetched by flatpak-builder so we have to inject them. See tool_inject_linked_deps
The build happens in three distinct phases, where the last one is performed in the network-less sandbox.
Manually executed - either in a codespace (after ./setup.sh) or on a local
machine (optional inside a docker container). It resolves the dependency graph
and writes the sources list in generated/*.json that will be commited.
These files include pnpm-sources.json holds ~900 entries, one per npm tarball
(URL + sha512), the electron/esbuild/node-gyp caches and a few generated
helper files (populate_pnpm_store.py, pnpm-manifest.json). Rust deps are
listed the same way in sources-rust.json.
This is also where the from-source core is linked into the lockfile (link_local.sh) and the linked packages' otherwise invisible runtime deps are injected (tool_inject_linked_deps.mjs).
The changes need to be committed so the flatpak builder that runs on flathub will process them. (flatpak-builder can also be run inside a local docker container to test the result)
flatpak-builder reads the sources: list and materializes every entry onto disk
before entering the sandbox. Network access is restricted to exactly the URLs
and hashes declared in generated/ (content addressed, cached). Afterwards every
npm tarball sits as a plain file under flatpak-node/pnpm-tarballs/, the caches
are extracted, and the helper scripts are written.
Everything in the manifest's build-commands runs here, inside bwrap with no
network. This is where the store is prefilled and then consumed:
populate_pnpm_store.py … pnpm-tarballs pnpm-store— rebuilds the offline pnpm (v11, content-addressed) store from the raw tarballs fetched in phase 2. The store is never committed or downloaded whole; it is reconstructed offline from the individual tarballs on every build.storeDir: …/pnpm-storeis appended topnpm-workspace.yamlso pnpm uses it.pnpm install --offline --frozen-lockfileresolves the committed lockfile entirely from that store — no network.- the desktop is built and packaged with electron-builder.
Note: flatpak-node-generator would normally run
populate_pnpm_store.pyitself as an auto step during phase 2. We strip that (thejqfilter ingenerate.sh) and run it ourselves in phase 3 — see the comments inbuild-commands.
The easiest environment is a github Codespace, but the generate step can equally be run with Docker or on a native checkout. The release steps themselves are the same:
-
create a new PR release-x.x.x
-
edit
generate.shin codespace and change the tags forCORE_CHECKOUT=vx.x.x DESKTOP_CHECKOUT=vx.x.x -
add the new release in
<releases>in chat.delta.desktop.appdata.xml- add a link to the Release Changelog
- add some more info about the release
- see Release info
-
start the setup script in console
./setup.sh -
start the generate script in console
./generate.sh -
commit all changes
-
wait for the build of the preview
-
install the preview locally and check if it works
-
after merging the PR the new version will be released
Alternative to the Codespace: only docker is needed on the machine, nothing else gets installed.
./docker/generate_in_docker.shThis builds a container image with all dependencies (node, pnpm,
flatpak-node-generator, ...) and runs generate.sh
inside it. Local checkouts of deltachat-desktop and core are expected next to
this repo (in ../deltachat-desktop and ../core, override with
DESKTOP_REPO=... CORE_REPO=...). They are mounted read-only and cloned inside
the container, so they stay untouched — which also means uncommitted changes in
them are not used; generate.sh builds the tags it has pinned.
Clones and caches are kept in the docker volume chat-delta-generate-work
between runs (docker volume rm chat-delta-generate-work to start fresh).
./docker/generate_in_docker.sh bash opens a shell inside the environment.
Afterwards review and commit the changes in generated/ before you trigger a flathub build.
To run the full flatpak-builder build without installing flatpak and the
runtimes on the machine (useful for verifying manifest changes end-to-end
without a CI round-trip):
./docker/build_in_docker.shThis builds a container image (docker/Dockerfile.flatpak) with flatpak +
flatpak-builder and runs the build against the current working tree, so
uncommitted manifest changes are included. It needs the generated/ cache
(run docker/generate_in_docker.sh first if needed) and network
access (github repos, npm tarballs, electron, flatpak runtimes). Only the
host's architecture is built.
The flatpak runtimes, the flatpak-builder cache and the build dir live in the
docker volume chat-delta-flatpak-work and are reused between runs
(docker volume rm chat-delta-flatpak-work to start fresh). The first run
downloads several GB of runtimes and compiles the Rust core, so expect
that to take some time; later runs are much faster. ./docker/build_in_docker.sh bash
opens a shell in the environment (the packaged app is at /work/build-dir/files/delta
inside the volume).
The build above leaves the app inside the volume, not on the host. To turn it
into a single installable bundle (./chat.delta.desktop.flatpak), export it with:
./docker/bundle_in_docker.shThen install and run it with the host's flatpak (needs flatpak installed on
the host). The app depends on the Electron base app, so install that once:
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install --user flathub org.electronjs.Electron2.BaseApp//25.08 # once
flatpak install --user ./chat.delta.desktop.flatpak
flatpak run chat.delta.desktopThe locally built bundle is unsigned, so flatpak asks you to confirm the install.
Remove it again with flatpak uninstall --user chat.delta.desktop.
Notes on why it works the way it does:
- The container runs as root and uses a system flatpak installation,
because
bwrap(which flatpak-builder uses for every build step) cannot set up its uid map as a non-root user inside docker, whileflatpak --userrefuses to run as root. - It runs with
--privileged(plus/dev/fuse) so the nested bwrap/user namespaces are allowed. This is a local trusted-developer tool and is not part of the flathub build.
If you'd like to locally build this flatpak, you'll need both flatpak
and flatpak-builder installed. E.g. on Debian you can run apt install flatpak flatpak-builder to install these tools. See
https://flatpak.org/setup/ for more information on this for your
platform.
If you haven't done so yet, you need to have flathub set up as a remote repository:
flatpak remote-add --if-not-exists \
flathub https://flathub.org/repo/flathub.flatpakrepo
To simply build the application in a build-directory invoke
flatpak-builder pointing to the manifest:
flatpak-builder --install-deps-from=flathub build-dir chat.delta.desktop.yml
To install the local build you can add the --install flag. To
upload the built application to a repository, which can just be a
local directory, add the --repo=repo flag.
Each commit to the https://github.com/flathub/chat.delta.desktop master branch will result in a new release being published to flathub. So once a pull request is merged no more work needs to be done to publish the release.
A third option besides Codespaces and
Docker: run generate.sh
directly on your machine. You need nodejs >= 22 and python3.
Make sure this repo is checked out in its own folder with nothing else beside it
(otherwise setup.sh may not do what it should), then run ./setup.sh to clone
the sibling repos and install the tooling.
to reset you can run
rm -rf ../.venv/ ../deltachat-* ../flatpak-builder-tools/But be careful as this could destroy your work if you haven't followed the instructions above correctly.
manual setup (instead of setup.sh)
install the flatpak-node-generator tool with pipx:
git clone https://github.com/flatpak/flatpak-builder-tools.git
pip install pipx
pipx install flatpak-builder-tools/nodeinstall nodejs >= 22 (e.g. via fnm or nvm)
create a python virtual env, activate it, then install the generator's python deps:
python -m venv .venv
source .venv/bin/activate
pip install aiohttp toml tomlkit
Then set the target tags at the top of generate.sh and run ./generate.sh:
# edit these in generate.sh
CORE_CHECKOUT=vX.Y.Z
DESKTOP_CHECKOUT=vX.Y.ZThe remaining release steps (PR, appdata entry, preview, merge) are the same as
the release flow above — only the environment differs.
To verify the result locally, build with flatpak-builder (see
Building locally); --ccache speeds up repeat builds:
rm -r build-dir/ || true && flatpak-builder --install-deps-from=flathub build-dir chat.delta.desktop.yml --ccache