Pinning nixpkgs in configuration.nix using niv

I’m trying to pin nixpkgs in my system configuration with niv. The installed system is 20.09, and I’m trying to replace it with unstable. I could upgrade to unstable with nix channels and then do this, but the errors I’m encountering here suggest that both the methods I found don’t actually guarantee reproducibility as they depend on things outside the configuration folder.

So far I’ve tried:

{ config, ... }:

let
	sources = import ./nix/sources.nix;
	pkgs = import sources.nixpkgs {
		config = config.nixpkgs.config // {
			allowUnfree = true;
		};
	};
in
{
	# configuration

and

{ config, pkgs, ... }:
{

	nixpkgs.pkgs = let
		sources = import ./nix/sources.nix;
	in import sources.nixpkgs {
		config = config.nixpkgs.config // {
			allowUnfree = true;
		};
	};
	# configuration

The first method doesn’t seem to do anything. The system derivation is named /nix/store/qhpm821ppi9fixc46ywpx43mrp8fhy6s-nixos-system-illustris-thinkpad-20.09.3882.5c0e6a8c319.drv. It’s using 20.09 despite the nixpkgs provided by niv being unstable.

The second method throws this error:

# nixos-rebuild test
building Nix...
building the system configuration...
error: 'makeDBusConf' at /nix/store/4dhw8s1gz46715m3zzgklczx882k2agw-nixpkgs-src/pkgs/top-level/all-packages.nix:14102:18 called without required argument 'apparmor', at /nix/var/nix/profiles/per-user/root/channels/nixos/nixos/modules/services/system/dbus.nix:13:15
(use '--show-trace' to show detailed location information)

This method also appears to be using nixpkgs from /nix/var/nix/profiles instead of niv.

What is the correct way to make sure configuration.nix uses only the nixpkgs version provided by niv for packages and modules?

I was using somethink like this in the past:

let
  nixexprs = builtins.fetchTarball {
    url = "https://nixos.org/channels/nixos-20.09/nixexprs.tar.xz";
  };
in
{
  nixpkgs.pkgs = import "${nixexprs}" {
    inherit (config.nixpkgs) config overlays localSystem crossSystem;
  };
}

Should be easy to adapt to Niv.

This has the same issue as before.

let
  nixexprs = builtins.fetchTarball {
    url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz";
  };
in

{
  nixpkgs.pkgs = import "${nixexprs}" {
    inherit (config.nixpkgs) config overlays localSystem crossSystem;
  };
  # and so on...
# nixos-rebuild test
building Nix...
building the system configuration...
error: 'makeDBusConf' at /nix/store/sgzx9z0nshhgajjpjp23mi7fvr187g9x-source/pkgs/top-level/all-packages.nix:14093:18 called without required argument 'apparmor', at /nix/var/nix/profiles/per-user/root/channels/nixos/nixos/modules/services/system/dbus.nix:13:15
(use '--show-trace' to show detailed location information)

it still tries to use /nix/var/nix/profiles

Impossible to say what is going on without seeing the rest. Can you make a minimal example?

{ config, pkgs, ... }:

let
  nixexprs = builtins.fetchTarball {
    url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz";
  };
in

{
	nixpkgs.pkgs = import "${nixexprs}" {
		inherit (config.nixpkgs) config overlays localSystem crossSystem;
	};

	nix.autoOptimiseStore = true;

	imports = [
		# Include the results of the hardware scan.
		./hardware-configuration.nix
		./desktop-configuration.nix
		./networking-configuration.nix
	];

	nixpkgs.overlays = [
	];

	boot.kernelPackages = pkgs.linuxPackages_latest;

	# Use the systemd-boot EFI boot loader.
	boot.loader.systemd-boot.enable = true;
	boot.loader.efi.canTouchEfiVariables = true;

	networking.hostName = "thinkpad"; # Define your hostname.

	hardware.bluetooth = {
		enable = true;
		config = {
			General = {
				Enable = "Source,Sink,Media,Socket";
			};
		};
	};
	services.blueman.enable = true;

	# Set your time zone.
	time.timeZone = "Asia/Kolkata";

	networking.hostId = "dddddddd"; # needed by ZFS, not networking

	users.users = {
		illustris = {
			isNormalUser = true;
			extraGroups = [ "wheel" ];
			openssh.authorizedKeys.keyFiles = [ ./secrets/ssh_pubkeys ];
		};
		root.openssh.authorizedKeys.keyFiles = [ ./secrets/ssh_pubkeys ];
	};

	# List packages installed in system profile. To search, run:
	# $ nix search wget
	environment.systemPackages = with pkgs; [
		networkmanager
	];



	# Enable the OpenSSH daemon.
	services.openssh = {
		enable = true;
		forwardX11 = true;
	};


	# Open ports in the firewall.
	networking.firewall.allowedTCPPorts = [ 22 ];
	# 60000-3 opened for mosh
	networking.firewall.allowedUDPPorts = [ ];

	# This value determines the NixOS release from which the default
	# settings for stateful data, like file locations and database versions
	# on your system were taken. It‘s perfectly fine and recommended to leave
	# this value at the release version of the first install of this system.
	# Before changing this value read the documentation for this option
	# (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
	system.stateVersion = "20.09"; # Did you read the comment?

}

I think the easiest way to “pin” nixpkgs, is to do it from the “outside”, similar to how the “home-manager template” does it for Home-Manager.

The technique remains the same.

Create a nix shell that has a NIX_PATH set that points to the pinned nixpkgs version and run all rebuilds through this command only.

An alternative to that approach is to use flakes, which get you the pinning for “free”, at the cost of using an indev and explicitely marked as experimental system.

Indeed, I can reproduce with the very simple:

{ config, pkgs, ... }:

let
  nixexprs = builtins.fetchTarball {
    url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz";
  };
in
{
  boot.loader.grub.device = "/dev/vda";
  fileSystems."/" = {
    device = "/dev/vda1";
    fsType = "ext4";
  };
  nixpkgs.pkgs = import "${nixexprs}" {
    inherit (config.nixpkgs) config overlays localSystem crossSystem;
  };
}

To try building it, I used

nix-build '<nixpkgs/nixos>' --arg configuration ./vm.nix -A system

which is what nixos-rebuild does under the hood. I’ll investigate this further, because this definitely worked in the past. I used this for quite some time before I switched to flakes.

I found out why this used to work for me, because effectively my configuration was

let
  sources = import ./nix/sources.nix;
in
{
  environment.etc."nixpkgs".source = sources.nixpkgs;
  nix.nixPath = [
    "nixpkgs=/etc/nixpkgs"
    "nixos-config=/etc/nixos/configuration.nix"
    "/nix/var/nix/profiles/per-user/root/channels"
  ];
  nixpkgs.pkgs = import sources.nixpkgs {
    inherit (config.nixpkgs) config overlays localSystem crossSystem;
  };
}

i.e. I had pinned nixpkgs system-wide. If you choose to do this, the first rebuild has to be done using

nixos-rebuild -I "nixpkgs=$(jq -r '.nixpkgs.url' nix/sources.json)"

to use the pinned nixpkgs for the first evaluation.

I gave up and did the two stage solution suggested by @NobbZ

For anyone else that wants to do the same, here’s the shell.nix:

{
        sources ? import ./nix/sources.nix,
        pkgs ? import sources.nixpkgs {}
}:

pkgs.mkShell {
        buildInputs = with pkgs; [
                niv
        ];
        shellHook = ''
                export nixpkgs=${sources.nixpkgs.outPath}
                export NIX_PATH=nixpkgs=${sources.nixpkgs.outPath}:nixos-config=/etc/nixos/configuration.nix
        '';
}

This mostly works. I’ve added aliases to run test and switch with this shell to avoid accidentally using system nixpkgs. as per @hmenke 's suggestion I’ve also had to add

let sources = import ./nix/sources.nix; in
{
  programs.bash.interactiveShellInit = "export NIX_PATH=nixpkgs=${sources.nixpkgs.outPath}:nixos-config=/etc/nixos/configuration.nix";
  nix.nixPath = [ "nixpkgs=${sources.nixpkgs.outPath}" "nixos-config=/etc/nixos/configuration.nix" ];
}

so that nix-shell uses the same nixpkgs.