Question about defining environment.systemPackages via builtins.attrValues versus direct list

I’ve been pursuing a fairly stripped down flake approach with the barest minimum amount of seemingly magical language mess that doesn’t seem to make too much sense or be directly relevant, where possible. I’ve arrived at something that looks like this structure:

flake.nix:

{
  description = "nipsy's NixOS configuration";

  inputs = {
    disko.url = "github:nix-community/disko";
    disko.inputs.nixpkgs.follows = "nixpkgs-unstable";

    home-manager-stable = {
      url = "github:nix-community/home-manager/release-23.11";
      inputs.nixpkgs.follows = "nixpkgs-stable";
    };

    home-manager-unstable = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs-unstable";
    };

    nixos-hardware.url = "github:nixos/nixos-hardware";

    nixpkgs-stable.url = "github:nixos/nixpkgs/release-23.11";
    nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable";
  };

  outputs = inputs@{ home-manager-stable, home-manager-unstable, nixos-hardware, nixpkgs-stable, nixpkgs-unstable, ... }: rec {
    nixosConfigurations = {
      ginaz = nixpkgs-unstable.lib.nixosSystem {
        pkgs = pkgs-unstable;
        system = "x86_64-linux";
        modules = [
          ./hosts/ginaz
          nixos-hardware.nixosModules.lenovo-yoga-7-14ARH7.amdgpu
          home-manager-unstable.nixosModules.home-manager {
            home-manager.users.nipsy = import ./home/nipsy/ginaz.nix;
          }
        ];
      };

      richese = nixpkgs-unstable.lib.nixosSystem {
        pkgs = pkgs-unstable;
        system = "x86_64-linux";
        modules = [
          ./hosts/richese
          home-manager-unstable.nixosModules.home-manager {
            home-manager.users.nipsy = import ./home/nipsy/richese.nix;
          }
        ];
      };
    };

    pkgs-stable = import nixpkgs-stable {
      system = "x86_64-linux";
      config.allowUnfree = true;
      overlays = [(import ./pkgs)];
    };

    pkgs-unstable = import nixpkgs-unstable {
      system = "x86_64-linux";
      config.allowUnfree = true;
      overlays = [(import ./pkgs)];
    };
  };
}

I’m trying to make every input be defined as explicitly as possible without simply assigning something directly to nixpkgs or home-manager. I’m not sure this goal even has much purpose, but it seems to make more sense in my mind by keeping everything clearly delineated.

Then, looking at one of my host configurations, you’ll see how things sort of evolve from there:

% cat hosts/ginaz/default.nix
{ config, pkgs, ... }: {
  boot = {
    initrd.kernelModules = [ "amdgpu" "zfs" ];
    kernelPackages = config.boot.zfs.package.latestCompatibleLinuxPackages;
    loader = {
      efi.canTouchEfiVariables = true;
      systemd-boot.enable = true;
      timeout = 3;
    };
    supportedFilesystems = [ "zfs" ];
    zfs.devNodes = "/dev/disk/by-label";
  };

  imports = [
    ./hardware-configuration.nix
    ../common/core
    ../common/optional/db.nix
    ../common/optional/dev.nix
    ../common/optional/games.nix
    ../common/optional/google-authenticator.nix
    ../common/optional/multimedia.nix
    ../common/optional/pipewire.nix
    ../common/optional/sdr.nix
    ../common/optional/services/openssh.nix
    ../common/optional/services/xorg.nix
    ../common/optional/sound.nix
    ../common/optional/zfs.nix
    ../common/users/nipsy
    ../common/users/root
  ];

  networking = {
    hostId = "12345678";
    hostName = "ginaz";
    networkmanager.enable = true;
    nftables.enable = true;
  };

  services.xserver.videoDrivers = [ "amdgpu" ];

  system.stateVersion = "23.11";
}

As you’d expect, the subsequently included modules are all mostly package lists with a smattering of further defined programs and services options, along with some home-manager specific settings of course.

One of the problems I ran into with all of this was when defining my package lists. I initially had been using this construct (this is from my hosts/common/optional/sdr.nix):

  environment.systemPackages = builtins.attrValues {
    inherit (pkgs)
      fldigi
      sdrconnect;
  };

Defining the list this way via the builtin function seems to be used quite frequently in most people’s repos for doing NixOS stuff. However, I ran into a problem defining things this way, which led to me using this direct list construct elsewhere instead:

  environment.systemPackages = with pkgs; [
    (pass.withExtensions (ext: with ext; [pass-otp]))
    pass
    wineWowPackages.stagingFull
  ];

because neither the embedded whatever is happening with pass extensions in that first line nor the period in the wine package are allowed when trying to use the other approach.

