0d9feb0556342a7563dcdce7235dd7513fec200f
30
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
209ae36163 |
Free the desktop job's test binaries before its release build
Benchmarks / CPU and I/O (per commit) (push) Successful in 5m18s
Benchmarks / Frame budget (on demand) (push) Skipped
🐳 Android image / Build and push (push) Successful in 3s
Build and test / android-image (push) Successful in 3s
Build and test / Desktop (Linux) (push) Successful in 47m36s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / windows-image (push) Successful in 1s
Build and test / Layer separation (push) Successful in 32s
Traceability / Requirement traces (push) Successful in 1m37s
Build and test / Android (aarch64) (push) Successful in 30m39s
Build and test / Windows (x86_64, cross) (push) Successful in 35m42s
Build and test / Publish the release (push) Skipped
The single CI runner has one 99 GB disk shared with its container images. The desktop job reached 92 GB used (2.1 GB free) on v0.18.0's run, and on v0.18.1's the release build died with "No space left on device", so that tag has no release page. At rest the runner holds about 23 GB; the restored target cache, the models and the dependency build bring a job to about 78 GB before a test runs, and tests plus the release build take the rest. The test executables and examples in target/debug are the largest part of that, are relinked whenever a source changes, and are not what the cache exists for — the dependency rlibs are. Deleting them after the Test step, before the release build, frees several GB at the moment the job is fullest without costing the next run anything the cache would have saved. The step prints the disk afterwards, beside the existing Disk before/after lines. |
||
|
|
70583b9b2f |
Fail CI when the manual shows a picture no scene makes
The traceability job now runs `tools/manual/record.sh --check`: every picture docs/manual/README.md shows must be made by a scene in tools/manual/scenes.py, and every picture a scene makes must be shown. It reads the two files and nothing else, so it needs no app, display or LFS pull. --changed now dates a scene by the newest commit among its pictures rather than each picture alone. A scene that also makes a picture which re-records byte for byte (panorama-aligned beside panorama.gif) no longer stays listed for ever. A scene all of whose pictures come out identical (launch) stays listed until one differs, which costs one harmless re-run. |
||
|
|
9d1e31ffbb |
Fail CI when a key is bound but not in the gesture book, or listed but not bound
The gesture book is generated from GESTURE tags, so it could not describe a gesture nobody tagged, but nothing made anyone tag one. The arrow keys, Enter, P, X, U, Delete, F1 and F2 all worked in the grid with no line in the help sheet, and a tag could name a key whose handler had gone. Key handlers now compare one canonical string, Keys.chord(event) == "Ctrl+Z", instead of reading event.text and the modifiers themselves. keys.slint folds the key and its modifiers into that spelling, so the literal in the handler is the whole binding and the checker reads exactly what the handler dispatches on. Each handler carries a KEYMAP comment naming the gesture-book section its keys belong to, and a tag's keys field names its keys between backticks. gestures-check now fails when a handler binds a key no tag in that section names, when a tag names a key no handler there binds, when any .slint file other than keys.slint reads event.text, when a compared literal is not canonical, and when keys.slint's named keys drift from the Rust list. Spellings are normalised in one place, chord.rs: Ctrl+z, Control+Z and LeftArrow all mean what the handler's "Ctrl+Z" and "Left" mean. Shift and Alt count only for letters and named keys, because on the French layout every digit needs shift and a 6 has to be a 6 however it was typed. A Rust keymap that both dispatched and was read by the generator was the alternative. It would have moved the handlers' decisions away from the Slint state they depend on, and a window that forgot to install it would have had no working keys at all. The keys that were already bound and undocumented are now tagged. |
||
|
|
d8f26fb5cd |
Ship the manual with the Arch package, the Windows installer and the APK
The rendered manual was in the repository and nowhere else, so an installed application still had nothing to open. Each packager now carries docs/manual/index.html and its pictures, to where the application will look for them: /usr/share/darkroom/manual on Arch, manual\ beside darkroom.exe on Windows (where the models already are, and where dr_plat::system_data_dirs points), and assets/manual in the APK, stored rather than deflated since a GIF or PNG is already compressed. The manual is about 27 MB, which the APK and the installer both grow by; the pictures are 1600x1100 screenshots and short GIFs, and against an APK that already carries 170 MB of inference runtime and 70 MB of models they are not worth re-encoding for. The pictures are LFS objects, so each packager refuses a pointer where a picture should be, as it already does for the models: shipped, a pointer is a manual of broken images that nothing reports. The Android and Windows CI legs therefore fetch docs/manual/media, which they excluded while nothing they built read it, and the installer smoke test checks that the page and every picture were installed. |
||
|
|
10216355c1 |
Render the manual as one HTML page the application can carry
The manual existed only as docs/manual/README.md, which the forge renders and nothing else does. An installed copy of the application, on a laptop with no network or on a tablet, had no manual it could open. `traces manual` renders the README to docs/manual/index.html with pulldown-cmark (already in the tree as Slint's Markdown parser, so this adds a dependency edge and no crate). The page is one file with an inline stylesheet that follows the system's light or dark preference, a contents list of every section and subsection, and the pictures by their relative media/ paths. Each heading carries the id the forge gives it, so README.md#rating-and-flagging and index.html#rating-and-flagging are the same link. A picture alone in its paragraph becomes a figure whose alt text is shown as the caption, and every picture reserves its 16:11 box before it loads, so a jump into the middle of the page lands where it aimed rather than a screenful above. Links to design documents, which the installed page has no copy of, point at the forge. The page is committed rather than rendered at build time, as the gesture book is: it is user-facing text reviewed in the diff, and the three packagers then only copy it. `traces manual-check` fails in CI when the committed page is not the render of the README, and the pre-commit hook regenerates it when the README is staged. |
||
|
|
d6d27fb062 |
Publish a Gitea Release from CI on every v* tag
Benchmarks / CPU and I/O (per commit) (push) Successful in 2m28s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Successful in 45m9s
Build and test / Layer separation (push) Successful in 38s
Traceability / Requirement traces (push) Successful in 44s
🐳 Android image / Build and push (push) Successful in 5s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 5s
Build and test / windows-image (push) Successful in 1s
Build and test / Android (aarch64) (push) Successful in 14m25s
Build and test / Windows (x86_64, cross) (push) Successful in 34m30s
Build and test / Publish the release (push) Skipped
Nothing made a release. CI built the APK and the installer on the master push and kept them as workflow artefacts, the Linux binary was not kept at all, and most tags went out with no downloads until they were attached by hand. build-and-test now also runs on v* tags. On a tag the desktop job keeps its release binary, and a release job that needs desktop, Android and Windows collects the three, names them with the version and runs tools/publish-release.sh. The script titles and describes the release from the annotated tag's message as the server holds it, writes SHA256SUMS, and attaches what is not already there, so a re-run after an interrupted upload finishes the job instead of duplicating it. The same script is how a release is made or finished by hand. Tried on v0.14.1, whose release was made by hand with the same files: it found the release, reported all four files attached, and changed nothing. |
||
|
|
af162dd010 | Merge: origin's sync ordering and adoption work, its scan-complete trigger ported into library_ui/ | ||
|
|
bfadd9c409 |
Ship the border filler in the Windows installer, and count what is staged
Benchmarks / CPU and I/O (per commit) (push) Successful in 8m2s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 25m17s
Build and test / Layer separation (push) Failing after 0s
Traceability / Requirement traces (push) Failing after 0s
🐳 Android image / Build and push (push) Failing after 0s
Build and test / android-image (push) Failing after 0s
Build and test / Android (aarch64) (push) Skipped
🐳 Windows image / Build and push (push) Failing after 0s
Build and test / windows-image (push) Failing after 0s
Build and test / Windows (x86_64, cross) (push) Skipped
The installer smoke test asserted seven model files, the number on the
day it was written; models/face has since gained the eye-state trio's
companions and the int8 detector forms, and the run on
|
||
|
|
84fade99ec |
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. |
||
|
|
d790961b28 |
Add the manual: every feature pictured from the application itself
Benchmarks / CPU and I/O (per commit) (push) Failing after 30s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 47s
Build and test / Layer separation (push) Successful in 27s
🐳 Android image / Build and push (push) Successful in 3s
Build and test / android-image (push) Successful in 4s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / windows-image (push) Successful in 1s
Traceability / Requirement traces (push) Failing after 38s
Build and test / Android (aarch64) (push) Failing after 2m24s
Build and test / Windows (x86_64, cross) (push) Failing after 3m21s
docs/manual/README.md is a tour for a photographer opening DarkRoom for the first time — one picture per thing, moving where movement is the point. tools/manual/ is how the pictures are made: drive.py puppeteers the desktop build on a private Xvfb (launch, click, drag, type, screenshot, record), scenes.py is each picture as a script, and record.sh runs them all over a folder and writes the results into docs/manual/media/. The media is in LFS, with the CI pulls excluding it as they exclude the fixtures; a screenshot changes wholesale when the interface does. Nothing in the pictures shows a person, by design: the demo library is seventy urban and alpine frames, chosen from the catalog's rows that face detection found nobody in. The traceability matrix is regenerated here after the rebase that brought this branch up to master. |
||
|
|
44fdcbc6f7 |
Add the twelve-frame 6D panorama set as an LFS fixture
fixtures/pano/2025-08-05: _MG_8320 … 8331, one portrait hand-held sweep at 50 mm with a stop of shutter drift and sky in every frame — the set §3.11 is built against, with each of those facts named as the test it is. fixtures/** is tracked in LFS like the models but with the opposite default: CI's pulls exclude it, so a build never fetches 325 MB it does not use. |
||
|
|
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. |
||
|
|
c48896bd95 |
Merge: touch selection and drag, from the gallery-selection branch
Verified before merge: fmt clean, clippy -D warnings clean, 563 dr-ui tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> # Conflicts: # docs/traceability.md |
||
|
|
e8e96eed40 |
Measure the performance targets §8 has been promising, and fail on a regression
docs/requirements.md §8 has said since it was written that performance is verified by "an automated benchmark suite against a synthetic 50k catalog, run per-commit … A regression beyond stated tolerance fails the build." There was none. No benches/, no [[bench]], no criterion, no synthetic catalog, and three CI workflows that between them measured nothing. Ten performance requirements could therefore be neither passed nor failed, and five of them carried a TRACES: tag regardless. tools/bench is the half of that promise that can be kept honestly on a runner with no GPU and no display. # The fixture Rows are cheap and pixels are not, so it builds fifty thousand catalog rows over a pool of a dozen real files, each referenced by several thousand of them. Everything the catalog half touches is rows and is exact at full scale; everything the pixel half touches is one file at a time and does not care how many rows point at it. Fourteen megabytes on disk instead of two terabytes, and neither half is flattered by the trade. It is reproducible from a seed, and a stamp beside it — seed, row count, source size, dr-catalog's schema version — rebuilds it rather than letting a run be compared against a baseline that describes a different library. # What it can now pass or fail NFR-P1, and R2's second sentence with it: Catalog::open plus the count, first window and timeline the grid cannot paint without. The interesting part turned out to be the open itself — schema::backfill runs three passes over the images table on every open, which is O(library) work on a path whose budget is stated in absolute seconds. Tagged TRACES: NFR-P1, on a gate that fails if it breaks. NFR-P3: thumbnail throughput on the embedded preview path, through the same per-image work spawn_thumbnail_sweep does and in the same shape — chunks of 96, lanes owning disjoint slices, the single thread that owns the store writing the finished chunk. Mirrored rather than called, because that function takes a RemoteBackend and would measure somebody's network. Tagged TRACES: NFR-P3. # What it deliberately does not claim NFR-P7 is the whole chain, and only the encode half of it runs without an adapter. So the export row is a one-sided gate — over two seconds in the encode alone violates the requirement; under it proves nothing — and there is no TRACES: NFR-P7 anywhere. NFR-P8 is about the application at idle, and the probe is a process holding the catalog and nothing else, so it records the catalog layer's share and carries no budget until somebody decides what that share should be. No tag there either. CONTRIBUTING.md asks that a requirement be closed by a test that would fail if the behaviour were removed, and two more plumbing tags is what this repository already has too many of. NFR-P8 also gets the answer §4.1 demands: RSS is exclusive of device-local GPU allocations and cannot be made otherwise, because such an allocation never enters the process's address space. The requirement should be restated as two figures, and docs/benchmarks.md says so. # Two gates, and why one of them steps aside off the reference desktop The budget is the requirement's own number and never moves. The baseline is what the reference desktop last measured, and drifting 15% past it fails the build even while still inside the budget — which is how performance rot actually arrives, never over the line, always a little worse. A budget written for twenty-four threads cannot be asserted on a two-core container. §8 names the reference desktop, not CI, so each metric declares whether its budget is machine-sensitive; those are asserted under --reference and reported everywhere else. Catalog open is not one of them: two seconds against an expected figure two orders of magnitude smaller is a threshold any machine can be held to. This is the trap core/dr-gpu/tests/frame_budget.rs already refuses — a red gate everybody learns to ignore. # The baseline ships with no numbers in it Every recorded field is null, because nobody has run it yet. Writing plausible-looking figures would make every later comparison a comparison against a guess, and the first real regression would be invisible. Run `dr-bench record --reference` on the reference desktop and commit the diff; until then the budget gate works and the report says the other one cannot. # CI .gitea/workflows/benchmark.yml, and its own workflow rather than a step in build-and-test.yml: a red "Build and test" says the code is wrong, a red "Benchmarks" says it got slower, and the second must not be reachable by retrying a flaky compile. The cpu job runs on every push and builds -p dr-bench alone — which is why that crate depends on no GPU and no UI crate. The gpu job is the frame budget that already exists and already skips without an adapter, on workflow_dispatch, because building wgpu on every commit to rediscover that the runner has no device is not a use of anybody's minutes. |
||
|
|
e26f71d15d |
Gather every model under one tree at the repository root
The weights were in two places: face detection and recognition in `models/face/`, segmentation in `core/dr-segment/models/`. Nothing was wrong with either path, but between them there was nowhere to look to answer "how much model does this application carry", and that number is about to start growing. So the crate-local copy moves up beside the other. `models/` now holds `face/` and `segment/`, and a `du -sh` of one directory is the whole answer. No content changes: the .onnx and its vocabulary are byte-identical, and `LICENCE.md` moves up a level to cover the tree rather than one crate. The LFS pattern in `.gitattributes` is `*.onnx` and already matched both locations, so only its comment needed the new path. `include_bytes!` is relative to the source file and `build.rs` runs with the crate root as its working directory, which is why the two paths climb a different number of levels. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
091306f736 |
Gate the gesture vocabulary the way the matrix is gated
🐳 Android image / Build and push (push) Successful in 3s
Build and test / android-image (push) Successful in 3s
Build and test / Desktop (Linux) (push) Failing after 1h16m50s
Build and test / Layer separation (push) Successful in 39s
Traceability / Requirement traces (push) Successful in 50s
Build and test / Android (aarch64) (push) Successful in 22m27s
Three holes, all found by the gate catching itself out. **Prose that mentions the tag was read as a tag.** `dr-ui`'s module list carries a comment saying where the generated table comes from, and it names `GESTURE:` in passing; the scan extracted that sentence fragment as a gesture with no place and no way to perform it. A tag must now *open* its comment. A line that merely mentions it is describing the mechanism, not declaring a member of it, and position is the only thing that tells the two apart — which also makes the string-literal guard fall out for free rather than being a special case. **Neither artefact was regenerated on commit.** They cite line numbers, so they go stale on anything that moves a line — the sheet commit made the document wrong about every gesture in `library.slint` without touching a single one. The pre-commit hook that already keeps the matrix in step now keeps these too, and unlike the matrix it *fails* rather than shrugging when the scan does: a matrix that will not build leaves a stale one in place, where a malformed gesture block means a user about to be told the wrong thing. **CI did not check them at all.** It does now, blocking. The matrix is read; the gesture table is *shown to somebody using the application*, and a stale one tells them to perform a gesture that no longer exists — from which they will conclude the application is broken rather than the page. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d70f5c14e0 |
Name rust-analyzer everywhere the toolchain is installed
Build and test / Desktop (Linux) (push) Failing after 1h18m38s
Build and test / Layer separation (push) Successful in 48s
🐳 Android image / Build and push (push) Successful in 9m41s
Build and test / android-image (push) Successful in 9m41s
Traceability / Requirement traces (push) Successful in 48s
Build and test / Android (aarch64) (push) Successful in 21m54s
VS Code's rust-analyzer extension ships a server newer than 1.92.0 supports and prompts, on every window, to add one. Listing the component in rust-toolchain.toml makes rustup supply the server matching the pin — the same thing the pin buys everywhere else. The cost lands outside the editor, which is why this is four files rather than one. rustup reconciles that component list against the installed toolchain on the first cargo call in the work tree and downloads what is missing, inside whatever job happens to be running. An unasked-for fetch in the middle of a build step is nobody's line item and hard to find in a log. So every environment that builds this repo names it too: baked into the Android image, and in the install step of each CI job that rolls its own toolchain. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0bba882fb1 |
Read the Android API level out of the ELF, not out of file(1)
Build and test / Desktop (Linux) (push) Successful in 2h8m13s
Build and test / Layer separation (push) Successful in 1m3s
🐳 Android image / Build and push (push) Successful in 2s
Build and test / android-image (push) Successful in 2s
Traceability / Requirement traces (push) Successful in 41s
Build and test / Android (aarch64) (push) Successful in 1h0m55s
Every push has failed the Android job for weeks — back through fourteen runs —
with "FAIL: linked for Android 'unknown', expected 28", on a .so that was linked
perfectly correctly at API 28 the whole time.
The check ran `file` on the linked object and pulled the level out of its
description with a regex. `file` only prints "for Android 28" when its magic
database is new enough to decode `.note.android.ident`, and this image's is not
— it stops at "dynamically linked, not stripped". The regex matched nothing, and
`${API:-unknown}` turned that silence into a confident-looking failure about the
artefact rather than about the tool inspecting it.
The API level is the first word of that note, little-endian, so it is read
straight out of the ELF with `readelf` — which is in the image via
build-essential, and cannot go out of date the way a magic database can. An
absent note is now its own message rather than being folded into the mismatch
case, since "nothing states an API level" and "states the wrong one" are
different faults.
`file` stays in the image and in the log: it names the NDK that built the
object, which is worth having when this does go wrong. Nothing depends on it.
Verified inside the real container against the linked .so: the note reads
1c 00 00 00, and the step prints "OK: linked for Android 28".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
40e6334bb1 |
Sign the APK with a real key when one is configured
🐳 Android image / Build and push (push) Successful in 3s
Build and test / android-image (push) Successful in 3s
Build and test / Desktop (Linux) (push) Successful in 21m23s
Build and test / Layer separation (push) Successful in 29s
Traceability / Requirement traces (push) Failing after 28s
Build and test / Android (aarch64) (push) Failing after 33m28s
The APK has been debug-signed with a key generated on the spot, which is right for putting a build on a test device and useless for anything else: a different signature every run, so nothing can ever update in place. Four secrets now select a real signature -- ANDROID_KEYSTORE_BASE64 and its password, alias and key password. The names are JellyTau's, because that repo already signs its Android build this way against this same runner and one convention across both is one thing to remember. Absence of the secrets is not an error. A fork or a branch build has no access to them and should still produce an installable APK, so the debug path stays exactly as it was. The reverse is an error: if a keystore is supplied and cannot be read, the build fails rather than quietly falling back to a debug key, because a release that is silently debug-signed is worse than no release. Passwords reach apksigner and keytool as `env:`, never `pass:`. `pass:` puts the password in the process table for anything on the box to read. The keystore is written to a 0700 mktemp directory and never into the workspace, which is both what actions/cache saves and what the upload step globs. Also: upload-artifact drops from v4 to v3. v4 was a guess about what this Gitea supports. v3 is what JellyTau uploads its APK with on this runner today, which makes it the version known to work rather than the one that ought to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
56bdd457dd |
Stop the test build filling the runner's disk
🐳 Android image / Build and push (push) Successful in 4s
Build and test / android-image (push) Successful in 4s
Build and test / Desktop (Linux) (push) Successful in 1h58m2s
Build and test / Layer separation (push) Successful in 2m3s
Traceability / Requirement traces (push) Successful in 31s
Build and test / Android (aarch64) (push) Failing after 22m34s
The desktop job died mid-link with LLVM reporting "IO failure on output stream", which reads like a compiler crash and is not one: underneath it is `No space left on device`. The runner ran out of disk while linking. Worth knowing what it was spending it on. `target/debug` was 24 GB against `target/release`'s 2.6 GB -- the test build is roughly ninety percent of the footprint -- and of that, 15 GB was debug info in `debug/deps` and 3.6 GB was incremental state. Neither buys anything here. Nothing attaches a debugger to a CI run, and incremental compilation exists to make the second build in a working tree fast, which is not a thing a fresh checkout ever has. With both off the same tree is 3.3 GB, `debug/deps` 2.8 GB, and the test binaries build unchanged. Backtraces keep function names and lose file and line numbers; if a failure ever needs those, DEBUG=1 gives line tables back for a fraction of the 15 GB. A `df -h` either side of the expensive steps, so the next time this happens it says so in one line rather than as an error from LLVM. This is a mitigation and it should not be mistaken for the fix. It bounds what this job asks for; it cannot help if the runner is full of anything else, and 24 GB of build output is not obviously the largest thing on a host that also keeps every cached target directory this workflow has ever saved. If it fails here again, the disk needs looking at on draco-x86. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
834b219c3f |
Publish the Android APK as a build artefact
Build and test / Desktop (Linux) (push) Failing after 1h12m6s
Build and test / Layer separation (push) Successful in 41s
Traceability / Requirement traces (push) Successful in 23s
🐳 Android image / Build and push (push) Successful in 10m59s
Build and test / android-image (push) Successful in 11m2s
Build and test / Android (aarch64) (push) Failing after 22m52s
The android job proved the app links for aarch64 and then threw the result away. There was no APK anywhere in CI and no upload step in the repo at all, so a green run left nothing anybody could install -- the artefact list was empty by construction, not by failure. It now assembles the APK with the script package.sh uses and uploads it. The .so comes from the build the API-level check already ran; -o only adds a copy of it where the packaging step looks, so this costs one copy rather than a second twenty-minute cross-compile. The signing key is the part worth being careful about. KEYSTORE points at a mktemp directory rather than its default under target-android, because that directory is precisely what actions/cache saves and restores -- the default would have written a private key into the build cache and kept it there. Nothing but the .apk is uploaded. A fresh debug key each run is the right trade for an artefact meant to reach a test device: the only thing a stable key buys is installing over a previous build without uninstalling first, and a key that survives in cache storage to buy it is a bad exchange. if-no-files-found: error because the failure being guarded against is a green run with an empty artefact list, which reads as success right up until somebody goes looking for the file. Debug-signed, arm64-v8a only -- the ABI the job already builds. Neither is a release story; this is a build you can install. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
606f85df34 |
Send the LFS object endpoint one Authorization header, not two
🐳 Android image / Build and push (push) Successful in 2s
Build and test / android-image (push) Successful in 3s
Build and test / Desktop (Linux) (push) Failing after 1h12m2s
Build and test / Layer separation (push) Successful in 36s
Traceability / Requirement traces (push) Failing after 31s
Build and test / Android (aarch64) (push) Failing after 36m17s
The model fetch has been failing on every run with LFS: Client error: .../info/lfs/objects/0672d7a7... which reads like a rejected credential and is not one. The object is on the server and downloads fine; what fails is the shape of the request. `git lfs pull` makes two calls. The first, to `/info/lfs/objects/batch`, succeeds -- and Gitea answers it with a short-lived `Bearer` JWT scoped to that one object, for git-lfs to use on the second. git-lfs sends that JWT *and* the `Authorization` header this step had installed in git config, and two `Authorization` headers is a 400 from Gitea. Hence a client error on the object one step after the batch call it just made successfully, which is what made this look like an auth problem rather than a duplication. Confirmed directly against the server: the JWT alone on that URL is a 200, the JWT plus any second `Authorization` is a 400, and a lone token header that is merely wrong is a 401 -- so the scheme was never the issue. `lfs: true` on the checkout fails the same way and for the same reason, because actions/checkout persists a header of its own; the comment here blaming a credential the endpoint would not accept was wrong on both counts. So the headers are stripped -- checkout's included, since nothing later in either job talks to the remote -- and the token is handed to git-lfs as an ordinary credential instead. It authenticates the batch call and leaves the per-object JWT alone. This is what fails the Android job today: the build script sees a 133-byte pointer and panics by design, which is the message it is supposed to give and the one nobody could act on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
33847a0bbc |
Open one GPU device for the tests, and stop the checkout dying over LFS
Build and test / Desktop (Linux) (push) Failing after 9m18s
Build and test / Layer separation (push) Successful in 26s
Traceability / Requirement traces (push) Failing after 25s
🐳 Android image / Build and push (push) Successful in 3s
Build and test / android-image (push) Successful in 3s
Build and test / Android (aarch64) (push) Failing after 22m46s
Two CI failures, unrelated except that both were mine. **The test binary faulted under parallel threads.** Every GPU test opened its own `GpuContext`, and `cargo test` runs on as many threads as there are cores — so a full run asked the driver to bring up a dozen Vulkan devices at once and died with SIGSEGV. Serially it passed, which made it look like flakiness rather than a fault in the harness. One device now, behind a `OnceLock`: a `GpuContext` is an `Arc<Device>` and an `Arc<Queue>`, so sharing it is a refcount, and the losers of the race block until the winner is done. 416 tests now pass in parallel, in half the time twenty-six devices took. **`lfs: true` on the checkout made the checkout fail.** The intent was right — the model is in LFS, a plain checkout writes a 133-byte pointer, and the build script panics on it — but on this server `git lfs fetch` is rejected at `/info/lfs/objects/<oid>` with a client error: the credential `actions/checkout` installs for git is not one the LFS endpoint accepts. So a fetch problem presented as a checkout problem and took the whole job with it. The object is on the server; a clean clone over SSH with `git lfs install --local` pulls all 11 MB of it. It is now its own step with an explicit token, and `continue-on-error` so a credential problem cannot masquerade as a broken checkout — if it fails, the build still runs and fails with the build script's own message, which names the real problem. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ec8582032e |
Fetch the model CI has been building without
Both failing jobs failed for the same reason, and it was not the reason either of them appeared to fail. Desktop reported a Clippy failure and Android reported the API-level check; both were `dr-segment`'s build script panicking because `models/yolo26n-seg.onnx` was a 133-byte LFS pointer rather than the 11 MB model. No checkout step asked for LFS objects. `.gitattributes` predicted this exactly — "a clone without git-lfs gets a ~130-byte pointer file where the model should be" — and the build script fails loudly by design rather than embedding a pointer and failing at inference. That design worked; nothing was reading the message. It stayed hidden because the Android job built `-p dr-gpu`, which never reaches dr-segment. Building the app crate does, which is how one fix surfaced another. Only the two jobs that compile get `lfs: true`. `layering` runs `cargo tree` and builds nothing, so it has no reason to fetch 11 MB. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1297e3259b |
Stop claiming the app crates cannot cross-compile
The note above the core-crate check said the UI and app crates would join "once the Android shell exists". They already had. Building `darkroom-android` for aarch64 takes 4m23s and produces `libdarkroom.so`, linked for Android 28 — which is what the step below now checks, and what a device installs. Rewritten to say what the core check is actually for: a fast, link-free gate that fails early and names a smaller suspect, ahead of the full build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
83528c068a |
Check the API level of an object a linker actually produced
The step built `-p dr-gpu` and then looked for a `*.so` to read the API level out of with `file`. dr-gpu declares no `crate-type`, so it produces an rlib — an archive of object files no linker has touched, and about which `file` has nothing to say. The step could therefore only ever reach its own "no aarch64 .so was produced" branch, whatever the linker did, which is the one thing it exists to rule out. Builds `darkroom-android` instead: the workspace's only `crate-type = ["cdylib"]`, and the artefact that ships in the APK. The API level verified is now the one a device will refuse to install against. The neighbouring comment claiming only core crates cross-compile is left alone until the build proves otherwise; it is either stale or this commit is wrong, and the same run answers both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b044a8c067 |
Build the Android CI image in CI, not on a laptop
The Android job ran in gitea.tourolle.paris/dtourolle/darkroom-android:latest, a tag that had never been pushed. The image existed only as a local darkroom-android:latest on one machine, so every Android job died at docker pull with "manifest unknown" before reaching a step. The registry API confirms it: that manifest is a 404 while the other builder images answer 200. android-image.yml now builds and pushes it, following KPN's docker.yaml — host runner rather than a container, so it has the Docker daemon and the host's cached registry credentials, and a plain-git checkout because that host has no Node for actions/checkout. Where it diverges from KPN: that workflow gates on dorny/paths-filter running inside the builder image, which here would need the very image that is missing. The tag is the git tree hash of docker/android instead, which changes when and only when a file there changes. An unrelated push reuses the image, a Dockerfile edit cannot keep serving a stale latest, and a missing tag rebuilds itself without a manual step. The presence probe is curl against the registry API, not `docker manifest inspect`. The latter exits 1 on this registry even for tags that are plainly there — jellytau-builder:latest answers HTTP 200 while docker reports "manifest unknown" for it — and trusting it would have rebuilt 7 GB on every push. The HEAD request also yields Docker-Content-Digest, so latest is repointed only when the digests actually disagree, without pulling any layers. A probe that cannot authenticate falls through to building. Rebuilding when it was unnecessary costs minutes; skipping a build that was needed is the failure this commit exists to remove. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
03326242a1 |
Make the CI checks say what they mean, and format the workspace
The Android job's "Verify minimum API level" step has never verified the minimum API level. It took the first `*.so` anywhere under the target directory, which is a host proc-macro from debug/deps — an x86-64 object built by the runner's gcc, whose .comment section cannot mention Android and so can never contradict the expected value. It now reads the artifact under the target triple, compares against MIN_API parsed from the Dockerfile rather than a second copy of the number, and fails on a mismatch. Both sides are checked non-empty first: two failed parses would otherwise compare equal and pass, which is the same silent success in a new costume. The Android image installs one SDK package per layer and keeps the output. sdkmanager is a JVM program that aborts when it cannot get memory, and the single `> /dev/null` step reported that as a bare "exit code 134" while a retry re-downloaded everything that had already succeeded. tools/ci-local.sh runs all four jobs — desktop, android, layering, traceability — against the host toolchain, which is pinned to the same 1.92.0 CI installs. Its matrix check compares regeneration against the working tree rather than against HEAD: CI starts from a clean checkout, so git's answer is the right one there and reports every local run stale here. The rest is rustfmt across the workspace, and the clippy findings that surfaced once it did: manual_contains in dr-thumbs and collections_ui, a map iterated as pairs for its keys, an index loop over a slice, and two runtime assertions on a constant now made at compile time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4d78041d1d | Many imorovments | ||
|
|
0f202fd3f9 |
Add requirements traceability gate and Gitea pipelines
Ports JellyTau's traceability tooling to Rust, carrying across the bug it was repaired for. That gate divided a traced count by frozen literal denominators; the requirements file outgrew them and it reported 158% coverage, so it could never fail its own threshold. Two rules, both enforced by the extractor's own tests: - denominators parsed from docs/requirements.md at run time - coverage is |traced ∩ defined| / |defined|, never a raw traced count The gate additionally fails hard on a misconfigured run — zero requirements parsed or zero files scanned — rather than reporting a plausible 0%, and on any orphan tag naming a requirement that does not exist. Adapted for DarkRoom: IDs are FR-CAT-1 / NFR-P13 / FR-DEV-3a shapes rather than JellyTau's fixed three digits, and decisions (D), spikes (S), milestone items (M) and test ids remain taggable while being excluded from the denominator — counting them inflated it by 25. Also adds dr-sync: the RemoteBackend trait and capability model, so the Nextcloud connector is one implementation rather than the only shape the engine understands. No mature Nextcloud crate exists (reqwest_dav is too thin), so the connector will be hand-rolled over reqwest per D7. Gitea workflows follow the same style: containerised, commented with the reasoning, desktop and Android on every push, plus a CI check that no core/ crate depends on the UI toolkit (ARCH §6.5a). Coverage today: 13.3% (19/143). 50 tests passing. |