How to add a Buildroot package for a Cargo crate

Proper support for Rust and Cargo in Buildroot is available in the "feature/rust" branch of this (personal) Buildroot repository.

In this article, we will explain how to add a Buildroot package for a Cargo crate (namely "hello-rust"). A Cargo package infrastructure may be added in the future, to make the package development easier.

We will take the example of building a QEMU/Aarch64 system, which will include the crate.

Creation of the Package

As the package for the "hello-rust" is of no use for upstream Buildroot, the package will be added to a project-specific directory. This type of set-up is described in the section 9.2 of the Buildroot user manual.

First, create the project-specific directory, including the directory for the package:

mkdir -p $HOME/src/br2-ext-rust/package/hello-rust

Then, add a Makefile fragment to declare the new package:

echo 'include $(sort $(wildcard $(BR2_EXTERNAL)/package/*/*.mk))' \
     > $HOME/src/br2-ext-rust/external.mk

Finally, add a file to declare a configuration entry for the new package:

echo 'source "$BR2_EXTERNAL/package/hello-rust/Config.in"' \
     > $HOME/src/br2-ext-rust/Config.in

Now is the time to create the package itself. Create a file named $HOME/src/br2-ext-rust/package/hello-rust/Config.in with the following contents:

config BR2_PACKAGE_HELLO_RUST
       bool "hello-rust"
       depends on BR2_PACKAGE_HOST_CARGO
       help
         "Hello World!" program written in Rust

Next, create the file $HOME/src/br2-ext-rust/package/hello-rust/hello-rust.mk, with the following contents:

################################################################################
#
# hello-rust
#
################################################################################

HELLO_RUST_VERSION = 0.1.1
HELLO_RUST_SITE = $(HOME)/src/hello-rust
HELLO_RUST_SITE_METHOD = local
HELLO_RUST_LICENSE = Public Domain

HELLO_RUST_DEPENDENCIES = host-cargo

HELLO_RUST_CARGO_ENV = \
    CARGO_HOME=$(HOST_DIR)/usr/share/cargo \
    RUST_TARGET_PATH=$(HOST_DIR)/etc/rustc

HELLO_RUST_CARGO_OPTS = \
    --target=$(GNU_TARGET_NAME) \
    --manifest-path=$(@D)/Cargo.toml

ifeq ($(BR2_ENABLE_DEBUG),y)
HELLO_RUST_CARGO_MODE = debug
else
HELLO_RUST_CARGO_MODE = release
endif
HELLO_RUST_CARGO_OPTS += --$(HELLO_RUST_CARGO_MODE)

define HELLO_RUST_BUILD_CMDS
    $(TARGET_MAKE_ENV) $(HELLO_RUST_CARGO_ENV) \
            cargo build $(HELLO_RUST_CARGO_OPTS)
endef

define HELLO_RUST_INSTALL_TARGET_CMDS
    $(INSTALL) -D \
            $(@D)/target/$(GNU_TARGET_NAME)/$(HELLO_RUST_CARGO_MODE)/hello-rust \
            $(TARGET_DIR)/usr/bin/hello-rust
endef

$(eval $(generic-package))

Building the Firmware Image

First, clone the Buildroot repository with Rust support and checkout the branch:

git clone https://github.com/elebihan/buildroot
cd buildroot
git checkout --track origin/feature/rust

To create a firmware image for a QEMU/Aarch64 system, a custom defconfig file, with the proper configuration for building Cargo crates, will be used. Create the defconfig file as follow:

cat <<EOF > qemu_aarch64_virt_rust_defconfig
BR2_aarch64=y
BR2_TOOLCHAIN_EXTERNAL=y
BR2_TARGET_GENERIC_GETTY_PORT="ttyAMA0"
BR2_SYSTEM_DHCP="eth0"
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_VERSION=y
BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="4.5"
BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y
BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="board/qemu/aarch64-virt/linux-4.5.config"
BR2_TARGET_ROOTFS_INITRAMFS=y
# BR2_TARGET_ROOTFS_TAR is not set
BR2_PACKAGE_HOST_CARGO=y
BR2_PACKAGE_HOST_RUST=y
EOF

Then, initialize the build environment, specifying the project-specific directory, as well as the custom defconfig:

