distribution.md §4, outstanding.md's FR-PLAT-LIN-3 entry, the Flatpak
manifest's comment and the README all said a library was chosen by
typing a path and that nothing in the tree called the FileChooser
portal. Since 6683c14 every folder the desktop asks for is chosen
through rfd's xdg-portal backend, so those sentences were false.
What they now say instead is narrower than "it works in the sandbox":
no Flatpak has been built here, so whether the portal's path opens a
library, holds across a restart and takes a sidecar is unobserved, and
volumes() still cannot see a host card. The chooser also landed in ui/
rather than behind the dr-plat seam distribution.md had proposed, and
both documents say so. FR-PLAT-LIN-3 gets a status note to the same
effect.
15 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; folders chosen through the FileChooser portal since 0.17.0, never built or run here (§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.darkroomis the AppStream component id, the.desktopbasename, the Flatpak application id, and the stringdr_ui::runsets as the Waylandapp_idand X11WM_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.xmlis the single description of the application, installed by every channel that has somewhere to put it. Itsmetadata_licenseis CC0-1.0 and itsproject_licenseis 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 was decided on 2026-09-19 against a CPU pipeline: without a device the library, the grid and the judgements work on embedded previews, and develop and export say they are unavailable. 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 pullhas ~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. - So are the manual's pictures. Since 0.15.0 the Arch package, the Windows
installer and the APK carry
docs/manual/index.htmland itsmedia/, wheredr_ui::manuallooks for them (/usr/share/darkroom/manual,manual\besidedarkroom.exe, the APK'sassets/manual), and each refuses a pointer where a picture should be — shipped, a pointer is a manual of broken images that nothing reports. The Flatpak manifest does not install it yet.
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, and what has been done about it.
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_HOMEandXDG_CACHE_HOMEinto~/.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_DIRSrather than hard-coding/usr/share, which is exactly why/app/shareinside a Flatpak is found by the same lookup that finds the Arch package's copy. - Opening a photograph from a file manager. The
.desktopentry declares the RAW MIME types andExec=darkroom-desktop %F; under Flatpak the file is exported through the document portal and arrives inargvas a path under/run/user/$UID/doc/, which is mounted in every sandbox.main.rstakes paths fromargvandcollect()handles a file or a directory. This is genuine portal-mediated access and it needs nothing new. - The Nextcloud sign-in browser.
open_in_browserspawnsxdg-open; the freedesktop runtime'sxdg-openforwards to the OpenURI portal, and portal calls need no--talk-namebecause 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-tooland Seahorse, which is what keeps it individually revocable by the user.
4. Choosing a library: built, not yet proved in the sandbox
FR-PLAT-LIN-3 was not satisfied up to 0.16.0, and is still not shown to be. What changed in 0.17.0 is the code; what has not changed is that no Flatpak has been built here, so nothing below has been observed inside one.
Up to 0.16.0 a folder library was chosen by typing an absolute path, and
nothing in the tree called the FileChooser portal. 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 failed the exists() check and the launch screen said so — a
truthful message about a situation the user could not fix from inside the
application.
Every folder the desktop asks for is now chosen in the platform's
dialogue (FR-EXP-6): the library folder on the launch screen (Choose folder…), an import's source and second copy, a folder or file of
Lightroom presets, and an album's folder on this device.
ui/dr-ui/src/folder_dialog.rs asks through rfd with its xdg-portal
backend — org.freedesktop.portal.FileChooser over D-Bus, which Flatpak
always permits without a --talk-name — and the common item dialogue on
Windows. No path is typed anywhere on the desktop any more; Android keeps
its fields (Pickers.local-paths), because SAF returns document trees rather
than paths.
What the portal hands back inside a sandbox is a path under
/run/user/$UID/doc/ that the document portal has exported, and the chosen
library goes through the same normalise_endpoint a typed one did, which
checks it with std::fs. That is the route this section used to ask for —
rfd drives ashpd underneath, the crate it named. Two things differ from
the plan, and both are stated rather than smoothed over:
- It is not behind a platform seam. The plan put the chooser beside
LocalStorage::grantindr-plat, the one place aPathenters the application. It is inui/, and the path it returns reaches the folder connector as a string, as a typed one did. - Nothing has confirmed the sandbox half. Whether the exported path still resolves after a restart — a library is remembered across launches, so it has to — and whether the export is writable, so a sidecar can be written beside a photograph, are the first two things a Flatpak build has to check.
Import is still 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" — but the import page now offers Browse… beside that
message, and the dialogue it opens is the portal's, which can reach the 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 path resolve without the portal — it makes
the same design work by not testing it, only in a smaller directory.
So the manifest grants no filesystem access at all, and a Flatpak built from it reaches the user's photographs only through what the portal hands it.
What closes it
- Build it and run it.
flatpak-builderis not installed on the machine this is developed on, so the manifest has never produced a package. - Removable volumes. There is no portal for "list the mounted cards".
Under a sandbox
imports_supported()should report the samefalseit reports on Android, for the same reason — the volume list cannot be right however hard the user tries — and leaveBrowse…as the way to a card.
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, write a sidecar back into it, and open it again after a
restart.
Running a Flatpak build before then
If the portal's path turns out not to hold across a restart, the rest of the application can still be tested inside the sandbox by granting 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.44references 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
dlopenslibvulkan.so.1, andx11rbandwayland-clientspeak 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.