Over the last year, I’ve been learning Rust and diving into the ecosystem. One of my goals has been to explore the Rust embedded development space. With that, I recently acquired a micro:bit board with the intention of following the Rust Discovery Book. And while I have experience with embedded at my current job, especially with the STM32 series of boards, it’s all in C/C++.

One of the hardest parts of embedded I’ve found coming from a background in web is getting a development environment up and running, especially one that allows for quick iteration. There is a lot of promise for this in Rust with tooling such as probe-rs.

At the same time, I wanted to use Nix and see if I can I can put together a nix-shell that covers all the requirements for the Discovery book, making it simple to get up and running. Below you will find my journey and thoughts. (If you’d just like what I eventually landed on, skip down to # Using overlays.)

Tool requirements

The original tool requirements in the Discovery book are:

  • Rust >= 1.53.0
  • gdb-multiarch
  • cargo-binutils >= 0.3.3
  • cargo-embed >= 0.11.0
  • minicom (but we’ll use picocom because of preference)

Since we’ll be cross-compiling, it will be easier to just create the rust-toolchain.toml file with the Rust version and additional components we need:

# rust-toolchain.toml
[toolchain]
channel = "1.53"
components = [ "rust-src", "llvm-tools-preview" ]
targets = [ "thumbv7em-none-eabihf" ]

Using these requirements, let’s go about creating an environment with nix-shell.

First pass

My first thought was, I already have Rust on my machine, can I just bring in gdb-multiarch and picocom to get this working? Thus, our first pass at a shell.nix:

# shell.nix
let
  pkgs = import <nixpkgs> {};
in
with pkgs;
mkShell {
  buildInputs = [
    gdb
    picocom
    udev pkg-config # this is needed by cargo-embed
  ];
}

After manually installing cargo-embed and cargo-binutils, then pulling in some additional packages because of installation errors, we should be in business. Following the instructions in the book, we’ll now flash the board:

$ cargo embed --features v2 --target thumbv7em-none-eabihf

And… we get this error:

error: build failed
error: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.33' not found (required by /home/vagrant/discovery/microbit/target/debug/deps/libcortex_m_rt_macros-7e316e67fc081064.so)
   --> /home/vagrant/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.7.1/src/lib.rs:431:1
    |
431 | extern crate cortex_m_rt_macros as macros;
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

       Error Failed to run cargo build: exit code = Some(101).

Hmm… one of our dependencies is attempting to use the local version of libc. Checking the output of ldd:

$ ldd /home/vagrant/discovery/microbit/target/debug/deps/libcortex_m_rt_macros-7e316e67fc081064.so
        ... some other output ...
        libc.so.6 => /nix/store/s9qbqh7gzacs7h68b2jfmn9l6q4jwfjz-glibc-2.33-59/lib/libc.so.6 (0x00007f29ae255000)
        /nix/store/s9qbqh7gzacs7h68b2jfmn9l6q4jwfjz-glibc-2.33-59/lib64/ld-linux-x86-64.so.2 (0x00007f29aeae8000)

That is strange, because it is linking against the correct version of libc. It turns out rustc hardcodes the search path where it expects libraries to be located. There is a mismatch with what rustc expects versus what is actually linked.

After going down the rabbit hole, it looks like the nixpkgs version of Rust fixes this problem by patching the location using patchelf.

Second pass

I’d rather not have to run patchelf manually on every build, so let’s just bring in Rust through Nix. I suppose this is good for us anyway since it would provide a more “packaged” solution, not requiring Rust be available ahead of time.

After reading the NixOS wiki, I realized the first solution of grabbing via nixpkgs won’t work for us since we’d like a specific version and a non-standard compilation target. Doing more searching I found fenix, which packages Rust toolchains.

Taking a second pass at shell.nix using fenix:

let
  pkgs = import <nixpkgs> {};
  fenix = import (fetchTarball "https://github.com/nix-community/fenix/archive/main.tar.gz") {};
in
with pkgs;
mkShell {
  buildInputs = [
    (fenix.fromToolchainFile {
      dir = ./.;
    })
    gdb
    picocom
    udev pkg-config
  ];
}

Attempting to run this we get a cryptic error:

error: value is null while a set was expected

       at /nix/store/jx6m33r5x2rsr5rs6qy0y6wdkz0i2p5q-source/default.nix:113:14:

          112|           (filter (component: toolchain ? ${component}) (unique
          113|             (toolchain.manifest.profiles.${t.profile or "default"}
             |              ^
          114|               ++ t.components or [ ])))

It turns out fenix doesn’t (yet?) support numbered versions of Rust, and the way to fix that was to change the channel to stable in the toolchain file. This is generally not a big deal, but I would definitely like more control over the version of Rust I’m running, especially if I’m following a book.

Additionally, it looks like fenix only supports the rust-std component for my particular target. But we also need rust-src to get rust-analyzer up and running.

Using overlays

To get it working I had to use a different method to bring in Rust. This method uses overlays. Essentially, an overlay overrides and extends the default nixpkgs. This particular overlay extends the supported Rust versions and toolchains by quite a bit. Here’s what our shell.nix looks like now:

let
  rust_overlay = import (builtins.fetchTarball "https://github.com/oxalica/rust-overlay/archive/master.tar.gz");
  pkgs = import <nixpkgs> { overlays = [ rust_overlay ]; };
in
with pkgs;
mkShell {
  buildInputs = [
    gdb
    picocom
    udev pkg-config
    (rust-bin.fromRustupToolchainFile ./rust-toolchain.toml)
  ];
}

With this, I am able to flash my board and get the example programs working. One thing that remains manual is installing cargo-embed and cargo-binutils the first time the shell is instantiated. This can be automated with a shellHook that should idempotentally check and install them.

Final thoughts

This turned out to be a fun dive into learning how to get a Rust environment up and running for embedded development. Additionally, it was a great introduction to Nix, I was able to use it in somewhat limited scope, using only nix-shell. When I was wanting to learn it a few months back, I didn’t have an idea of where to start, and going all-in installing NixOS seemed daunting. Now I have a decent understanding of how it works, and I can start to delve deeper.