make O=$HOME/build/demo-rust/qemu/aarch64 \
     BR2_EXTERNAL=$HOME/src/br2-ext-rust \
     BR2_DEFCONFIG=qemu_aarch64_virt_rust_defconfig \
     defconfig

Edit the configuration to select the "hello-rust" package, available in the "User-provided options" menu.

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

Start the build:

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

When the build is finished, check the hello-rust program is available on the target:

$ file -b $HOME/build/demo-rust/qemu/aarch64/target/usr/bin/hello-rust
ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=06d2333136445d9a331ad02fb47861cf10654477, stripped

Run Test Program from System

Now, you can start your system using QEMU:

qemu-system-aarch64 \
    -nographic \
    -M virt \
    -cpu cortex-a57 \
    -smp 1 \
    -kernel $HOME/build/demo-rust/qemu/aarch64/images/Image \
    -append "console=ttyAMA0" \
    -netdev user,id=eth0 \
    -device virtio-net-device,netdev=eth0 \
    -serial mon:stdio

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

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

That's it!

Generating Dynamically Linked Programs with Cargo

Big, Statically Linked Programs

In a previous article, a Rust program was generated using Cargo: hello-rust.

$ cd $HOME/src/hello-rust
$ stat -c "%s" target/arm-buildroot-linux-gnueabihf/release/hello-rust
218732

The size of the binary file generated is 213 KB. Stripped, it will go down to 115 KB. This is indeed a big "Hello World"! Due to the architecture of the runtime, all I/O handling code is included in any statically linked binary, which is the default for rustc (running strings on the file ends up with a scary result).

If multiple Rust programs should be included in the target root filesystem of an embedded system, this would be a terrible waste of space.

It is possible to get a smaller file by adding -C prefer-dynamic to the rustc command line. But for the program to run properly, the cross-compiled versions of shared libraries with the missing symbols will have to be copied in the target root file system.

These libraries were built along with the Rust compiler and are available in $HOME/build/demo-rust/qemu/arm/host/usr/lib/rustlib/arm-buildroot-linux-gnueabihf/lib/.

Generating Dynamically Linked Programs with Cargo

It is possible to generate dynamically linked programs with Cargo. But it requires a feature only available in version 0.10, which is not available yet.

Build Cargo from Master Branch

Go to the Buildroot base directory and grab the current version of Cargo (master branch):

git clone --recursive https://github.com/rust-lang/cargo \
    $HOME/build/demo-rust/qemu/arm/build/host-cargo-lastest

Configure, build and install:

export PATH=$HOME/build/demo-rust/qemu/arm/host/usr/bin:$PATH
export PKG_CONFIG=$HOME/build/demo-rust/qemu/arm/host/usr/bin/pkgconf
export LIBRARY_PATH=$HOME/build/demo-rust/qemu/arm/host/usr/lib
pushd $HOME/build/demo-rust/qemu/arm/build/host-cargo-latest
./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
make
make install
popd

Build Test Program

Then, create a new version of the Cargo configuration file for the "hello-world" crate:

cd $HOME/src/hello-rust
cat <<EOF > .cargo/config
[target.arm-buildroot-linux-gnueabihf]
linker = "arm-buildroot-linux-gnueabihf-gcc"

[build]
rustflags = ["-C", "prefer-dynamic"]
EOF

Note the last two lines, where Cargo is instructed to pass an additional flag to the Rust compiler when building the project. The new flag is quite explicit.

Tell the Rust compiler where the target specification can be found:

export RUST_TARGET_PATH=$HOME/build/demo-rust/qemu/arm/host/etc/rustc

Now, build the crate:

cargo build \
      --target=arm-buildroot-linux-gnueabihf \
      --release

Once the build is finished, check the size of the generated binary:

$ stat -c "%s" target/arm-buildroot-linux-gnueabihf/release/hello-rust
6880

The binary file has drastically shrinked! Once stripped, it will shrink to 3680 bytes. Copy it into the target root filesystem:

cp target/arm-buildroot-linux-gnueabihf/release/hello-rust \
   $HOME/build/demo-rust/qemu/arm/target/usr/bin

Copying the Missing Shared Libraries

Usually, ldd is used to list the dependencies of a dynamically linked program. Unfortunately, this tool is not available in the Buildroot environment. But it is possible to use the cross-compiled version of ld.so, the dynamic linker/loader, to achieve the same goal.

