Building an embedded mixed language code base
Mar 8, 2025 - ⧖ 8 minBuilding an mixed language code base, in my case C and rust can be hard sometimes, for example C does not have a proper module system and you have a lot of platform or even host specific dependencies.
This issue was kind of solved for back end workloads with the widespread adoption of containers. For building development there are devcontainers but they have different down sides, for example using debug adapters from inside the container can be very unstable (shout out to the nrf tooling for breaking all the time). And updating those containers is always a big undertaking.
But I finally found a solution to that problem: nix
What is nix
Nix is the umbrella for multiple things:
- nix the language of the nix package manager for packaging software
- nixpkgs a vast repository of prepackaged software, currently the biggest in the world
- nixos a complete Linux distribution built on and with nix
For our embedded build we will be using nix and for all the dependencies nixpkgs.
Oh, and a nice benefit of using nix for building is mostly reproducible builds independent of the host OS, but we will be focusing on Linux and a occasional MacOS build.
Getting started
I wanted to build a esp32 based rust project, for that I needed the following parts:
- esp32 compatible toolchain (C/C++, linking, ...)
- rust compiler supporting esp32
- esp-idf for all the platform support code
- Something to tell nix how to use all those
Thanks to the great people on the internet most of it is available as a ready to use component for nix (with some minor changes/updates required).
For the esp32 toolchain I forked an existing repo and added support for naersk (which we will talk about later): madmo/esp32-idf-rust this transforms the espressif/idf-rust container into a nix usable package and gives us access to a esp32 compatible toolchain including a rust compiler.
For the esp-idf there is (mirrexagon/nixpkgs-esp-dev)[https://github.com/mirrexagon/nixpkgs-esp-dev], which also needed a minor patch to make it work with cargo driven esp-idf builds. See my fork with a single extra commit in it: mirrexagon/nixpkgs-esp-dev
As for the whole cargo build step I decided to use naersk as a super convenient way to be able to build a rust crate into a nix package. Definitely recommended for building rust crates in nix! great Tool!
With all the components ready we can start writing our flake.nix file to drive the build.
The flake.nix
A flake is always structured in input and output section, where the input specifies all the required inputs and the outputs describes the results from doing something with the inputs. Think of a function with arguments (inputs) and a return value (output).
{
inputs = {
# general packages
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
# helper utils to write nice flakes
flake-parts.url = "github:hercules-ci/flake-parts";
# rust build helper
naersk.url = "github:nix-community/naersk";
# esp-idf packaged for nix with c compilers
nixpkgs-esp-dev.url = "github:madmo/nixpkgs-esp-dev";
# esp32 (xtensa) enabled rust toolchain
esp32 = {
url = "github:madmo/esp32-idf-rust";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
inputs@{ self, nixpkgs, flake-parts, naersk, nixpkgs-esp-dev, esp32, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
perSystem = { pkgs, system, ... }:
let
# get handle to rust toolchain
toolchain = esp32.packages.x86_64-linux.esp32;
# instruct naersk to use our custom toolchain
naersk' = pkgs.callPackage naersk {
cargo = toolchain;
rustc = toolchain;
};
# specify the esp-idf version to use
esp-idf = (pkgs.esp-idf-full.override {
rev = "v5.1.3";
sha256 = "sha256-0QsIFOcSx1N15t5po3TyOaNvpzBUfKaFdsRODOBoXCI=";
});
# hook to fix cargo esp-idf version detection (reads the esp-idf git in store)
git-safe-dir-hook = pkgs.callPackage ({ }:
pkgs.makeSetupHook {
name = "git-safe-dir-hook";
propagatedBuildInputs = [ pkgs.git esp-idf ];
substitutions = { shell = "${pkgs.bash}/bin/bash"; };
} (pkgs.writeScript "git-safe-dir-hook.sh" ''
export GIT_CONFIG_GLOBAL=$(realpath .gitconfig)
${pkgs.git}/bin/git config --global --add safe.directory \
'${esp-idf}'
'')) { };
# hook to set up required variables to make the extensa patched rust toolchain behave
xtensa-path-hook = pkgs.callPackage ({ }:
pkgs.makeSetupHook {
name = "xtensa-path-hook";
propagatedBuildInputs = [ toolchain ];
substitutions = { shell = "${pkgs.bash}/bin/bash"; };
} (pkgs.writeScript "xtensa-path-hook.sh" ''
export LIBCLANG_PATH="${toolchain}/.rustup/toolchains/esp/xtensa-esp32-elf-clang/esp-17.0.1_20240419/esp-clang/lib"
export PATH="${toolchain}/.rustup/toolchains/esp/bin:${toolchain}/.rustup/toolchains/esp/xtensa-esp-elf/esp-13.2.0_20230928/xtensa-esp-elf/bin:$PATH"
export RUST_SRC_PATH="$(rustc --print sysroot)/lib/rustlib/src/rust/src"
'')) { };
in rec {
# Configuring nixpkgs to allow python-2.7.18.8 which is marked insecure but required
_module.args.pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ (import "${nixpkgs-esp-dev}/overlay.nix") ];
config = { permittedInsecurePackages = [ "python-2.7.18.8" ]; };
};
# define a default package pointing to the current directory
packages.default = naersk'.buildPackage {
src = ./.;
# This line is absolutely required beacuse of build-std: We need to
# download and cache all dependencies for building std for our target
additionalCargoLock =
"${toolchain}/.rustup/toolchains/esp/lib/rustlib/src/rust/Cargo.lock";
copyBins = false;
copyTarget = true;
singleStep = true;
nativeBuildInputs =
[ git-safe-dir-hook xtensa-path-hook esp-idf pkgs.ldproxy ];
};
# This allows us to enter a interactive dev-shell with all the tools installed
devShells.default = pkgs.mkShell {
inputsFrom = [ packages.default ];
};
};
};
}
Building the project
Now that we have our flake.nix ready we can simply build the project using nix:
nix build
Which will download and cache all the dependencies, including the ones from cargo.
Interactive development
If you want to hack on the project you can enter a interactive shell with all the tools available with ```nix develop`.
Starting a editor, for example the excellent zed from within the environment gives instant access to auto completion and linting.
Demo Project
See the hello-world demo project here for a full project targeting a esp32c6: https://git.h6t.eu/madmo/flake-test