From 49243c29bff55fc2988501772dd0213a03db59a5 Mon Sep 17 00:00:00 2001 From: Babken Egoian <101829110+green2grey@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:39:08 -0700 Subject: [PATCH 1/4] feat: async + sandboxed image loading via glycin with GDK fallback Replace the synchronous Texture::from_file in load_file with async decoding off the main thread (implements ADR-0004, NFR-001/NFR-002): - new decode module: glycin sandboxed loader process when installed, in-process GDK decoding on a worker thread otherwise - backend chosen once per session by probing installed glycin loaders; choice is logged at first decode - fallback is strictly session-wide, never per-file: a file glycin rejects is a failed load and is never re-fed to the unsandboxed GDK decoder - decode_job_id guard (same monotonic pattern as OCR) drops stale decodes during fast navigation; FileInfo emission and OCR kickoff move into the finish_decode completion handler - previous image stays visible under the busy spinner until the new decode completes; spinner carries through into OCR - remove the unused glycin cargo feature (crate is pure Rust and now always compiled) - packaging: add glycin-loaders module to the Flatpak manifest (GNOME runtime does not ship it); update PKGBUILD optdepends - docs: ADR-0004 implementation notes, PHASED_PLAN Phase 7, CLAUDE.md current state, README/DEPENDENCIES/DECISIONS wording --- .claude/CLAUDE.md | 12 +-- Cargo.lock | 1 + README.md | 2 +- adrs/ADR-0004-Image-Decoding.md | 14 +++ crates/quickview-ui/Cargo.toml | 10 +-- crates/quickview-ui/src/decode.rs | 93 ++++++++++++++++++++ crates/quickview-ui/src/lib.rs | 1 + crates/quickview-ui/src/windows/shared.rs | 51 ++++++++--- docs/DECISIONS.md | 6 +- docs/DEPENDENCIES.md | 5 +- docs/PHASED_PLAN.md | 19 ++-- packaging/arch/PKGBUILD | 3 +- packaging/flatpak/com.example.QuickView.json | 20 +++++ 13 files changed, 199 insertions(+), 38 deletions(-) create mode 100644 crates/quickview-ui/src/decode.rs diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 4a87909..d5cd21a 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -44,19 +44,21 @@ assets/ .desktop file, icons, AppStream metainfo The scaffold is functional with image display, async OCR pipeline, drag-select overlay, quick preview mode, and directory navigation. See `docs/PHASED_PLAN.md` for what's done and what's next. ### What works: -- Image loading and display (GdkTexture) +- Async, sandboxed image loading (glycin loader process when installed, + runtime-probed once per session; in-process GDK decoding on a worker thread + otherwise — fallback is session-wide only, never per-file) - Quick Preview window (borderless, layer-shell, Space/Esc dismiss) - Full Viewer window (headerbar, arrow key navigation) +- File info in the headerbar (filename, dimensions, file size) - Async OCR (Tesseract TSV → word bounding boxes) - Drag-select overlay with word highlighting -- Ctrl+C clipboard copy -- Stale OCR result cancellation via monotonic job IDs +- Ctrl+C clipboard copy and right-click context menu (Copy / Copy All Text) +- Stale decode and OCR result cancellation via monotonic job IDs - Zoom & pan (Ctrl+scroll, pinch, +/- keys, middle-drag pan) via custom `ZoomableCanvas` widget ### What's not implemented yet: -- Info panel (filename, dimensions, file size) -- Context menu (right-click copy) - OCR caching (cache module exists but is not wired up) +- Quick Preview click-outside-to-close and single-instance toggle - Performance benchmarks ## Development diff --git a/Cargo.lock b/Cargo.lock index a1327a8..a40f77c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -937,6 +937,7 @@ dependencies = [ "futures-lite", "futures-timer", "futures-util", + "gdk4", "gio", "glib", "glycin-common", diff --git a/README.md b/README.md index b691168..978430a 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ sudo pacman -S --needed \ Optional: - `wl-clipboard` — CLI clipboard fallback -- `glycin` — sandboxed image decoding (future) +- `glycin` — sandboxed image decoding (recommended; detected at runtime, QuickView falls back to in-process GDK decoding without it) > `gtk4-layer-shell` provides true overlay behavior on wlroots compositors. On GNOME/Mutter, Quick Preview falls back to an undecorated window. diff --git a/adrs/ADR-0004-Image-Decoding.md b/adrs/ADR-0004-Image-Decoding.md index 35815a9..0287597 100644 --- a/adrs/ADR-0004-Image-Decoding.md +++ b/adrs/ADR-0004-Image-Decoding.md @@ -20,6 +20,20 @@ Prefer **`glycin`** for decoding where available, with a fallback to GTK/GDK def Glycin decodes via sandboxed modular loaders, improving safety when handling untrusted images. +## Implementation notes (2026-07-05) + +- The `glycin` client crate is **always compiled** (pure Rust, no system build + deps); no cargo feature. Which backend is used is decided **at runtime** by + probing the installed loader configs once per session + (`quickview-ui/src/decode.rs`). +- The GDK fallback is strictly **session-wide, never per-file**: a file glycin + rejects is a failed load. Retrying it with the unsandboxed GDK decoder would + let a crafted image reach the unsandboxed path simply by making the sandboxed + loader fail. +- Both backends run off the GTK main thread (glycin in its sandboxed loader + process, GDK on a worker thread); stale decodes are dropped via a monotonic + job ID, the same pattern used for OCR (ADR-0010). + ## Consequences ### Positive diff --git a/crates/quickview-ui/Cargo.toml b/crates/quickview-ui/Cargo.toml index c350b37..7a125c7 100644 --- a/crates/quickview-ui/Cargo.toml +++ b/crates/quickview-ui/Cargo.toml @@ -5,11 +5,6 @@ edition.workspace = true rust-version.workspace = true license.workspace = true -[features] -# Optional safer image decoding path. -# Disabled by default in this scaffold to keep builds simple. -glycin = ["dep:glycin"] - [dependencies] anyhow.workspace = true tracing.workspace = true @@ -23,5 +18,6 @@ gtk4-layer-shell = "0.7" glib = "0.21" async-channel = "2" -# Optional: sandboxed image decoding (requires system glycin libs) -glycin = { version = "3", optional = true } +# Sandboxed image decoding. Always compiled (pure Rust client, no system build +# deps); whether it is used is decided at runtime by probing installed loaders. +glycin = { version = "3", features = ["gdk4"] } diff --git a/crates/quickview-ui/src/decode.rs b/crates/quickview-ui/src/decode.rs new file mode 100644 index 0000000..ecd2a10 --- /dev/null +++ b/crates/quickview-ui/src/decode.rs @@ -0,0 +1,93 @@ +//! Async image decoding off the GTK main thread. +//! +//! Two backends, chosen once per session (ADR-0004): +//! - **glycin** — decodes in a sandboxed loader process. Used whenever the +//! system has glycin loaders installed. +//! - **GDK** — in-process `gdk::Texture` decoding on a worker thread. Used +//! only when the loader probe finds no glycin loaders for this session. +//! +//! The fallback is strictly session-wide, never per-file: a file glycin +//! rejects is a failed load. Re-feeding it to the unsandboxed GDK decoder +//! would let a crafted image reach the unsandboxed path simply by making the +//! sandboxed loader fail (NFR-002). + +use std::{cell::Cell, path::Path}; + +use gtk4 as gtk; + +use gtk::{gdk, gio}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Backend { + Glycin, + Gdk, +} + +/// The session-wide decode backend, probed on first use. +/// +/// Main-thread only, like the rest of the UI crate. Concurrent first calls +/// may both run the probe; that is harmless (same result, cached once). +pub async fn backend() -> Backend { + thread_local! { + static BACKEND: Cell> = const { Cell::new(None) }; + } + + if let Some(backend) = BACKEND.get() { + return backend; + } + + let backend = if glycin::Loader::supported_mime_types().await.is_empty() { + tracing::warn!( + "image decoding: no glycin loaders installed; \ + falling back to in-process GDK decoding (unsandboxed)" + ); + Backend::Gdk + } else { + tracing::info!("image decoding: glycin (sandboxed loader process)"); + Backend::Glycin + }; + BACKEND.set(Some(backend)); + backend +} + +/// Decode `path` into a texture without blocking the UI. +/// +/// Await this from `glib::MainContext::spawn_local`; the actual decode work +/// happens in a sandboxed glycin process or on a worker thread. +pub async fn decode_texture(path: &Path) -> anyhow::Result { + match backend().await { + Backend::Glycin => decode_glycin(path).await, + Backend::Gdk => decode_gdk(path).await, + } +} + +async fn decode_glycin(path: &Path) -> anyhow::Result { + let file = gio::File::for_path(path); + let image = glycin::Loader::new(file) + .load() + .await + .map_err(|err| anyhow::anyhow!("glycin load failed: {err}"))?; + let frame = image + .next_frame() + .await + .map_err(|err| anyhow::anyhow!("glycin frame decode failed: {err}"))?; + Ok(frame.texture()) +} + +async fn decode_gdk(path: &Path) -> anyhow::Result { + let (sender, receiver) = async_channel::bounded::>(1); + + let path = path.to_path_buf(); + // GdkTexture is immutable and thread-safe, so it can be created on a + // worker thread and sent back to the main thread. + std::thread::spawn(move || { + let result = gdk::Texture::from_filename(&path); + let _ = sender.send_blocking(result); + }); + + let result = receiver + .recv() + .await + .map_err(|_| anyhow::anyhow!("GDK decode thread terminated unexpectedly"))?; + Ok(result?) +} diff --git a/crates/quickview-ui/src/lib.rs b/crates/quickview-ui/src/lib.rs index 628306c..91f052d 100644 --- a/crates/quickview-ui/src/lib.rs +++ b/crates/quickview-ui/src/lib.rs @@ -6,6 +6,7 @@ use anyhow::Result; use adw::prelude::*; +mod decode; pub mod widgets; pub mod windows; diff --git a/crates/quickview-ui/src/windows/shared.rs b/crates/quickview-ui/src/windows/shared.rs index c9f1a2b..caa7fd7 100644 --- a/crates/quickview-ui/src/windows/shared.rs +++ b/crates/quickview-ui/src/windows/shared.rs @@ -41,7 +41,8 @@ pub struct ViewerController { ocr_lang: Rc>, - // Monotonic id to ignore late OCR results. + // Monotonic ids to ignore late results from superseded jobs. + decode_job_id: Rc>, ocr_job_id: Rc>, on_file_loaded: Rc>>, @@ -61,6 +62,7 @@ impl ViewerController { dir_images: Rc::new(RefCell::new(dir_images)), dir_index: Rc::new(Cell::new(dir_index)), ocr_lang: Rc::new(RefCell::new(ocr_lang)), + decode_job_id: Rc::new(Cell::new(0)), ocr_job_id: Rc::new(Cell::new(0)), on_file_loaded: Rc::new(RefCell::new(None)), last_file_info: Rc::new(RefCell::new(None)), @@ -110,9 +112,41 @@ impl ViewerController { .unwrap_or_else(|| path.display().to_string()); let size_bytes = std::fs::metadata(path).ok().map(|m| m.len()); - // Load image synchronously for now. - let file = gtk::gio::File::for_path(path); - match gtk::gdk::Texture::from_file(&file) { + // Supersede any in-flight decode. + let decode_id = self.decode_job_id.get().wrapping_add(1); + self.decode_job_id.set(decode_id); + // Also invalidate any in-flight OCR from the previous image: its late + // result would otherwise clear the busy spinner mid-decode (and, on a + // failed load, repopulate the cleared canvas). + self.ocr_job_id.set(self.ocr_job_id.get().wrapping_add(1)); + + // The previous image stays visible under the busy spinner until the + // decode finishes; the spinner then carries through into OCR. + self.overlay.set_ocr_busy(true); + + let this = self.clone(); + let path = path.to_path_buf(); + glib::MainContext::default().spawn_local(async move { + let result = crate::decode::decode_texture(&path).await; + this.finish_decode(decode_id, path, name, size_bytes, result); + }); + } + + /// Apply a finished decode, unless a newer `load_file` superseded it. + fn finish_decode( + &self, + job_id: u64, + path: PathBuf, + name: String, + size_bytes: Option, + result: anyhow::Result, + ) { + if job_id != self.decode_job_id.get() { + // Late result for a file the user already navigated away from. + return; + } + + match result { Ok(texture) => { let info = FileInfo { name, @@ -123,12 +157,10 @@ impl ViewerController { }; self.overlay.set_texture(texture); self.emit_file_loaded(info); + self.start_ocr(path); } Err(err) => { - tracing::error!("Failed to load image: {err}"); - // Invalidate any in-flight OCR job from the previous image so - // a late result can't repopulate the cleared canvas. - self.ocr_job_id.set(self.ocr_job_id.get().wrapping_add(1)); + tracing::error!("Failed to load image: {err:#}"); // Clear the stale image (and its OCR state) so the canvas // matches the load_failed info shown in the headerbar. self.overlay.clear_texture(); @@ -140,11 +172,8 @@ impl ViewerController { size_bytes, load_failed: true, }); - return; } } - - self.start_ocr(path.to_path_buf()); } pub fn next_image(&self) { diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 70ad51f..b85ba03 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -16,8 +16,10 @@ For more detail, see the ADRs in `adrs/`. - **OS targets:** Wayland-only (no X11 fallback for v1) ### Image rendering/decoding -- **Decode library (preferred, optional):** **glycin** (sandboxed modular loaders) -- **Fallback decoder (default in this scaffold):** GTK/GDK `Texture::from_file` path +- **Decode library (default when loaders are installed):** **glycin** (sandboxed + modular loaders; probed once per session at runtime) +- **Fallback decoder (session-wide only, never per-file):** GTK/GDK texture + decoding on a worker thread ### OCR - **Default OCR engine:** Tesseract (offline) diff --git a/docs/DEPENDENCIES.md b/docs/DEPENDENCIES.md index 1600f31..c3eb692 100644 --- a/docs/DEPENDENCIES.md +++ b/docs/DEPENDENCIES.md @@ -13,12 +13,15 @@ Required: Optional: - wl-clipboard -- glycin + glycin-gtk4 +- glycin (sandboxed image decoding loaders; detected at runtime — without them + QuickView falls back to in-process GDK decoding. On Debian/Ubuntu the package + is `glycin-loaders`.) ## Rust crates (workspace) - `gtk4` (GTK4 bindings, `v4_10` feature enabled) - `libadwaita` (Adwaita widgets) +- `glycin` (sandboxed image decoding client; pure Rust, always compiled) - `gtk4-layer-shell` (Layer Shell integration) - `clap` (CLI) - `tracing` + `tracing-subscriber` (logging) diff --git a/docs/PHASED_PLAN.md b/docs/PHASED_PLAN.md index de8cd0c..f048721 100644 --- a/docs/PHASED_PLAN.md +++ b/docs/PHASED_PLAN.md @@ -144,18 +144,19 @@ If priorities change, you can reshuffle phases, but try to keep the “render fi --- -## Phase 7 — Hardening + performance (not started) +## Phase 7 — Hardening + performance (in progress) **Core tasks** -- Async + sandboxed image loading — implements ADR-0004, fixes NFR-001/NFR-002. - Do this first: the cache DoD below is unverifiable while decode still blocks - the main thread. - - replace the synchronous `Texture::from_file` in `load_file` with glycin's - async loader where available (behind a cargo feature), falling back to GDK - decoding on a background thread via the existing async-channel pattern - - show the previous image or a placeholder until decode completes +- Async + sandboxed image loading ✅ — implements ADR-0004, fixes NFR-001/NFR-002. + - synchronous `Texture::from_file` in `load_file` replaced with glycin's + async loader (always compiled, chosen by a runtime loader probe — no cargo + feature), falling back to GDK decoding on a background thread via the + existing async-channel pattern ✅ + - fallback is session-wide only, never per-file: a file glycin rejects is a + failed load and is never re-fed to the unsandboxed GDK decoder ✅ + - previous image stays visible (under the busy spinner) until decode completes ✅ - stale-result guard (monotonic job ID, same pattern as OCR) so fast - arrow-key navigation cannot race decodes + arrow-key navigation cannot race decodes ✅ - Add cache (in-memory first; `cache.rs` on-disk key derivation exists but is unwired) - Add basic benchmarking hooks (decode + OCR timing) - Improve OCR accuracy options: diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 70b38ff..c737efe 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -18,8 +18,7 @@ depends=( optdepends=( "tesseract-data-eng: English OCR" "wl-clipboard: CLI clipboard fallback (wl-copy/wl-paste)" - "glycin: sandboxed image decoding (optional feature)" - "glycin-gtk4: glycin → GDK texture conversion" + "glycin: sandboxed image decoding (recommended; runtime-detected, falls back to in-process GDK decoding)" ) makedepends=( diff --git a/packaging/flatpak/com.example.QuickView.json b/packaging/flatpak/com.example.QuickView.json index f334a61..d7c06e6 100644 --- a/packaging/flatpak/com.example.QuickView.json +++ b/packaging/flatpak/com.example.QuickView.json @@ -10,6 +10,26 @@ "--filesystem=host:ro" ], "modules": [ + { + "name": "glycin-loaders", + "buildsystem": "meson", + "config-opts": [ + "-Dglycin-loaders=true", + "-Dglycin-thumbnailer=false", + "-Dlibglycin=false", + "-Dlibglycin-gtk4=false", + "-Dintrospection=false", + "-Dvapi=false", + "-Dtests=false" + ], + "sources": [ + { + "type": "archive", + "url": "https://download.gnome.org/sources/glycin/2.1/glycin-2.1.5.tar.xz", + "sha256": "6c09757ee906330a60b6705753aa56bca007ad219b95e6e3537510d41bc341c8" + } + ] + }, { "name": "quickview", "buildsystem": "simple", From b8e81aab7f885a08895228b3d38eb867d4f0f3a4 Mon Sep 17 00:00:00 2001 From: Babken Egoian <101829110+green2grey@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:53:11 -0700 Subject: [PATCH 2/4] fix: declare glycin's native link dependencies; ignore paste advisory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups for PR #3: - Codex: the glycin client crate links libseccomp, lcms2, and fontconfig (sandbox seccomp filter, ICC, font paths) — needed at build/link time regardless of the runtime backend. Declare them in PKGBUILD depends, the README install line, and DEPENDENCIES.md, and correct the inaccurate 'pure Rust, no system deps' wording in Cargo.toml and ADR-0004. - CI: cargo-deny failed on RUSTSEC-2024-0436 (paste is unmaintained). paste is a build-time proc-macro pulled in transitively by glycin's deps with no fixed release; add a scoped, documented ignore. --- README.md | 3 ++- adrs/ADR-0004-Image-Decoding.md | 10 ++++++---- crates/quickview-ui/Cargo.toml | 5 +++-- deny.toml | 8 +++++++- docs/DEPENDENCIES.md | 7 ++++++- packaging/arch/PKGBUILD | 4 ++++ 6 files changed, 28 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 978430a..57c8231 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,8 @@ sudo pacman -S --needed \ base-devel rustup pkgconf \ gtk4 libadwaita \ tesseract tesseract-data-eng \ - gtk4-layer-shell + gtk4-layer-shell \ + libseccomp lcms2 fontconfig ``` Optional: diff --git a/adrs/ADR-0004-Image-Decoding.md b/adrs/ADR-0004-Image-Decoding.md index 0287597..ed6a948 100644 --- a/adrs/ADR-0004-Image-Decoding.md +++ b/adrs/ADR-0004-Image-Decoding.md @@ -22,10 +22,12 @@ Glycin decodes via sandboxed modular loaders, improving safety when handling unt ## Implementation notes (2026-07-05) -- The `glycin` client crate is **always compiled** (pure Rust, no system build - deps); no cargo feature. Which backend is used is decided **at runtime** by - probing the installed loader configs once per session - (`quickview-ui/src/decode.rs`). +- The `glycin` client crate is **always compiled**; no cargo feature. Which + backend is used is decided **at runtime** by probing the installed loader + configs once per session (`quickview-ui/src/decode.rs`). The client links + libseccomp, lcms2, and fontconfig — ubiquitous on any GTK-capable system, + and required at build time regardless of which backend runs (see + `docs/DEPENDENCIES.md`). - The GDK fallback is strictly **session-wide, never per-file**: a file glycin rejects is a failed load. Retrying it with the unsandboxed GDK decoder would let a crafted image reach the unsandboxed path simply by making the sandboxed diff --git a/crates/quickview-ui/Cargo.toml b/crates/quickview-ui/Cargo.toml index 7a125c7..091c78b 100644 --- a/crates/quickview-ui/Cargo.toml +++ b/crates/quickview-ui/Cargo.toml @@ -18,6 +18,7 @@ gtk4-layer-shell = "0.7" glib = "0.21" async-channel = "2" -# Sandboxed image decoding. Always compiled (pure Rust client, no system build -# deps); whether it is used is decided at runtime by probing installed loaders. +# Sandboxed image decoding. Always compiled; whether it is used is decided at +# runtime by probing installed loaders. Links libseccomp, lcms2, and +# fontconfig (see docs/DEPENDENCIES.md). glycin = { version = "3", features = ["gdk4"] } diff --git a/deny.toml b/deny.toml index 1d4162a..84b62fc 100644 --- a/deny.toml +++ b/deny.toml @@ -2,7 +2,13 @@ # https://embarkstudios.github.io/cargo-deny/ [advisories] -ignore = [] +ignore = [ + # `paste` (proc-macro, build-time only) is archived/unmaintained. It is a + # transitive dependency of glycin (via glycin-common/glycin-utils/gufo); + # no fixed release exists. Drop this once glycin's deps migrate to the + # `pastey` fork. + "RUSTSEC-2024-0436", +] [licenses] allow = [ diff --git a/docs/DEPENDENCIES.md b/docs/DEPENDENCIES.md index c3eb692..453c1d7 100644 --- a/docs/DEPENDENCIES.md +++ b/docs/DEPENDENCIES.md @@ -10,6 +10,10 @@ Required: - tesseract - tesseract language pack(s) (at least English: `tesseract-data-eng`) - gtk4-layer-shell +- libseccomp, lcms2, fontconfig — linked by the glycin client crate (sandbox + seccomp filter, ICC color management, font paths). Needed at build and link + time even when the runtime falls back to GDK decoding. On Debian/Ubuntu the + build needs `libseccomp-dev`, `liblcms2-dev`, and `libfontconfig-dev`. Optional: - wl-clipboard @@ -21,7 +25,8 @@ Optional: - `gtk4` (GTK4 bindings, `v4_10` feature enabled) - `libadwaita` (Adwaita widgets) -- `glycin` (sandboxed image decoding client; pure Rust, always compiled) +- `glycin` (sandboxed image decoding client; always compiled — links + libseccomp, lcms2, and fontconfig) - `gtk4-layer-shell` (Layer Shell integration) - `clap` (CLI) - `tracing` + `tracing-subscriber` (logging) diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index c737efe..70489ad 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -13,6 +13,10 @@ depends=( "libadwaita" "tesseract" "gtk4-layer-shell" + # linked by the glycin client crate (sandbox seccomp filter, ICC, fonts) + "libseccomp" + "lcms2" + "fontconfig" ) optdepends=( From 3b1677054675362d9b05cc64486d27bee6003f6f Mon Sep 17 00:00:00 2001 From: Babken Egoian <101829110+green2grey@users.noreply.github.com> Date: Sun, 5 Jul 2026 22:02:57 -0700 Subject: [PATCH 3/4] ci: declare glycin's native deps explicitly; use prebuilt cargo-deny - Add libseccomp/lcms2/fontconfig to both install blocks. They were already present transitively via gtk4's dependency closure (which is why CI passed), but the workflow shouldn't rely on another package's dep tree for libraries we link directly. - Replace 'cargo install cargo-deny' with the pinned prebuilt musl binary: the source build cost ~100s per run whenever the cache missed or upstream released; the download takes ~2s. Drop the now unneeded ~/.cargo/bin cache path. --- .github/workflows/ci.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c45730a..3685478 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,8 @@ jobs: rustup \ gtk4 libadwaita \ tesseract tesseract-data-eng \ - gtk4-layer-shell + gtk4-layer-shell \ + libseccomp lcms2 fontconfig pkg-config --atleast-version=4.10 gtk4 rustup default stable @@ -74,24 +75,30 @@ jobs: rustup \ gtk4 libadwaita \ tesseract tesseract-data-eng \ - gtk4-layer-shell + gtk4-layer-shell \ + libseccomp lcms2 fontconfig pkg-config --atleast-version=4.10 gtk4 rustup default stable - - name: Cache cargo registry, build artifacts, and tools + - name: Cache cargo registry and build artifacts uses: actions/cache@v4 with: path: | /root/.cargo/registry /root/.cargo/git - /root/.cargo/bin target key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-build- + # Prebuilt static binary: `cargo install` recompiles cargo-deny from + # source on every cache miss or upstream release (~100s per run). - name: Install cargo-deny - run: cargo install cargo-deny --locked + run: | + DENY_VERSION=0.19.9 + curl -sSfL "https://github.com/EmbarkStudios/cargo-deny/releases/download/${DENY_VERSION}/cargo-deny-${DENY_VERSION}-x86_64-unknown-linux-musl.tar.gz" \ + | tar -xz -C /usr/local/bin --strip-components=1 \ + "cargo-deny-${DENY_VERSION}-x86_64-unknown-linux-musl/cargo-deny" - name: Dependency audit run: cargo deny check From 90806e75f924447158ea372b70544478e80474b9 Mon Sep 17 00:00:00 2001 From: Babken Egoian <101829110+green2grey@users.noreply.github.com> Date: Sun, 5 Jul 2026 22:09:03 -0700 Subject: [PATCH 4/4] ci: verify cargo-deny tarball against pinned sha256 before extraction Checksum is hardcoded in the workflow rather than fetched from the release, so a swapped artifact fails the build instead of running. --- .github/workflows/ci.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3685478..94bf512 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,12 +93,19 @@ jobs: # Prebuilt static binary: `cargo install` recompiles cargo-deny from # source on every cache miss or upstream release (~100s per run). + # Checksum is pinned here (not fetched from the release) so a swapped + # artifact fails loudly. When bumping DENY_VERSION, update it from the + # release's .tar.gz.sha256 asset. - name: Install cargo-deny run: | DENY_VERSION=0.19.9 - curl -sSfL "https://github.com/EmbarkStudios/cargo-deny/releases/download/${DENY_VERSION}/cargo-deny-${DENY_VERSION}-x86_64-unknown-linux-musl.tar.gz" \ - | tar -xz -C /usr/local/bin --strip-components=1 \ - "cargo-deny-${DENY_VERSION}-x86_64-unknown-linux-musl/cargo-deny" + DENY_SHA256=f1f8eedc2a3ac297c540873f93785d4104b102c0079506b2a6b3221b7ec956af + curl -sSfL -o /tmp/cargo-deny.tar.gz \ + "https://github.com/EmbarkStudios/cargo-deny/releases/download/${DENY_VERSION}/cargo-deny-${DENY_VERSION}-x86_64-unknown-linux-musl.tar.gz" + echo "${DENY_SHA256} /tmp/cargo-deny.tar.gz" | sha256sum -c - + tar -xz -C /usr/local/bin --strip-components=1 \ + -f /tmp/cargo-deny.tar.gz \ + "cargo-deny-${DENY_VERSION}-x86_64-unknown-linux-musl/cargo-deny" - name: Dependency audit run: cargo deny check