I’m not clear enough on the Nix language here to understand the exact difference between how these two constructs are working compared to each other. I was hoping someone here could shed some light on it. Why do those peculiar package name variations not work in the first form? Is there a way to make them work in the first form? Is there any advantage or disadvantage to using either form?

I’m clearly using a mixture of both strewn throughout these various modules, and that is also working fine seemingly. It would be nice to understand a little more about what is happening here under the covers, so to speak.

And more generally, is this just a terrible approach to trying to provision mixed stable/unstable based hosts via the same flake? It looks to be the cleanest approach I’ve seen thus far, but I’m not sure about any potential sacrifices I’ve made with this particular structure, specifically declaring home-manager in the way I’m doing it here versus the way I’ve seen it more commonly elsewhere (and had been using myself previously) where you have something like:

homeConfigurations = {
  "nipsy@ginaz" = lib.homeManagerConfiguration ...

Lot to chew on there. Sorry about that. I’ve tried to wrap my head around as much of this as I can. The skill cliff is very real!

Then don’t worry about it. Both are fine to use; it’s a matter of taste.

Though if you do want to use the attrValues pattern, I’d urge you to learn what that function does and how to work with it.

This snippet

is equivalent to/syntactic sugar for

  environment.systemPackages = builtins.attrValues {
    fldigi = pkgs.fldigi;
    sdrconnect = pkgs.sdrconnect;
  };

All builtins.attrValues does is to turn all the values of this attrset into a list while discarding the keys. The expression is entirely equivalent to:

  environment.systemPackages = [
    pkgs.fldigi
    pkgs.sdrconnect
  ];

which could also be written like this:

  environment.systemPackages = with pkgs; [
    fldigi
    sdrconnect
  ];

It’s the same data, just different ways of declaring it.

As an example to illustrate how to work with the attrValues pattern, let’s say you wanted to merge your two snippets using the attrValues pattern:

  environment.systemPackages = builtins.attrValues {
    inherit (pkgs)
      fldigi
      pass # <- You probably don't want this
      sdrconnect;

    # Key names are irrelevant here; chose at will
    customPass = pass.withExtensions (ext: with ext; [pass-otp]);
    myWine = wineWowPackages.stagingFull;
  };

Though I think your other snippet is flawed because it adds two versions of pass: Once with customisations and once without.

I can’t help you with flakes but do be aware that using GUI apps from different revisions of Nixpkgs is not guaranteed to work because drivers are impure. Expect breakages.

Related

3 Likes

Okay, that makes so much more sense. I was reading the manual about attrValues where it says “Return the values of the attributes in the set” and I knew that it was talking about returning the value side of the name/value pair of an attribute set, but I had no idea what form it was coaxing the provided input into to make that make any sense really. Interesting that the name side can be completely arbitrary.

I actually side stepped the pass issue entirely by specifying it via home-manager as such:

  programs.password-store = {
    enable = true;
    package =  pkgs.pass.withExtensions (exts: with exts; [
      pass-otp
    ]);
    settings = {
      PASSWORD_STORE_DIR = "${config.home.homeDirectory}/.password-store";
    };
  };

But I was still ultimately curious to know how and why it wasn’t working one way versus the other, and your rundown pretty much cleared all that up entirely @Atemu, so thanks!

I was asking about the two different approaches more to understand if there was any performance impact of one versus the other. I had seen mentions previously of using “with” too much potentially causing issues. But again, with not understanding how the language works fully, I wasn’t clear on when or how using “with” might be an issue, which is why I was asking about all of this and pros/cons to each approach potentially.

As for mixing packages, the intent of this layout was not to mix packages between stable and unstable on the same system necessarily. It was only to allow for defining systems as being purely from a stable or unstable source initially within the same flake. I get that mixing from both in the same system is a potential massive cause of breakage and unexpected behavior.

All of this sort of gets me to another question I’ve had for a bit now, which is what are the pros/cons of specifying packages via a particular user’s home.packages versus specifying at the system level in environment.systemPackages? I assume it doesn’t matter one way or the other too much since it all ends up in the store regardless. I assume it’s mostly just preference more than anything and whether you want all commands available everywhere in the path for any user or only want them available under specific user names?

Thanks again for the solid examples to explain how the builtins.attrValues approach actually works.

I’m not aware of any performance issues but with has the potential to introduce name conflicts and reduce clarity of any value’s origin; especially when used in a widely-scoped manner such as globally for an entire file.

I personally think that using it in tightly bound scopes such as a single simple binding like foo = with bar; [ ... ]; is fine but, as I said, it’s a matter of taste.

That is pretty much what flakes are intended to do.

That’s precisely it. System-wide environment vs. user-specific environment.