[object Object]

← back to Exo

nix: add Rust builds with crane and fenix

e6434ec446a72af870f074d5934ceadd273a5c1b · 2026-01-12 17:27:47 +0000 · Jake Hillion

The Rust workspace lacked Nix build support, making it difficult to
build packages reproducibly or run checks in CI.

Added a flake-parts module at rust/parts.nix that uses crane for Rust
builds and fenix for the nightly toolchain. The source filter isolates
rust/ and root Cargo files to prevent Python/docs changes from
triggering Rust rebuilds. Exports packages (system_custodian,
exo_pyo3_bindings wheel, exo-rust-workspace) and checks (cargo-nextest,
cargo-doc) for all three target platforms.

The devShell now uses inputsFrom to inherit build dependencies from the
workspace package, removing the need for manual pkg-config/openssl setup.

Test plan:
- Ran `nix flake check` successfully
- Built `nix build ".#checks.x86_64-linux.cargo-nextest"` and tests pass
- Built `nix build ".#exo_pyo3_bindings"` and wheel is produced

Files touched

Diff

commit e6434ec446a72af870f074d5934ceadd273a5c1b
Author: Jake Hillion <jake@hillion.co.uk>
Date:   Mon Jan 12 17:27:47 2026 +0000

    nix: add Rust builds with crane and fenix
    
    The Rust workspace lacked Nix build support, making it difficult to
    build packages reproducibly or run checks in CI.
    
    Added a flake-parts module at rust/parts.nix that uses crane for Rust
    builds and fenix for the nightly toolchain. The source filter isolates
    rust/ and root Cargo files to prevent Python/docs changes from
    triggering Rust rebuilds. Exports packages (system_custodian,
    exo_pyo3_bindings wheel, exo-rust-workspace) and checks (cargo-nextest,
    cargo-doc) for all three target platforms.
    
    The devShell now uses inputsFrom to inherit build dependencies from the
    workspace package, removing the need for manual pkg-config/openssl setup.
    
    Test plan:
    - Ran `nix flake check` successfully
    - Built `nix build ".#checks.x86_64-linux.cargo-nextest"` and tests pass
    - Built `nix build ".#exo_pyo3_bindings"` and wheel is produced
---
 flake.lock     |  16 ++++++
 flake.nix      |  44 +++++++---------
 rust/parts.nix | 159 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 193 insertions(+), 26 deletions(-)

