Files
DarkRoom/docs/distribution.md
T
dtourolle 43f70c4765 Build the Windows installer in CI
The fourth leg of build-and-test.yml, in the shape of the Android one:
an image workflow that builds docker/windows and pushes it tagged by
the directory's tree id, and a job inside that image that lints the
Windows target — the only place the cfg(windows) branches are ever
compiled by CI — builds, runs the smoke tests docs/windows.md §6
specifies, packages, installs and uninstalls under Wine, and uploads
the installer. Every step was run by hand in the same container first.

The spec's open list closes with this: the four §3.2 items, the
licence page, and the leg. What remains is what Wine cannot show, and
§10 now lists it as the first real Windows run's checklist.
2026-09-12 07:34:10 +02:00

13 KiB

DarkRoom — Distribution

Satisfies: NFR-COMPAT-2 (v1 channels) · FR-PLAT-LIN-3 (sandboxed distribution) Companion to: requirements.md §3.8, §4.8 · storage.md

NFR-COMPAT-2 asks for the v1 channels to be stated, and says why in its own second sentence: the channel decision and the storage design are coupled. A channel is not a build target. It is a set of constraints that reach back into the code — what the application is allowed to see, what it may ask for, and what it must be able to do without asking. This document records which channels v1 targets and what each one costs, and it is where to look before adding a permission to a package rather than after.


1. The channels

Platform Channel State What it constrains
Linux Arch source package — packaging/PKGBUILD Built, in tree Nothing. Full filesystem access, system Vulkan, system secret daemon
Linux Flatpak — packaging/flatpak/ Manifest in tree, library selection does not work (§4) Portals only. No --filesystem=, no host mount table, no typed paths
Linux AppImage v1 channel, recipe not yet written (§5) Oldest supported glibc, and no sandbox at all
Android F-Droid v1 channel, not yet submitted GPLv3-clean build, reproducible, no proprietary blobs
Android Play Store Not v1 (§6) Would make ARCH §6.9 binding as policy rather than as engineering
Windows NSIS per-user installer, cross-built — windows.md Built by CI, untested on Windows. Not v1 Known folders in place of XDG; no sandbox; unsigned until there is a certificate

Four of these six exist as recipes and two do not. That is stated rather than smoothed over, because the value of writing the channels down is knowing which constraints are already being met and which are promises.

What every channel has to get right

Independent of packaging format, and each of these has bitten a package somewhere:

  • One identifier, four places. paris.tourolle.darkroom is the AppStream component id, the .desktop basename, the Flatpak application id, and the string dr_ui::run sets as the Wayland app_id and X11 WM_CLASS. A rename that misses one of them costs the icon in the shell or the association in the software centre, and neither failure announces itself.
  • The metainfo, not just the desktop entry. packaging/paris.tourolle.darkroom.metainfo.xml is the single description of the application, installed by every channel that has somewhere to put it. Its metadata_license is CC0-1.0 and its project_license is GPL-3.0-or-later; those differ on purpose — see the comment in the file.
  • Vulkan is a requirement, not a preference. The develop pipeline is compute shaders through wgpu, and NFR-R8 — how far a CPU fallback goes — is still open, so today there is nothing behind it. A package that installs onto a machine with no working ICD produces an application that starts and cannot develop.
  • A Secret Service implementation, or an honest degraded mode. FR-NC-2 is explicit that the absence of a secrets daemon is a stated degraded mode and never a silent fall back to plaintext. Packages express this as an optional dependency (the PKGBUILD) or a talk hole (the Flatpak manifest), never as a hard dependency — a headless or minimal-WM install is a supported way to run.
  • The face models are Git LFS objects. A checkout without git lfs pull has ~130-byte pointers where 11 MB models should be. Both the PKGBUILD and the Flatpak manifest check the file size and refuse, because the alternative is a package whose face indexing fails inside the graph loader on a user's machine rather than on the packager's.

2. Why Flatpak is the channel that matters most

Not because it is expected to be the most used. Because it is the only one that tests anything.

