Skip to content

Building embedded Rust projects with Crane #954

@ldanko

Description

@ldanko

Building bare-metal/embedded Rust projects with Crane requires several
workarounds due to the unique constraints of #![no_std] and #![no_main]
targets.

1. Custom Linker Override

Problem: Crane's mkCrossToolchainEnv automatically sets
CARGO_TARGET_*_LINKER environment variable:

CARGO_TARGET_${cargoEnv}_LINKER = "${stdenv.cc}/bin/${stdenv.cc.targetPrefix}cc";

This environment variable takes precedence over .cargo/config.toml settings,
causing cargo to use the system linker (clang/gcc) instead of
embedded-specific linkers like flip-link.

Solution: Override the CARGO_TARGET_*_LINKER environment variable
directly in commonArgs:

CARGO_TARGET_THUMBV6M_NONE_EABI_LINKER = "flip-link";

2. Disable Tests

Problem: Embedded targets cannot run tests (no test harness, no std
library).

Solution: Set doCheck = false in commonArgs:

doCheck = false;

3. Custom buildDepsOnly setup

Problem: Embedded projects may require custom linker scripts (e.g., memory.x)
that must be included in both the dummy source and final build.

Solution: Use extraDummyScript to copy linker scripts and build.rs, and
filter them in source cleaning:

Problem: Crane's default dummy binary doesn't compile for some embedded projects. Embassy (https://embassy.dev) projects require additional setup like #[entry] and bind_interrupts! macro.

Solution: Provide custom dummyrs and dummyBuildrs in buildDepsOnly

cleanSrc = lib.cleanSourceWith {
  src = src;
  filter = path: type:
    (craneLib.filterCargoSources path type) ||
    (builtins.match ".*/(memory\\.x)$" path != null);
};

cargoArtifacts = craneLib.buildDepsOnly (commonArgs // {
  dummyBuildrs = "build.rs";
  dummyrs = pkgs.writeText "dummy.rs" ''
    #![no_main]
    #![no_std]

    use panic_reset as _;
    use cortex_m_rt::entry;
    use embassy_rp::bind_interrupts;

    bind_interrupts!(struct Irqs {});

    #[entry]
    fn main() -> ! {
        loop { }
    }
  '';

  extraDummyScript = ''
    cp ${src}/memory.x $out/memory.x
    cp ${src}/build.rs $out/build.rs
  '';
});

4. Complete Example

{
  cleanSrc = lib.cleanSourceWith {
    src = src;
    filter = path: type:
      (craneLib.filterCargoSources path type) ||
      (builtins.match ".*/(memory\\.x)$" path != null);
  };

  commonArgs = {
    src = cleanSrc;
    strictDeps = true;
    doCheck = false;  # Embedded targets can't run tests

    CARGO_BUILD_TARGET = "thumbv6m-none-eabi";

    # Override crane's automatic linker configuration
    CARGO_TARGET_THUMBV6M_NONE_EABI_LINKER = "flip-link";

    nativeBuildInputs = [ pkgs.flip-link ];
  };

  cargoArtifacts = craneLib.buildDepsOnly (commonArgs // {
    # Custom dummy binary for no_std/no_main projects
    dummyBuildrs = "build.rs";
    dummyrs = pkgs.writeText "dummy.rs" ''
      #![no_main]
      #![no_std]

      use panic_reset as _;
      use cortex_m_rt::entry;
      use embassy_rp::bind_interrupts;

      bind_interrupts!(struct Irqs {});

      #[entry]
      fn main() -> ! {
          loop { }
      }
    '';

    # Include linker scripts and build.rs in dummy source
    extraDummyScript = ''
      cp ${src}/memory.x $out/memory.x
      cp ${src}/build.rs $out/build.rs
    '';
  });

  firmware = craneLib.buildPackage (commonArgs // {
    inherit cargoArtifacts;
  });
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions