← 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
M flake.lockM flake.nixA rust/parts.nix
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 →