The Arch package and an AppImage both hand the application the same unrestricted process the developer runs it in, so neither can discover that a design assumed unrestricted access. Flatpak takes that assumption away, and FR-PLAT-LIN-3 exists to make the discovery happen deliberately rather than in a bug report. §4 is what it discovered.

The same argument runs the other way on Android, where SAF has been the only option since before the first line was written (ARCH §6.9) and SourceRef exists because of it. Linux got the abstraction — LocalStorage::grant is the one place a Path enters — and never got the constraint that would have proved it worked.


3. What already works inside the sandbox, unchanged

Worth listing, because it is the part FR-PLAT-LIN-1 quietly paid for in advance:

  • XDG directories. Flatpak redirects XDG_CONFIG_HOME, XDG_DATA_HOME and XDG_CACHE_HOME into ~/.var/app/paris.tourolle.darkroom/. Settings (settings_store.rs), accounts (dr_sync::account), the catalog and the thumbnail store all read those variables, so every one of them lands in the application's own directory with no code change and no permission.
  • The face models. system_face_models_dirs() reads $XDG_DATA_DIRS rather than hard-coding /usr/share, which is exactly why /app/share inside a Flatpak is found by the same lookup that finds the Arch package's copy.
  • Opening a photograph from a file manager. The .desktop entry declares the RAW MIME types and Exec=darkroom-desktop %F; under Flatpak the file is exported through the document portal and arrives in argv as a path under /run/user/$UID/doc/, which is mounted in every sandbox. main.rs takes paths from argv and collect() handles a file or a directory. This is genuine portal-mediated access and it needs nothing new.
  • The Nextcloud sign-in browser. open_in_browser spawns xdg-open; the freedesktop runtime's xdg-open forwards to the OpenURI portal, and portal calls need no --talk-name because Flatpak always permits them. FR-NC-1's "system browser, never an embedded webview" therefore holds inside the sandbox for the same reason it holds outside it.
  • Credentials. The keyring crate speaks the Secret Service D-Bus interface, reached through the session-bus proxy with one talk hole. The app password stays visible to secret-tool and Seahorse, which is what keeps it individually revocable by the user.

4. What does not work: choosing a library

FR-PLAT-LIN-3 is not satisfied today, and the manifest does not pretend otherwise.

A folder library is chosen by typing an absolute path. dr-sync-folder's provider declares SignIn::EndpointOnly with the placeholder /home/you/Pictures, and normalise_endpoint expands ~, requires the path to be absolute, and checks it with std::fs. Nothing in the tree calls the FileChooser portal — there is no ashpd, no rfd, and no toolkit file dialog anywhere in ui/, platform/ or core/.

Inside a sandbox with no --filesystem=, $HOME still resolves to the real home path but that directory holds only the application's own .var/app/… tree. So a typed ~/Pictures fails the exists() check and the launch screen says No folder at /home/you/Pictures. — a truthful message about a situation the user cannot fix from inside the application.

Import is blocked one step earlier. dr_plat::volumes() finds a camera card by reading /proc/self/mountinfo and the removable flag under /sys. A sandboxed process is in its own mount namespace, so the table it reads describes the sandbox; a card mounted at /run/media/… on the host is not in it. volumes() correctly returns an empty list, which the interface presents as "no card found" — right for the code, wrong for the user, who is looking at a card.

The permission that would hide this, and why it is not in the manifest

--filesystem=host makes both work immediately and is the thing FR-PLAT-LIN-3 names as the alternative to portals. Granting it would mean the sandboxed build never exercises the sandbox, which removes the entire reason for shipping one (§2). --filesystem=xdg-pictures is narrower and would be tempting, but it is still a static grant that lets a typed path resolve — it makes the same design work by not testing it, only in a smaller directory.

So the manifest grants no filesystem access at all. The consequence is stated plainly: a Flatpak built from this manifest can open photographs handed to it and cannot yet be pointed at a library.

What closes it

