Recommended style to cross-compile flake nixosSystems

I’m currently tinkering with cross-compiling NixOS from an AMD machine (“x86_64-linux”) to Raspberry Pi 5 (“aarch64-linux”) and Starfive VisionFive 2 (“riscv-linux”), and would like to know about the latest best practice to recommend in guides. I found several similar styles, but i’m not sure which one is the latest and best:

 mySystem = nixpkgs.lib.nixosSystem {
        modules = [
          {
            nixpkgs.buildPlatform = "x86_64-linux";
            nixpkgs.hostPlatform = "riscv64-linux";
          }
        ];
      };

like it seems to be recommended in the official NixOS manual cross-compile section (but unclear as it mentions other styles as well) and in Jörg Thalheims blog post.

Also recommended by @waffle8946 in Recommended style to cross-compile flake nixosSystems - #16 by waffle8946

 mySystem = nixpkgs.lib.nixosSystem {
        modules = [
          {
            nixpkgs.localSystem.config = "x86_64-unknown-linux-gnu";
            nixpkgs.crossSystem.config = "riscv64-unknown-linux-gnu";
          }
        ];
      };

that seems to be recommended in the official nix.dev cross-compile guide (although that style is shown only for pkgsCross usage)

 mySystem = nixpkgs.lib.nixosSystem {
        modules = [
          {
            nixpkgs.localSystem.system = "x86_64-linux";
            nixpkgs.crossSystem.system = "riscv64-linux";
          }
        ];
      };

that seems to be used in multiple blogs like the fantastic recently published handbook guide by Ryan Yin.

 mySystem = nixpkgs.lib.nixosSystem {
       system = "x86_64-linux"
        modules = [
          {
            nixpkgs.crossSystem.system = "riscv64-linux";
          }
        ];
      };

I have seen this style many times in blog posts.

 mySystem = nixpkgs.lib.nixosSystem {
       system = "x86_64-linux"
        modules = [
          {
            nixpkgs.crossSystem.config = "riscv64-unknown-linux-gnu";
            nixpkgs.crossSystem.system = "riscv64-linux";
          }
        ];
      };

I have sometimes also have seem both system and config having been defined.

So which one is the latest, recommended way? (Or what are the trade-offs?). I could create a PR against the official documentation as well if you can point me to the right place.

8 Likes

Shameless bump as i stumbled upon this part-hilarious blog post https://ianthehenry.com/posts/how-to-learn-nix/cross-compilation/ about the state of the Nix manual on cross-compiling and it describes the same questions and confusion i have. I would like to improve the documentation and start with this low-hanging fruit here.

6 Likes

What did you decide on is ultimately the best currently?

1 Like

I am also confused why we would encode the build platform.

That means if I wanted to do a nixos-rebuild switch on my other machine (i.e. Raspberry Pi) it would now fail?

Okay I could do:

        {
          # This is the architecture we build from (pkgs.system from above)
          nixpkgs.buildPlatform = builtins.currentSystem;
          # pkgsCross.<yourtarget>.system
          nixpkgs.hostPlatform = "aarch64-linux";
        }

but you have to pass --impure but i’m not sure otherwise how to make a nixosConfiguration that I can either build on my laptop or on my raspberry pi. I feel like I’m missin gsomething.

1 Like

I enabled this on my notebook to build the config for the Raspberry Pis:

I have two different “configs” however for x86 and aarch64, don’t know if there is a better way but it’s what I’ve been using for a while now.

That’s flake-specific. With flakes, you’re supposed to create additional outputs that have different system configurations.

I haven’t decided. I’m still waiting for some Nix experts to give recommendations. I think i remember all variants working, but it’s confusing for non-experts as every guide is slightly different.

That’s because, as with many things in Nix, there isn’t one proven best way of doing things. If you ask 4 experts, you’ll get 4 opinions on how things should be done.

You’ll have to think for yourself here.

I couldn’t get it to work. Something was failing.
I resorted back to binfmt sadly.

I also had to disable check phase for a few packages that were expensive to do in emulation.

@malteneuss
I’d like to throw in two more ways to declare cross compilation:

mySystem = nixpkgs.lib.nixosSystem {
  system = "x86_64-linux"
  modules = [
    {
      nixpkgs.buildPlatform = "x86_64-linux";
      nixpkgs.hostPlatform = nixpkgs.lib.systems.examples.armv7l-hf-multiplatform;
    }
  ];
};
mySystem = nixpkgs.legacyPackages.x86_64-linux.pkgsCross.armv7l-hf-multiplatform.nixos {
  imports = [
    ./my-module.nix
  ];
};

Thank you for sharing :slight_smile:

