I have a small improvement that I found while wrangling nixos-rebuild-ng ignoring the NIX_PATH I gave it.
In my rebuild script I have the rebuild step with these 2 lines (written in fish):
set -l nixpkgs_path (nix-instantiate --json --eval npins/default.nix -A nixpkgs.outPath | jq -r .)
sudo nixos-rebuild switch -I nixos-config=/home/$USER/nixos-config/configuration.nix -I nixpkgs=$nixpkgs_path 2>&1 | tee rebuild.log
and in my config I have a pinning.nix module
{
config,
pkgs,
...
}: let
sources = import ./npins;
in {
# kill channels
nix = {
channel.enable = false;
nixPath = [
"nixpkgs=/etc/nixos/nixpkgs"
"nixos-config=/home/USERNAME/nixos-config/configuration.nix"
];
};
environment.etc = {
"nixos/nixpkgs".source = builtins.storePath pkgs.path;
};
# pinning
nixpkgs = {
config = {
packageOverrides = pkgs: {
unstable = import sources.nixpkgs-unstable {config = config.nixpkgs.config;};
vpncpin = import sources.nixpkgs-vpncpin {config = config.nixpkgs.config;};
};
};
};
}
overlays are useful for readability when I use them elsewhere.
For anyone reading the 2 blog posts mentioned originally, this and the discussion here It basically boils down to:
No flake features, eval for nixos-rebuild is done using Jade’s one liner with nix-instantiate
pin using npins
give nixos-rebuild the latest nixpkgs by passing -I which bypasses all other includes
make <nixpkgs> be consistent across the system (see piegames) blog
I also use overrides for the imported pins, but feel free to integrate however you like
see my full config here
EDIT:
save yourself the next headache and point the command-not-found to the correct database
programs.command-not-found.dbPath = "/etc/nixos/nixpkgs/programs.sqlite";
now, why is this hardcoded to point to the root nixos channel is beyond me, but yeah that fixes the
DBI connect('dbname=/nix/var/nix/profiles/per-user/root/channels/nixos/programs.sqlite','',...) failed: unable to open database file at /run/current-system/sw/bin/command-not-found line 13.
cannot open database `/nix/var/nix/profiles/per-user/root/channels/nixos/programs.sqlite' at /run/current-system/sw/bin/command-not-found line 13.
there seems to be some activity on the module in nixpkgs (including explicit fish integration :DD), but I don’t care enough to override it so I’ll just wait until 25.11. This works well enough
5 Likes
rkjnsn
April 25, 2026, 10:45pm
22
For overlays, is there a reason not to
Create an overlayed.nix that evaluates to the result of import sources.nixpkgs { overlays = …; }
Point the nixpkgs path to that?
Yes, because it breaks stuff like `lib = import <nixpkgs/lib> {}` (not sure if I got the details right about how to import it, it’s been a while). But generally `` does not point to an expression, but to a path, and while most uses simply end up importing it (where your solution would work fine), this is not always the case
rkjnsn
April 26, 2026, 8:03pm
24
Ah, that makes sense. Maybe something like this, then, if I really want to avoid patching:
let
pins = import ./npins;
pkgs = import pins.nixos {};
files = builtins.mapAttrs (name: _: "${pins.nixos}/${name}") (builtins.readDir pins.nixos);
wrapper = pkgs.writeText "default.nix" ''
{ ... } @ args:
import ./default.orig.nix (args // {
overlays = [ (final: prev: { path = ./.; }) ]
++ (import ${./overlays.nix})
++ (args.overlays or []);
})
'';
links = pkgs.lib.mapAttrsToList (name: path: {inherit name path;})
(files // { "default.nix" = wrapper; "default.orig.nix" = files."default.nix"; });
nixosWrapped = pkgs.linkFarm "nixos-wrapped" links;
in
{ nixos = nixosWrapped; nixpkgs = nixosWrapped; }
rkjnsn
April 26, 2026, 8:57pm
25
Or this to avoid a chance of the overlays getting lost due to symlink expansion, though I’d not sure it’s too much different than your patch at that point:
let
pins = import ./npins;
pkgs = import pins.nixos {};
wrapper = pkgs.writeText "default.nix" ''
{ ... } @ args:
import ./default.orig.nix (args // {
overlays = (import ${./overlays.nix}) ++ (args.overlays or []);
})
'';
nixosWrapped = pkgs.runCommand "nixos-wrapped" {} ''
cp -a ${pins.nixos} $out
chmod u+w $out
mv $out/default.nix $out/default.orig.nix
cp ${wrapper} $out/default.nix
'';
in
{ nixos = nixosWrapped; nixpkgs = nixosWrapped; }
rkjnsn
April 27, 2026, 1:52am
26
Rather than using your direnv approach, I ended up writing this script to execute a command with a set of sources loaded into NIX_PATH. That way I can do
with-nix-sources /etc/nixos/sources.nix -- nixos-rebuild build
to test building with updated pins (sources.nix is the wrapper from my previous post), and then do
with-nix-sources /etc/nixos/sources.nix ~/.config/home-manager/npins -- home-manager build
to test build my Home Manager config with the updated npins before switching.
set -euo pipefail
usage() {
echo "Usage: $0 FILE [FILE...] -- COMMAND [ARGS...]" >&2
exit "${1:-1}"
}
files=()
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help) usage 0 ;;
--) shift; break ;;
*) files+=("$1"); shift ;;
esac
done
[[ ${#files[@]} -gt 0 && $# -gt 0 ]] || usage
args=()
count=${#files[@]}
width=${#count}
for i in "${!files[@]}"; do
args+=(--argstr "$(printf "file%0${width}d" "$i")" "$(realpath -- "${files[$i]}")")
done
expr='{ ... }@args:
let
pkgs = import <nixpkgs> {};
sources = builtins.concatLists
(pkgs.lib.mapAttrsToList
(_: file: pkgs.lib.mapAttrsToList
(name: path: "${name}=${path}")
(import file))
args);
in
pkgs.writeText "NIX_PATH" (pkgs.lib.concatStringsSep ":" sources)'
# Using nix-build to realize a file containing the NIX_PATH value
# ensures that the constituent paths are also realized.
nix_path=$(< "$(nix-build --no-out-link -E "$expr" "${args[@]}")")
if [[ -n $nix_path ]]; then
export NIX_PATH="$nix_path${NIX_PATH:+:$NIX_PATH}"
fi
exec "$@"