From dfe87240b09331e09d42efdde176bc3a0daaedbc Mon Sep 17 00:00:00 2001 From: dev-null-undefined Date: Tue, 13 Oct 2026 13:07:36 +0200 Subject: [PATCH 1/2] Nix: evaluate the flake in CI so a new git dependency can't break nix build / nix develop again Since UI.9 (9bfabc98) Cargo.lock has a git source, craft-ui from storytold/craft-libs. importCargoLock needs a hash for every git dependency, so evaluation stopped with "No hash was found while vendoring the git dependency craft-ui-0.1.0" and both `nix build` and `nix develop` failed on NixOS for two days (#1314 fixed it with cargoLock.allowBuiltinFetchGit). The flake is not built in CI, so nothing noticed: the new Nix workflow runs `nix flake check --no-build --all-systems` whenever flake.nix, flake.lock, nix/ or Cargo.lock change. Co-Authored-By: Claude --- .github/workflows/nix.yml | 28 ++++++++++++++++++++++++++++ AGENTS.md | 4 ++-- CLAUDE.md | 4 ++-- 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/nix.yml diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 000000000..d992f7433 --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,28 @@ +# Evaluates the Nix flake (no build, about a minute): `nix build` / `nix develop` stop at evaluation +# when Cargo.lock gains something nix/package.nix can't vendor, and nothing else would notice. +name: Nix + +on: + push: + branches: [main] + paths: ["flake.nix", "flake.lock", "nix/**", "Cargo.lock", ".github/workflows/nix.yml"] + pull_request: + paths: ["flake.nix", "flake.lock", "nix/**", "Cargo.lock", ".github/workflows/nix.yml"] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: nix-${{ github.ref }} + cancel-in-progress: true + +jobs: + eval: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1 + - name: nix flake check --no-build + run: nix flake check --no-build --all-systems diff --git a/AGENTS.md b/AGENTS.md index 8cd199ff6..297682755 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -131,8 +131,8 @@ of it. Full standard: `../craftrules/standards/never-crash.md` → `ui-egui`, `mcp` (L5) → apps `lightcraft` (desktop), `lightcraft-cli` (render/commands/MCP). `scenes` generates demo photos. `xtask` = tooling (`ci`, `layers`, `assets`, `parity`, `wasm`, `corpus`, `stats`). `flake.nix` + `nix/package.nix` = the Nix package (`nix build` builds both binaries with the craft-fonts input, installs the -desktop file/icons/AppStream metadata and runs `cargo test --workspace`; `nix develop` = dev shell). Community-maintained and not -in CI: it may lag behind the workspace; see README → Quick start. +desktop file/icons/AppStream metadata and runs `cargo test --workspace`; `nix develop` = dev shell). Community-maintained; CI only +evaluates it (`.github/workflows/nix.yml`), so the build may lag behind the workspace; see README → Quick start. ## Contributor credits (About window) diff --git a/CLAUDE.md b/CLAUDE.md index 4a736f12a..6099b1d2f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -138,5 +138,5 @@ of it. Full standard: `../craftrules/standards/never-crash.md` → `ui-egui`, `mcp` (L5) → apps `lightcraft` (desktop), `lightcraft-cli` (render/commands/MCP). `scenes` generates demo photos. `xtask` = tooling (`ci`, `layers`, `assets`, `parity`, `wasm`, `corpus`, `stats`). `flake.nix` + `nix/package.nix` = the Nix package (`nix build` builds both binaries with the craft-fonts input, installs the -desktop file/icons/AppStream metadata and runs `cargo test --workspace`; `nix develop` = dev shell). Community-maintained and not -in CI: it may lag behind the workspace; see README → Quick start. +desktop file/icons/AppStream metadata and runs `cargo test --workspace`; `nix develop` = dev shell). Community-maintained; CI only +evaluates it (`.github/workflows/nix.yml`), so the build may lag behind the workspace; see README → Quick start. From 6c5b4d5701334e7b1db103eb355c88ac71bdf9e3 Mon Sep 17 00:00:00 2001 From: dev-null-undefined Date: Mon, 12 Oct 2026 11:46:03 +0200 Subject: [PATCH 2/2] =?UTF-8?q?UI:=20find=20gsettings=20on=20PATH=20for=20?= =?UTF-8?q?Appearance=20Mode=20=E2=96=B8=20Auto=20(NixOS=20has=20no=20/usr?= =?UTF-8?q?/bin/gsettings)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without the XDG settings portal, the start-up fallback asks gsettings for the GNOME colour scheme, but only looked in /usr/bin, /bin and /usr/local/bin. NixOS keeps it in /run/current-system/sw/bin or the per-user profile, so Auto never followed the desktop there. It now tries the absolute PATH entries first, then those three; relative entries are still skipped, so nothing runs from the working folder. Co-Authored-By: Claude --- apps/lightcraft/src/appearance.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/apps/lightcraft/src/appearance.rs b/apps/lightcraft/src/appearance.rs index 74333a301..223dbc560 100644 --- a/apps/lightcraft/src/appearance.rs +++ b/apps/lightcraft/src/appearance.rs @@ -51,7 +51,7 @@ fn decode(code: u32) -> Option { /// absolute path, and is never repeated. #[cfg(target_os = "linux")] fn gtk_theme() -> Option { - let exe = ["/usr/bin/gsettings", "/bin/gsettings", "/usr/local/bin/gsettings"].into_iter().find(|p| std::path::Path::new(p).is_file())?; + let exe = gsettings_exe(std::env::var_os("PATH").as_deref())?; let output = std::process::Command::new(exe).args(["get", "org.gnome.desktop.interface", "color-scheme"]).output().ok()?; if !output.status.success() { return None; @@ -59,6 +59,15 @@ fn gtk_theme() -> Option { parse_gtk_scheme(std::str::from_utf8(&output.stdout).ok()?) } +/// `gsettings` by absolute path: the absolute `PATH` entries first (NixOS has no /usr/bin/gsettings), +/// then the usual places for a sparse `PATH`. Relative entries are skipped, so nothing runs from +/// the working folder. +#[cfg(target_os = "linux")] +fn gsettings_exe(path: Option<&std::ffi::OsStr>) -> Option { + let listed = path.map(std::env::split_paths).into_iter().flatten().filter(|dir| dir.is_absolute()); + listed.chain(["/usr/bin", "/bin", "/usr/local/bin"].map(std::path::PathBuf::from)).map(|dir| dir.join("gsettings")).find(|exe| exe.is_file()) +} + #[cfg(target_os = "linux")] fn parse_gtk_scheme(s: &str) -> Option { match s.trim().trim_matches('\'') { @@ -174,6 +183,24 @@ mod tests { assert_eq!(calls.load(Ordering::Relaxed), after_settle); } + /// NixOS keeps `gsettings` in /run/current-system/sw/bin or the per-user profile: it is found + /// on `PATH`, but never through a relative entry, even one that leads to a real file. + #[cfg(target_os = "linux")] + #[test] + fn gsettings_is_found_on_absolute_path_entries_only() { + let dir = std::env::temp_dir().join(format!("lightcraft-gsettings-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write(dir.join("gsettings"), "").unwrap(); + let path = std::env::join_paths([std::path::Path::new("relative/bin"), &dir]).unwrap(); + assert_eq!(super::gsettings_exe(Some(&path)), Some(dir.join("gsettings"))); + let cwd = std::env::current_dir().unwrap(); + let relative = std::path::PathBuf::from("../".repeat(cwd.components().count() - 1)).join(dir.strip_prefix("/").unwrap()); + assert!(relative.join("gsettings").is_file()); + let found = super::gsettings_exe(Some(relative.as_os_str())); + assert!(found.as_ref().is_none_or(|p| p.is_absolute()), "{found:?}"); + let _ = std::fs::remove_dir_all(&dir); + } + #[cfg(target_os = "linux")] #[test] fn portal_values_and_gsettings_output_decode() {