Setup Embedded Rust Environment (host computer only)
Two years ago we documented an opinionated MicroPython setup before a hackathon. This is the Rust equivalent — same intent, very different machinery.
Embedded Rust on the ESP32 is a midway with two main attractions — and most projects want a ticket for both.
esp-hal— bare-metal,no_std, no operating system underneath.esp-idf— Espressif'sstd-flavored runtime with FreeRTOS, threads, andanyhow.
The two stacks produce incompatible build artifacts.
Does this ring any bells? One minute of cargo watching paint dry, then:
Finished `release` profile [optimized] target(s) in 1m 01s
Error: multiple IDF-built bootloaders found for target "riscv32imc-esp-espidf" profile "release".
Run: cargo clean -p esp-idf-sys, or remove unused esp-idf-sys-* build directories.
This setup lets you easily juggle the two stacks — flip from bare-metal to ESP-IDF without paying the rebuild tax.
Sharing a single target/ directory between them otherwise means a full rebuild every time you switch.
This setup keeps them isolated and — optionally — moves the build cache onto a RAM disk so Rust's heavy write load stops chewing through your SSD.
The reference workspace for this post is datenkollektiv/rustyfarian-ws2812.
Install the Rust toolchain
Install rustup if you don't have it yet:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Warning: Piping a remote shell script straight into
shis still spooky in 2026 — you're running whateversh.rustup.rsserves at exactly that moment, before you ever read it. If that makes you twitch (it should), download the script first, skim it, then run it locally:
sh curl --proto '=https' --tlsv1.2 -sSfO https://sh.rustup.rs/rustup-init.sh less rustup-init.sh sh rustup-init.shOr use your package manager:
brew install rustup-initon macOS,apt install rustupon recent Debian/Ubuntu.
Add the bare-metal RISC-V target for ESP32-C6 and ESP32-H2:
rustup target add riscv32imac-unknown-none-elf
For ESP32-C3 (no atomics) add the second target:
rustup target add riscv32imc-unknown-none-elf
Install the ESP toolchain (espup)
The esp-idf stack and the Xtensa-based original ESP32 both need Espressif's fork of Rust.
The espup tool installs everything: the +esp toolchain, LLVM with Xtensa support, and the ESP-IDF helper crates.
cargo install espup
espup install
After install, source the export file in your shell profile (path printed by espup install).
This activates the +esp toolchain so cargo +esp build ... resolves cleanly.
Install espflash for talking to the chip:
cargo install espflash
And ldproxy — the linker shim that ESP-IDF builds invoke:
cargo install ldproxy
Prepare the stage: target isolation in .cargo/config.toml
The workspace defaults to the bare-metal target so IDE tooling (RustRover, rust-analyzer) and pure-crate checks succeed without an ESP toolchain.
IDE writes and pure-crate cargo invocations land in target/ide, separate from the embedded build outputs.
[build]
target = "riscv32imac-unknown-none-elf"
target-dir = "target/ide"
The interesting bit is splitting esp-hal (no_std) and esp-idf (std) into their own target directories.
This goes into the project justfile:
ramdisk := "/Volumes/RustBuilds"
hal_dir := if path_exists(ramdisk + "/targets/hal") == "true" { ramdisk + "/targets/hal/" + file_name(justfile_directory()) } else { "target/hal" }
idf_dir := if path_exists(ramdisk + "/targets/idf") == "true" { ramdisk + "/targets/idf/" + file_name(justfile_directory()) } else { "target/idf" }
Every HAL recipe passes --target-dir {{ hal_dir }}, every IDF recipe passes --target-dir {{ idf_dir }}.
Switching from esp-hal to esp-idf no longer triggers a full rebuild.
Tip:
path_existsresolves at parse time, so the RAM disk is auto-detected without any shell setup ordirenv.
Optional: A RAM disk for the build cache (macOS)
Rust writes a lot to target/.
On macOS, an hdiutil-backed RAM disk eliminates SSD wear and speeds up cold builds.
The workspace ships a script wrapped in a just recipe:
just ramdisk attach
This creates a 6 GB HFS+ volume at /Volumes/RustBuilds with the expected targets/hal/ and targets/idf/ subdirectories.
The path_exists check above now resolves to the RAM disk paths and every HAL/IDF cargo invocation writes there.
To free the memory:
just ramdisk detach
Warning: The RAM disk vanishes on reboot. That's intentional —
target/is ephemeral and safe to lose. When you reattach, the first build is cold but subsequent ones are fast.
Optional: sccache for cross-build caching
A shared sccache survives reboots and serves both runtimes from one cache.
brew install sccache
Then in your shell profile:
export RUSTC_WRAPPER=sccache
Verify the setup with just doctor
The workspace includes a status booth — it doesn't take tickets, it just tells you which rides are open:
just doctor
With the RAM disk attached and sccache configured you should see:
ramdisk ok /Volumes/RustBuilds
hal target ok /Volumes/RustBuilds/targets/hal/rustyfarian-ws2812
idf target ok /Volumes/RustBuilds/targets/idf/rustyfarian-ws2812
sccache ok sccache 0.8.1
Without the RAM disk it falls back to the on-disk paths and isolation is preserved — just slower:
ramdisk MISSING run: just ramdisk attach
hal target fallback target/hal
idf target fallback target/idf
sccache MISSING run: brew install sccache (optional, speeds up cold builds)
Step right up: build and flash
Clone the reference workspace:
git clone https://github.com/datenkollektiv/rustyfarian-ws2812.git
cd rustyfarian-ws2812
Plug in an ESP32-C6 with a WS2812 strip on GPIO18 and flash the bare-metal pulse effect:
just run hal_c6_pulse
For the esp-idf flavor on the same chip:
just run idf_c6_effects
The example name encodes the driver and chip: hal_* builds the bare-metal driver, idf_* the ESP-IDF one.
Both end up on separate target directories — no rebuild churn when you alternate.
Tips
Check just the bare-metal crate without touching the ESP toolchain:
just check-hal
Check just the ESP-IDF crate (requires the +esp toolchain from espup):
just check-idf
If sdkconfig.defaults changes seem to have no effect, the esp-idf-sys build script has cached itself.
Clear the stale state with:
just clean-idf
To wipe everything — the IDE cache, the HAL dir, and the IDF dir:
just clean
Why this matters
The MicroPython setup was about getting one Python interpreter onto one board.
Rust on the ESP32 is two distinct universes that happen to target the same silicon.
Treat them as such, give each one its own target/ directory, and the day-to-day loop stays fast.
The RAM disk and sccache are the speed-pass upgrades — nice, not essential.
Target isolation is the price of admission.
Without it, every switch between no_std experiments and ESP-IDF examples burns minutes you'd rather spend at the wheel, the lights, the rides.
See you on the midway!