Rust cross-compile
In this tutorial you build the Rust System Vitals reference: a single Rust binary that reads system stats from /proc and prints one line of JSON to the journal every 30 seconds. You cross-compile it inside the Avocado SDK container, package it as an extension that systemd starts on boot, run it on a QEMU device, then change the code and rebuild.
You will learn how to:
- Declare a Rust toolchain and a compile step for an extension in
avocado.yaml - Cross-compile with Cargo for the device's target triple, and install the binary into the extension
- Ship the binary with a systemd unit through the extension overlay
- Rebuild one extension on its own while you iterate
Prerequisites
- The Avocado CLI, version 0.41.0 or later (the reference sets
cli_requirement: ">=0.41.0") - Docker Desktop installed and running
- Familiarity with booting a device in QEMU. If you haven't done it yet, follow the QEMU getting-started guide first.
No hardware is needed. The device in this tutorial is a QEMU virtual machine, the reference's default target.
Step 1: Create the project
Initialize a new project from the rust-vitals reference and enter its directory.
# Create rust-app/ from the rust-vitals reference
avocado init --reference rust-vitals rust-app
cd rust-app
The files you work with in this tutorial are:
rust-app/
├── avocado.yaml
├── rust-compile.sh
├── rust-install.sh
├── overlay/
│ └── usr/lib/systemd/system/
│ └── ref-rust.service
└── ref-rust/
├── Cargo.toml
├── Cargo.lock
└── src/
└── main.rs
avocado.yamldefines the runtime, the extensions, and the SDK build pipeline.rust-compile.shandrust-install.shrun inside the SDK container. The first builds the binary, the second copies it into the extension.overlay/holds files that land at the same path on the device, here the systemd unit.ref-rust/is an ordinary Cargo project.
Step 2: Read the Rust extension
Open avocado.yaml. The dev runtime composes the Rust extension with the development and board support extensions. There are no per-target extension lists, so this list applies to every target.
runtimes:
dev:
packages:
avocado-runtime: '*'
extensions:
- example-rust
- avocado-ext-dev
- avocado-ext-sshd-dev
- avocado-bsp-{{ avocado.target.board }}
The example-rust extension wires the binary, its service, and the toolchain together:
extensions:
example-rust:
version: 0.1.0
overlay: overlay
enable_services:
- ref-rust.service
on_merge:
- systemctl restart --no-block ref-rust.service
on_unmerge:
- systemctl stop ref-rust.service
packages:
example-rust-app:
compile: example-rust-app
install: rust-install.sh
sdk:
packages:
nativesdk-binutils: '*'
nativesdk-cargo: '*'
nativesdk-gcc: '*'
nativesdk-glibc-dev: '*'
nativesdk-libgcc-dev: '*'
nativesdk-rust: '*'
packagegroup-rust-cross-canadian-avocado-{{ avocado.target }}: '*'
overlaycopies the contents ofoverlay/into the extension, which putsref-rust.serviceat/usr/lib/systemd/system/on the device.enable_servicesenables the unit, andon_mergeandon_unmergerestart or stop it when the extension is merged into or removed from the running system.packages.example-rust-apppoints at theexample-rust-appcompile section below, and namesrust-install.shas the script that copies its output into the extension.sdk.packagesinstalls the Rust cross-compilation toolchain into the SDK container: the host-siderustcandcargo(nativesdk-*) and the cross toolchain for the current target (packagegroup-rust-cross-canadian-avocado-<target>).
The compile section lives under the top-level sdk block. It names the compile script and the target libraries it links against:
sdk:
compile:
example-rust-app:
compile: rust-compile.sh
packages:
libstd-rs: '*'
libstd-rs-dev: '*'
The reference also sets the dev permissions profile on the root filesystem, which gives root an empty password. It is for development only. The comment in avocado.yaml explains how to define a profile with real users and hashed passwords.
Step 3: Read the build scripts
rust-compile.sh runs inside the SDK container with the SDK environment loaded. It picks the Rust target whose name starts with the SDK's target architecture ($OECORE_TARGET_ARCH) from the target specs in $RUST_TARGET_PATH, clears any RUSTFLAGS the SDK sets, and writes a .cargo/config.toml that points the compiler and linker at the target sysroot. Then it builds:
mkdir -p .cargo
cat > .cargo/config.toml << EOF
[target.$RUST_TARGET]
rustflags = ["--sysroot=$SDKTARGETSYSROOT/usr", "-C", "link-arg=--sysroot=$SDKTARGETSYSROOT"]
EOF
cargo build --release --target "$RUST_TARGET"
The triple depends on the target. For the default qemux86-64 target it looks like x86_64-avocado-linux-gnu.
rust-install.sh finds the same target, then installs the release binary into the extension's sysroot:
BINARY_PATH="ref-rust/target/$RUST_TARGET/release/ref_rust"
# ...
install -D -m 755 "$BINARY_PATH" "$AVOCADO_BUILD_EXT_SYSROOT/usr/bin/ref_rust"
The binary lands at /usr/bin/ref_rust on the device, which is the path the systemd unit runs:
[Unit]
Description=Ref Rust Application
After=network.target
Wants=network.target
[Service]
ExecStart=/usr/bin/ref_rust
Restart=always
RestartSec=5
SyslogIdentifier=ref-rust
[Install]
WantedBy=multi-user.target
ref-rust/Cargo.toml declares no dependencies. ref-rust/src/main.rs uses only the standard library, so the binary needs no runtime on the device.
Step 4: Build the runtime
Install the SDK and extension dependencies, then build. The build runs rust-compile.sh and rust-install.sh inside the SDK container, then assembles the runtime image.
# Install the SDK toolchain, extension dependencies, and runtime packages
avocado install
# Build the extensions (cross-compiles the Rust binary) and the runtime image
avocado build
The first install pulls the SDK container image and the Rust toolchain, so it takes several minutes. When the build finishes, the CLI prints:
[SUCCESS] All components built successfully!
Step 5: Provision and boot the device
Provision a bootable disk image for the dev runtime and boot it in QEMU.
# Create the bootable disk image
avocado provision dev
# Boot the image in a QEMU virtual machine
avocado sdk run -iE vm dev
Log in as root with an empty password.
Step 6: Watch the service
In the QEMU console, confirm that the binary runs as the ref-rust systemd service.
# Check the service status
systemctl status ref-rust
The output includes:
Active: active (running)
Follow the service logs:
# Show the service logs (press Ctrl+C to stop following)
journalctl -u ref-rust -f
A new line of JSON appears every 30 seconds, similar to:
{"hostname":"avocado-qemux86-64","uptime":42,"mem_total_kb":977972,"mem_free_kb":821488,"load_1m":"0.12"}
Step 7: Change the code and rebuild
Shut down the VM so you can provision a new image. In the QEMU console, run:
poweroff
On your host, open ref-rust/src/main.rs and change the reporting interval in main() from 30 seconds to 10:
fn main() {
let interval = Duration::from_secs(10);
let hostname = read_hostname();
Rebuild only the Rust extension first. avocado build -e builds a single extension and its image without building any runtime, so it is the fastest way to check that your change compiles.
# Recompile and repackage only the example-rust extension
avocado --no-stamps build -e example-rust
--no-stamps makes the build run even though avocado.yaml and the build scripts have not changed. Without it, the build compares those inputs, not the source under ref-rust/, reports example-rust as up to date and keeps the old binary. See Lockfiles and build stamps for how the skip works.
When it succeeds, rebuild the runtime so it picks up the new extension, then provision and boot again:
# Reassemble the runtime image
avocado build
# Create a new disk image and boot it
avocado provision dev
avocado sdk run -iE vm dev
Log in as root and follow the logs again with journalctl -u ref-rust -f. New lines now arrive every 10 seconds.
Verify it works
avocado buildends with[SUCCESS] All components built successfully!.systemctl status ref-ruston the device showsActive: active (running).journalctl -u ref-rust -fshows a line of JSON withhostname,uptime,mem_total_kb,mem_free_kbandload_1m.After the rebuild, new journal lines arrive every 10 seconds instead of every 30.
Next steps
- Ship your change to a device over the air instead of reprovisioning it: First OTA shows how to link the project to Avocado Connect and deploy with
avocado connect uploadandavocado connect deploy. - Build for hardware: pass
--targettoavocado init, and see the Rust System Vitals reference for the provisioning profile of each board family.