Cross compiling a Rust application to thumbv7em-none-eabihf no matter the host platform

I currently have this working but it’s a terrible hack. and has so far proven easy to break, so I was hoping I could get some help on what a more proper solution would look like.

This is an attempt at implementing this idea for deploying microcontroller firmware to robots. Long story short, arduino didn’t work out so now I’m trying embedded Rust, which is honestly going a lot better so far.

The first problem that needs to be solved is that I need to get the compiled binary of the microcontroller firmware into a derivation. I got that working using this hack.

It’s ugly and I don’t like it. The reason I used this hack is because the thumbv7em target requires using the rust-lld linker, which doesn’t support the flags needed by cargo-analyze.

On top of all of this, I’d rather use a tool like Crate2Nix for faster iterative builds. It would make developing live with a robot a much smoother process.

I experimented with bringing up a Rust crosscompiler toolchain using something like the following:

    pkgs_cross = import pkgs.path {
    config = {
      allowUnsupportedSystem = true;
    };
    crossSystem = pkgs.lib.systems.examples.arm-embedded // {
      rustc = {
        config = "thumbv7em-none-eabihf";
      };
    };
  };

But this results in both compiling a lot of stuff that’s not necessary when building from my nix shell and the following error when building the default derivation:

error: linking with `/nix/store/qscyzi80rf5csi2dpgzid0aas1xsmxqy-arm-none-eabi-gcc-wrapper-13.3.0/bin/arm-none-eabi-cc` failed: exit status: 1
  |
  = note:  "/nix/store/qscyzi80rf5csi2dpgzid0aas1xsmxqy-arm-none-eabi-gcc-wrapper-13.3.0/bin/arm-none-eabi-cc" "-flavor" "gnu" "/build/rustcHzD5Ch/symbols.o" "<1 object files omitt
ed>" "--as-needed" "-Bstatic" "/build/rustcHzD5Ch/{libcortex_m-9adfbf1eb21637ce.rlib}.rlib" "/nix/store/jgv8gan1jlc4gbawav7pqyw4wfs7nvxa-rust-std-1.87.0-thumbv7em-none-eabihf/lib/r
ustlib/thumbv7em-none-eabihf/lib/{libcompiler_builtins-864ef9e5a5c23cb6.rlib}.rlib" "-L" "/build/rustcHzD5Ch/raw-dylibs" "-Bdynamic" "--eh-frame-hdr" "-z" "noexecstack" "-L" "/buil
d/firmware/target/thumbv7em-none-eabihf/release/build/cortex-m-00d14484f5f0176a/out" "-L" "/build/firmware/target/thumbv7em-none-eabihf/release/build/cortex-m-rt-214b43e34ea7341a/o
ut" "-L" "/build/firmware/target/thumbv7em-none-eabihf/release/build/defmt-64480040370a606c/out" "-L" "/build/embedded_firmware_demo-0.0.1-vendor.tar.gz/stm32-metapac/src/chips/stm
32f303vc" "-o" "/build/firmware/target/thumbv7em-none-eabihf/release/deps/dfu_demo-9b69883c4a411960" "--gc-sections" "-O1" "--nmagic" "-Tlink.x" "-Tdefmt.x"
  = note: some arguments are omitted. use `--verbose` to show all linker arguments
  = note: arm-none-eabi-gcc: error: unrecognized command-line option '-flavor'
          arm-none-eabi-gcc: error: unrecognized command-line option '--as-needed'
          arm-none-eabi-gcc: error: unrecognized command-line option '--eh-frame-hdr'
          arm-none-eabi-gcc: error: unrecognized command-line option '--gc-sections'; did you mean '--data-sections'?
          arm-none-eabi-gcc: error: unrecognized command-line option '--nmagic'

I would like advise on how I could go about reliably and cleanly producing this binary for the microcontroller.

I think this just needs a:

RUSTFLAGS = [
    "-C"
    "linker=${stdenv.cc.targetPrefix}ld"
  ];

Hours of me trying things and that only took like, 2 minutes to get working.
I had to change the stdenv to pkgs_cross.stdenv but other than that, it just worked.

Thank you for your help.

1 Like

