From a5966f9b2e010dfede22e953850909a1c1de532b Mon Sep 17 00:00:00 2001 From: Anarion Dunedain Date: Wed, 7 Oct 2026 13:54:51 +0200 Subject: [PATCH] ci: add nix build package --- .gitignore | 3 + AGENTS.md | 5 +- CLAUDE.md | 5 +- README.md | 25 ++++++ flake.lock | 45 +++++++++++ flake.nix | 115 +++++++++++++++++++++++++++ nix/package.nix | 206 ++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 402 insertions(+), 2 deletions(-) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/package.nix diff --git a/.gitignore b/.gitignore index 823f9276..accaa0de 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ plan/ .DS_Store .claude/ /craft-fonts +# nix build / nix develop +/result +/result-* diff --git a/AGENTS.md b/AGENTS.md index 393db2b1..d3bc859c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -119,4 +119,7 @@ of it. Full standard: `../craftrules/standards/never-crash.md` ## Map of the code `geom`, `color`, `raster`, `tiff` (L0) → `raw`, `codecs`, `meta`, `develop` (L1) → `pipeline` → `catalog` → `engine` → `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`). +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). Keep it building +when the workspace layout, `Cargo.lock`, the packaging files or the fonts change; see README → Quick start. diff --git a/CLAUDE.md b/CLAUDE.md index 393db2b1..d3bc859c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -119,4 +119,7 @@ of it. Full standard: `../craftrules/standards/never-crash.md` ## Map of the code `geom`, `color`, `raster`, `tiff` (L0) → `raw`, `codecs`, `meta`, `develop` (L1) → `pipeline` → `catalog` → `engine` → `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`). +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). Keep it building +when the workspace layout, `Cargo.lock`, the packaging files or the fonts change; see README → Quick start. diff --git a/README.md b/README.md index 5e741b7c..afa40967 100644 --- a/README.md +++ b/README.md @@ -347,6 +347,31 @@ repo; see [craftrules `standards/fonts.md`](https://github.com/storytold/craftru The web build needs the `wasm32-unknown-unknown` target and the matching `wasm-bindgen` CLI (`cargo xtask web` prints the exact install command); see [docs/web.md](docs/web.md). +**Nix** builds the desktop app and `lightcraft-cli` (the Nix build always includes the craft-fonts input, so Japanese +text has glyphs): + +```sh +nix run github:storytold/lightcraft # the desktop app +nix build github:storytold/lightcraft # → ./result/bin/{lightcraft,lightcraft-cli} +nix develop github:storytold/lightcraft # rust toolchain + native deps + fonts +``` + +In a flake configuration (NixOS, home-manager, nix-darwin): + +```nix +# flake.nix +inputs.lightcraft.url = "github:storytold/lightcraft"; +# optional: build against your own nixpkgs instead of the one LightCraft pins +# inputs.lightcraft.inputs.nixpkgs.follows = "nixpkgs"; + +# then, in a NixOS or home-manager module (where `inputs` is in scope): +nixpkgs.overlays = [ inputs.lightcraft.overlays.default ]; # makes `pkgs.lightcraft` available +environment.systemPackages = [ pkgs.lightcraft ]; # home-manager: home.packages = [ pkgs.lightcraft ]; +``` + +`nix build` installs the same desktop file, hicolor icons and AppStream metadata as the .deb/.rpm, and runs +`cargo test --workspace` as its check phase (skip it with `pkgs.lightcraft.overrideAttrs { doCheck = false; }`). + **Keyboard:** G grid · D detail · E edit · C crop · M masking · Shift+P presets · \\ original · Y before/after · Z zoom · J clipping · ⌘Z undo · ⌘/ all shortcuts. diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..5c94f166 --- /dev/null +++ b/flake.lock @@ -0,0 +1,45 @@ +{ + "nodes": { + "craft-fonts": { + "flake": false, + "locked": { + "lastModified": 1791295400, + "narHash": "sha256-e+6HpOYoAFTh6mNfGBBw2njnBzcE8W8+Jybv1+dKOCA=", + "owner": "storytold", + "repo": "craft-fonts", + "rev": "abb83316d96aa59c1cf64784289e378fe9fa5695", + "type": "github" + }, + "original": { + "owner": "storytold", + "repo": "craft-fonts", + "rev": "abb83316d96aa59c1cf64784289e378fe9fa5695", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1791260994, + "narHash": "sha256-Miqqk/ammqnTUxaoCyvtwoPeLbI1ksFyLxi/CazZpWY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "151fa4e8ddfdd8dd25d945ad94ed54a13de9f6e4", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "craft-fonts": "craft-fonts", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..918053ac --- /dev/null +++ b/flake.nix @@ -0,0 +1,115 @@ +{ + description = "LightCraft — photo library and non-destructive raw developer, in pure Rust"; + + inputs = { + # LightCraft needs edition 2024 and rustc ≥ 1.90, so the flake tracks unstable. Consumers on a + # release channel can retarget it: `inputs.lightcraft.inputs.nixpkgs.follows = "nixpkgs";` + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + # Optional build input of the repository: the Japanese fonts (BIZ UDPGothic, BIZ UDMincho, + # Shippori Mincho) that `crates/engine/build.rs` embeds when `CRAFT_FONTS_DIR` is set. Official + # releases always build with them; without them everything works but Japanese text has no + # glyphs. Pinned to the revision the release workflow uses — bump deliberately + # (`nix flake update craft-fonts`); craftrules standards/fonts.md. + craft-fonts = { + url = "github:storytold/craft-fonts/abb83316d96aa59c1cf64784289e378fe9fa5695"; + flake = false; + }; + }; + + outputs = + { + self, + nixpkgs, + craft-fonts, + }: + let + inherit (nixpkgs) lib; + + # No x86_64-darwin: the pinned nixpkgs (26.11, "nixos-unstable") dropped support for it. On + # an Intel Mac, retarget nixpkgs at a release that still has it, or install the .dmg from the + # GitHub release. + systems = [ + "x86_64-linux" + "aarch64-linux" + "aarch64-darwin" + ]; + + # The AppStream metadata carries a release date. The flake source's own commit date is + # reproducible, unlike "now"; a tarball or dirty tree without one falls back to the epoch. + buildDate = + let + d = self.lastModifiedDate or ""; + in + if builtins.stringLength d >= 8 then + "${builtins.substring 0 4 d}-${builtins.substring 4 2 d}-${builtins.substring 6 2 d}" + else + "1970-01-01"; + + packageArgs = { + inherit craft-fonts buildDate; + }; + + forAllSystems = f: lib.genAttrs systems (system: f (import nixpkgs { inherit system; }) system); + in + { + packages = forAllSystems ( + pkgs: _: + let + lightcraft = pkgs.callPackage ./nix/package.nix packageArgs; + in + { + inherit lightcraft; + default = lightcraft; + } + ); + + # `nixpkgs.overlays = [ inputs.lightcraft.overlays.default ]` → `pkgs.lightcraft` everywhere. + overlays.default = final: _prev: { + lightcraft = final.callPackage ./nix/package.nix packageArgs; + }; + + devShells = forAllSystems ( + pkgs: system: + let + lightcraft = self.packages.${system}.lightcraft; + in + { + default = pkgs.mkShell ( + { + # The package's own native inputs, so `cargo build` in the shell needs no extra setup. + inputsFrom = [ lightcraft ]; + packages = with pkgs; [ + cargo + rustc + clippy + rustfmt + rust-analyzer + ]; + # `cargo run` dlopens the Vulkan loader and libxkbcommon like the packaged binary. + LD_LIBRARY_PATH = lib.makeLibraryPath lightcraft.passthru.runtimeLibs; + RUST_BACKTRACE = "1"; + } + // lib.optionalAttrs (craft-fonts != null) { + # Japanese glyphs in `cargo run` too; build.rs reads this as CRAFT_FONTS_DIR. + CRAFT_FONTS_DIR = toString craft-fonts; + } + ); + } + ); + + checks = forAllSystems ( + pkgs: system: { + lightcraft = self.packages.${system}.lightcraft; + + # `nix fmt` formats the flake; this keeps it formatted. + formatting = pkgs.runCommand "lightcraft-nix-formatting" { nativeBuildInputs = [ pkgs.nixfmt ]; } '' + nixfmt --check ${./flake.nix} ${./nix} + touch $out + ''; + } + ); + + formatter = forAllSystems (pkgs: _: pkgs.nixfmt); + }; +} diff --git a/nix/package.nix b/nix/package.nix new file mode 100644 index 00000000..20b895c7 --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,206 @@ +# LightCraft — the desktop app (`lightcraft`) and the CLI/MCP server (`lightcraft-cli`), built from +# this repository: +# +# nix build # → ./result/bin/lightcraft, ./result/bin/lightcraft-cli +# +# Kept in step with the release workflow (.github/workflows/release.yml) and packaging/env.sh: +# * `Cargo.lock` drives the dependencies (`cargoLock`), so there is no vendorHash to bump — only +# crates.io, no git sources; +# * `CRAFT_FONTS_DIR` embeds the Japanese fonts from storytold/craft-fonts (the `craft-fonts` +# flake input); without it everything builds and runs, but Japanese text has no glyphs; +# * the desktop file, hicolor icons and AppStream metadata are the same files the .deb/.rpm ship +# (packaging/linux/), so `apt` and NixOS users see one identical LightCraft; +# * `doCheck` runs `cargo test --workspace`, what `cargo xtask ci` runs. `nix build` runs it too; +# `pkgs.lightcraft.overrideAttrs { doCheck = false; }` skips it for a faster, build-only install. +# +# Everything in the product is pure Rust (no C/C++ dependencies), so this needs no build system +# beyond cargo plus the windowing headers winit's build scripts look for. +{ + lib, + stdenv, + rustPlatform, + pkg-config, + makeWrapper, + # Windowing/GPU libraries: needed at build time by the crates that link them, at run time by the + # ones that open them with dlopen() (see `runtimeLibs`), and by the dev shell (flake.nix). + wayland, + wayland-scanner, + libxkbcommon, + libx11, + libxcb, + libxcursor, + libxi, + libxrandr, + vulkan-loader, + # Validates the desktop file and the AppStream metadata we install (as CI does). + desktop-file-utils, + appstream, + # Adds the driver search path (/run/opengl-driver/lib) to the binaries' RPATH so wgpu finds the + # installed Vulkan driver on NixOS. Optional: harmless on nixpkgs without it. + autoAddDriverRunpath ? null, + # storytold/craft-fonts checkout (flake input). `null` builds without Japanese glyphs. + craft-fonts ? null, + # Release date for the AppStream metadata (YYYY-MM-DD); the flake derives it from its own commit. + buildDate ? "1970-01-01", +}: + +let + root = ../.; + + # The version lives in exactly one place: `[workspace.package] version` in the root Cargo.toml. + version = (builtins.fromTOML (builtins.readFile (root + "/Cargo.toml"))).workspace.package.version; + + isLinux = stdenv.hostPlatform.isLinux; + + # AppStream id: also the desktop file name, and the icon name in share/icons/hicolor. + appId = "ai.storyteller.lightcraft"; + + # Libraries the binaries open at run time with dlopen(): nothing links them, so no RPATH points at + # them — winit loads libxkbcommon/libxcb, wgpu the Vulkan loader (NixOS patches that loader to + # find the drivers in /run/opengl-driver). The wrapper below points LD_LIBRARY_PATH here. + runtimeLibs = lib.optionals isLinux [ + libxkbcommon + wayland + libx11 + libxcb + libxcursor + libxi + libxrandr + vulkan-loader + ]; + + # The two native binaries. The wasm app (apps/lightcraft-web) is built by `cargo xtask web`, not + # here; xtask is tooling. + binaries = [ + "lightcraft" + "lightcraft-cli" + ]; +in +rustPlatform.buildRustPackage { + pname = "lightcraft"; + inherit version; + + src = lib.cleanSourceWith { + src = lib.cleanSource root; + # The Nix files are not part of the build: editing them must not rebuild any Rust. + filter = + path: _type: + !(builtins.elem (baseNameOf path) [ + "flake.nix" + "flake.lock" + "nix" + ]); + }; + + cargoLock.lockFile = root + "/Cargo.lock"; + + # Both binaries, as the .deb/.rpm install them (`packaging/linux/package.sh`). + cargoBuildFlags = [ + "--locked" + "-p" + "lightcraft" + "-p" + "lightcraft-cli" + ]; + + # `cargo xtask ci` runs `cargo test --workspace`. Tests that need what the sandbox cannot have + # skip themselves with a note: corpus/ (gitignored, `cargo xtask corpus --download`) and the GPU + # equivalence tests (no adapter). + cargoTestFlags = [ "--workspace" ]; + + # One test thread at a time (RUST_TEST_THREADS=1). `lightcraft-catalog`'s `tests_lock` tests share + # a process-global resource: `flock` is inherited across `fork`, so a child process that another + # test forked keeps the parent's just-released library lock alive until its `execve` closes the + # (CLOEXEC) fd — long enough that the sibling test sees a spurious `LockError::InUse`, on ~30 % of + # runs on a many-core machine. Serializing the tests keeps `nix build` deterministic without + # touching the repository. The underlying window (any `fork` in a process holding the library + # lock — the app spawns `xdg-open`/`open`/`explorer` and `sysctl`) is a product-level question: + # `LibraryLock::acquire` could retry on `WouldBlock` for a few hundred ms before reporting InUse. + dontUseCargoParallelTests = true; + + strictDeps = true; + + nativeBuildInputs = [ + pkg-config + ] + ++ lib.optionals isLinux [ + makeWrapper + wayland-scanner + desktop-file-utils + appstream + ] + ++ lib.optionals (isLinux && autoAddDriverRunpath != null) [ autoAddDriverRunpath ]; + + buildInputs = lib.optionals isLinux [ + wayland + libxkbcommon + libx11 + libxcb + libxcursor + libxi + libxrandr + ]; + + env = lib.optionalAttrs (craft-fonts != null) { + CRAFT_FONTS_DIR = toString craft-fonts; + # A malformed font input must fail the build rather than silently ship without glyphs. + CRAFT_FONTS_REQUIRED = "1"; + }; + + postInstall = '' + # Desktop integration: byte-for-byte the files the .deb/.rpm/.AppImage install. + install -Dm644 packaging/linux/${appId}.desktop $out/share/applications/${appId}.desktop + install -Dm644 packaging/linux/${appId}.mime.xml $out/share/mime/packages/${appId}.xml + cp -r assets/app-icon/hicolor $out/share/icons/ + install -d $out/share/metainfo + substitute packaging/linux/${appId}.metainfo.xml.in \ + $out/share/metainfo/${appId}.metainfo.xml \ + --subst-var-by VERSION ${version} \ + --subst-var-by DATE ${buildDate} + + # Licences for everything embedded in the binaries: Inter, the app icon, each craft font. + doc=$out/share/doc/lightcraft + install -Dm644 -t "$doc" \ + README.md LICENSE-MIT LICENSE-APACHE NOTICE \ + assets/ATTRIBUTION.md assets/fonts/OFL-Inter.txt + '' + + lib.optionalString (craft-fonts != null) '' + for lic in ${toString craft-fonts}/fonts/*/OFL.txt; do + install -Dm644 "$lic" "$doc/OFL-$(basename "$(dirname "$lic")").txt" + done + '' + + lib.optionalString isLinux '' + desktop-file-validate $out/share/applications/${appId}.desktop + XDG_CACHE_HOME=$TMPDIR/.cache appstreamcli validate --no-net \ + $out/share/metainfo/${appId}.metainfo.xml + + # makeWrapper keeps the binaries working when the libs above are only dlopen'd. + for bin in ${lib.concatStringsSep " " (map (b: "$out/bin/" + b) binaries)}; do + wrapProgram "$bin" --prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath runtimeLibs}" + done + ''; + + passthru = { + # Shared with the dev shell so `cargo run` inside `nix develop` finds the same dlopen'd libs. + inherit runtimeLibs; + }; + + meta = { + description = "Photo library and non-destructive raw developer"; + longDescription = '' + LightCraft organises a photo library and develops raw files non-destructively: masks, + presets, colour grading, local adjustments and batch export, driven by the same command + layer as its UI. It ships with lightcraft-cli, a headless renderer, command runner and MCP + server for AI agents. + ''; + homepage = "https://getartcraft.com/apps/lightcraft"; + license = with lib.licenses; [ + mit + asl20 + ]; + sourceProvenance = with lib.sourceTypes; [ fromSource ]; + platforms = lib.platforms.linux ++ lib.platforms.darwin; + # `nix run` and the desktop entry both start the GUI. + mainProgram = "lightcraft"; + }; +}