Building Embedded Rust Firmware with Nix

The rust section in the manual says compiling a rust crate for an embedded target should be as simple as this [1]:

import <nixpkgs> {
  crossSystem = (import <nixpkgs/lib>).systems.examples.armhf-embedded // {
    rustc.config = "thumbv7em-none-eabi";
  };
}

This seems to have been broken for a while. Building rustc with this configuration will fail:

rustc> /build/rustc-1.63.0-src/src/bootstrap/bootstrap.py:5: DeprecationWarning: The distutils package is deprecated and slated for removal in Python 3.12. Use setuptools or check PEP 632 for potential alternatives
rustc>   import distutils.version
rustc> info: using vendored source, but .cargo/config is already present.
rustc>       Reusing the current configuration file. But you may want to configure vendoring like this:
rustc> [source.crates-io]
rustc> replace-with = 'vendored-sources'
rustc> registry = 'https://example.com'
rustc> [source.vendored-sources]
rustc> directory = '/build/rustc-1.63.0-src/vendor'
rustc> Building rustbuild
rustc>     Finished dev [unoptimized] target(s) in 0.16s
rustc> thread 'main' panicked at 'building std documentation for no_std target thumbv7em-none-eabi is not supported
rustc> Set `docs = false` in the config to disable documentation.', src/bootstrap/doc.rs:435:13
rustc> note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
rustc> Build completed unsuccessfully in 0:00:00
rustc> make: *** [Makefile:13: all] Error 1

That error comes from here in rustc: https://github.com/rust-lang/rust/blob/6e4a9ab650b135ae0ff761e4a37d96c8bcaf7b3d/src/bootstrap/doc.rs#L435-L440

I added an overlay to set docs = false in config.toml as the error message indicates:

        overlays = [
          (final: prev: {
            rustc = prev.rustc.overrideAttrs (oA: {
              postConfigure = oA.postConfigure ++ ''
                substituteInPlace config.toml \
                  --replace '#docs = true' 'docs = false`
              '';
            });
          })
        ];

However that overlay does not apply to the rustPlatform.buildRustPackage I use to build the package. I have tried various permutations of overlaying rustPlatform, but I think I am heading down the wrong path with this (?). Any advice on how to disable docs for rustc is appreciated :slight_smile:

The full code is here if anyone wants to take a stab at it (nix branch): https://github.com/newAM/nucleo-wl55jc2-rs/blob/295fe8e53481519ac6a3098ef65910cb253eaada/flake.nix

@newAM, I am new enough to nix and rust that I do not have a clear answer for you. I’m not even sure how to replicate what you are trying to do.

However, I had success building a riscv toolchain by following Justin Restivo’s article Writing a “hello world” Riscv Kernel with Nix in Rust.

I made some minor changes to update flake.nix for nix 2.9. My final flake.nix is

