Building Rust WASM projects with Nix

Historically, there have been a number of approaches to the problem of building Rust projects with Nix1. The most common one these days is buildRustPackage. It integrates reasonably well with cargo and supports cross-compilation out of the box, but if you want to build a project targeting wasm32-unknown-unknown, you will not find much help online.

Since I recently had to figure that out for prisma-fmt-wasm, this will be a short post walking through a setup that works. The whole working example used in this post is on GitHub. This post assumes some prior knowledge of Nix2 and how to build Rust projects for wasm32-unknown-unknown3.

Initial setup

Let’s start a regular Rust project with cargo new --lib and write a function we’ll want to expose in src/lib.rs:

/// Computes the [greatest common divisor](https://en.wikipedia.org/wiki/Greatest_common_divisor)
/// of two integers using Euclid's algorithm.
///
/// ```
/// # use gcd::*;
/// assert_eq!(gcd(3, 3), 3);
/// assert_eq!(gcd(4, 2), 2);
/// assert_eq!(gcd(20, 15), 5);
/// assert_eq!(gcd(-20, 15), 5);
/// assert_eq!(gcd(-20, -15), 5);
/// assert_eq!(gcd(105, 252), 21);
/// assert_eq!(gcd(-48, 18), 6);
/// ```
pub fn gcd(a: i64, b: i64) -> u64 {
    if b == 0 {
        a.unsigned_abs()
    } else {
        gcd(b, a % b)
    }
}

cargo test confirms that this works.

Next, we write a basic flake, with buildRustPackage:

{
  description = "Minimal rust wasm32-unknown-unknown example";

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

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let pkgs = import nixpkgs { inherit system; }; in
      {
        defaultPackage = pkgs.rustPlatform.buildRustPackage {
          pname = "gcd";
          version = "1.0.0";
          src = ./.;

          cargoLock = {
            lockFile = ./Cargo.lock;
          };
        };
      }
    );
}

Running nix build confirms that this works.

Manual wasm build

Now we want to build a wasm module from this.

We’ll go through the steps manually, without writing a nix derivation first, so we have a clear picture of what we need:

    devShell = pkgs.mkShell {
      packages = [ pkgs.wasm-bindgen-cli ];
    };

And test it:

  $ nix develop
  warning: Git tree '/home/tom/src/rust-nix-wasm32-unknown-unknown-example' is dirty

  [tom@xps13:~/src/rust-nix-wasm32-unknown-unknown-example]$ wasm-bindgen --version
  wasm-bindgen 0.2.78

Great! Now let’s add that version to Cargo.toml, as well as the crate-type (as shown in the wasm-bindgen hello world).

  [package]
  name = "gcd"
  version = "0.1.0"
  edition = "2018"

  [lib]
  crate-type = ["cdylib"]

  [dependencies]
  wasm-bindgen = "=0.2.78"

And let’s not forget the annotation in lib.rs:

  use wasm_bindgen::prelude::*;

  // ...

  #[wasm_bindgen::wasm_bindgen]
  pub fn gcd(a: i64, b: i64) -> u64 {
      if b == 0 {
          a.unsigned_abs()
      } else {
          gcd(b, a % b)
      }
  }
  inputs = {
    flake-utils.url = "github:numtide/flake-utils";
    rust-overlay = {
      url = "github:oxalica/rust-overlay";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    nixpkgs.url = "nixpkgs/nixos-unstable";
  };

and use the overlay in our local pkgs:

  let
      overlays = [ rust-overlay.overlay ];
      pkgs = import nixpkgs { inherit overlays system; };
  in

We then define which rust toolchain we want. There are several ways to do this with rust-overlay, we’ll use a toolchain file (it’s a format defined by rustup, so it’s not nix-specific):

  let
      overlays = [ rust-overlay.overlay ];
      pkgs = import nixpkgs { inherit system overlays; };
      rust = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;

and write a rust-toolchain.toml:

  [toolchain]
  channel = "stable"
  components = []
  targets = [ "wasm32-unknown-unknown" ]
  profile = "default"
  $ nix develop
  warning: Git tree '/home/tom/src/rust-nix-wasm32-unknown-unknown-example' is dirty

  [tom@xps13:~/src/rust-nix-wasm32-unknown-unknown-example]$ cargo build --target=wasm32-unknown-unknown
      Finished dev [unoptimized + debuginfo] target(s) in 0.06s

  [tom@xps13:~/src/rust-nix-wasm32-unknown-unknown-example]$ ls target/wasm32-unknown-unknown/debug/
  build  deps  examples  gcd.d  gcd.wasm  incremental  libgcd.d  libgcd.rlib

  [tom@xps13:~/src/rust-nix-wasm32-unknown-unknown-example]$ wasm-bindgen --target=nodejs --out-dir=tmp target/wasm32-unknown-unknown/debug/gcd.wasm

  [tom@xps13:~/src/rust-nix-wasm32-unknown-unknown-example]$ ls tmp/
  gcd_bg.wasm  gcd_bg.wasm.d.ts  gcd.d.ts  gcd.js

  [tom@xps13:~/src/rust-nix-wasm32-unknown-unknown-example]$ cat tmp/gcd.d.ts
  /* tslint:disable */
  /* eslint-disable */
  /**
  * Computes the [greatest common divisor](https://en.wikipedia.org/wiki/Greatest_common_divisor)
  * of two integers using Euclid's algorithm.
  *
  * ...
  * @param {BigInt} a
  * @param {BigInt} b
  * @returns {BigInt}
  */
  export function gcd(a: BigInt, b: BigInt): BigInt;

Looks good!

Adjust the derivation

We now want to translate that into the derivation (the defaultPackage). Here, familiarity with chapter 6 of the nixpkgs manual is helpful. We’re going to override the default phases of the rust build, and basically only keep the phases before buildPhase, because that is where the main point of buildRustPackage happens, namely the vendoring of dependencies.

This can be discovered again by trial and error from the basic buildRustPackage we had. The bulk of the new code is going to be the buildPhase:

cargo build --release --target=wasm32-unknown-unknown

echo 'Creating out dir...'
mkdir -p $out/src;

# Optional, of course
echo 'Copying package.json...'
cp ./package.json $out/;

echo 'Generating node module...'
wasm-bindgen \
  --target nodejs \
  --out-dir $out/src \
  target/wasm32-unknown-unknown/release/gcd.wasm;

Now nix build gets far into the process, it just fails at the install phase. We can just skip it:

installPhase = "echo 'Skipping installPhase'";

And voilĂ !

As an extra nicety, you’ll notice the checkPhase doesn’t test anything, so it can be replaced by something custom like this shell script.

Epilogue

The whole working repository for this post is available at https://github.com/tomhoule/rust-nix-wasm32-unknown-unknown-example.

This is not straightforward as it should be. The whole process could be further streamlined and automated. On the other hand, I take this as a credit to Nix that something like this is possible, supported, and relatively clean.

  1. 1See the “Building Rust packages section” on the NixOS wiki.
  2. 2nix.dev is a helpful resource if you are curious about Nix and want to learn what it’s about.
  3. 3The wasm-bindgen guide is a good resource to learn that.