I wish there was a way to search for specific hydra built images and inspect the sources for all those artifacts. I am hoping this could help me overcome my current armv7l cross compilation issues.

2 Likes

Is there any kind of consensus now?

Not that i’m aware of. It would be great though for someone with more experience to explain the motivation between those different designs.

Don’t take my word as gospel since it’s been a while since I touched this topic.
Here is an implicit way to cross compile with flakes (don’t ask me why this works).
The only argument I can give for this method is that it looks clean.

As a blob of general knowledge I can add that cross-compiled derivations have a slightly different output because the compiler does slightly different things depending on instruction set. This is the reason cross-compiled derivations are impure?! and means when the target system wants to rebuild it will rebuild everything.
Instruction level emulation (binfmt) of the native compiler avoids this, at the cost of massively reduced speed.

These lines do nothing in your code, and this is not cross-compiling. Probably you’re using emulated compilation. If you want to cross compile, one way would be to do this:

diff --git a/flake.nix b/flake.nix
index e0956fe..45b2dc6 100755
--- a/flake.nix
+++ b/flake.nix
@@ -4,16 +4,10 @@
   };
   outputs = { nixpkgs, ... }:
   let
-    system = "x86_64-linux";
-    pkgs = import nixpkgs {
-      inherit system;
-    };
-    lib = nixpkgs.lib;
+   lib = nixpkgs.lib;
   in rec
   {
     nixosConfigurations.solaraspi = lib.nixosSystem {
-      system = "aarch64-linux";
-
       modules = [
         {
           imports = [
@@ -22,6 +16,10 @@
             ./configuration.nix
           ];
 
+          nixpkgs = {
+            hostPlatform = "aarch64-linux";
+            buildPlatform = "x86_64-linux";
+          };
           boot.kernelParams = lib.mkOverride 0 [ "console=ttyS1,115200" "console=tty1" ]; # Enable serial console on pins 8,10
           nix.extraOptions = ''experimental-features = nix-command flakes'';
         }
@@ -30,4 +28,4 @@
 
   solaraspi-image = nixosConfigurations.solaraspi.config.system.build.sdImage;
   };
-}
\ No newline at end of file
+}

See https://search.nixos.org/options?channel=24.11&show=nixpkgs.buildPlatform&from=0&size=50&sort=relevance&type=packages&query=buildplatform

Other cross-compilation methods mentioned above are somewhat outdated.

2 Likes

@endimi @malteneuss I suggest sticking with method 1, as passing system to lib.nixosSystem is deprecated.

4 Likes

You’re right. However, the flake does produce a working image without enabling emulation in the host config. What is the default behavior here?

Not possible unless you built it already.

IIUC there is another approach where your NixOS configuration knows nothing about the cross-compilation and instead you deal with it when you instantiate nixpkgs. For example:

In this commit I figured out a way to use deploy-rs from an x86 host for deploying to a Raspberry Pi. The key magic here is:

      pkgs = import nixpkgs { system = "x86_64-linux"; };
      # This is a rather bananas dance to create a cross-compiled deploy-rs.
      # There is a binary in there that needs to be build for the target
      # architecture, so this sets up a version of nixpkgs that's cross-compiled
      # to aarch64. Then deploy-rs provides an overlay that will build the
      # pacakge via this cross compilation. This does still require building
      # rustc though lmao.
      # Note this ISN'T used for the actual NixOS system, for that it's just
      # built "natively" so you'll need the binfmt_misc magic to make it work.
      # That is fine in practice because you just get everything from the binary
      # cache.
      # https://nixos.wiki/wiki/Cross_Compiling has a section about "lazy
      # cross-compiling" that seems like a more elegant way to achieve something
      # kinda similar to this.
      pkgsCross = import nixpkgs {
        localSystem = "x86_64-linux";
        crossSystem = { config = "aarch64-unknown-linux-gnu"; };
        overlays = [ deploy-rs.overlays.default ];
      };

Then wherever you need a cross-compiled package you refer to it from pkgsCross instead of pkgs.

(Note this example is kinda confusing because pkgs refers to an x86 nixpkgs, pkgsCross refers to an aarch64-built-on-x86 nixpkgs, and then I think there is a third version of nixpkgs that is being used implicitly as a result of me passing system = "aarch64-linux" to nixpkgs.lib.nixosSystem, and that’s the native aarch64 one. But in my current configuration I never need to refer to that explicitly).

This is obviously also pretty janky but it seem like it might be an avenue worth exploring if you want fine-grained control. For example I use it as a hack to cross-compile stuff that I need to compile, and then use the natively-compiled versions of packages that are in the binary cache. So I don’t have to cross-compile the whole of NixOS.