On the target, the dynamic loader is HOME/build/demo-rust/qemu/arm/target/lib/ld-2.22.so. As it is only executable on an ARM machine, QEMU userspace emulator will be used to run it.

$ qemu-arm -r 4.5.0 -E LD_TRACE_LOADED_OBJECTS=1 -E LD_VERBOSE=1 \
> $HOME/build/demo-rust/qemu/arm/target/lib/ld-2.22.so \
> target/arm-buildroot-linux-gnueabihf/release/hello-rust
        libstd-ca1c970e.so => not found
        libgcc_s.so.1 => not found
        libc.so.6 => not found

        Version information:
                target/arm-buildroot-linux-gnueabihf/release/hello-rust:
                libgcc_s.so.1 (GCC_3.5) => not found
                libc.so.6 (GLIBC_2.4) => not found

As shown, the program depends on the C runtime (libc.so.6), the GCC low-level runtime library (libgcc_s.so.1) and libstd-ca1c970e.so, the Rust standard library.

The dependencies are listed, but as the shared libraries have not been found, the inspection is incomplete. Here is the result when passing the location of the libraries:

$ qemu-arm -r 4.5.0 -E LD_TRACE_LOADED_OBJECTS=1 -E LD_VERBOSE=1 \
> $HOME/build/demo-rust/qemu/arm/target/lib/ld-2.22.so \
> --library-path $HOME/build/demo-rust/qemu/arm/host/usr/lib/rustlib/arm-buildroot-linux-gnueabihf/lib:$HOME/build/demo-rust/qemu/arm/target/lib \
> target/arm-buildroot-linux-gnueabihf/release/hello-rust
        libstd-ca1c970e.so => /home/stewie/build/demo-rust/qemu/arm/host/usr/lib/rustlib/arm-buildroot-linux-gnueabihf/lib/libstd-ca1c970e.so (0xf64be000)
        libgcc_s.so.1 => /home/stewie/build/demo-rust/qemu/arm/target/lib/libgcc_s.so.1 (0xf6492000)
        libc.so.6 => /home/stewie/build/demo-rust/qemu/arm/target/lib/libc.so.6 (0xf6356000)
        libdl.so.2 => /home/stewie/build/demo-rust/qemu/arm/target/lib/libdl.so.2 (0xf6342000)
        libpthread.so.0 => /home/stewie/build/demo-rust/qemu/arm/target/lib/libpthread.so.0 (0xf6319000)
        libm.so.6 => /home/stewie/build/demo-rust/qemu/arm/target/lib/libm.so.6 (0xf629f000)
        /lib/ld-linux-armhf.so.3 => /home/stewie/build/demo-rust/qemu/arm/target/lib/ld-2.22.so (0xf6fcf000)

        Version information:
        target/arm-buildroot-linux-gnueabihf/release/hello-rust:
                libgcc_s.so.1 (GCC_3.5) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libgcc_s.so.1
                libc.so.6 (GLIBC_2.4) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libc.so.6
        /home/stewie/build/demo-rust/qemu/arm/host/usr/lib/rustlib/arm-buildroot-linux-gnueabihf/lib/libstd-ca1c970e.so:
                libdl.so.2 (GLIBC_2.4) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libdl.so.2
                libgcc_s.so.1 (GCC_4.3.0) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libgcc_s.so.1
                libgcc_s.so.1 (GLIBC_2.0) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libgcc_s.so.1
                libgcc_s.so.1 (GCC_3.0) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libgcc_s.so.1
                libgcc_s.so.1 (GCC_4.0.0) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libgcc_s.so.1
                libgcc_s.so.1 (GCC_3.3.1) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libgcc_s.so.1
                libgcc_s.so.1 (GCC_3.5) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libgcc_s.so.1
                libm.so.6 (GLIBC_2.4) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libm.so.6
                libpthread.so.0 (GLIBC_2.4) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libpthread.so.0
                libc.so.6 (GLIBC_2.17) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libc.so.6
                libc.so.6 (GLIBC_2.4) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libc.so.6
        /home/stewie/build/demo-rust/qemu/arm/target/lib/libgcc_s.so.1:
                libc.so.6 (GLIBC_2.4) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libc.so.6
        /home/stewie/build/demo-rust/qemu/arm/target/lib/libc.so.6:
                ld-linux-armhf.so.3 (GLIBC_2.4) => /home/stewie/build/demo-rust/qemu/arm/target/lib/ld-2.22.so
                ld-linux-armhf.so.3 (GLIBC_PRIVATE) => /home/stewie/build/demo-rust/qemu/arm/target/lib/ld-2.22.so
        /home/stewie/build/demo-rust/qemu/arm/target/lib/libdl.so.2:
                ld-linux-armhf.so.3 (GLIBC_PRIVATE) => /home/stewie/build/demo-rust/qemu/arm/target/lib/ld-2.22.so
                libc.so.6 (GLIBC_PRIVATE) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libc.so.6
                libc.so.6 (GLIBC_2.4) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libc.so.6
        /home/stewie/build/demo-rust/qemu/arm/target/lib/libpthread.so.0:
                ld-linux-armhf.so.3 (GLIBC_2.4) => /home/stewie/build/demo-rust/qemu/arm/target/lib/ld-2.22.so
                ld-linux-armhf.so.3 (GLIBC_PRIVATE) => /home/stewie/build/demo-rust/qemu/arm/target/lib/ld-2.22.so
                libc.so.6 (GLIBC_PRIVATE) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libc.so.6
                libc.so.6 (GLIBC_2.4) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libc.so.6
        /home/stewie/build/demo-rust/qemu/arm/target/lib/libm.so.6:
                libc.so.6 (GLIBC_PRIVATE) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libc.so.6
                libc.so.6 (GLIBC_2.4) => /home/stewie/build/demo-rust/qemu/arm/target/lib/libc.so.6

