From 38c2038d9b4b967994c9f6337ccaa48722489abf Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Mon, 31 Aug 2026 00:05:47 -0400 Subject: [PATCH 1/5] style(dng-real): apply nightly rustfmt to the real-camera conformance tier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cargo fmt --all` formats workspace members, and every crate under `tooling/` is a `[workspace].exclude` entry, so this crate has never been fmt-gated. Its four Rust files had drifted: `println!`/`format!`/`assert!` argument wraps in gaps.rs, probe.rs and real_corpus.rs, and one `read_to_string` wrap in lib.rs. The two SHA-256 tables in lib.rs get `#[rustfmt::skip]` instead. Every element is 11 chars, one over rustfmt's `short_array_element_width_threshold`, so rustfmt would break the 64 round constants and the 8 initial hash values one per line — 72 lines of noise in place of the spec's own rows. Skipping them keeps the diff at +27/-7 rather than +92/-17. Pure formatting: no behaviour changes. --- tooling/gamut-dng-real-conformance/examples/gaps.rs | 5 ++++- tooling/gamut-dng-real-conformance/examples/probe.rs | 11 +++++++++-- tooling/gamut-dng-real-conformance/src/lib.rs | 9 +++++++-- .../gamut-dng-real-conformance/tests/real_corpus.rs | 9 +++++++-- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/tooling/gamut-dng-real-conformance/examples/gaps.rs b/tooling/gamut-dng-real-conformance/examples/gaps.rs index a0b3f449..be2591ec 100644 --- a/tooling/gamut-dng-real-conformance/examples/gaps.rs +++ b/tooling/gamut-dng-real-conformance/examples/gaps.rs @@ -57,7 +57,10 @@ fn main() { println!(" preceded by: {:?}", before.map(|s| s.kind)); println!(" followed by: {:?}", after.map(|s| s.kind)); let peek_end = (end as usize).min(start as usize + 24); - println!(" bytes : {:02x?}", &data[start as usize..peek_end]); + println!( + " bytes : {:02x?}", + &data[start as usize..peek_end] + ); if let Ok(text) = std::str::from_utf8(&data[start as usize..peek_end]) && text.chars().all(|c| c.is_ascii_graphic() || c == '\0') { diff --git a/tooling/gamut-dng-real-conformance/examples/probe.rs b/tooling/gamut-dng-real-conformance/examples/probe.rs index b2f599ac..e217518a 100644 --- a/tooling/gamut-dng-real-conformance/examples/probe.rs +++ b/tooling/gamut-dng-real-conformance/examples/probe.rs @@ -113,13 +113,20 @@ fn report_decode(data: &[u8]) { SubImageData::Undecoded { compression, chunks, - } => format!("Undecoded(compression={compression}, {} chunks)", chunks.len()), + } => format!( + "Undecoded(compression={compression}, {} chunks)", + chunks.len() + ), SubImageData::Decoded(v) => format!("Decoded({} samples)", v.len()), _ => "Other".to_string(), }; println!( " kind={:?} {}x{} photometric={} bits={} spp={} -> {payload}", - s.kind, s.dimensions.width, s.dimensions.height, s.photometric, s.bits_per_sample, + s.kind, + s.dimensions.width, + s.dimensions.height, + s.photometric, + s.bits_per_sample, s.samples_per_pixel, ); } diff --git a/tooling/gamut-dng-real-conformance/src/lib.rs b/tooling/gamut-dng-real-conformance/src/lib.rs index 05f98b02..b1e1d76c 100644 --- a/tooling/gamut-dng-real-conformance/src/lib.rs +++ b/tooling/gamut-dng-real-conformance/src/lib.rs @@ -114,8 +114,8 @@ pub fn corpus_dir() -> PathBuf { #[must_use] pub fn manifest() -> Manifest { let path = corpus_dir().join("MANIFEST.toml"); - let text = std::fs::read_to_string(&path) - .unwrap_or_else(|e| panic!("read {}: {e}", path.display())); + let text = + std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display())); toml::from_str(&text).unwrap_or_else(|e| panic!("parse {}: {e}", path.display())) } @@ -125,6 +125,10 @@ pub fn manifest() -> Manifest { /// from the workspace, and the hash is needed for exactly one purpose. #[must_use] pub fn sha256(data: &[u8]) -> [u8; 32] { + // FIPS 180-4 round constants and initial hash values, laid out in the spec's own rows. + // Skipped because every element is 11 chars, one over rustfmt's + // `short_array_element_width_threshold`, which would otherwise break them one per line. + #[rustfmt::skip] const K: [u32; 64] = [ 0x428a_2f98, 0x7137_4491, 0xb5c0_fbcf, 0xe9b5_dba5, 0x3956_c25b, 0x59f1_11f1, 0x923f_82a4, 0xab1c_5ed5, 0xd807_aa98, 0x1283_5b01, 0x2431_85be, 0x550c_7dc3, 0x72be_5d74, 0x80de_b1fe, @@ -137,6 +141,7 @@ pub fn sha256(data: &[u8]) -> [u8; 32] { 0x748f_82ee, 0x78a5_636f, 0x84c8_7814, 0x8cc7_0208, 0x90be_fffa, 0xa450_6ceb, 0xbef9_a3f7, 0xc671_78f2, ]; + #[rustfmt::skip] let mut h: [u32; 8] = [ 0x6a09_e667, 0xbb67_ae85, 0x3c6e_f372, 0xa54f_f53a, 0x510e_527f, 0x9b05_688c, 0x1f83_d9ab, 0x5be0_cd19, diff --git a/tooling/gamut-dng-real-conformance/tests/real_corpus.rs b/tooling/gamut-dng-real-conformance/tests/real_corpus.rs index df281289..2fca4986 100644 --- a/tooling/gamut-dng-real-conformance/tests/real_corpus.rs +++ b/tooling/gamut-dng-real-conformance/tests/real_corpus.rs @@ -220,8 +220,13 @@ fn every_real_file_survives_a_preserving_rewrite() { } Err(e) => panic!("{name}: open: {e}"), }; - assert!(expect.rewritable, "{name}: opened, but manifest says it must not"); - let out = rewrite.write().unwrap_or_else(|e| panic!("{name}: write: {e}")); + assert!( + expect.rewritable, + "{name}: opened, but manifest says it must not" + ); + let out = rewrite + .write() + .unwrap_or_else(|e| panic!("{name}: write: {e}")); assert_eq!( maker_note_name(out.maker_note), From e0d0bd5bc8f6c9dcc644c280dd916ac747c9f6bc Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Mon, 31 Aug 2026 00:06:36 -0400 Subject: [PATCH 2/5] ci: fmt-gate the workspace-excluded tooling crates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cargo fmt --all` reaches workspace members, and `members = ["crates/*"]`, so no crate under `tooling/` has ever been fmt-gated — the drift the previous commit cleared had accumulated unnoticed because nothing was looking. This is wider than the compile gap `check-dng-real` closes. That one is unique to `gamut-dng-real-conformance`, the only excluded crate nothing depends on; the other fourteen oracles are compiled by `clippy --workspace --all-targets` through their path dev-dependencies. rustfmt has no such reach: a dependency edge does nothing for `fmt --all`, so it is blind to all fifteen. `fmt-tooling`/`fmt-tooling-check` iterate the manifests and hang off `fmt`/`fmt-check` via `depends_post`, so the hooks (hk.pkl) and CI's existing `mise run fmt-check` step inherit the gate with no second call site to keep in sync. The check variant deliberately does not stop at the first drifting crate, so one run names all of them. rustfmt compiles nothing, so the added cost is seconds and the `fast` CI job still builds no native code. Also corrects the sccache rationale comment in ci.yml, which claimed the vendored oracles "are workspace members under `tooling/`". They are the opposite — `[workspace].exclude` entries reached as path dev-dependencies. The comment's actual point survives: rust-cache drops them for being local path crates, which is why sccache earns its place. --- .github/workflows/ci.yml | 9 +++++---- mise.toml | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd2d9e3e..93095251 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,10 +12,11 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} # The vendored C/C++ oracles (libaom, dav1d, libavif, libheif, exiv2, libjpeg-turbo, the DNG SDK, -# libjxl, …) dominate CI: they are workspace members under `tooling/`, and Swatinem/rust-cache -# deliberately drops workspace crates — including their build-script OUT_DIRs, where every static -# archive lives — so before sccache each heavy job rebuilt all fourteen from scratch (~9 min, paid -# twice per PR). +# libjxl, …) dominate CI: they are local path crates — the `tooling/` oracles are +# `[workspace].exclude` entries reached as path dev-dependencies, and libjxl builds inside the +# member `gamut-jxl-sys` — and Swatinem/rust-cache deliberately drops local crates, including +# their build-script OUT_DIRs where every static archive lives, so before sccache each heavy job +# rebuilt all fourteen from scratch (~9 min, paid twice per PR). # # sccache is the right tool because it is content-addressed on preprocessed source + compiler + # flags: a `third_party/` submodule bump changes the content, changes the hash, and misses. There diff --git a/mise.toml b/mise.toml index 0f36cd53..1d48b4d5 100644 --- a/mise.toml +++ b/mise.toml @@ -46,11 +46,33 @@ run = "rustup component add rustfmt --toolchain nightly 2>/dev/null || rustup to description = "Format code (nightly rustfmt; merge-resilient imports)" depends = ["ensure-nightly-rustfmt"] run = "cargo +nightly fmt --all" +depends_post = ["fmt-tooling"] [tasks.fmt-check] description = "Check formatting without modifying (nightly rustfmt)" depends = ["ensure-nightly-rustfmt"] run = "cargo +nightly fmt --all --check" +depends_post = ["fmt-tooling-check"] + +# `cargo fmt --all` formats workspace *members*, and `members = ["crates/*"]` — so no crate under +# `tooling/` has ever been fmt-gated. This is wider than the compile gap `check-dng-real` closes: +# that one is unique to `gamut-dng-real-conformance`, the only excluded crate nothing depends on, +# whereas rustfmt is blind to all fifteen — a path dev-dependency edge makes a crate reachable to +# `clippy --workspace --all-targets`, but does nothing for `fmt --all`. Hung off `fmt`/`fmt-check` +# rather than exposed as a separate gate so the hooks (hk.pkl) and CI inherit it with no second +# call site to keep in sync. rustfmt compiles nothing, so iterating the manifests costs seconds. +# The glob tracks `[workspace].exclude`; a `tooling/` crate promoted to a member would just be +# formatted twice, which is idempotent. +[tasks.fmt-tooling] +description = "Format the workspace-excluded tooling crates (`fmt --all` stops at the workspace boundary)" +depends = ["ensure-nightly-rustfmt"] +run = "for m in tooling/*/Cargo.toml; do cargo +nightly fmt --manifest-path \"$m\" --all; done" + +[tasks.fmt-tooling-check] +description = "Check formatting of the workspace-excluded tooling crates" +depends = ["ensure-nightly-rustfmt"] +# Deliberately does not stop at the first drifting crate, so one run names every one of them. +run = "fail=0; for m in tooling/*/Cargo.toml; do cargo +nightly fmt --manifest-path \"$m\" --all --check || fail=1; done; exit $fail" [tasks.lint] description = "Lint with Clippy (warnings as errors)" From 0109d3fc7cefd6f29447a40b2dedeaca8c6b45b8 Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Mon, 31 Aug 2026 00:06:45 -0400 Subject: [PATCH 3/5] docs: record the lost-exec-bit recovery for target/debug/build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A build script that cannot be run — as distinct from one that fails to compile — is repeatedly misread as a code problem and "fixed" in the build script. It is neither. The binaries under `target/debug/build/` lose the owner-execute bit and land at `-rw-rwx---`, where the group keeps `x`, so `ls` output looks unremarkable while cargo, which runs as the owner, cannot execute them. Documented next to the `tooling/build-env` paragraph because it is the adjacent trap: both are environmental failures that present as native-build breakage and invite a spurious change to the build. States the symptom and the recovery only — the mechanism that clears the bit is outside this repo and not yet established, so the note does not name a cause it cannot support. --- AGENTS.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 4d288c63..dc213112 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -157,6 +157,13 @@ chokepoint. So a native build failure is **not** explained by the invoking shell settings — do not "fix" it by overriding `CC`/`CXX` per command. `GAMUT_BUILD_KEEP_ENV=1` opts out (and confirms a suspected env interaction is real). +A build-script failure that is **not** a compile error — cargo reporting that it could not run +the build script itself — is usually a lost owner-execute bit under `target/debug/build/`, not a +code problem. The damaged mode is `-rw-rwx---`: the group keeps `x`, so `ls` output looks +unremarkable, but cargo runs as the owner and the owner class is what the kernel checks. Recover +with `chmod -R u+x target/debug/build/` and re-run. The cause is environmental and outside this +repo — nothing here sets file modes — so do not work around it by editing build scripts. + ## Conventions - All `pub` items need doc comments. Mark fallible/owning return types `#[must_use]` where From 34bf5cbf67feb4e99cd274488ccad980401348c6 Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Mon, 31 Aug 2026 03:43:07 -0400 Subject: [PATCH 4/5] ci: scope the tooling fmt gate to the tooling crates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cargo fmt --all` means "all packages, and also their local path-based dependencies", so running it per excluded manifest followed those edges out of `tooling/` and back into the workspace: 459 file-visits across 212 files, 181 of them under `crates/` and already covered by the `fmt --all` the task runs first. It also reported one drifting file once per dependent crate that could see it — mangling `libtiff-oracle/src/lib.rs` named it three times over. Dropping `--all` leaves each manifest formatting its own package, and the loop already iterates all fifteen: 31 file-visits over the same 31 `tooling/` files, none duplicated, none under `crates/`. Coverage is unchanged — verified the gate still catches drift in lib, examples and tests, the last two being where the drift this branch fixes actually was. --- mise.toml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/mise.toml b/mise.toml index 054a71d2..47ac869c 100644 --- a/mise.toml +++ b/mise.toml @@ -63,16 +63,23 @@ depends_post = ["fmt-tooling-check"] # call site to keep in sync. rustfmt compiles nothing, so iterating the manifests costs seconds. # The glob tracks `[workspace].exclude`; a `tooling/` crate promoted to a member would just be # formatted twice, which is idempotent. +# +# Deliberately *not* `--all` on each manifest: that flag means "all packages, and also their local +# path-based dependencies", so `gamut-dng-real-conformance` alone drags the whole `crates/` tree +# back in — 181 of the 212 files such a loop reaches, every one already covered by the `fmt --all` +# above — and reports one drifting file once per dependent crate that can see it. Iterating the +# manifests bare visits each crate's own targets (lib, examples, tests) exactly once: the same +# `tooling/` coverage, 31 file-visits instead of 459. [tasks.fmt-tooling] description = "Format the workspace-excluded tooling crates (`fmt --all` stops at the workspace boundary)" depends = ["ensure-nightly-rustfmt"] -run = "for m in tooling/*/Cargo.toml; do cargo +nightly fmt --manifest-path \"$m\" --all; done" +run = "for m in tooling/*/Cargo.toml; do cargo +nightly fmt --manifest-path \"$m\"; done" [tasks.fmt-tooling-check] description = "Check formatting of the workspace-excluded tooling crates" depends = ["ensure-nightly-rustfmt"] # Deliberately does not stop at the first drifting crate, so one run names every one of them. -run = "fail=0; for m in tooling/*/Cargo.toml; do cargo +nightly fmt --manifest-path \"$m\" --all --check || fail=1; done; exit $fail" +run = "fail=0; for m in tooling/*/Cargo.toml; do cargo +nightly fmt --manifest-path \"$m\" --check || fail=1; done; exit $fail" [tasks.lint] description = "Lint with Clippy (warnings as errors)" From ccb6c7b0407142c8a649095eb733ab36e1cbb9b1 Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Mon, 31 Aug 2026 14:59:09 -0400 Subject: [PATCH 5/5] fix(ci): correct the tooling fmt-gate scoping measurements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four figures in the comment above `fmt-tooling` were produced by parsing rustfmt's verbose output, which lists only a subset of the files it visits, so every one of them undercounted. Re-measured by ground truth — dirty every `.rs` file in the tree, then read back which paths the gate reports: file-visits distinct files of which under `crates/` with --all 1131 518 483 without 35 35 0 The argument the comment makes is unchanged and stronger. What `--all` drags in is 483 of 518 files, not 181 of 212, and the duplicate reporting it causes peaks at 18 repetitions of a single file (`crates/gamut-jxl/tests/common/mod.rs`), not the three a narrower probe had suggested. Also records what the numbers alone do not: the `tooling/` coverage is set-identical between the two forms — the same 35 files across all 15 excluded crates, each visited exactly once — so dropping the flag costs no reach. Comment only; no task body, glob, or `depends_post` wiring changes. --- mise.toml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mise.toml b/mise.toml index 47ac869c..d3ad33a7 100644 --- a/mise.toml +++ b/mise.toml @@ -66,10 +66,11 @@ depends_post = ["fmt-tooling-check"] # # Deliberately *not* `--all` on each manifest: that flag means "all packages, and also their local # path-based dependencies", so `gamut-dng-real-conformance` alone drags the whole `crates/` tree -# back in — 181 of the 212 files such a loop reaches, every one already covered by the `fmt --all` -# above — and reports one drifting file once per dependent crate that can see it. Iterating the -# manifests bare visits each crate's own targets (lib, examples, tests) exactly once: the same -# `tooling/` coverage, 31 file-visits instead of 459. +# back in — 483 of the 518 files such a loop reaches, every one already covered by the `fmt --all` +# above — and reports one drifting file once per dependent crate that can see it, up to 18 times +# for a single file. Iterating the manifests bare visits each crate's own targets (lib, examples, +# tests) exactly once: the same `tooling/` coverage — set-identical, all 35 files across all 15 +# crates — for 35 file-visits instead of 1131. [tasks.fmt-tooling] description = "Format the workspace-excluded tooling crates (`fmt --all` stops at the workspace boundary)" depends = ["ensure-nightly-rustfmt"]