Put the developer docs under docs/dev and index the folder for users first

docs/ had 26 developer documents flat beside the manual, and the two
audiences are very differently sized: most readers want the manual and
the gesture reference, a few want the register, the designs and the
measurements. The manual and gestures.md stay at the top; everything for
someone changing the code moves to docs/dev/, and the two documents that
name their own successors — the v0.1 milestone and the UI-refinement plan
— go to docs/dev/archive/ rather than being deleted, since both are still
cited. docs/README.md is the index, users first.

Every reference follows: code comments, Cargo manifests, the workflows,
the pre-commit hook, the bench and traceability tools (which locate the
repo root by docs/dev/requirements.md now), packaging, the Docker READMEs,
CLAUDE.md, CONTRIBUTING.md and the README. The matrix links one level
deeper and is regenerated. Links out of the moved documents into the tree
gain a level; a link checker over every Markdown file finds none broken.
This commit is contained in:
2026-09-20 16:20:15 +02:00
parent 08727cff5a
commit 6b1aac477d
137 changed files with 658 additions and 572 deletions
+254
View File
@@ -0,0 +1,254 @@
# 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, **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](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 — 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`](../../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:
```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.