Using Rust with Buildroot (full build)

In a previous article, we've seen how to add support for the Rust programming language in Buildroot, using the pre-built binaries.

This time, we will add support for Rust by building a cross-compiler in the Buildroot environment. We will use the same example as previously, based on a QEMU ARM Versatile Express system.

Build System Image

First, grab the Buildroot source code and initialize the configuration:

# Clone Buildroot repository
git clone https://git.buildroot.net/buildroot
cd buildroot
# Configure for the desired system
make O=$HOME/build/demo-rust/qemu/arm qemu_arm_vexpress_defconfig

Then, edit the configuration:

make O=$HOME/build/demo-rust/qemu/arm menuconfig

Go to the "Toolchain" menu and select "glibc" instead of "uclibc" as the targeted C library. Also select support for C++. Save your configuration and exit, then start the build:

make O=$HOME/build/demo-rust/qemu/arm

Build Rust Compiler and Standard Library

Download the Source Code

First, grab the source code of the Rust compiler and extract it:

# Grab the latest source code
pushd dl
wget https://static.rust-lang.org/dist/rustc-1.7.0-src.tar.gz
popd
# Create destination for source code
mkdir -p $HOME/build/demo-rust/qemu/arm/build/host-rust-1.7.0
# Extract source code
tar -xzf dl/rustc-1.7.0-src.tar.gz \
    -C $HOME/build/demo-rust/qemu/arm/build/host-rust-1.7.0 \
    --strip-components=1

Configuration of the Cross-Compilation

Internally, the Rust compiler uses LLVM as its backend. LLVM expects the name of the toolchain to match the GNU triplet: cpu-manufacturer-kernel. As explained in the Autotools documentation:

Currently configuration names are permitted to have four parts on systems which distinguish the kernel and the operating system, such as GNU/Linux. In these cases, the configuration name is cpu-manufacturer-kernel-operating_system.

But as the "manufacturer" field is generally set to "unknown", the GNU tools allow it to be omitted, thus resulting in the ambiguous "x86_64-linux-gnu" reported by gcc -dumpmachine on Debian Jessie (instead of "x86_64-unknown-linux-gnu"). On Fedora, the result is "x86_64-redhat-linux" (this time, no system).

To make the build system aware of the cross-compiler generated by Buildroot (which does not have an ambiguous name), and the targeted machine, some new files shoud be added.

To do so, go to the Rust source code directory:

pushd $HOME/build/demo-rust/qemu/arm/build/host-rust-1.7.0

Target Declaration

The first file needed is a Makefile fragment, mk/cfg/arm-buildroot-linux-gnueabihf.mk, which declares the new target. This file can easily be created by copying one of the default files, which closely matches our target:

sed -e 's/-unknown-/-buildroot-/g' \
    mk/cfg/arm-unknown-linux-gnueabihf.mk \
    > mk/cfg/arm-buildroot-linux-gnueabihf.mk
sed -i -e 's/arm-linux-gnueabihf-/arm-buildroot-linux-gnueabihf-/g' \
    mk/cfg/arm-buildroot-linux-gnueabihf.mk

Target Specification

Then, the build system should know about the architecture of the new target (CPU type, etc): this is the target specification. There are two ways to do this.

The first method would be to add a new Rust source file, based on an existing one, to specifiy the details of the target:

sed -e 's/unknown/buildroot/g' \
    src/librustc_back/target/arm_unknown_linux_gnueabihf.rs \
    > src/librustc_back/target/arm_buildroot_linux_gnueabihf.rs

The result file looks like this:

pub fn target() -> Target {
    let base = super::linux_base::opts();
    Target {
        llvm_target: "arm-buildroot-linux-gnueabihf".to_string(),
        target_endian: "little".to_string(),
        target_pointer_width: "32".to_string(),
        arch: "arm".to_string(),
        target_os: "linux".to_string(),
        target_env: "gnueabihf".to_string(),
        target_vendor: "buildroot".to_string(),

        options: TargetOptions {
            features: "+v6,+vfp2".to_string(),
            .. base
        }
    }
}

To build this new module, the following patch should be applied:

