Make the trivial flake multi system
The trivial flake is currently the default that you get when you run nix flake init and thus is pretty important, but it has the drawback that the flake's outputs are single system. And so if you're on, say, a recent Mac (like me) then nix flake init gives you something not terribly useful. This PR provides a flake that's more widely useful and easily editable to exclude non-applicable systems or include systems not in this list.
Maybe we should keep the trivial flake in its current form, since this PR makes it no longer entirely trivial. But we could make the default template point to this one.
@edolstra Okay, I've done that. I don't love the idea of any of the templates being single system or suggesting that x86 Linux is somehow the "default" experience of using Nix but I'm fine with a compromise here.
@llakala Actually, I did some benchmarking of the two approaches, comparing these flakes:
import style:
{
inputs.nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0";
outputs =
{ self, ... }@inputs:
let
pkgs = import inputs.nixpkgs { system = "aarch64-darwin"; };
in
{
packages.aarch64-darwin.default = pkgs.jq;
};
}
legacyPackages style:
{
inputs.nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0";
outputs =
{ self, ... }@inputs:
let
pkgs = inputs.nixpkgs.legacyPackages.aarch64-darwin;
in
{
packages.aarch64-darwin.default = pkgs.jq;
};
}
Results:
# import
❯ hyperfine --runs 100 --prepare 'nix flake prefetch-inputs' 'nix eval --no-eval-cache .#packages.aarch64-darwin.default'
Benchmark 1: nix eval --no-eval-cache .#packages.aarch64-darwin.default
Time (mean ± σ): 325.7 ms ± 16.5 ms [User: 248.1 ms, System: 52.7 ms]
Range (min … max): 301.1 ms … 371.4 ms 100 runs
# legacyPackages
❯ hyperfine --runs 100 --prepare 'nix flake prefetch-inputs' 'nix eval --no-eval-cache .#packages.aarch64-darwin.default'
Benchmark 1: nix eval --no-eval-cache .#packages.aarch64-darwin.default
Time (mean ± σ): 436.3 ms ± 95.6 ms [User: 341.7 ms, System: 63.3 ms]
Range (min … max): 395.0 ms … 1095.4 ms 100 runs
And so import style does appear to be faster, although this is of course just one scenario.
And so import style does appear to be faster, although this is of course just one scenario.
This may be true for this benchmark, but I don't think it's measuring the right thing. The whole idea behind using nixpkgs.legacyPackages.${system} is that since Nix is maximally lazy. if two inputs access the same attribute, the second reference will be O(1). However, Nix doesn't employ any memoisation, so a function application with the same parameter will be recomputed.
Take this flake.nix as an example:
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
nix-darwin = {
url = "github:LnL7/nix-darwin/master";
inputs.nixpkgs.follows = "nixpkgs";
};
home-manager = {
url = "github:nix-community/home-manager/master";
inputs.nixpkgs.follows = "nixpkgs";
};
};
}
If both home-manager and nix-darwin are pinned to the same nixpkgs version with follows, and they both use nixpkgs.legacyPackages.${pkgs.system}, you'll only pay the performance penalty of one nixpkgs instantiation. But if they both use import nixpkgs { inherit system; }, you'll be paying the price of two instantiations, when you could just be paying for one.
@llakala I ran it again on this flake, which involves a Home Manager config inside a nix-darwin config, which is much less trivial than just evaluating a package:
{
inputs = {
nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0";
home-manager = {
url = "https://flakehub.com/f/nix-community/home-manager/0";
inputs.nixpkgs.follows = "nixpkgs";
};
nix-darwin = {
url = "https://flakehub.com/f/nix-darwin/nix-darwin/0";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{ self, ... }@inputs:
let
system = "aarch64-darwin";
pkgsImport = import inputs.nixpkgs { inherit system; };
pkgsLegacy = inputs.nixpkgs.legacyPackages.${system};
in
{
darwinConfigurations = {
withImport = inputs.nix-darwin.lib.darwinSystem {
inherit system;
modules = [
inputs.home-manager.darwinModules.home-manager
{
system.stateVersion = 6;
users.users.just-me = {
home = "/Users/just-me";
};
environment.systemPackages = with pkgsImport; [ git ];
home-manager = {
useGlobalPkgs = true;
useUserPackages = true;
users.just-me =
{ ... }:
{
home = {
packages = with pkgsImport; [
apacheKafka
postgresql
];
stateVersion = "25.05";
};
programs.zsh.enable = true;
};
};
}
];
};
withLegacy = inputs.nix-darwin.lib.darwinSystem {
inherit system;
modules = [
inputs.home-manager.darwinModules.home-manager
{
system.stateVersion = 6;
users.users.just-me = {
home = "/Users/just-me";
};
environment.systemPackages = with pkgsLegacy; [ git ];
home-manager = {
useGlobalPkgs = true;
useUserPackages = true;
users.just-me =
{ ... }:
{
home = {
packages = with pkgsLegacy; [
apacheKafka
postgresql
];
stateVersion = "25.05";
};
programs.zsh.enable = true;
};
};
}
];
};
};
};
}
The results:
❯ hyperfine \
--runs 100 \
--prepare 'nix flake prefetch-inputs' \
'nix eval --no-eval-cache .#darwinConfigurations.withImport.config.system.build.toplevel' \
'nix eval --no-eval-cache .#darwinConfigurations.withLegacy.config.system.build.toplevel'
Benchmark 1: nix eval --no-eval-cache .#darwinConfigurations.withImport.config.system.build.toplevel
Time (mean ± σ): 1.799 s ± 0.047 s [User: 1.866 s, System: 0.234 s]
Range (min … max): 1.727 s … 1.978 s 100 runs
Benchmark 2: nix eval --no-eval-cache .#darwinConfigurations.withLegacy.config.system.build.toplevel
Time (mean ± σ): 1.859 s ± 0.041 s [User: 1.949 s, System: 0.239 s]
Range (min … max): 1.797 s … 1.958 s 100 runs
Summary
nix eval --no-eval-cache .#darwinConfigurations.withImport.config.system.build.toplevel ran
1.03 ± 0.04 times faster than nix eval --no-eval-cache .#darwinConfigurations.withLegacy.config.system.build.toplevel
The import strategy is again faster. Not by a lot, of course, but given that it's more ergonomic (as you can't pass any arguments to legacyPackages), I remain unconvinced.