templates icon indicating copy to clipboard operation
templates copied to clipboard

Make the trivial flake multi system

Open lucperkins opened this issue 5 months ago • 2 comments

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.

lucperkins avatar Aug 14 '25 16:08 lucperkins

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 avatar Aug 25 '25 18:08 edolstra

@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.

lucperkins avatar Aug 26 '25 13:08 lucperkins

@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.

lucperkins avatar Oct 18 '25 23:10 lucperkins

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 avatar Oct 19 '25 01:10 llakala

@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.

lucperkins avatar Oct 19 '25 06:10 lucperkins