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.
276 lines
15 KiB
Markdown
276 lines
15 KiB
Markdown
# DarkRoom — Distribution
|
|
|
|
**Satisfies:** NFR-COMPAT-2 (v1 channels) · FR-PLAT-LIN-3 (sandboxed distribution)
|
|
**Companion to:** [requirements.md](requirements.md) §3.8, §4.8 · [storage.md](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`](../../packaging/PKGBUILD) | Built, in tree | Nothing. Full filesystem access, system Vulkan, system secret daemon |
|
|
| Linux | Flatpak — [`packaging/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](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`](../../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 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 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.
|
|
- **So are the manual's pictures.** Since 0.15.0 the Arch package, the Windows
|
|
installer and the APK carry `docs/manual/index.html` and its `media/`, where
|
|
`dr_ui::manual` looks for them (`/usr/share/darkroom/manual`, `manual\` beside
|
|
`darkroom.exe`, the APK's `assets/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_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. 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::grant` in `dr-plat`, the one place a `Path` enters the
|
|
application. It is in `ui/`, 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
|
|
|
|
1. **Build it and run it.** `flatpak-builder` is not installed on the machine
|
|
this is developed on, so the manifest has never produced a package.
|
|
2. **Removable volumes.** There is no portal for "list the mounted cards".
|
|
Under a sandbox `imports_supported()` should report the same `false` it
|
|
reports on Android, for the same reason — the volume list cannot be right
|
|
however hard the user tries — and leave `Browse…` as the way to a card.
|
|
|
|
**Done when:** a Flatpak built from
|
|
[`packaging/flatpak/paris.tourolle.darkroom.yml`](../../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:
|
|
|
|
```bash
|
|
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 `dlopen`s
|
|
`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.
|