`extraArgs` vs overlay

I want to use unstable consul in stable nixos, so at first i tried passing an unstable copy of nixpkgs down to my modules via extraArgs:`

{ sources ? import ./nix/sources.nix,
  system ? builtins.currentSystem,
  extraConfiguration ? {},
}:
let
  pkgs-unstable = import sources.nixpkgs-unstable { inherit system; config = {}; };

  eval = import (sources.nixpkgs + "/nixos/lib/eval-config.nix") {
    inherit system;
    modules = [
       ...
    ];

    extraModules = [ extraConfiguration ];
    extraArgs = { inherit pkgs-unstable; };
  };
in
  eval.config.system.build.customOutput

And in my module, I’d do services.consul.package = pkgs-unstable.consul.

Then I thought this isn’t very clean, as I’m passing a whole additional copy of nixpkgs down to the module, and changed to using an overlay instead:

{ sources ? import ./nix/sources.nix,
  system ? builtins.currentSystem,
  extraConfiguration ? {},
}:
let
  pkgs =
    let
      pkgs-unstable = import sources.nixpkgs-unstable { inherit system; config = {}; };
      unstable-hashicorp-overlay = final: prev: {
        unstable-consul = pkgs-unstable.consul;
        unstable-nomad = pkgs-unstable.nomad;
        unstable-vault-bin = pkgs-unstable.vault-bin;
      };
    in
    import sources.nixpkgs {
      inherit system;
      config = {};
      overlays = [ unstable-hashicorp-overlay ];
    };

  eval = import (pkgs.path + "/nixos/lib/eval-config.nix") {
    inherit system pkgs;
    modules = [
       ...
    ];
    extraModules = [ extraConfiguration ];
  };
in
  eval.config.system.build.customOutput

And in my module, I do services.consul.package = pkgs.unstable-consul

The overlay approach seems better, but what confuses me is that this caused a rebuild, whereas it theoretically shouldn’t since we’re talking about the same consul.

Is there something going on i’m not understanding? Are there any side-effects I’m not seeing when passing a second copy of nixpkgs as extraArgs?

Main thing would be closure bloat. But consul is a go application, so it’s runtime dependencies should just be the binary.

Just to check that I understand this correctly:

In both cases, the nixos system derivation that the above default.nix evaluates to, has the unstable nixpkgs source as part of its closure.

In the first case, because it’s passed as extraArgs. In the second, because of inherit pkgs.

So in which case is there closure bloat? Shouldn’t the closure be identical?

Yes, it’s identical.

There is also a third option to pass it around: passing it as a module option. By keeping everything as a NixOS module, it simplifies the reasoning. Everything that comes as a module is documented and overridable.

{ sources ? import ./nix/sources.nix,
  system ? builtins.currentSystem,
  extraConfiguration ? {},
}:
let
  pkgs-unstable = import sources.nixpkgs-unstable { inherit system; config = {}; };

  pkgsType = mkOptionType {
    name = "nixpkgs";
    description = "An evaluation of Nixpkgs; the top level attribute set of packages";
    check = builtins.isAttrs;
  };

  pkgs-unstable-module = { lib, ... }: {
    options.nixpkgs.pkgs-unstable = lib.mkOption {
      type = pkgsType;
    };
    config.nixpkgs.pkgs-unstable = lib.mkDefault pkgs-unstable;
  };

  eval = import (sources.nixpkgs + "/nixos/lib/eval-config.nix") {
    inherit system;
    modules = [
       pkgs-unstable-module,
       ...
    ];

    extraModules = [ extraConfiguration ];
  };
in
  eval.config.system.build.customOutput

But then why does building with the two approaches result in different build outputs?

Is there a way to pinpoint the node in the closure graph where the two closures diverge, for a pretty large graph (i.e. not visually)?

You can use a tool like nix-diff to inspect the difference between two .drv files but it won’t tell you what caused the difference to happen.

If you have your instantiated NixOS config, it’s possible to nix-instantiate -A config.nixpkgs.pkgs.consul and try to poke at the differences like that. Or extract the different instances of nixpkgs in a separate file and see if there is any diff there.

From my understanding, there shouldn’t be any diff between nixpkgs-unstable.consul and nixpkgs.unstable-consul.

This is the output of nix-diff --character-oriented on the two output files.

I see for example that in the one using the unstable overlay, dbus is compiled with X support (the --without-x configure flag is removed).

EDIT: for the record, pkgs is at 0f85665118d850aae5164d385d24783d0b16cf1b (nixos-21.11), pkgs-unstable at 73ad5f9e147c0d2a2061f1d4bd91e05078dc0b58 (nixos-unstable).

The problem seems to be the inherit pkgs in the ovelay approach. It is the reason why in that approach, I am getting dbus from unstable (and this commit - which is on nixos-unstable but not on nixos-21.11 - is where the change in the dbus package happened).

I found this out by commenting out the services.consul.package = pkgs.unstable-consul sections, so that the overlay, although applied, isn’t used (did the same for all packages the overlay provided). Depending on whether the inherit pkgs is there, dbus is affected, i.e. differs between the two approaches.

So, for my usecase, the two approaches are not equivalent, and passing a whole copy of nixpkgs via extraArgs (urgh!) is the correct approach.

Could you fix that by using pkgs = pkgs-unstable?

Edit: Probably not, since that would modify all packages? I’m curious why this mixup happens.

Instead of doing inherit pkgs;, I now configure it as a module:

  eval = import (pkgs.path + "/nixos/lib/eval-config.nix") {
    inherit system;
    modules = [
      # ...
      ({ ... }: {
        nixpkgs.pkgs = pkgs;
      })
    ];
    extraModules = [ extraConfiguration ];
  };

and this gets rid of all weird side effects!

So it seems like this comment is onto something.

1 Like