Naive NixOS Rust Development

tl;dr: To work on Rust project with nix-shell, rls and extensions such as rust-analysis, rust-src, without caring too much about specific Rust toolchain version (except for it being stable), use the following shell.nix:

let
  moz_overlay = import (builtins.fetchTarball https://github.com/mozilla/nixpkgs-mozilla/archive/master.tar.gz);
  nixpkgs = import <nixpkgs> {
    overlays = [ moz_overlay ];
  };
  ruststable = (nixpkgs.latest.rustChannels.stable.rust.override {
    extensions = [ "rust-src" "rust-analysis" ];}
  );
in
  with nixpkgs;
  stdenv.mkDerivation {
    name = "rust";
    buildInputs = [ rustup ruststable ];
  }

When you have a Nix hammer, everything looks like a Nix expression.

Having used NixOS on a real PC for a number of days, this is the impression I get from the world of Nix. Unfortunately, so far, it's been a negative for me.

One of the most exciting thing I want to use Nix for is to bootstrap development environment with nix-shell. I imagined it to be similar to using pipenv with Python, except for everything. Well, I've since learned that it's not true (yet?) for many reasons.

Modern programming languages come with their own attempt at reproducibility. Some does it better than others. To make it concrete, I'm talking about things like Stack for Haskell or rustup for Rust: given the source code, how do I make it build in the way the project intended? What's the correct version of the compiler, runtime, and tools that works best with this revision of the source code? The common solution usually follows this pattern: as author of a project, specify as much as you can, the environment best suited for the current state of the project. As a "builder", use a single program that's capable of updating itself, as well as ensuring that the project builds exactly as specified, including managing the compiler/runtime/tooling versions, etc.

This single program's role is very much the same as the Nix system, except the latter is independent of programming languages: rustup installs Rust, so does Nix. That's a bad thing. As a package manager, Nix either have to tightly integrate with each of these other package managers, leveraging their evolving behaviors to give its user the build environment; or, it must replace them. The former is impractical; the latter, well, sucks.

Back to reality. This is the experience I want to have with NixOS: Some programs I use daily such as Alacritty, NeoVim, Firfox, etc, are installed globally and readily available. They are part of my /etc/nixos/configuration.nix. So far so good. Now, I regularly program in a few languages. For each of the project, I'd like to have a shell.nix that brings in its compilers, libraries, LSP servers, etc. This is what nix-shell is supposed to give me! This is known as the "per project" setup.

Let's see: with Rust, that means rustc (compiler), cargo (package manager), rls, rust-src and rust-analysis (LSP). In macOS, I'd install all of these globally with rustup. In NixOS...well, I can ask for rustup for my project:

with import <nixpkgs> {};

stdenv.mkDerivation {
  name = "rust";
  nativeBuildInputs = [ rustup ];
}

...which gives me rustup and nothing else. That's right, you don't even get a rustc after running nix-shell. But rustup can get you everything else, all I need to do is ask. Hmm, do I need to run a series of set-up commands with rustup every time I enter the environment? No? I just need to run it the first time? Until the cached tools get deleted by some garbage collection mechanism? That seems unsatisfying, doesn't it?

Instead of rustup, I could also ask Nix to use the rustc/cargo/rls it knows about directly. This is marginally better. Except I still need rust-src and rust-analysis for my needs. As far as I can tell, these RLS components are out of Nix's control (as of today).

Everywhere on the internet I looked, for every problem that Nix-the-package-manager doesn't work out-of-the-box, there's someone responding along the line of "you can write some Nix expression yourself". In other words, Nix-the-language is powerful enough to solve it, probably. In the case of Rust, luckily, Mozilla wrote enough Nix expressions for us and provides them via an overlay. These expressions are rich enough to meet my needs. As you can see in the tl;dr at the top, when entering the development environment, nix-shell would: download the overlay's source code (from the internet, or local cache), load the expression it includes, mix in my customization, and execute it.

That marks the end of my search. I like the final solution because it's mostly "vanilla" Nix and doesn't require me to mess with a bunch of other tools. For solutions that do, read this.


At end of the day, my needs are pretty basic: consistency from rustup and convenience from nix-shell. I didn't need to pin the compiler to a specific Rust release, or checksum the final build output.

I'm very new to both technologies so there may be a follow-up post sometime in the future.