4.3 KiB
Multi-System Outputs
This note covers 03-multi-system/, which makes a flake's packages, dev shells, and checks available on multiple platforms from a single flake.nix.
1. The Problem
Flake outputs are keyed by system: packages.x86_64-linux.default, packages.aarch64-darwin.default, and so on. If you hard-code one system (as 01-devshell and 02-package do), the flake only works on that platform.
To support multiple systems, you need to generate the per-system attrset for each platform you care about.
2. The forAllSystems Pattern
The 03-multi-system example defines a local helper:
forAllSystems = f:
nixpkgs.lib.genAttrs supportedSystems (system:
f (import nixpkgs { inherit system; })
);
genAttrs takes a list of keys and a function, and returns { key1 = f key1; key2 = f key2; … }. Wrapping it so the callback receives pkgs (already imported for the right system) keeps the per-system output definitions clean:
packages = forAllSystems (pkgs: {
default = pkgs.writeShellScriptBin "hello-multi" ''
echo "hello from $(uname -s)/$(uname -m)"
'';
});
This produces:
packages.x86_64-linux.default
packages.aarch64-linux.default
packages.aarch64-darwin.default
packages.x86_64-darwin.default
No external dependencies. You control the system list explicitly.
3. The flake-utils Alternative
flake-utils is a community flake that provides eachDefaultSystem, which does the same thing with less boilerplate:
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let pkgs = import nixpkgs { inherit system; }; in {
packages.default = pkgs.hello;
});
Differences from the hand-rolled approach:
eachDefaultSystemcovers a predefined list of common systems (currentlyx86_64-linux,aarch64-linux,x86_64-darwin,aarch64-darwin). You can useeachSystemto specify your own list.- The callback returns a flat attrset (
{ packages.default = …; devShells.default = …; }), andeachDefaultSystemnests it under each system automatically. - It adds one input to
flake.lock. Usefollowsto keep its nixpkgs in sync with yours.
Both approaches are common. The hand-rolled helper is better when you want full control; flake-utils is better when you want less repetition. flake-parts is a third option for larger projects that benefit from a module system on top of flakes.
4. The checks Output
03-multi-system also demonstrates a checks output:
checks = forAllSystems (pkgs: {
runs = pkgs.runCommand "hello-multi-runs" {} ''
${self.packages.${pkgs.system}.default}/bin/hello-multi > $out
'';
});
nix flake check evaluates and builds everything under checks.<system>. If any check derivation fails to build, the command exits non-zero.
runCommand is a convenience wrapper around mkDerivation that runs a single shell snippet. It needs to produce $out (a file or directory) to succeed. Here it runs the built script and writes its output to $out, which proves the binary at least executes without error.
This is the simplest form of a flake-level test. More involved checks might run a test suite, validate config syntax, or compare outputs against expected snapshots.
5. Commands to Try
cd 03-multi-system
nix flake show # see outputs for all four systems
nix build # build default package for your system
nix run # build + run
nix flake check # build all checks (proves the script runs)
# Inspect what other systems would get (eval only, no cross-build):
nix eval .#packages.aarch64-darwin.default.name
6. When Cross-System Matters
Multi-system outputs are useful when:
- You collaborate across macOS and Linux machines.
- CI runs on a different platform than your laptop.
- You publish a flake for others to consume (they expect their system to be present).
- You want
nix flake checkto evaluate outputs for every platform, catching typos that would only surface on the other OS.
If you only ever build on one machine, a single hard-coded system string is fine. You can always add forAllSystems later without changing any output behavior on your own platform.