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.3cargo-embed
>= 0.11.0minicom
(but we’ll usepicocom
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.