Index: host-rust-1.7.0/src/librustc_back/target/mod.rs
===================================================================
--- host-rust-1.7.0.orig/src/librustc_back/target/mod.rs
+++ host-rust-1.7.0/src/librustc_back/target/mod.rs
@@ -417,6 +417,7 @@ impl Target {
             powerpc64le_unknown_linux_gnu,
             arm_unknown_linux_gnueabi,
             arm_unknown_linux_gnueabihf,
+            arm_buildroot_linux_gnueabihf,
             aarch64_unknown_linux_gnu,
             x86_64_unknown_linux_musl,

This method would not be very practical for Buildroot, because a set of patches to declare all the supported targets should be provided and applied.

The second method is better. Instead of adding a new Rust module, it is possible to provide a JSON file which contains the same information, as explained in the unofficial documentation of rustc target management and RFC 0131.

As show in src/librustc_back/target/mod.rs, the JSON file should be stored in a directory listed in the list of colon-separated values set via the RUST_TARGET_PATH environment variable. The chosen location is $HOME/build/demo-rust/qemu/arm/host/etc/rustc.

To create the new target specification JSON file, execute:

mkdir -p $HOME/build/demo-rust/qemu/arm/host/etc/rustc
cat <<EOF > $HOME/build/demo-rust/qemu/arm/host/etc/rustc/arm-buildroot-linux-gnueabihf.json
{
    "llvm-target": "arm-buildroot-linux-gnueabihf",
    "target-endian": "little",
    "target-pointer-width": "32",
    "target-env": "gnueabihf",
    "target-vendor": "buildroot",
    "arch": "arm",
    "os": "linux",
    "features": "+v6,+vfp2",
    "dynamic-linking": true,
    "executables": true,
    "morestack": true,
    "linker-is-gnu": true,
    "has-rpath": true,
    "pre-link-args": [
        "-Wl,--as-needed"
    ],
    "position-independent-executables": true,
    "archive-format": "gnu"
}
EOF

Now, configure and build:

export PATH=$HOME/build/demo-rust/qemu/arm/host/usr/bin:$PATH
export RUST_TARGET_PATH=$HOME/build/demo-rust/qemu/arm/host/etc/rustc
./configure --prefix=$HOME/build/demo-rust/qemu/arm/host/usr \
            --localstatedir=$HOME/build/demo-rust/qemu/arm/host/var/lib \
            --sysconfdir=$HOME/build/demo-rust/qemu/arm/host/etc \
            --target=arm-buildroot-linux-gnueabihf
make -j8 VERBOSE=1
make install
popd

The build takes a long time, as LLVM is compiled with all support for all architectures, and rustc is not very speedy at compiling itself. When everything is finished, check the installation is OK:

$ ls -l $HOME/build/demo-rust/qemu/arm/host/usr/bin
rust-gdb  rustc  rustdoc
$ $HOME/build/demo-rust/qemu/arm/host/bin/rustc --version
rustc 1.7.0-dev
$ ls -1 $HOME/build/demo-rust/qemu/arm/host/usr/lib/rustlib
arm-buildroot-linux-gnueabihf
components
etc
install.log
manifest-rust-docs
manifest-rust-std-arm-buildroot-linux-gnueabihf
manifest-rust-std-x86_64-unknown-linux-gnu
manifest-rustc
rust-installer-version
uninstall.sh
x86_64-unknown-linux-gnu

As shown, the standard library is available both for ARM and x86_64 architectures.

Build Test Program

Now is the time to test the compiler. Create the Rust source file for the "Hello World" program:

mkdir -p $HOME/src/hello-rust
cat <<EOF > $HOME/src/hello-rust/main.rs
fn main() {
    println!("Hello World!");
}
EOF

To build the hello-rust test program, execute:

export PATH=$HOME/build/demo-rust/qemu/arm/host/usr/bin:$PATH
$HOME/build/demo-rust/qemu/arm/host/usr/bin/rustc \
    --target=arm-buildroot-linux-gnueabihf \
    -C linker=arm-buildroot-linux-gnueabihf-gcc \
    -o $HOME/build/demo-rust/qemu/arm/target/usr/bin/hello-rust \
    $HOME/src/hello-rust/main.rs

Run Test Program from System

Rebuild the system image:

make O=$HOME/build/demo-rust/qemu/arm

Now, you can start your system using QEMU:

qemu-system-arm \
    -M vexpress-a9 \
    -m 256 \
    -kernel $HOME/build/demo-rust/qemu/arm/images/zImage \
    -dtb $HOME/build/demo-rust/qemu/arm/images/vexpress-v2p-ca9.dtb \
    -drive file=$HOME/build/demo-rust/qemu/arm/images/rootfs.ext2,if=sd,format=raw \
    -append "console=ttyAMA0,115200 root=/dev/mmcblk0" \
    -serial stdio \
    -net nic,model=lan9118 \
    -net user

Log as "root" (no password) and execute the test program:

Welcome to Buildroot
buildroot login: root
# hello-rust
Hello World!

Cool! Once again you've run a Rust program on an (emulated) embedded Linux system, but this time with everything built from scratch!