From 2df28344b828da32a70bf11c67219329e8db3000 Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Mon, 1 Jun 2026 17:09:29 +0200 Subject: [PATCH 1/4] build(nix): add per-crate crane workspace builds Add crane-based package outputs for the main OpenShell crates and a default symlinkJoin package. The new workspace helper derives each crate's transitive workspace dependency closure, builds from minimal source trees, and declares the assets each crate needs at compile time. Build each crate in three layers: 1. crates.io dependencies with crane buildDepsOnly 2. first-party workspace dependency libraries 3. the final real crate The workspace-libs layer builds the selected package with the same `-p ` selection as final so Cargo feature unification matches, but overlays a crane-generated dummy source for the leaf crate. After that layer builds, remove the dummy leaf artifacts with `cargo clean --release -p ` so the final layer cannot reuse or package stub outputs. This lets leaf edits reuse cached first-party libs while still compiling and linking the real leaf crate. Add explicit `[lib]` target names and `path = "src/lib.rs"` entries to workspace crates. The Nix source minimizer keeps every member Cargo.toml but omits source trees outside the selected crate closure; explicit target paths let Cargo resolve those member manifests without relying on auto-discovery of files that are intentionally absent. They also give crane's dummy source generation a stable target shape. Guard the openshell-core build script's `.git` rerun paths so Cargo does not mark core dirty in Nix source trees where `.git` is absent. Without this, core recompiled in the final layer and cascaded into its dependents. Known limitation: the VM driver package is wired into the flake, but the Nix build does not yet provide the compressed VM runtime artifacts that openshell-driver-vm embeds. For now that crate builds via its stub-resource fallback rather than producing a fully usable VM driver package. Ignore Nix `result*` symlinks created by local builds. --- .gitignore | 3 + crates/openshell-bootstrap/Cargo.toml | 4 + crates/openshell-core/Cargo.toml | 4 + crates/openshell-core/build.rs | 8 +- crates/openshell-driver-docker/Cargo.toml | 4 + crates/openshell-driver-kubernetes/Cargo.toml | 4 + crates/openshell-driver-podman/Cargo.toml | 4 + crates/openshell-ocsf/Cargo.toml | 4 + crates/openshell-policy/Cargo.toml | 4 + crates/openshell-prover/Cargo.toml | 4 + crates/openshell-providers/Cargo.toml | 4 + crates/openshell-router/Cargo.toml | 4 + crates/openshell-server-macros/Cargo.toml | 2 + crates/openshell-tui/Cargo.toml | 4 + crates/openshell-vfio/Cargo.toml | 4 + flake.lock | 16 ++ flake.nix | 81 +++++++++ nix/workspace.nix | 158 ++++++++++++++++++ 18 files changed, 314 insertions(+), 2 deletions(-) create mode 100644 nix/workspace.nix diff --git a/.gitignore b/.gitignore index fb8679fa7..d7d7bad33 100644 --- a/.gitignore +++ b/.gitignore @@ -224,3 +224,6 @@ rfc.md # Markdown/mermaid lint tooling deps scripts/lint-mermaid/node_modules/ + +# Nix +result* diff --git a/crates/openshell-bootstrap/Cargo.toml b/crates/openshell-bootstrap/Cargo.toml index 578d59e65..2e53d1d41 100644 --- a/crates/openshell-bootstrap/Cargo.toml +++ b/crates/openshell-bootstrap/Cargo.toml @@ -9,6 +9,10 @@ license.workspace = true repository.workspace = true rust-version.workspace = true +[lib] +name = "openshell_bootstrap" +path = "src/lib.rs" + [dependencies] openshell-core = { path = "../openshell-core" } bollard = "0.20" diff --git a/crates/openshell-core/Cargo.toml b/crates/openshell-core/Cargo.toml index b03fb1494..8babd7d52 100644 --- a/crates/openshell-core/Cargo.toml +++ b/crates/openshell-core/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_core" +path = "src/lib.rs" + [dependencies] prost = { workspace = true } prost-types = { workspace = true } diff --git a/crates/openshell-core/build.rs b/crates/openshell-core/build.rs index 12e79a1dc..f5cdb13f5 100644 --- a/crates/openshell-core/build.rs +++ b/crates/openshell-core/build.rs @@ -12,8 +12,12 @@ fn main() -> Result<(), Box> { // builds where .git is absent, this silently does nothing and the binary // falls back to CARGO_PKG_VERSION (which is already sed-patched by the // build pipeline). - println!("cargo:rerun-if-changed=../../.git/HEAD"); - println!("cargo:rerun-if-changed=../../.git/refs/tags"); + if Path::new("../../.git/HEAD").exists() { + println!("cargo:rerun-if-changed=../../.git/HEAD"); + } + if Path::new("../../.git/refs/tags").exists() { + println!("cargo:rerun-if-changed=../../.git/refs/tags"); + } if let Some(version) = git_version() { println!("cargo:rustc-env=OPENSHELL_GIT_VERSION={version}"); diff --git a/crates/openshell-driver-docker/Cargo.toml b/crates/openshell-driver-docker/Cargo.toml index e2c97532a..0e423aef7 100644 --- a/crates/openshell-driver-docker/Cargo.toml +++ b/crates/openshell-driver-docker/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_driver_docker" +path = "src/lib.rs" + [dependencies] openshell-core = { path = "../openshell-core" } diff --git a/crates/openshell-driver-kubernetes/Cargo.toml b/crates/openshell-driver-kubernetes/Cargo.toml index c222c9c31..83683943a 100644 --- a/crates/openshell-driver-kubernetes/Cargo.toml +++ b/crates/openshell-driver-kubernetes/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_driver_kubernetes" +path = "src/lib.rs" + [[bin]] name = "openshell-driver-kubernetes" path = "src/main.rs" diff --git a/crates/openshell-driver-podman/Cargo.toml b/crates/openshell-driver-podman/Cargo.toml index 6f2963d92..41d1b7ccb 100644 --- a/crates/openshell-driver-podman/Cargo.toml +++ b/crates/openshell-driver-podman/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_driver_podman" +path = "src/lib.rs" + [[bin]] name = "openshell-driver-podman" path = "src/main.rs" diff --git a/crates/openshell-ocsf/Cargo.toml b/crates/openshell-ocsf/Cargo.toml index 14cc93ba3..fca761bd1 100644 --- a/crates/openshell-ocsf/Cargo.toml +++ b/crates/openshell-ocsf/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_ocsf" +path = "src/lib.rs" + [dependencies] chrono = { version = "0.4", features = ["serde"] } serde = { workspace = true } diff --git a/crates/openshell-policy/Cargo.toml b/crates/openshell-policy/Cargo.toml index 8936b85be..82c10a4e1 100644 --- a/crates/openshell-policy/Cargo.toml +++ b/crates/openshell-policy/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_policy" +path = "src/lib.rs" + [dependencies] openshell-core = { path = "../openshell-core" } serde = { workspace = true } diff --git a/crates/openshell-prover/Cargo.toml b/crates/openshell-prover/Cargo.toml index ee815f3a3..749c05379 100644 --- a/crates/openshell-prover/Cargo.toml +++ b/crates/openshell-prover/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_prover" +path = "src/lib.rs" + [features] bundled-z3 = ["z3/bundled"] diff --git a/crates/openshell-providers/Cargo.toml b/crates/openshell-providers/Cargo.toml index e82574d73..c9345f3e0 100644 --- a/crates/openshell-providers/Cargo.toml +++ b/crates/openshell-providers/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_providers" +path = "src/lib.rs" + [dependencies] openshell-core = { path = "../openshell-core" } serde = { workspace = true } diff --git a/crates/openshell-router/Cargo.toml b/crates/openshell-router/Cargo.toml index e4c3d5ea7..af3fdb78a 100644 --- a/crates/openshell-router/Cargo.toml +++ b/crates/openshell-router/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_router" +path = "src/lib.rs" + [dependencies] openshell-core = { path = "../openshell-core" } bytes = { workspace = true } diff --git a/crates/openshell-server-macros/Cargo.toml b/crates/openshell-server-macros/Cargo.toml index f929d43a6..fc04db568 100644 --- a/crates/openshell-server-macros/Cargo.toml +++ b/crates/openshell-server-macros/Cargo.toml @@ -10,6 +10,8 @@ license.workspace = true repository.workspace = true [lib] +name = "openshell_server_macros" +path = "src/lib.rs" proc-macro = true [dependencies] diff --git a/crates/openshell-tui/Cargo.toml b/crates/openshell-tui/Cargo.toml index b0ac0c7ca..386b1243f 100644 --- a/crates/openshell-tui/Cargo.toml +++ b/crates/openshell-tui/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_tui" +path = "src/lib.rs" + [dependencies] openshell-core = { path = "../openshell-core" } openshell-bootstrap = { path = "../openshell-bootstrap" } diff --git a/crates/openshell-vfio/Cargo.toml b/crates/openshell-vfio/Cargo.toml index b6d7cc3cd..7752d4543 100644 --- a/crates/openshell-vfio/Cargo.toml +++ b/crates/openshell-vfio/Cargo.toml @@ -10,6 +10,10 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[lib] +name = "openshell_vfio" +path = "src/lib.rs" + [dependencies] serde = { workspace = true } serde_json = { workspace = true } diff --git a/flake.lock b/flake.lock index 7b9881771..8de4f5362 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,20 @@ { "nodes": { + "crane": { + "locked": { + "lastModified": 1780099841, + "narHash": "sha256-EVZd2RsbpreRUDSi9rBwPY+ZxoyMaiEBbZxxhljbaS4=", + "owner": "ipetkov", + "repo": "crane", + "rev": "0532eb17955225173906d671fb36306bdeb1e2dc", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -36,6 +51,7 @@ }, "root": { "inputs": { + "crane": "crane", "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay", diff --git a/flake.nix b/flake.nix index 13c4857bc..37c782e51 100644 --- a/flake.nix +++ b/flake.nix @@ -11,6 +11,7 @@ url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "nixpkgs"; }; + crane.url = "github:ipetkov/crane"; treefmt-nix = { url = "github:numtide/treefmt-nix"; inputs.nixpkgs.follows = "nixpkgs"; @@ -22,6 +23,7 @@ flake-utils, nixpkgs, rust-overlay, + crane, treefmt-nix, ... }: @@ -32,13 +34,92 @@ inherit system; overlays = [ (import rust-overlay) ]; }; + lib = pkgs.lib; rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; + + craneLib = (crane.mkLib pkgs).overrideToolchain (_: rustToolchain); + + # Crate-by-crate crane helpers (workspace graph, minimal per-crate + # source, buildWorkspaceCrate). See nix/workspace.nix. + workspace = import ./nix/workspace.nix { + inherit lib pkgs craneLib; + root = ./.; + }; + inherit (workspace) buildWorkspaceCrate; + + # z3 (found via pkg-config) and libclang (for z3-sys bindgen) are only + # needed by crates whose closure contains openshell-prover. + withZ3 = { + nativeBuildInputs = [ pkgs.pkg-config ]; + buildInputs = [ pkgs.z3 ]; + env.LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; + }; + + # Each crate declares the compile-time assets its build needs — its own + # plus those of its workspace deps (proto/ arrives via openshell-core, + # providers/ via openshell-providers, registry/ via openshell-prover). + crates = { + openshell-cli = buildWorkspaceCrate ( + { + dir = "openshell-cli"; + assets = [ + ./proto + ./providers + ./crates/openshell-prover/registry + ]; + } + // withZ3 + ); + openshell-server = buildWorkspaceCrate ( + { + dir = "openshell-server"; + assets = [ + ./proto + ./providers + ./crates/openshell-prover/registry + ./crates/openshell-server/migrations + ]; + } + // withZ3 + ); + openshell-sandbox = buildWorkspaceCrate { + dir = "openshell-sandbox"; + assets = [ + ./proto + ./crates/openshell-sandbox/data + ./crates/openshell-sandbox/src/skills + ]; + }; + openshell-driver-vm = buildWorkspaceCrate { + dir = "openshell-driver-vm"; + assets = [ + ./proto + ./crates/openshell-driver-vm/scripts + ]; + }; + openshell-driver-kubernetes = buildWorkspaceCrate { + dir = "openshell-driver-kubernetes"; + assets = [ ./proto ]; + }; + openshell-driver-podman = buildWorkspaceCrate { + dir = "openshell-driver-podman"; + assets = [ ./proto ]; + }; + }; + treefmtEval = treefmt-nix.lib.evalModule pkgs { projectRootFile = "flake.nix"; programs.nixfmt.enable = true; }; in { + packages = crates // { + default = pkgs.symlinkJoin { + name = "openshell-0.0.0"; + paths = lib.attrValues crates; + }; + }; + devShells.default = pkgs.mkShell { packages = with pkgs; [ rustToolchain diff --git a/nix/workspace.nix b/nix/workspace.nix new file mode 100644 index 000000000..41f642192 --- /dev/null +++ b/nix/workspace.nix @@ -0,0 +1,158 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Crate-by-crate crane helpers for a Cargo workspace. +# +# Each crate is built from a minimal source, its own code plus that of its +# transitive workspace dependencies, and gets its own dependency cache. Crates +# outside that closure are reduced to their Cargo.toml so cargo can resolve +# the workspace without their source and without any Cargo.toml edits. +# Editing one crate never rebuilds an unrelated crate, and (because crane +# launders the source before building deps) never rebuilds any crate's +# dependency cache. +{ + lib, + pkgs, + craneLib, + # Workspace root: holds the virtual Cargo.toml, Cargo.lock and .cargo/. + root, + # Member directory, relative to root. + crateDir ? "crates", + # Version stamped onto every crate derivation. + version ? "0.0.0", +}: +let + cratesRoot = root + "/${crateDir}"; + + # Workspace dependency graph, derived from the Cargo.tomls + crateDirs = lib.attrNames (lib.filterAttrs (_: t: t == "directory") (builtins.readDir cratesRoot)); + + # Direct intra-workspace path-dependencies of a crate, as dir names. + directDeps = + dir: + let + manifest = builtins.fromTOML (builtins.readFile (cratesRoot + "/${dir}/Cargo.toml")); + in + lib.pipe (manifest.dependencies or { }) [ + lib.attrValues + (lib.filter (v: builtins.isAttrs v && v ? path)) + (map (v: baseNameOf v.path)) + (lib.filter (d: builtins.elem d crateDirs)) + ]; + + # Transitive closure of a crate within the workspace: its own dir plus every workspace dep. + closureOf = + dir: + map (e: e.key) ( + builtins.genericClosure { + startSet = [ { key = dir; } ]; + operator = e: map (key: { inherit key; }) (directDeps e.key); + } + ); + + # Every member's Cargo.toml, cargo must see all of them to resolve the + # workspace even for crates whose source we leave out. + allManifests = map (d: cratesRoot + "/${d}/Cargo.toml") crateDirs; + + # Source tree carrying the real sources of the given crate dirs, plus every + # member's Cargo.toml and the given assets. + mkSrc = + { + dirs, + assets ? [ ], + }: + lib.fileset.toSource { + inherit root; + fileset = lib.fileset.unions ( + [ + (root + "/Cargo.toml") + (root + "/Cargo.lock") + (root + "/.cargo") + ] + ++ allManifests + ++ map (d: craneLib.fileset.commonCargoSources (cratesRoot + "/${d}")) dirs + ++ assets + ); + }; + + # Build one workspace crate (pname == dir) in three cached layers. Every layer + # uses the SAME `-p ` selection, so cargo's feature unification is + # identical across them and the compiled artifacts are reusable: + # 1. crates.io deps — buildDepsOnly; immune to first-party code. + # 2. workspace-dep libs — build `-p ` with the crate's OWN source + # stubbed (real path-deps), so its libs compile with + # the crate's real feature set and get cached. + # 3. the crate itself — reuses 1 + 2; only the crate's own code recompiles. + buildWorkspaceCrate = + { + dir, + assets ? [ ], + nativeBuildInputs ? [ ], + buildInputs ? [ ], + env ? { }, + }: + let + closure = closureOf dir; + workspaceDeps = lib.filter (d: d != dir) closure; + common = { + pname = dir; + inherit + version + nativeBuildInputs + buildInputs + env + ; + strictDeps = true; + # Build only, skip the cargo test/check phase for now. + doCheck = false; + cargoExtraArgs = "--locked -p ${dir}"; + }; + + cratesDeps = craneLib.buildDepsOnly (common // { src = mkSrc { dirs = [ ]; }; }); + + mkWorkspaceLibsSrc = + let + base = mkSrc { + dirs = workspaceDeps; + inherit assets; + }; + dummyCrate = craneLib.mkDummySrc { src = cratesRoot + "/${dir}"; }; + in + pkgs.runCommandLocal "source" { } '' + cp -r ${base} $out + chmod -R u+w $out + rm -rf "$out/${crateDir}/${dir}" + cp -r ${dummyCrate} "$out/${crateDir}/${dir}" + ''; + + workspaceLibs = + if workspaceDeps == [ ] then + cratesDeps + else + craneLib.buildPackage ( + common + // { + pname = "${dir}-workspace-libs"; + src = mkWorkspaceLibsSrc; + cargoArtifacts = cratesDeps; + doInstallCargoArtifacts = true; + postInstall = '' + cargo clean --release -p ${dir} + ''; + } + ); + in + craneLib.buildPackage ( + common + // { + src = mkSrc { + dirs = closure; + inherit assets; + }; + cargoArtifacts = workspaceLibs; + } + ); +in +{ + inherit buildWorkspaceCrate; +} From 15dd9c30d2dd1bb59d0b462a0b291d06d2957cfc Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Tue, 2 Jun 2026 14:55:32 +0200 Subject: [PATCH 2/4] refactor(nix): centralize crate build specs --- flake.nix | 62 ++++----------------------------------------------- nix/crate.nix | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 58 deletions(-) create mode 100644 nix/crate.nix diff --git a/flake.nix b/flake.nix index 37c782e51..074aab395 100644 --- a/flake.nix +++ b/flake.nix @@ -47,65 +47,11 @@ }; inherit (workspace) buildWorkspaceCrate; - # z3 (found via pkg-config) and libclang (for z3-sys bindgen) are only - # needed by crates whose closure contains openshell-prover. - withZ3 = { - nativeBuildInputs = [ pkgs.pkg-config ]; - buildInputs = [ pkgs.z3 ]; - env.LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; - }; - - # Each crate declares the compile-time assets its build needs — its own - # plus those of its workspace deps (proto/ arrives via openshell-core, - # providers/ via openshell-providers, registry/ via openshell-prover). - crates = { - openshell-cli = buildWorkspaceCrate ( - { - dir = "openshell-cli"; - assets = [ - ./proto - ./providers - ./crates/openshell-prover/registry - ]; - } - // withZ3 - ); - openshell-server = buildWorkspaceCrate ( - { - dir = "openshell-server"; - assets = [ - ./proto - ./providers - ./crates/openshell-prover/registry - ./crates/openshell-server/migrations - ]; - } - // withZ3 - ); - openshell-sandbox = buildWorkspaceCrate { - dir = "openshell-sandbox"; - assets = [ - ./proto - ./crates/openshell-sandbox/data - ./crates/openshell-sandbox/src/skills - ]; - }; - openshell-driver-vm = buildWorkspaceCrate { - dir = "openshell-driver-vm"; - assets = [ - ./proto - ./crates/openshell-driver-vm/scripts - ]; - }; - openshell-driver-kubernetes = buildWorkspaceCrate { - dir = "openshell-driver-kubernetes"; - assets = [ ./proto ]; - }; - openshell-driver-podman = buildWorkspaceCrate { - dir = "openshell-driver-podman"; - assets = [ ./proto ]; - }; + crateSpecs = import ./nix/crate.nix { + inherit pkgs; + root = ./.; }; + crates = lib.mapAttrs (_: buildWorkspaceCrate) crateSpecs; treefmtEval = treefmt-nix.lib.evalModule pkgs { projectRootFile = "flake.nix"; diff --git a/nix/crate.nix b/nix/crate.nix new file mode 100644 index 000000000..3164d8fbb --- /dev/null +++ b/nix/crate.nix @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +{ + pkgs, + root, +}: +let + # z3 (found via pkg-config) and libclang (for z3-sys bindgen) are only needed + # by crates whose closure contains openshell-prover. + withZ3 = { + nativeBuildInputs = [ pkgs.pkg-config ]; + buildInputs = [ pkgs.z3 ]; + env.LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; + }; +in +{ + # Each crate declares the compile-time assets its build needs: its own plus + # those of its workspace deps (proto/ arrives via openshell-core, providers/ + # via openshell-providers, registry/ via openshell-prover). + openshell-cli = withZ3 // { + dir = "openshell-cli"; + assets = [ + (root + "/proto") + (root + "/providers") + (root + "/crates/openshell-prover/registry") + ]; + }; + openshell-server = withZ3 // { + dir = "openshell-server"; + assets = [ + (root + "/proto") + (root + "/providers") + (root + "/crates/openshell-prover/registry") + (root + "/crates/openshell-server/migrations") + ]; + }; + openshell-sandbox = { + dir = "openshell-sandbox"; + assets = [ + (root + "/proto") + (root + "/crates/openshell-sandbox/data") + (root + "/crates/openshell-sandbox/src/skills") + ]; + }; + openshell-driver-vm = { + dir = "openshell-driver-vm"; + assets = [ + (root + "/proto") + (root + "/crates/openshell-driver-vm/scripts") + ]; + }; + openshell-driver-kubernetes = { + dir = "openshell-driver-kubernetes"; + assets = [ (root + "/proto") ]; + }; + openshell-driver-podman = { + dir = "openshell-driver-podman"; + assets = [ (root + "/proto") ]; + }; +} From 8e61ef189d95e0c1e3c78ae3c7bf0b25d6c4dc62 Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Tue, 2 Jun 2026 16:04:30 +0200 Subject: [PATCH 3/4] fix(nix): provide protoc for builds --- crates/openshell-core/build.rs | 29 ++++++++++++++++++++--------- flake.nix | 2 ++ nix/crate.nix | 9 ++++++++- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/crates/openshell-core/build.rs b/crates/openshell-core/build.rs index f5cdb13f5..0d382d4c1 100644 --- a/crates/openshell-core/build.rs +++ b/crates/openshell-core/build.rs @@ -26,15 +26,16 @@ fn main() -> Result<(), Box> { // --- Protobuf compilation --- // Re-run when anything under proto/ changes (including newly added .proto files). println!("cargo:rerun-if-changed={PROTO_REL}"); - // Use bundled protoc from protobuf-src. The system protoc (from apt-get) - // does not bundle the well-known type includes (google/protobuf/struct.proto - // etc.), so we must use protobuf-src which ships both the binary and the - // include tree. - // SAFETY: This is run at build time in a single-threaded build script context. - // No other threads are reading environment variables concurrently. - #[allow(unsafe_code)] - unsafe { - env::set_var("PROTOC", protobuf_src::protoc()); + if env::var_os("PROTOC").is_none() && !path_has_protoc() { + // Keep non-Nix builds working without requiring users to install protoc. + // Nix builds provide protoc explicitly, so they do not rely on this + // vendored fallback. + // SAFETY: This is run at build time in a single-threaded build script context. + // No other threads are reading environment variables concurrently. + #[allow(unsafe_code)] + unsafe { + env::set_var("PROTOC", protobuf_src::protoc()); + } } let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); @@ -76,6 +77,16 @@ fn collect_proto_files(dir: &Path, out: &mut Vec) -> std::io::Result<() Ok(()) } +fn path_has_protoc() -> bool { + let Some(path) = env::var_os("PATH") else { + return false; + }; + + env::split_paths(&path) + .map(|dir| dir.join(format!("protoc{}", env::consts::EXE_SUFFIX))) + .any(|candidate| candidate.is_file()) +} + /// Derive a version string from `git describe --tags`. /// /// Implements the "guess-next-dev" convention used by the release pipeline diff --git a/flake.nix b/flake.nix index 074aab395..cf8d9ce51 100644 --- a/flake.nix +++ b/flake.nix @@ -71,6 +71,8 @@ rustToolchain # Required to find packages pkg-config + # Required for protobuf code generation. + protobuf # Required for bindgen generation. llvmPackages.libclang # system dependency for openshell-prover diff --git a/nix/crate.nix b/nix/crate.nix index 3164d8fbb..018bf6e9b 100644 --- a/nix/crate.nix +++ b/nix/crate.nix @@ -9,7 +9,10 @@ let # z3 (found via pkg-config) and libclang (for z3-sys bindgen) are only needed # by crates whose closure contains openshell-prover. withZ3 = { - nativeBuildInputs = [ pkgs.pkg-config ]; + nativeBuildInputs = [ + pkgs.pkg-config + pkgs.protobuf + ]; buildInputs = [ pkgs.z3 ]; env.LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; }; @@ -37,6 +40,7 @@ in }; openshell-sandbox = { dir = "openshell-sandbox"; + nativeBuildInputs = [ pkgs.protobuf ]; assets = [ (root + "/proto") (root + "/crates/openshell-sandbox/data") @@ -45,6 +49,7 @@ in }; openshell-driver-vm = { dir = "openshell-driver-vm"; + nativeBuildInputs = [ pkgs.protobuf ]; assets = [ (root + "/proto") (root + "/crates/openshell-driver-vm/scripts") @@ -52,10 +57,12 @@ in }; openshell-driver-kubernetes = { dir = "openshell-driver-kubernetes"; + nativeBuildInputs = [ pkgs.protobuf ]; assets = [ (root + "/proto") ]; }; openshell-driver-podman = { dir = "openshell-driver-podman"; + nativeBuildInputs = [ pkgs.protobuf ]; assets = [ (root + "/proto") ]; }; } From 634f499614b2eabff3630e9cf900be0aabba6f73 Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Tue, 2 Jun 2026 16:36:36 +0200 Subject: [PATCH 4/4] refactor(nix): propagate workspace crate inputs --- flake.nix | 18 ++++++++--- nix/crate.nix | 81 +++++++++++++++++++++++++++++++++++------------ nix/workspace.nix | 45 ++++++++++++++++---------- 3 files changed, 103 insertions(+), 41 deletions(-) diff --git a/flake.nix b/flake.nix index cf8d9ce51..50c56474b 100644 --- a/flake.nix +++ b/flake.nix @@ -39,19 +39,29 @@ craneLib = (crane.mkLib pkgs).overrideToolchain (_: rustToolchain); + crateSpecs = import ./nix/crate.nix { + inherit pkgs; + root = ./.; + }; + # Crate-by-crate crane helpers (workspace graph, minimal per-crate # source, buildWorkspaceCrate). See nix/workspace.nix. workspace = import ./nix/workspace.nix { inherit lib pkgs craneLib; root = ./.; + inherit crateSpecs; }; inherit (workspace) buildWorkspaceCrate; - crateSpecs = import ./nix/crate.nix { - inherit pkgs; - root = ./.; + workspaceCrates = lib.mapAttrs (_: buildWorkspaceCrate) crateSpecs; + crates = { + openshell = workspaceCrates.openshell-cli.package; + openshell-gateway = workspaceCrates.openshell-server.package; + openshell-sandbox = workspaceCrates.openshell-sandbox.package; + openshell-driver-vm = workspaceCrates.openshell-driver-vm.package; + openshell-driver-kubernetes = workspaceCrates.openshell-driver-kubernetes.package; + openshell-driver-podman = workspaceCrates.openshell-driver-podman.package; }; - crates = lib.mapAttrs (_: buildWorkspaceCrate) crateSpecs; treefmtEval = treefmt-nix.lib.evalModule pkgs { projectRootFile = "flake.nix"; diff --git a/nix/crate.nix b/nix/crate.nix index 018bf6e9b..1458e7ec3 100644 --- a/nix/crate.nix +++ b/nix/crate.nix @@ -5,23 +5,15 @@ pkgs, root, }: -let - # z3 (found via pkg-config) and libclang (for z3-sys bindgen) are only needed - # by crates whose closure contains openshell-prover. - withZ3 = { - nativeBuildInputs = [ - pkgs.pkg-config - pkgs.protobuf - ]; - buildInputs = [ pkgs.z3 ]; - env.LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; - }; -in { - # Each crate declares the compile-time assets its build needs: its own plus - # those of its workspace deps (proto/ arrives via openshell-core, providers/ - # via openshell-providers, registry/ via openshell-prover). - openshell-cli = withZ3 // { + # Each crate declares the compile-time assets and build tools it needs. The + # workspace builder collects nativeBuildInputs/buildInputs/env from the + # transitive Cargo closure. + openshell-bootstrap = { + dir = "openshell-bootstrap"; + assets = [ (root + "/proto") ]; + }; + openshell-cli = { dir = "openshell-cli"; assets = [ (root + "/proto") @@ -29,7 +21,7 @@ in (root + "/crates/openshell-prover/registry") ]; }; - openshell-server = withZ3 // { + openshell-server = { dir = "openshell-server"; assets = [ (root + "/proto") @@ -38,9 +30,17 @@ in (root + "/crates/openshell-server/migrations") ]; }; + openshell-core = { + dir = "openshell-core"; + nativeBuildInputs = [ pkgs.protobuf ]; + assets = [ (root + "/proto") ]; + }; + openshell-driver-docker = { + dir = "openshell-driver-docker"; + assets = [ (root + "/proto") ]; + }; openshell-sandbox = { dir = "openshell-sandbox"; - nativeBuildInputs = [ pkgs.protobuf ]; assets = [ (root + "/proto") (root + "/crates/openshell-sandbox/data") @@ -49,7 +49,6 @@ in }; openshell-driver-vm = { dir = "openshell-driver-vm"; - nativeBuildInputs = [ pkgs.protobuf ]; assets = [ (root + "/proto") (root + "/crates/openshell-driver-vm/scripts") @@ -57,12 +56,52 @@ in }; openshell-driver-kubernetes = { dir = "openshell-driver-kubernetes"; - nativeBuildInputs = [ pkgs.protobuf ]; assets = [ (root + "/proto") ]; }; openshell-driver-podman = { dir = "openshell-driver-podman"; - nativeBuildInputs = [ pkgs.protobuf ]; assets = [ (root + "/proto") ]; }; + openshell-ocsf = { + dir = "openshell-ocsf"; + assets = [ (root + "/crates/openshell-ocsf/schemas") ]; + }; + openshell-policy = { + dir = "openshell-policy"; + assets = [ (root + "/proto") ]; + }; + openshell-prover = { + dir = "openshell-prover"; + nativeBuildInputs = [ pkgs.pkg-config ]; + buildInputs = [ pkgs.z3 ]; + env.LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; + assets = [ + (root + "/crates/openshell-prover/registry") + (root + "/crates/openshell-prover/testdata") + ]; + }; + openshell-providers = { + dir = "openshell-providers"; + assets = [ + (root + "/proto") + (root + "/providers") + ]; + }; + openshell-router = { + dir = "openshell-router"; + assets = [ (root + "/proto") ]; + }; + openshell-server-macros = { + dir = "openshell-server-macros"; + }; + openshell-tui = { + dir = "openshell-tui"; + assets = [ + (root + "/proto") + (root + "/providers") + ]; + }; + openshell-vfio = { + dir = "openshell-vfio"; + }; } diff --git a/nix/workspace.nix b/nix/workspace.nix index 41f642192..4189d5a85 100644 --- a/nix/workspace.nix +++ b/nix/workspace.nix @@ -18,6 +18,8 @@ root, # Member directory, relative to root. crateDir ? "crates", + # Crate metadata keyed by workspace crate directory. + crateSpecs ? { }, # Version stamped onto every crate derivation. version ? "0.0.0", }: @@ -50,6 +52,12 @@ let } ); + specFor = dir: lib.attrByPath [ dir ] { } crateSpecs; + + closureList = closure: field: lib.concatLists (map (d: (specFor d).${field} or [ ]) closure); + + closureEnv = closure: lib.foldl' lib.recursiveUpdate { } (map (d: (specFor d).env or { }) closure); + # Every member's Cargo.toml, cargo must see all of them to resolve the # workspace even for crates whose source we leave out. allManifests = map (d: cratesRoot + "/${d}/Cargo.toml") crateDirs; @@ -94,14 +102,17 @@ let let closure = closureOf dir; workspaceDeps = lib.filter (d: d != dir) closure; + effectiveNativeBuildInputs = lib.unique ( + closureList closure "nativeBuildInputs" ++ nativeBuildInputs + ); + effectiveBuildInputs = lib.unique (closureList closure "buildInputs" ++ buildInputs); + effectiveEnv = lib.recursiveUpdate (closureEnv closure) env; common = { pname = dir; - inherit - version - nativeBuildInputs - buildInputs - env - ; + inherit version; + nativeBuildInputs = effectiveNativeBuildInputs; + buildInputs = effectiveBuildInputs; + env = effectiveEnv; strictDeps = true; # Build only, skip the cargo test/check phase for now. doCheck = false; @@ -142,16 +153,18 @@ let } ); in - craneLib.buildPackage ( - common - // { - src = mkSrc { - dirs = closure; - inherit assets; - }; - cargoArtifacts = workspaceLibs; - } - ); + { + package = craneLib.buildPackage ( + common + // { + src = mkSrc { + dirs = closure; + inherit assets; + }; + cargoArtifacts = workspaceLibs; + } + ); + }; in { inherit buildWorkspaceCrate;