Skip to content

Conversation

@lucperkins
Copy link

@lucperkins lucperkins commented Aug 14, 2025

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.

@edolstra
Copy link
Member

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.

@lucperkins
Copy link
Author

@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
Copy link
Author

lucperkins commented Oct 18, 2025

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

@llakala
Copy link

llakala commented Oct 19, 2025

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.

@lucperkins
Copy link
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants