Rust

From NixOS Wiki
Revision as of 21:33, 5 August 2022 by imported>Dnordstrom (nixpkgs-mozilla has flake support, perhaps not as sophisticated as the other two but it's a flake input nonetheless. :))

This article is about the Rust programming language. There are 3 methods to use the Rust compiler and toolchain in Nix/NixOS:

  1. via nixpkgs,
  2. via rustup,
  3. or with unofficial overlays on nixpkgs.

Installing via nixpkgs is the nix-iest way to use Rust, but there are valid reasons to use any approach.

Installing via nixpkgs

The cargo and rustc derivations provide the Rust toolchain in nixpkgs. An advantage of using nixpkgs is that it's dead simple and you get pinned versions, deterministic builds in nix-shell, etc. However, nixpkgs only maintains a single version of the Rust stable toolchain, so if you require a nightly toolchain or require switching between multiple toolchains then this approach may not be for you.

Here's an example shell.nix:

let
  # Pinned nixpkgs, deterministic. Last updated: 2/12/21.
  pkgs = import (fetchTarball("https://github.com/NixOS/nixpkgs/archive/a58a0b5098f0c2a389ee70eb69422a052982d990.tar.gz")) {};

  # Rolling updates, not deterministic.
  # pkgs = import (fetchTarball("channel:nixpkgs-unstable")) {};
in pkgs.mkShell {
  buildInputs = [ pkgs.cargo pkgs.rustc ];
}

Installation via rustup

The rustup tool is maintained by the Rust community and offers and interface to install and switch between Rust toolchains. In this scenario, rustup handles the "package management" of Rust toolchains and places them in $PATH. Nixpkgs offers rustup via the rustup derivation. More info on using rustup can be found on their official website: https://rustup.rs/.

If you want to have the most "normal" Rust experience I recommend using rustup with the following example shell.nix:

{ pkgs ? import <nixpkgs> {} }:
  pkgs.mkShell rec {
    buildInputs = with pkgs; [
      llvmPackages_latest.llvm
      llvmPackages_latest.bintools
      zlib.out
      rustup
      xorriso
      grub2
      qemu
      llvmPackages_latest.lld
      python3
    ];
    RUSTC_VERSION = pkgs.lib.readFile ./rust-toolchain;
    # https://github.com/rust-lang/rust-bindgen#environment-variables
    LIBCLANG_PATH = pkgs.lib.makeLibraryPath [ pkgs.llvmPackages_latest.libclang.lib ];
    HISTFILE = toString ./.history;
    shellHook = ''
      export PATH=$PATH:${CARGO_HOME:-~/.cargo}/bin
      export PATH=$PATH:${RUSTUP_HOME:-~/.rustup}/toolchains/$RUSTC_VERSION-x86_64-unknown-linux-gnu/bin/
      '';
    # Add libvmi precompiled library to rustc search path
    RUSTFLAGS = (builtins.map (a: ''-L ${a}/lib'') [
      pkgs.libvmi
    ]);
    # Add libvmi, glibc, clang, glib headers to bindgen search path
    BINDGEN_EXTRA_CLANG_ARGS = 
    # Includes with normal include path
    (builtins.map (a: ''-I"${a}/include"'') [
      pkgs.libvmi
      pkgs.glibc.dev 
    ])
    # Includes with special directory paths
    ++ [
      ''-I"${pkgs.llvmPackages_latest.libclang.lib}/lib/clang/${pkgs.llvmPackages_latest.libclang.version}/include"''
      ''-I"${pkgs.glib.dev}/include/glib-2.0"''
      ''-I${pkgs.glib.out}/lib/glib-2.0/include/''
    ];

  }

It's important to have a file named rust-toolchain lying in the same directory as the shell.nix. Its purpose is to pin the version of the used Rust compiler.

$ cat rust-toolchain
nightly-2021-09-19

The important part is that this also works with complex setups using bindgen and precompiled C libraries. To add a new C library in the search path of bindgen and rustc edit the variables BINDGEN_EXTRA_CLANG_ARGS and RUSTFLAGS

Cross-compiling

To Windows via rustup

Unofficial overlays

  1. https://github.com/oxalica/rust-overlay (Flake support, Nightly & Stable)
  2. https://github.com/nix-community/fenix (Flake support, Nightly & Stable)
  3. https://github.com/mozilla/nixpkgs-mozilla (Flake support, Nightly & Stable)

Rust Nightlies

  1. Use one of the overlays above,
  2. Or, use rustup

Developing Rust projects using Nix

The Nixpkgs manual uses buildRustPackage.

This blog post shows how to do it using crate2nix.

Using overrideAttrs with Rust Packages

This does not seem to be possible.

Using overrideArgs with Rust Packages

This is a bit tricky, you can't just use overrideArgs. Here is one example of how to do it. The trick is to use two nested calls to overrideAttrs; the outer call overrides the cargoDeps attribute, the inner call rebuilds the vendored tarball and provides the updated hash:

overlays = [
  (final: prev: {
    some-nixpkgs-package = prev.some-nixpkgs-package.overrideAttrs (oldAttrs: {
      cargoDeps = oldAttrs.cargoDeps.overrideAttrs (_: {
        # ...
      });
    });
  })
];

Packaging Rust projects with nix

At the time of writing, there are now no less than 8 different solutions for building Rust code with Nix. In the following table they are compared:

Name Cargo.lock solution Derivations Build logic Supports cross Notes
buildRustPackage Checksum 1 cargo Yes Built into nixpkgs
carnix Codegen Many buildRustCrate No? Unmaintained, AFAICT; repository is gone
crate2nix Codegen (with optional IFD) Many buildRustCrate No Spiritual successor to carnix; nobody currently spending much time maintaining it
naersk Import 2 cargo No? Seems to only support building on x86
cargo2nix Codegen Many cargo + custom Yes Defaults to the oxalica Rust overlay but this can be overridden with rustToolchain
import-cargo Import 1 cargo Unclear More of a proof of concept than a full working solution
crane Import 2 cargo Yes Inspired by naersk, with better support for composing Cargo invocations as completely separate derivations
dream2nix Codegen 1 or 2 buildRustPackage or crane Yes A framework for unifying 2nix converters across languages

Explanation for the columns

  • Cargo.lock solution: How does this solution handle reproducibly determining what crates need to be downloaded from the Cargo.lock file? “Checksum” means it requires you to specify the checksum of all the downloaded dependencies. “Import” means it dynamically imports and parses Cargo.lock from a Nix expression, which means Cargo.lock needs to be present in the same repository as the nix expressions (or IFD must be used). “Codegen” means it generates a .nix file from the Cargo.lock, which is then committed to source control.
  • Derivations: How many derivations does this solution use to compile Rust code? “1” means the project and all its dependencies are compiled in one derivation. “2” means all dependencies are moved into a separate derivation, so the project can be updated independently, but any change to the set of dependencies rebuilds everything. “Many” means each dependency is built in its own derivation, so changes to dependencies only do the minimal amount of rebuilding necessary (and, ideally, different projects can share dependencies, although I haven’t checked if this works in practice).
  • Build logic: How does this solution orchestrate building of crates? “Cargo” means it relies on Cargo; buildRustCrate means it uses Nixpkgs’ buildRustCrate; “custom” means it uses its own custom logic.
  • Supports cross: Does the solution allow for cross-compilation of crates?

Shell.nix example

{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
  nativeBuildInputs = with pkgs; [ rustc cargo gcc ];
  buildInputs = with pkgs; [ rustfmt clippy ];

  # Certain Rust tools won't work without this
  # This can also be fixed by using oxalica/rust-overlay and specifying the rust-src extension
  # See https://discourse.nixos.org/t/rust-src-not-found-and-other-misadventures-of-developing-rust-on-nixos/11570/3?u=samuela. for more details.
  RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
}

This will have the stable Rust compiler + the official formatter and linter inside the ephemeral shell. It'll also set the RUST_SRC_PATH environment variable to point to the right location, which tools, such as rust-analyzer, require to be set.

VSCode integration

The rust-lang.rust and rust-lang.rust-analyzer VSCode extensions offer Rust support.

You can use the arrterian.nix-env-selector extension to enable your nix-shell inside VSCode and have these settings picked up by other extensions.

Neovim Completion

Racer completion can be configured using the following snippet:

(neovim.override {
  configure = {
    customRC = ''
      if filereadable($HOME . "/.vimrc")
        source ~/.vimrc
      endif
      let $RUST_SRC_PATH = '${stdenv.mkDerivation {
        inherit (rustc) src;
        inherit (rustc.src) name;
        phases = ["unpackPhase" "installPhase"];
        installPhase = ''cp -r library $out'';
      }}'
    '';
    packages.nixbundle.start = with vimPlugins; [
      nvim-completion-manager
      nvim-cm-racer
    ];
  };
})

FAQ

Building Rust crates that require external system libraries

For example, the openssl-sys crate needs the OpenSSL static libraries and searches for the library path with pkg-config. That's why you need to have the Nix derivatives openssl and pkg-config in order to build that crate. You'll need to start a shell providing these packages:

$ nix-shell -p pkg-config openssl

In some cases (eg here) you may also need

PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig";

Similarly, the crate libsqlite3-sys, e.g. to use and compile the database ORM tool diesel-cli with Sqlite support, needs

$ nix-shell -p pkg-config sqlite

Otherwise the following error occurs:

error: linking with `cc` failed: exit status: 1
...
 = note: /nix/store/kmqs0wll31ylwbqkpmlgbjrn6ny3myik-binutils-2.35.1/bin/ld: cannot find -lsqlite3
          collect2: error: ld returned 1 exit status

Note that you need to use a nix-shell environment. Installing the Nix packages openssl or sqlite globally under systemPackages in NixOS or in nix-env is discouraged and doesn't always work (pkg-config may not be able to locate the libraries).

Building with a different Rust version than the one in Nixpkgs

The following uses the fenix overlay and makeRustPlatform to build a crate with Rust nightly:

{
  inputs = {
    fenix = {
      url = "github:nix-community/fenix";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    flake-utils.url = "github:numtide/flake-utils";
    nixpkgs.url = "nixpkgs/nixos-unstable";
  };

  outputs = { self, fenix, flake-utils, nixpkgs }:
    flake-utils.lib.eachDefaultSystem (system:
      let pkgs = nixpkgs.legacyPackages.${system}; in
      {
        defaultPackage = (pkgs.makeRustPlatform {
          inherit (fenix.packages.${system}.minimal) cargo rustc;
        }).buildRustPackage {
          pname = "hello";
          version = "0.1.0";
          src = ./.;
          cargoSha256 = nixpkgs.lib.fakeSha256;
        };
      });
}