Fighting with tooling, versions, and dependencies is a pain. There are solutions to it: Running specific databases via Docker containers, or using dev shells in VSCode (sad panda), or per project environment VMs.
Alternatively, you could just cd project
and have the entire environment there.
You'll be cradled in a Nix Shell, a magical pocket dimension where all the necessary tools are automatically summoned: Erlang, Elixir, NodeJS, Postgres, and everything needed for their language servers, linting, and code formatting. All with specific versions tailored for a single project.
The best part? Absolutely nothing gets permanently installed on your system. This is pure wizardry contained within the project directory – all these powerful tools vanish instantly when you exit
or leave the path (cd ..
). It's like having a portable, self-contained development universe.
Requirements
To get started we will assume a few things:
- You have NixOS installed
- You have direnv included in your
systemPackages
(optional) - Nix Flakes are enabled. (https://nixos.wiki/wiki/Flakes)
What We Will Get
When we are finished, you should have a fully functional development setup that automatically loads when entering the path. It will include the Rust compiler, the Cargo toolkit, and RustAnalyzer for LSP.
As with any Nix Shell, this wont permanently install anything to the system. All tooling will be available when inside the project path and wont persist when leaving it.
System Packages
Package | Description |
---|---|
rustc | The Rust compilier and toolchain |
rust-analyzer | Rust LSP server |
cargo | The Cargo toolkit for managing depenedencies and build tasks. |
The flake.nix
To get started, find any Rust project you want to setup; or create a new one with cargo new myapp
. Then, inside the project root create a flake.nix
file:
{
description = "My Cool Rust App";
inputs = { nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; };
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
in {
devShells.${system}.default = pkgs.mkShell {
packages = [ pkgs.rustc pkgs.cargo pkgs.rust-analyzer ];
};
packages.${system}.default = pkgs.rustPlatform.buildRustPackage {
pname = "myapp";
version = "0.0.1";
cargoLock.lockFile = ./Cargo.lock;
src = pkgs.lib.cleanSource ./.;
};
};
}
flake.nix
You should change the pname
variable to your binary name, the version
variable to your application's version, and set description
to something catchy.
git add flake.nix
. If this is a brand new project make sure you have ran cargo build
at once to generate a cargo.lock
file and stage or commit it to your repository as well.The Development Environment
Now you can entirely uninstall rustc
and cargo
, if you so desired. When you want to start development run nix develop
from the root of the project and everything will be available for you. When your done, exit
will return you from the development environment and poof everything is gone – magic!.
Building for Nix
cargo build
at once to generate a cargo.lock
file and stage or commit it to your repository as well.The flake.nix
configuration grants the ability to package your application for nix. To build your app run nix build
in the root of the project. cargo
will compile the app and a new path will be created; with your bin file at project_root/result/bin
.
.gitignore
file.Setting up DIRENV
With DIRENV, the nix shell will be loaded as soon as we enter the path. No more need to type nix develop
. Many IDEs/editors can also take advantage of DIRENV to load the environment automatically when the project is opened. Emacs and Neovim can both do this.
To setup DIRENV create a .envrc
file in the project root with the following line:
use flake
.envrc
Enable it explicitly:
direnv allow
Now when you cd
into the path the environment will be automatically loaded, when you leave the project path it will be unloaded.
A More Involved Environment
Previously, we created very simple Rust environment for dealing with basic Cargo apps. A more complex app might need additional targets, development libraries, and configuration. For a more involved example, this flake will create a much deeper environment for Rust. With the following added configuration:
- The ability to install development libraries
- Additional targets (the wasm target is available as an example)
- Additional shell environment configuration
{
description = "A more complicated Rust app";
inputs = {
nixpkgs.url = "nixpkgs/nixos-unstable";
rust-overlay = { url = "github:oxalica/rust-overlay"; };
};
outputs = { nixpkgs, rust-overlay, ... }:
let
system = "x86_64-linux";
supportedSystems = [ "x86_64-linux" ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
pkgsFor = nixpkgs.legacyPackages;
in {
packages.${system}.default =
let pkgs = import nixpkgs { inherit system; };
in pkgs.rustPlatform.buildRustPackage {
pname = "myapp";
version = "0.4.3";
cargoLock.lockFile = ./Cargo.lock;
src = pkgs.lib.cleanSource ./.;
};
devShells."${system}".default = let
pkgs = import nixpkgs {
inherit system;
overlays = [ rust-overlay.overlay ];
config.allowUnfree = true;
};
in pkgs.mkShell {
libraries = with pkgs; [
# Some example libraries
dbus
openssl_3
];
packages = with pkgs; [
rust-analyzer
(rust-bin.stable.latest.default.override {
extensions = [ "rust-src" ];
targets = [ "wasm32-unknown-unknown" ];
})
cargo
# Example dependencies
dbus
openssl_3
trunk
];
# Additional shell environment
shellHook = ''
LD=$CC
'';
};
};
}