Two changes, in this order:

  1. A portal file chooser behind a platform seam. ashpd's OpenFileRequest with directory(true) returns a URI the document portal has exported, which the sandbox can read and which stays valid across restarts. It resolves to a real path under /run/user/$UID/doc/, so normalise_endpoint accepts it as it stands — canonicalize() on a fuse path returns the path itself. The seam matters more than the crate: this belongs beside LocalStorage::grant in dr-plat, which is already the one place a Path enters the application, and must not become a second way for ui/ to learn about paths.
  2. Removable volumes through the same door. There is no portal for "list the mounted cards". The honest answer is that under a sandbox imports_supported() should report the same false it reports on Android, for the same reason it gives there — the operation cannot be performed however hard the user tries — and the import flow should offer the folder chooser instead of a volume list.

Done when: a Flatpak built from packaging/flatpak/paris.tourolle.darkroom.yml, with its finish-args unchanged and no flatpak override applied, can select a library root, scan it, and write a sidecar back into it.

Running a Flatpak build before then

For testing the rest of the application inside the sandbox, grant the access per-installation rather than in the manifest, so the file that describes the application keeps telling the truth:

flatpak override --user --filesystem=~/Pictures paris.tourolle.darkroom

5. AppImage

A v1 channel, and the recipe is outstanding work rather than a decision to be made. What it will have to account for, none of which is a surprise:

  • glibc. An AppImage links against the oldest glibc it must run on, so it is built in a container with an old base rather than on a rolling-release developer machine. A release binary built on a current rolling-release host carries GLIBC_2.44 references and would run on almost nothing else.
  • What to bundle and what not to. The binary links fontconfig, freetype, expat, libpng, zlib, brotli and bzip2 — bundle those. It does not link Vulkan, libxkbcommon or either display-server library: wgpu dlopens libvulkan.so.1, and x11rb and wayland-client speak the wire protocols in Rust. The Vulkan loader and the ICD must come from the host, and bundling a loader is the classic way to break an AppImage on a driver it did not expect.
  • The models. ~15 MB of ONNX weights inside the image, or a first-run download. In-tree is consistent with how the Lensfun database ships and with NFR-SEC-5's local-first posture; the licence question (D13) is the same one it is everywhere else and is not made easier or harder by this channel.
  • No sandbox. An AppImage tests nothing about FR-PLAT-LIN-3. It is a convenience channel for distributions the PKGBUILD does not serve, and should never be the channel a portal problem is discovered on.

6. Android: F-Droid in v1, Play deferred

NFR-COMPAT-2 says Play distribution is what makes ARCH §6.9's constraints binding, and that is worth reading precisely, because the constraint is already met and would be met whatever the channel.

§6.9 is verified, not assumed: MANAGE_EXTERNAL_STORAGE is not grantable under Play policy, and READ_MEDIA_IMAGES would not help because proprietary RAW is not typed image/* by the platform scanner and does not appear in MediaStore.Images. SAF is the only route that works, so FR-PLAT-AND-1 asks for it unconditionally and SourceRef (ARCH §3.1) exists to make it possible. A sideloaded or F-Droid build could ask for broader permissions; it would gain nothing by doing so.

So the coupling runs the opposite way from how it is usually described. Play is deferred for a reason that has nothing to do with storage: GPLv3 distribution through Play is generally workable but has not been confirmed for this project (ARCH §14), and F-Droid has no such question. Confirming it is a licence-reading exercise; nothing in the storage design waits on the answer.


7. Where the recipes live

packaging/
  PKGBUILD                                  Arch source package
  paris.tourolle.darkroom.desktop           the desktop entry, installed by every channel
  paris.tourolle.darkroom.metainfo.xml      AppStream, installed by every channel
  flatpak/
    paris.tourolle.darkroom.yml             the manifest, and where the permissions are argued
  windows/
    darkroom.nsi                            the installer; docker/windows/package.sh drives it

packaging/ also accumulates built .pkg.tar.zst artefacts from local makepkg runs. Those are not part of any channel and should not be committed.