Also to answer your other question about this hack. I see you are doing cross for stm32 with embassy_rs with rust-overlay. I’ve done something quite similar in the past. Although I was cross-compiling to thumbv7m-none-eabi which is slightly different. It’s definitely very hacky as I was pretty rushed at the time. Hopefully this will be helpful (adapted from my flake.nix):

let
  rust-overlay = builtins.fetchTarball "https://github.com/oxalica/rust-overlay/archive/stable.tar.gz";

  lib = (import <nixpkgs> { }).lib;

  pkgsCross = (
    import <nixpkgs> {
      system = builtins.currentSystem;
      crossSystem = lib.systems.examples.arm-embedded // {
        rust = {
          config = "arm-none-eabi";
          rustcTarget = "thumbv7m-none-eabi";
        };

        # NOTE: This is radically different to what pkgsStatic does.
        # For whatever reason it breaks `rustcTarget` and sets it the `arm-none-eabi`,
        # which is not a valid rustc target name.
        isStatic = true;
      };

      overlays = [
        (import rust-overlay)
        (final: _prev: {
          rustToolchain = (final.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml);
        })
      ];
    }
  );

  pkgsBuild = pkgsCross.buildPackages;

  rustPlatform = pkgsCross.makeRustPlatform {
    cargo = pkgsBuild.rustToolchain;
    rustc = pkgsBuild.rustToolchain;
  };
in

pkgsCross.callPackage (
  # You can put this in a separate file.
  {
    stdenv,
    lib,
  }:
  rustPlatform.buildRustPackage {
    pname = "my-app-name";
    version = "0";
    src = lib.cleanSource ./.;
    cargoLock = {
      lockFile = ./Cargo.lock;
      allowBuiltinFetchGit = true;
    };
    RUSTFLAGS = [
      "-C"
      "linker=${stdenv.cc.targetPrefix}ld"
    ];
  }
) { }

rust-toolchain.toml:

[toolchain]
channel = "1.83"
components = ["llvm-tools", "rust-analyzer", "rust-src"]
targets = ["thumbv7m-none-eabi"]
1 Like

This has been pretty valuable in helping me better understand how these things are supposed to be setup. I have a lot of projects to clean up now.

With that, I almost have create2nix working except for one package that keeps failing:

       > Building src/lib.rs (ahash)
       > Running rustc --crate-name ahash src/lib.rs --out-dir target/lib -L dependency=target/deps --cap-lints allow -C opt-level=3 -C codegen-units=1 --remap-path-prefix=/build=/
 --extern cfg_if=/nix/store/k9f01brwlsi454hppaggqy1qf41kijhc-rust_cfg-if-1.0.0-lib/lib/libcfg_if-11ac80c792.rlib --extern zerocopy=/nix/store/rgpy66d4g6fpzi96ls5rd49b2l8px0dw-rust_
zerocopy-0.8.25-lib/lib/libzerocopy-3f3661357c.rlib --edition 2018 -C linker=/nix/store/mzfk1sdv6bxvdh9slvddpj734ck99idi-gcc-wrapper-13.3.0/bin/cc -C metadata=104c9b94c3 -C extra-f
ilename=-104c9b94c3 --crate-type lib -L /build/ahash-0.8.12/target/build/ahash.out --cfg folded_multiply --color always
       > error[E0433]: failed to resolve: use of unresolved module or unlinked crate `once_cell`
       >    --> src/random_state.rs:111:13
       >     |
       > 111 |         use once_cell::race::OnceBox;
       >     |             ^^^^^^^^^ use of unresolved module or unlinked crate `once_cell`
       >     |
       >     = help: you might be missing a crate named `once_cell`
       >
       > error: aborting due to 1 previous error
       >
       > For more information about this error, try `rustc --explain E0433`.
       For full logs, run 'nix-store -l /nix/store/xvb2j20j41mr3x2nm6zkqhhwkxfi8yxk-rust_ahash-0.8.12.drv'.

Looking at the ahash repo, I found this in its Cargo.toml:

[target.'cfg(not(all(target_arch = "arm", target_os = "none")))'.dependencies]
once_cell = { version = "1.18.0", default-features = false, features = ["alloc"] }

And here we find the same condition in the source code where the library is referenced.

And looking at the arguments passed to rustc, this outcome makes sense. The --target flag was never passed to it, so those conditions can’t be met. This feels like a bug in Crate2Nix so I’ll probably be opening up an issue over there when I’m off work.