diff --git a/flake.lock b/flake.lock
index d45f6f50..41de94a6 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1,5 +1,20 @@
 {
   "nodes": {
+    "crane": {
+      "locked": {
+        "lastModified": 1767744144,
+        "narHash": "sha256-9/9ntI0D+HbN4G0TrK3KmHbTvwgswz7p8IEJsWyef8Q=",
+        "owner": "ipetkov",
+        "repo": "crane",
+        "rev": "2fb033290bf6b23f226d4c8b32f7f7a16b043d7e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "ipetkov",
+        "repo": "crane",
+        "type": "github"
+      }
+    },
     "fenix": {
       "inputs": {
         "nixpkgs": [
@@ -75,6 +90,7 @@
     },
     "root": {
       "inputs": {
+        "crane": "crane",
         "fenix": "fenix",
         "flake-parts": "flake-parts",
         "nixpkgs": "nixpkgs",
diff --git a/flake.nix b/flake.nix
index 15df8eef..a9ae9ca7 100644
--- a/flake.nix
+++ b/flake.nix
@@ -9,6 +9,8 @@
       inputs.nixpkgs-lib.follows = "nixpkgs";
     };
 
+    crane.url = "github:ipetkov/crane";
+
     fenix = {
       url = "github:nix-community/fenix";
       inputs.nixpkgs.follows = "nixpkgs";
@@ -39,10 +41,11 @@
 
       imports = [
         inputs.treefmt-nix.flakeModule
+        ./rust/parts.nix
       ];
 
       perSystem =
-        { config, inputs', pkgs, lib, system, ... }:
+        { config, self', inputs', pkgs, lib, system, ... }:
         let
           fenixToolchain = inputs'.fenix.packages.complete;
           # Use pinned nixpkgs for swift-format (swift is broken on x86_64-linux in newer nixpkgs)
@@ -59,7 +62,7 @@
               };
               rustfmt = {
                 enable = true;
-                package = fenixToolchain.rustfmt;
+                package = config.rust.toolchain;
               };
               prettier = {
                 enable = true;
@@ -79,6 +82,8 @@
           '';
 
           devShells.default = with pkgs; pkgs.mkShell {
+            inputsFrom = [ self'.checks.cargo-build ];
+
             packages =
               [
                 # FORMATTING
@@ -91,14 +96,8 @@
                 basedpyright
 
                 # RUST
-                (fenixToolchain.withComponents [
-                  "cargo"
-                  "rustc"
-                  "clippy"
-                  "rustfmt"
-                  "rust-src"
-                ])
-                rustup # Just here to make RustRover happy
+                config.rust.toolchain
+                maturin
 
                 # NIX
                 nixpkgs-fmt
@@ -110,26 +109,19 @@
                 just
                 jq
               ]
-              ++ (pkgs.lib.optionals pkgs.stdenv.isLinux [
-                # IFCONFIG
+              ++ lib.optionals stdenv.isLinux [
                 unixtools.ifconfig
-
-                # Build dependencies for Linux
-                pkg-config
-                openssl
-              ])
-              ++ (pkgs.lib.optionals pkgs.stdenv.isDarwin [
-                # MACMON
+              ]
+              ++ lib.optionals stdenv.isDarwin [
                 macmon
-              ]);
+              ];
+
+            OPENSSL_NO_VENDOR = "1";
 
             shellHook = ''
-              # PYTHON
-              export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.python313}/lib"
-              ${lib.optionalString pkgs.stdenv.isLinux ''
-                # Build environment for Linux
-                export PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig:$PKG_CONFIG_PATH"
-                export LD_LIBRARY_PATH="${pkgs.openssl.out}/lib:$LD_LIBRARY_PATH"
+              export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${python313}/lib"
+              ${lib.optionalString stdenv.isLinux ''
+                export LD_LIBRARY_PATH="${openssl.out}/lib:$LD_LIBRARY_PATH"
               ''}
             '';
           };
diff --git a/rust/parts.nix b/rust/parts.nix
new file mode 100644
index 00000000..e00b27c6
--- /dev/null
+++ b/rust/parts.nix
@@ -0,0 +1,159 @@
+{ inputs, ... }:
+{
+  perSystem =
+    { config, self', inputs', pkgs, lib, ... }:
+    let
+      # Fenix nightly toolchain with all components
+      fenixPkgs = inputs'.fenix.packages;
+      rustToolchain = fenixPkgs.complete.withComponents [
+        "cargo"
+        "rustc"
+        "clippy"
+        "rustfmt"
+        "rust-src"
+        "rust-analyzer"
+      ];
+
+      # Crane with fenix toolchain
+      craneLib = (inputs.crane.mkLib pkgs).overrideToolchain rustToolchain;
+
+      # Source filtering - only include rust/ directory and root Cargo files
+      # This ensures changes to Python/docs/etc don't trigger Rust rebuilds
+      src = lib.cleanSourceWith {
+        src = inputs.self;
+        filter =
+          path: type:
+          let
+            baseName = builtins.baseNameOf path;
+            parentDir = builtins.dirOf path;
+            inRustDir =
+              (lib.hasInfix "/rust/" path)
+              || (lib.hasSuffix "/rust" parentDir)
+              || (baseName == "rust" && type == "directory");
+            isRootCargoFile =
+              (baseName == "Cargo.toml" || baseName == "Cargo.lock")
+              && (builtins.dirOf path == toString inputs.self);
+          in
+          isRootCargoFile
+          || (inRustDir && (craneLib.filterCargoSources path type || lib.hasSuffix ".toml" path || lib.hasSuffix ".md" path));
+      };
+
+      # Common arguments for all Rust builds
+      commonArgs = {
+        inherit src;
+        pname = "exo-rust";
+        version = "0.0.1";
+        strictDeps = true;
+
+        nativeBuildInputs = [
+          pkgs.pkg-config
+          pkgs.python313 # Required for pyo3-build-config
+        ];
+
+        buildInputs = [
+          pkgs.openssl
+          pkgs.python313 # Required for pyo3 tests
+        ];
+
+        OPENSSL_NO_VENDOR = "1";
+
+        # Required for pyo3 tests to find libpython
+        LD_LIBRARY_PATH = lib.makeLibraryPath [ pkgs.python313 ];
+      };
+
+      # Build dependencies once for caching
+      cargoArtifacts = craneLib.buildDepsOnly (
+        commonArgs
+        // {
+          cargoExtraArgs = "--workspace";
+        }
+      );
+    in
+    {
+      # Export toolchain for use in treefmt and devShell
+      options.rust = {
+        toolchain = lib.mkOption {
+          type = lib.types.package;
+          default = rustToolchain;
+          description = "The Rust toolchain to use";
+        };
+      };
+
+      config = {
+        packages = {
+          # The system_custodian binary
+          system_custodian = craneLib.buildPackage (
+            commonArgs
+            // {
+              inherit cargoArtifacts;
+              cargoExtraArgs = "-p system_custodian";
+
+              meta = {
+                description = "System custodian daemon for exo";
+                mainProgram = "system_custodian";
+              };
+            }
+          );
+
+          # Python bindings wheel via maturin
+          exo_pyo3_bindings = craneLib.buildPackage (
+            commonArgs
+            // {
+              inherit cargoArtifacts;
+              pname = "exo_pyo3_bindings";
+
+              nativeBuildInputs = commonArgs.nativeBuildInputs ++ [
+                pkgs.maturin
+              ];
+
+              buildPhaseCargoCommand = ''
+                maturin build \
+                  --release \
+                  --manylinux off \
+                  --manifest-path rust/exo_pyo3_bindings/Cargo.toml \
+                  --features "pyo3/extension-module,pyo3/experimental-async" \
+                  --interpreter ${pkgs.python313}/bin/python \
+                  --out dist
+              '';
+
+              # Don't use crane's default install behavior
+              doNotPostBuildInstallCargoBinaries = true;
+
+              installPhaseCommand = ''
+                mkdir -p $out
+                cp dist/*.whl $out/
+              '';
+            }
+          );
+        };
+
+        checks = {
+          # Full workspace build (all crates)
+          cargo-build = craneLib.buildPackage (
+            commonArgs
+            // {
+              inherit cargoArtifacts;
+              cargoExtraArgs = "--workspace";
+            }
+          );
+          # Run tests with nextest
+          cargo-nextest = craneLib.cargoNextest (
+            commonArgs
+            // {
+              inherit cargoArtifacts;
+              cargoExtraArgs = "--workspace";
+            }
+          );
+
+          # Build documentation
+          cargo-doc = craneLib.cargoDoc (
+            commonArgs
+            // {
+              inherit cargoArtifacts;
+              cargoExtraArgs = "--workspace";
+            }
+          );
+        };
+      };
+    };
+}

← bdb43e1d nix: drop noisy echos from devshell  ·  back to Exo  ·  nix: add dashboard build with dream2nix 3671528f →