{
  description = "Example for presentation";
  inputs = {
    rust-overlay = {
      url = "github:oxalica/rust-overlay";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    naersk = {
      url = "github:nmattia/naersk";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs = inputs@{ self, nixpkgs, rust-overlay, naersk, ... }:
    let
      pkgs = import nixpkgs {
        localSystem = "${system}";
        overlays = [ rust-overlay.overlay ];
      };
      system = "x86_64-linux";
      riscvPkgs = import nixpkgs {
        localSystem = "${system}";
        crossSystem = {
          config = "riscv64-unknown-linux-gnu";
          abi = "lp64";
        };
      };
      rust_build = pkgs.rust-bin.nightly."2022-06-28".default.override {
        targets = [ "riscv64imac-unknown-none-elf" ];
        extensions = [ "rust-src" "clippy" "cargo" "rustfmt-preview" ];
      };
      naersk_lib = naersk.lib."${system}".override {
        rustc = rust_build;
        cargo = rust_build;
      };
      sample_package = naersk_lib.buildPackage {
        pname = "example_kernel";
        root = ./.;
        cargoBuild = _orig:
          ''
            CARGO_BUILD_TARGET_DIR=$out cargo rustc --release --target="riscv64imac-unknown-none-elf" -- -Clink-arg=-Tlinker.ld'';
      };
      sample_usage = pkgs.writeScript "run_toy_kernel" ''
        #!/usr/bin/env bash
        echo ""
        echo '~~~ `C-a x` to kill qemu; `C-a h` for other options. ~~~'
        ${pkgs.qemu}/bin/qemu-system-riscv64 -kernel ${sample_package}/riscv64imac-unknown-none-elf/release/nix_example_kernel -nographic -machine sifive_u
      '';
    in {
      devShell.x86_64-linux = pkgs.mkShell {
        nativeBuildInputs = [
          pkgs.qemu
          rust_build
          riscvPkgs.buildPackages.gcc
          riscvPkgs.buildPackages.gdb
        ];
      };
      packages.riscv64-linux.kernel = sample_package;
      packages.riscv64-linux.default = sample_package;
      apps.x86_64-linux.toy_kernel = {
        type = "app";
        program = "${sample_usage}";
      };
      apps.x86_64-linux.default = self.apps.x86_64-linux.toy_kernel;
    };
}

Hopefully you’ll find enough clues there to make it work.

That does help, thanks :slight_smile:

One difference is that the oxalica rust-overlay uses pre-compiled rustc binaries, I would prefer to build rustc from source.

Much exploration later I got this to build.
Lots of this is needlessly verbose, hopefully I can find a minimal working set and feed this back to the docs.

# flake.nix
{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";

  outputs = { self, nixpkgs }:
    let
      pkgs = import nixpkgs {
        system = "x86_64-linux";
        config.allowUnsupportedSystem = true;
        overlays = [
          (final: prev: {
            rustc = prev.rustc.overrideAttrs (oA: {
              RUSTFLAGS = "-Ccodegen-units=32";
              postConfigure = oA.postConfigure + ''
                substituteInPlace config.toml \
                  --replace '#docs = true' 'docs = false'
              '';
            });
          })
        ];
      };
      thumbv7emPkgs = import nixpkgs {
        system = "x86_64-linux";
        crossSystem = nixpkgs.lib.systems.examples.arm-embedded // {
          rustc.config = "thumbv7em-none-eabi";
        };
        config.allowUnsupportedSystem = true;
      };
    in
    rec {
      packages.x86_64-linux.rustc = pkgs.rustc.override {
        stdenv = pkgs.stdenv.override {
          targetPlatform = thumbv7emPkgs.stdenv.targetPlatform;
          hostPlatform = pkgs.stdenv.hostPlatform;
          buildPlatform = pkgs.stdenv.buildPlatform;
        };
        pkgsBuildBuild = pkgs;
        pkgsBuildHost = pkgs;
        pkgsBuildTarget.targetPackages.stdenv.cc = pkgs.pkgsCross.arm-embedded.stdenv.cc;
        enableRustcDev = false;
      };
      packages.x86_64-linux.rustPlatform = thumbv7emPkgs.makeRustPlatform {
        inherit (packages.x86_64-linux) rustc;
        inherit (pkgs) cargo;
      };
      packages.x86_64-linux.default = pkgs.callPackage ./package.nix {
        inherit (packages.x86_64-linux) rustPlatform;
      };
    };
}
# package.nix
{ lib
, rustPlatform
, lld
}:

let
  cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
in
rustPlatform.buildRustPackage {
  inherit (cargoToml.package) version;
  pname = cargoToml.package.name;

  src = ./.;

  cargoLock.lockFile = ./Cargo.lock;

  nativeBuildInputs = [
    lld
  ];

  # no tests for no_std
  doCheck = false;

  meta = with lib; {
    inherit (cargoToml.package) description;
    licenses = with licenses; [ mit ];
  };
}
1 Like

Excellent. And I see you’ve begun trimming it in https://github.com/newAM/nucleo-wl55jc2-rs/tree/nix. I’ll want to revisit that when I get chance to play with some Cortex-M0 boards, soon I hope.

I opened a PR to fix rustc for everyone, making the overlay unnecessary: https://github.com/NixOS/nixpkgs/pull/190697

With that applied the flake.nix boilerplate goes away, and it works just like the manual says it will:

{
  inputs.nixpkgs.url = "github:newAM/nixpkgs/rustc-fix-embedded";

  outputs = { self, nixpkgs }:
    let
      thumbv7emPkgs = import nixpkgs {
        system = "x86_64-linux";
        crossSystem = nixpkgs.lib.systems.examples.arm-embedded // {
          rustc.config = "thumbv7em-none-eabi";
        };
        config.allowUnsupportedSystem = true;
      };
    in
    {
      packages.x86_64-linux.default = thumbv7emPkgs.callPackage ./package.nix { };
    };
}
1 Like

I’m sure that I did many things the wrong way, but I recently wrote an article on getting nix to compile rust for the ESP32C3 (riscv32). It took me a couple weeks to figure out, so I thought I’d leave a link in case it helps anyone else: https://n8henrie.com/2023/09/compiling-rust-for-the-esp32-with-nix/