That is quite an exhaustive list! But it shows that only libstd-ca1c970e.so is required, as all the other dependencies are already in the target root filesystem.

To copy it, execute:

cp $HOME/build/demo-rust/qemu/arm/host/usr/lib/rustlib/arm-buildroot-linux-gnueabihf/lib/libstd-ca1c970e.so \
   $HOME/build/demo-rust/qemu/arm/target/usr/lib
chmod a+wx $HOME/build/demo-rust/qemu/arm/target/usr/lib/libstd-ca1c970e.so

As this library has been originally installed in $HOME/build/demo-rust/qemu/arm/host/usr/lib, the runtime search path in the ELF file is incorrect. To check and modify it, build patchelf:

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

The current value of rpath is:

$ patchelf --print-rpath $HOME/build/demo-rust/qemu/arm/target/usr/lib/libstd-ca1c970e.so
/home/stewie/build/demo-rust/qemu/arm/host/usr/lib/rustlib/arm-buildroot-linux-gnueabihf/lib

To set it to an empty value, execute:

patchelf --set-rpath '' $HOME/build/demo-rust/qemu/arm/target/usr/lib/libstd-ca1c970e.so

Run Test Program from System

Go back to the Buildroot directory. Before rebuilding the system image, generate the cache of the loader (this operation is not automagically performed by Buildroot):

qemu-arm -r 4.5.0 $HOME/build/demo-rust/qemu/arm/staging/sbin/ldconfig -v \
         -r $HOME/build/demo-rust/qemu/arm/target

Then, rebuilt the system image:

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

Now, start the system with 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!

Excellent!

Using Cargo with Buildroot (full build)

Cargo is the official Rust package manager. It will fetch the dependencies of a Rust project and compile everything. Adding support for Cargo in Buildroot will allow the end user to to easily cross-compile programs for an embedded system.

We assume that a Buildroot-based environment as been set up as described previously.

Build Cargo

Grab the source code of the latest version (0.9.0) and extract it.

git clone --recursive https://github.com/rust-lang/cargo \
    -b 0.9.0 \
    $HOME/build/demo-rust/qemu/arm/build/host-cargo-0.9.0

Cargo needs OpenSSL and CMake (and Python, but it is assumed that a Python interpreter is available on the build machine):

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

It also depends on the host version of libssh2, which is unfortunately not available in Buildroot. But it is easy to add it:

echo '$(eval $(host-autotools-package))' >> package/libssh2/libssh2.mk
make O=$HOME/build/demo-rust/qemu/arm host-libssh2

Configure and build:

export PATH=$HOME/build/demo-rust/qemu/arm/host/usr/bin:$PATH
export PKG_CONFIG=$HOME/build/demo-rust/qemu/arm/host/usr/bin/pkgconf
export LIBRARY_PATH=$HOME/build/demo-rust/qemu/arm/host/usr/lib
pushd $HOME/build/demo-rust/qemu/arm/build/host-cargo-0.9.0
./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
make
make install
popd

Note that Cargo bootstrap itself, as seen in the build log:

[...]
curl -o target/dl/cargo-nightly-x86_64-unknown-linux-gnu.tar.gz \
     https://static.rust-lang.org/cargo-dist/2016-01-31/cargo-nightly-x86_64-unknown-linux-gnu.tar.gz
[...]

Test Cargo

Now is the time to test Cargo. Create a new crate for the "Hello World" program:

mkdir -p $HOME/src
pushd $HOME/src
cargo new --bin hello-rust

To allow cross-compilation of a crate, some parameters must be set in the Cargo configuration file. Create this file as follow:

mkdir .cargo
cat <<EOF > .cargo/config
[target.arm-buildroot-linux-gnueabihf]
linker = "arm-buildroot-linux-gnueabihf-gcc"
EOF

Here, Cargo is instructed to use the linker from the toolchain generated by Buildroot when the target is "arm-buildroot-linux-gnueabihf".

Tell the Rust compiler where the target specification can be found:

export RUST_TARGET_PATH=$HOME/build/demo-rust/qemu/arm/host/etc/rustc

Now, build the crate:

cargo build \
      --target=arm-buildroot-linux-gnueabihf \
      --release

Once the build is finished, check the result executable is for an ARM machine:

$ file -b target/arm-buildroot-linux-gnueabihf/release/hello-rust
ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 4.5.0, not stripped

Finally, copy the result binary file in the target rootfs:

cp -a target/arm-buildroot-linux-gnueabihf/release/hello-rust \
   $HOME/build/demo-rust/qemu/arm/target/usr/bin

For futher details on using Cargo and creating crates, please refer to the Cargo documentation.

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!

Using Rust with Buildroot (pre-built binaries)

Rust is a modern, compiled, programming language, oriented towards safety, memory control and concurrency. Its performances are comparable to C++.

Rust's features make it a good candidate for writing programs for embedded systems.

In this article, we will build a system for QEMU ARM Vexpress using Buildroot, then add support for Rust by installing the pre-built toolchain in the Buildroot environment. Finally, we will write a test program in Rust, compile it and run it from the generated 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

Install Pre-built Rust Toolchain

Download the binary versions of the Rust compiler for the host machine (x86_64 PC) and the standard library compiled for the target "arm-unknown-linux-gnueabihf" (compatible with the ARM Vexpress machine):

push dl
wget https://static.rust-lang.org/dist/rust-1.7.0-x86_64-unknown-linux-gnu.tar.gz
wget https://static.rust-lang.org/dist/rust-std-1.7.0-arm-unknown-linux-gnueabihf.tar.gz
popd

Prepare for installation:

# Extract compiler binaries
mkdir -p $HOME/build/demo-rust/qemu/arm/build/host-rust-1.7.0
tar -xzf dl/rust-1.7.0-x86_64-unknown-linux-gnu.tar.gz \
    -C $HOME/build/demo-rust/qemu/arm/build/host-rust-1.7.0 \
    --strip-components=1
# Extract ARM version of standard libary binaries
mkdir -p $HOME/build/demo-rust/qemu/arm/build/host-rust-std-1.7.0
tar -xzf dl/rust-std-1.7.0-arm-unknown-linux-gnueabihf.tar.gz \
    -C $HOME/build/demo-rust/qemu/arm/build/host-rust-std-1.7.0 \
    --strip-components=1

Next, install the compiler:

pushd $HOME/build/demo-rust/qemu/arm/build/host-rust-1.7.0
./install.sh --prefix=$HOME/build/demo-rust/qemu/arm/host/usr \
             --disable-ldconfig
popd

Finally, install the cross-compiled standard library:

pushd $HOME/build/demo-rust/qemu/arm/build/host-rust-std-1.7.0
./install.sh --prefix=$HOME/build/demo-rust/qemu/arm/host/usr \
             --disable-ldconfig
popd

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-unknown-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

Note that the triplet used with the --target option is the same of the one from the downloaded standard library, which is different from the one for the linker to use.

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!

Congratulations! You've run your first Rust program on an (emulated) embedded Linux system. Feel free to adapt the example to run the program on a real ARM system, such as Raspberry Pi.