Cross-Compiling Rust Programs to Self-Contained MIPS Binaries
Introduction
I wanted to cross compile Rust programs into self-contained, statically-linked binaries targeting embedded Linux devices, like my MIPS-based router. There are a number of Rust cross-compilation guides/tools, but none seem to do exactly what I want. This is what worked for me.
Prerequisites
I suspect the experience will be substantially similar on any x86-64 Linux distribution). But here's what I'm using:
- Ubuntu 20.04.1 on x86-64
- Rust 1.49.0 (2020-12-29)
- Buildroot 2020.02.9
Installing Rust
First, install Rust. Follow Rust's own documentation for this.
Install MIPS target for Rust
Rust comes with a utility for managing targets and toolchains, rustup
.
Use it to install precompiled Rust libraries for a musl-based MIPS target. Rust supports using musl to produce fully static binaries.
$ rustup target add mips-unknown-linux-musl
After that installs, you might be thinking “Sweet – now I can cross compile Rust programs into self-contained binaries for MIPS. All done!”.
Nope! If only it was that easy. We also need to provide Rust with a cross-compilation toolchain. I like buildroot so let's use that.
Building a Cross-Compilation Toolchain with Buildroot
Buildroot is a great project that can help you build cross compilers or even full Linux firmware images. We're just going to use it to build a cross compiler, though.
- Download a buildroot tarball from Buildroot's download page.
- Extract it, and
cd
into the buildroot folder. - Run
make menuconfig
to configure some options: - Under
Target Options
configure the target architecture and architecture variant. In my case, this isMIPS (big-endian)
andMIPS32R2
. - Under
Build Options
configure buildroot to build only static libraries. We want to produce self-contained Rust binaries and this is slightly easier if we don't even have any dynamic libraries to mistakenly link against. - Under
Toolchain
:- Choose musl as the C library. It's small-ish and works well with static linking.
- Select the Kernel headers version. You should select a version less than or equal to the kernel running on your target device. Things built against older headers will almost always work on newer kernels, but this doesn't work in the other direction. If the version you want isn't listed in the menu, there is an option to enter a free-form version. I've built buildroot toolchains against header versions as old as 2.6.36 just fine.
- You can also enable C++ support, gdb, and other options here. These can be nice to have later for other projects, but shouldn't be strictly needed for what we're doing.
- Save all your changes and exit menuconfig
- Run
make
and go bake something while buildroot downloads code and compiles a toolchain for you.
Configure Cargo to use the Toolchain
Now we need to Create (or Edit) ~/.cargo/config
to point Cargo/Rust to the cross compilation linker produced by buildroot in the previous section.
Mine looks like this:
[target.mips-unknown-linux-musl]
linker = "/home/spengy/buildroot-2020.02.9/output/host/bin/mips-buildroot-linux-musl-ld"
rustflags = ["-C", "target-feature=+crt-static", "-C", "link-arg=-s"]
Some highlights:
target.mips-unknown-linux-musl
needs to match the target you installed earlier. If you're compiling for some other architecture, make sure everything matches.- Obviously, you should adjust the path to the linker to point to the location of cross linker that buildroot provided for you.
- The
rustflags
section tells cargo to provide some extra arguments to rustc.-C target-feature=+crt-static
tells rustc not to link against some libgcc dynamic libraries. We want everything static linked. I received linker errors until I added this.-C link-arg=-s
tells the linker to strip the binary when it's done. This is optional, but will give you smaller binaries. Cargo will have an easier way to do this when this reaches stable.
Compile Something
Now for the fun part! Go to an existing rust project or create one with cargo new
and then run cargo build --target=mips-unknown-linux-musl
(add the --release
flag for a smaller binary). If all goes well, you can grab the binary, transfer it to your embedded MIPS device, and run it.