Rust
Rust has become increasingly popular for systems programming, offering memory safety without garbage collection. Rust is a favorite amongst many for its safety, speed, and correctness qualities.
Rust and Nix: special concerns
Cargo and dependency management
Rust uses Cargo for dependency management, which downloads dependencies from crates.io at build time. This conflicts with Nix’s requirement for reproducible, offline builds. Nix solves this by:
- Pre-fetching dependencies: Nix downloads all Cargo dependencies before the build
- Vendoring: Dependencies are placed in a local directory that Cargo uses instead of downloading
- Hash verification: A hash of all dependencies ensures reproducibility
This means Rust packages in Nix require an additional hash (cargoHash or cargoSha256)
that covers all dependencies listed in Cargo.lock.
Cargo.lock is essential
Unlike development where Cargo.lock may be gitignored for libraries, Nix requires
Cargo.lock to determine exact dependency versions. If a project doesn’t ship
Cargo.lock, you’ll need to generate it.
Build artifacts and incremental compilation
Cargo caches build artifacts for incremental compilation. In Nix:
- Each build starts fresh (no incremental compilation)
- Build artifacts from dependencies are cached separately
- This ensures reproducibility but may increase build times
Basic Rust package
Rust packages in nixpkgs use rustPlatform.buildRustPackage. Here’s a minimal example:
{ lib, rustPlatform, fetchFromGitHub }:
rustPlatform.buildRustPackage rec {
pname = "ripgrep";
version = "14.0.3";
src = fetchFromGitHub {
owner = "BurntSushi";
repo = "ripgrep";
rev = version;
sha256 = "sha256-...";
};
cargoHash = "sha256-...";
meta = with lib; {
description = "A line-oriented search tool";
homepage = "https://github.com/BurntSushi/ripgrep";
license = licenses.unlicense;
};
}
The buildRustPackage function handles:
- Setting up the Rust toolchain
- Fetching and vendoring Cargo dependencies
- Running
cargo build --release - Installing binaries to
$out/bin
Common Rust build options
cargoHash vs cargoSha256
Two attributes are available for specifying the dependency hash:
cargoHash (preferred, modern):
cargoHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
cargoSha256 (legacy, still supported):
cargoSha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
To get the hash, set it to an empty string or fake hash and let Nix tell you the correct one:
cargoHash = ""; # or lib.fakeHash
Cargo build flags
Control how Cargo builds the package:
# Build only specific binaries
cargoBuildFlags = [ "--bin" "main-binary" ];
# Pass features to Cargo
buildFeatures = [ "feature1" "feature2" ];
buildNoDefaultFeatures = true;
# Or use cargoFlags for full control
cargoFlags = [ "--all-features" ];
Testing
Rust tests run by default during the check phase:
# Disable tests entirely
doCheck = false;
# Skip specific tests
checkFlags = [
"--skip test_requires_network"
"--skip integration_test"
];
# Test only specific packages in a workspace
cargoTestFlags = [ "--package" "subcrate" ];
Cargo workspaces
For projects with multiple crates in a workspace:
# Build all workspace members
cargoBuildFlags = [ "--workspace" ];
# Or build specific members
cargoBuildFlags = [ "--package" "crate1" "--package" "crate2" ];
Native dependencies
Rust projects often link to C libraries:
{ lib, rustPlatform, fetchFromGitHub, pkg-config, openssl }:
rustPlatform.buildRustPackage rec {
pname = "cargo-audit";
version = "0.18.3";
src = fetchFromGitHub {
owner = "rustsec";
repo = "rustsec";
rev = "cargo-audit/v${version}";
sha256 = "sha256-...";
};
cargoHash = "sha256-...";
nativeBuildInputs = [ pkg-config ];
buildInputs = [ openssl ];
# Set environment variables for build scripts
env.OPENSSL_NO_VENDOR = 1;
meta = with lib; {
description = "Audit Cargo.lock for security vulnerabilities";
license = licenses.asl20;
};
}
Common issues and fixes
Missing Cargo.lock
If the source doesn’t include Cargo.lock:
postPatch = ''
# Generate Cargo.lock if missing
cargo generate-lockfile
'';
# Or use cargoLock attribute to specify it separately
cargoLock = {
lockFile = ./Cargo.lock;
};
Build script failures
Cargo build scripts (build.rs) sometimes have issues in Nix:
# Disable specific build scripts
postPatch = ''
# Remove problematic build script
rm subcrate/build.rs
'';
# Set environment variables build scripts need
env.SOME_VAR = "value";
# Provide tools build scripts expect
nativeBuildInputs = [ cmake protobuf ];
Linking errors
When Rust code fails to link against system libraries:
# Ensure pkg-config can find libraries
nativeBuildInputs = [ pkg-config ];
buildInputs = [ openssl zlib ];
# Or set environment variables manually
env.PKG_CONFIG_PATH = "${lib.getDev openssl}/lib/pkgconfig";
# For libraries that don't use pkg-config
preBuild = ''
export LIBRARY_PATH="${lib.getLib openssl}/lib:$LIBRARY_PATH"
'';
Vendoring issues with git dependencies
Cargo dependencies from git repositories need special handling:
cargoLock = {
lockFile = ./Cargo.lock;
outputHashes = {
"some-crate-0.1.0" = "sha256-...";
};
};
Tests require network or filesystem access
Disable problematic tests:
# Skip network-dependent tests
checkFlags = [
"--skip test_downloads"
"--skip test_api"
];
# Or disable the check phase entirely
doCheck = false;
Cross-compilation
Rust has excellent cross-compilation support. Specify the target:
# Set in the derivation
cargoExtraArgs = [ "--target" "x86_64-unknown-linux-musl" ];
# Or configure via environment
env.CARGO_BUILD_TARGET = "x86_64-unknown-linux-musl";
Binary size optimization
Reduce binary size by stripping debug info:
# Strip debug symbols (usually done by default)
dontStrip = false;
# Use release profile optimizations
cargoFlags = [ "--profile" "release" ];
# Or configure in Cargo.toml instead
Detailed example
Here’s a comprehensive example of a Rust CLI application with native dependencies, tests, and multiple binaries:
{ lib
, rustPlatform
, fetchFromGitHub
, pkg-config
, installShellFiles
, stdenv
, openssl
, sqlite
, zlib
}:
rustPlatform.buildRustPackage rec {
pname = "example-rust-cli";
version = "2.5.0";
src = fetchFromGitHub {
owner = "example";
repo = "rust-cli";
rev = "v${version}";
hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
};
cargoHash = "sha256-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=";
nativeBuildInputs = [
pkg-config
installShellFiles
];
buildInputs = [
openssl
sqlite
zlib
];
# Build only the main binary, not helper tools
cargoBuildFlags = [ "--bin" "example-cli" ];
# Enable all features except "unstable"
buildNoDefaultFeatures = true;
buildFeatures = [ "tls" "sqlite" "compression" ];
# Skip tests that require network access
checkFlags = [
"--skip=integration::test_remote_api"
"--skip=sync::test_download"
];
# Don't run doc tests as they may be flaky
cargoTestFlags = [ "--lib" "--bins" ];
# Set environment variables for the build
env = {
OPENSSL_NO_VENDOR = 1;
ZSTD_SYS_USE_PKG_CONFIG = 1;
};
# Generate Cargo.lock if it's missing (usually it should be committed)
# postPatch = ''
# cargo generate-lockfile
# '';
postInstall = ''
# Install shell completions
installShellCompletion --cmd example-cli \
--bash <($out/bin/example-cli completions bash) \
--fish <($out/bin/example-cli completions fish) \
--zsh <($out/bin/example-cli completions zsh)
# Install man page
installManPage docs/example-cli.1
'';
meta = with lib; {
description = "Example Rust CLI tool for file management";
longDescription = ''
A comprehensive command-line tool written in Rust, demonstrating
best practices for packaging Rust applications in nixpkgs including
native dependencies, feature flags, and shell completion installation.
'';
homepage = "https://github.com/example/rust-cli";
changelog = "https://github.com/example/rust-cli/releases/tag/v${version}";
license = licenses.mit;
# Only build on platforms where all dependencies are available
platforms = platforms.unix;
# Mark as broken on specific platforms if needed
# broken = stdenv.isDarwin;
};
}
This example demonstrates:
- Using
buildRustPackagefor a CLI application - Cross-platform dependencies (macOS frameworks)
- Native library dependencies (OpenSSL, SQLite, zlib)
- Selective binary building with
cargoBuildFlags - Feature flag management
- Test filtering for problematic tests
- Environment variable configuration for build scripts
- Post-install tasks (shell completions, man pages)
- Comprehensive metadata with platform constraints
Working with Cargo workspaces
For projects with multiple crates:
rustPlatform.buildRustPackage rec {
pname = "workspace-project";
version = "1.0.0";
src = fetchFromGitHub {
owner = "example";
repo = pname;
rev = "v${version}";
hash = "sha256-...";
};
cargoHash = "sha256-...";
# Build specific workspace members
cargoBuildFlags = [
"--package" "main-app"
"--package" "helper-tool"
];
# Run tests for all workspace members
cargoTestFlags = [ "--workspace" ];
# Install binaries from multiple crates
postInstall = ''
install -Dm755 target/release/main-app $out/bin/main-app
install -Dm755 target/release/helper-tool $out/bin/helper-tool
'';
meta = with lib; {
description = "Multi-crate Rust workspace";
license = licenses.asl20;
};
}
Using buildRustCrate for advanced cases
For more control over the build process, use buildRustCrate (lower-level):
{ lib, rustPlatform }:
rustPlatform.buildRustCrate {
pname = "mycrate";
version = "1.0.0";
src = ./.;
# Specify dependencies manually
dependencies = [
# ... other crates
];
}
This is rarely needed; buildRustPackage handles most use cases.
Summary
Rust packaging in Nix leverages Cargo while maintaining reproducibility:
- Use
rustPlatform.buildRustPackagefor most Rust projects - Specify
cargoHashto lock dependency versions - Ensure
Cargo.lockis present in the source - Use
pkg-configand list native dependencies inbuildInputs - Control features with
buildFeaturesandbuildNoDefaultFeatures - Platform-specific dependencies (like macOS frameworks) should use conditionals
- Check existing nixpkgs Rust packages for patterns and solutions