Flakes inputs with custom nixpkgs

The pattern of passing inputs to all modules via specialArgs seems useful, but I’m not sure the best pattern to “configure” alternate nixpkgs inputs. Unlike other flake inputs, it seems nixpkgs requires some evaluation (import) before it can be used (e.g. for its packages). How can this be configured?

inputs = {
  nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
  nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
  nixpkgs-legacy.url = "github:NixOS/nixpkgs/a3a3dda3bacf61e8a39258a0ed9c924eeca8e293";
};

outputs = inputs@{self, nixpkgs, nixpkgs-unstable, nixpkgs-legacy}:
  let
    # configure nixpkgs-unstable and nixpkgs-legacy
    unstable = import nixpkgs-unstable {
      system = "x86_64-linux";
      config = {
        allowUnfree = true;
      };
    };
    legacy = import nixpkgs-legacy {
      system = "x86_64-linux";
      config = {
        allowUnfree = true;
      };
    };
    # weird hack to try to merge these into inputs
    inputs = {
      inherit unstable legacy;
    };
  in
  nixosConfigurations.laptop = nixpkgs.lib.nixosSystem {
    system = "x86_64-linux";
    specialArgs = { inherit inputs; };
    modules = [
      ./configuration.nix
    ];
  };

Within configuration.nix,

{ config, lib, inputs, ...}
  environment.systemPackages = with pkgs; [
    inputs.unstable.obsidian
  ];

The approach above works, but I’m not sure I understand why. And its cumbersome / unreadable. Trying other approaches in the let block cause infinite recursion

inputs.unstable = import nixpkgs-unstable {
  system = "x86_64-linux";
  config = {
    allowUnfree = true;
  };
};

Are there better ways? What’s going on here?

In the second example (the infinite recursion one) did you try using inputs = rec {…}?

I didn’t use such a setup, but I consider this doable.

Actually, you do not have to put them as inputs. Inputs’ use is to let you pin the versions, they are not related to the input of the configuration.nix whatsoever.

A more elegant way to do this is just pass them as specialArgs.

# part of flake.nix
{
  inputs = {...};
  let
    unstable = import nixpkgs-unstable {...};
    legacy = import nixpkgs-legacy {...};
  in
    nixosConfigurations.laptop = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      specialArgs = { inherit inputs, unstable, legacy; };
      modules = [
        ./configuration.nix
      ];
}
# configuration.nix
{self, inputs, pkgs, unstable, legacy, ...} : {
  environment.systemPackages = [ unstable.foo legacy.bar pkgs.baz ];
}

I crafted this without much testing, ping me if this doesn’t work or so.

Thanks for your suggestion. I’ll try with inputs = rec {} tonight.

I’d ideally like to be able to put all these “pinned things” into the inputs attribute set, since it is convenient to only have to add one argument to the top of configuration.nix and or other modules.

I had thought it would be simple to add unstable (or any arbitrary key/value) into the inputs attrset with inputs.unstable = import nixpkgs-unstable {...} or inputs = inputs // { unstable = unstable}; and I’m confused.

Actually I wouldn’t quite recommend using the first suggestion. You might look for anything but the first piece of advice…

Inputs for flakes should be ALL its impure part and it is not designed to be used to pass arguments. You should pass arguments directly with the specialArgs attribute.

The design philosophy of nix itself is to be as pure as possible. The configuration.nix is a function which handles channels as one of its inputs(pkgs) and builds a derivation(system). (To be exact, it configures a system to be built.)

When you use flakes, you specify and pin the version of ALL your impurities(get rid of channels, if you have specified the nixpkgs as one of the inputs). This is the use of the inputs part for every flake. Inputs is not used to feed arguments to the configuration.nix. You should feed it with specialArgs instead.

I think it’s just a difference between referring to the these values as some-dependency vs inputs.some-dependency in configuration.nix.

If someone wants to group them in an attribute set for convenience, that seems like a valid aim. I can’t see how this relates to introducing impurity.

This is not related to impurities. inputs is used for flakes to pin their versions, they can be fed to the flake output function. They are not designed for the function configuration.nix.

We do use the attribute inputs in configuration.nix sometimes. But usually the inputs is not “tweaked”, and all of inputs are pinned with flake.lock.

Your use is feasible, but I won’t say that it is typical, because merging inputs and feeding them all into configuration.nix is rather cumbersome and can be misleading.

I would typically just pass the inputs as-is, they’d be all the pinned dependencies needed by the flake. But for nixpkgs those need to be imported and configured (e.g allowUnfree) in order to be usable to me, so that’s how I’ve arrived here.

I can’t assign them to the same name since that would be a mutation (not allowed). So we name it something like unstable and want to pass that along as input (nixpkgs-unstable has served its purpose).

# can't reuse nixpkgs-unstable name here, needs to be named something else like unstable
nixpkgs-unstable = import nixpkgs-unstable {
  system = "x86_64-linux";
  config = {
    allowUnfree = true;
  };
};

To add unstable and legacy to inputs, I have a better picture of what’s going on. This isn’t valid because we can’t mutate an attribute set, only make a new attribute set.

# not valid Nix
inputs.unstable = ...

This produces an infinite recursion I guess just in how Nix implements this merge. It works in toy examples in the repl, but not here.

inputs = inputs // { inherit unstable legacy; };

So instead we need to construct a “new” attribute set. As far as I can tell there is no way to inherit all the attributes of (inputs) without listing them out. ... isn’t valid syntax and Nix has no splat operators.

# not valid Nix
inputs = {
  inherit (inputs) ...
  inherit unstable legacy
}

So what I’ve arrived at is just define inputs to be exactly the attributes I’d like, verbatim.

inputs = {
  inherit home-manager sops-nix unstable legacy
}

So in configuration.nix, we can refer to inputs.home-manager, inputs.unstable.some-package, etc. I feel like it follows the spirit of passing dependencies, its just that nixpkgs requires import+configuration and there’s doesn’t seem to be a great place to put that otherwise.

I’ve done some experiments, here’s my flake:

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    nix-master.url = "github:nixos/nixpkgs";
    telega-overlay.url = "github:ipvych/telega-overlay";
    telega-overlay.inputs.nixpkgs.follows = "nixpkgs";
    emacs-overlay.url = "github:nix-community/emacs-overlay";
    home-manager.url = "github:nix-community/home-manager";
    home-manager.inputs.nixpkgs.follows = "nixpkgs";
    blender-bin.url = "https://flakehub.com/f/edolstra/blender-bin/1.0.5.tar.gz";
 };
  
  outputs = { self, nixpkgs, home-manager, nix-master, ... }@attrs:{
    nixosConfigurations.Its-erina-here = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      specialArgs = attrs // { master = (import nix-master {config={allowUnfree = true;};}); };
      modules = [
        ./configuration.nix 
        home-manager.nixosModules.home-manager
        {
          home-manager.useGlobalPkgs = true;
          home-manager.useUserPackages = true;
        }
      ];
    };
  };
}

The matching config might look like this:

{ self, pkgs, master, ... } :
{
  environment.systemPackages = [ master.sageWithDoc neovim ];
}

Actually it can also be like so:

{self, pkgs, nix-master, ...} :
let
  master = import nix-master {config={allowUnfree=true;};};
in
{
  ......
}

I really discourage hacking inputs, as it is of special meaning to a flake.