From 30cb9399cc7e42e31bc2db104a49ac9d5e0544a7 Mon Sep 17 00:00:00 2001 From: Elec3137 Date: Tue, 10 Feb 2026 11:49:28 -0800 Subject: [PATCH 1/8] nixos-rebuild-ng: add --diff argument This displays the package updates, additions, and removals to the user. --- .../ni/nixos-rebuild-ng/nixos-rebuild.8.scd | 8 ++++++++ .../nixos-rebuild-ng/src/nixos_rebuild/__init__.py | 14 ++++++++++++++ .../ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py | 12 ++++++++++++ .../nixos-rebuild-ng/src/nixos_rebuild/services.py | 8 ++++++++ 4 files changed, 42 insertions(+) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd b/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd index 43b8c233e290..b2298ce10639 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd +++ b/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd @@ -308,6 +308,14 @@ It must be one of the following: option, it is possible to build non-flake NixOS configurations even if the current NixOS systems uses flakes. +*--diff* + show the diff between the system closure in /run/current-system + and the newly built system closure. + (avaliable for actions: build, boot, test, switch) + + This is similar to running: + "nix store diff-closures /run/current-system result" after build + In addition, *nixos-rebuild* accepts following options from nix commands that the tool calls: diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py index ad0f45b3971b..29e05617d60d 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py @@ -197,6 +197,12 @@ def get_parser() -> tuple[argparse.ArgumentParser, dict[str, argparse.ArgumentPa help="Selects an image variant to build from the " "config.system.build.images attribute of the given configuration", ) + main_parser.add_argument( + "--diff", + action="store_true", + help="prints out the diff between the current system " + "and the newly built one using nix store diff-closures" + ) main_parser.add_argument("action", choices=Action.values(), nargs="?") return main_parser, sub_parsers @@ -259,6 +265,14 @@ def parse_args( if args.no_build_nix: parser_warn("--no-build-nix is deprecated, we do not build nix anymore") + if args.diff and args.action not in ( + Action.SWITCH.value, + Action.BOOT.value, + Action.BUILD.value, + Action.TEST.value + ): + parser_warn(f"--diff is a no-op with '{args.action}'") + if args.action == Action.EDIT.value and (args.file or args.attr): parser.error("--file and --attr are not supported with 'edit'") diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py index d2c5ceace95c..9ac72d824ba1 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py @@ -537,6 +537,18 @@ def list_generations(profile: Profile) -> list[GenerationJson]: reverse=True, ) +def diff_closures(path_to_config: Path): + run_wrapper( + [ + "nix", + *FLAKE_FLAGS, + "store", + "diff-closures", + "/run/current-system", + path_to_config + ] + ) + def repl(build_attr: BuildAttr, nix_flags: Args | None = None) -> None: run_args = ["nix", "repl", "--file", build_attr.path] diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py index 735be1680013..95ff0bede090 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py @@ -321,6 +321,14 @@ def build_and_activate_system( grouped_nix_args=grouped_nix_args, ) + if args.diff and args.action in ( + Action.SWITCH.value, + Action.BOOT.value, + Action.BUILD.value, + Action.TEST.value + ): + nix.diff_closures(path_to_config) + _activate_system( path_to_config=path_to_config, action=action, From aaae02bd2aa4c99f6d3224ae099c7d20cac65038 Mon Sep 17 00:00:00 2001 From: Electria <117232210+Elec3137@users.noreply.github.com> Date: Wed, 11 Feb 2026 06:59:04 -0800 Subject: [PATCH 2/8] nixos-rebuild-ng: generalize diff_closures also adds support for remote hosts Co-authored-by: Thiago Kenji Okada --- .../by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py index 9ac72d824ba1..5726853dd8ba 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py @@ -537,16 +537,17 @@ def list_generations(profile: Profile) -> list[GenerationJson]: reverse=True, ) -def diff_closures(path_to_config: Path): +def diff_closures(current_config: Path, new_config: Path, target_host: Remote | None = None): run_wrapper( [ "nix", *FLAKE_FLAGS, "store", "diff-closures", - "/run/current-system", - path_to_config - ] + current_config, + new_config, + ], + remote=target_host, ) From c1e16f898e73574891eecab69537a864fbc56ac1 Mon Sep 17 00:00:00 2001 From: Electria <117232210+Elec3137@users.noreply.github.com> Date: Wed, 11 Feb 2026 07:12:27 -0800 Subject: [PATCH 3/8] nixos-rebuild-ng: adapt to general diff_closures also adds a check and warning for if the current_config is missing Co-authored-by: Thiago Kenji Okada --- .../nixos-rebuild-ng/src/nixos_rebuild/services.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py index 95ff0bede090..2a49084c6f85 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py @@ -321,13 +321,12 @@ def build_and_activate_system( grouped_nix_args=grouped_nix_args, ) - if args.diff and args.action in ( - Action.SWITCH.value, - Action.BOOT.value, - Action.BUILD.value, - Action.TEST.value - ): - nix.diff_closures(path_to_config) + current_config = Path("/run/current-system") + if args.diff: + if current_config.exists(): + nix.diff_closures(current_config=current_config, new_config=path_to_config, target_host=target_host) + else: + logger.warn(f"missing '{str(current_config)}', skipping configuration diff...") _activate_system( path_to_config=path_to_config, From 388d8d23cd7ccb08f6b365ca39d806c678281fe2 Mon Sep 17 00:00:00 2001 From: Elec3137 Date: Wed, 11 Feb 2026 07:21:30 -0800 Subject: [PATCH 4/8] nixos-rebuild-ng: align condition for no-op warning with relaxed diff_closures call --- .../ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py index 29e05617d60d..814d6c481c2f 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py @@ -266,10 +266,16 @@ def parse_args( parser_warn("--no-build-nix is deprecated, we do not build nix anymore") if args.diff and args.action not in ( + # case for calling build_and_activate_system + # except excluding DRY_BUILD and DRY_ACTIVATE, + # in which --diff is uniquely a no-op Action.SWITCH.value, Action.BOOT.value, + Action.TEST.value, Action.BUILD.value, - Action.TEST.value + Action.BUILD_IMAGE.value, + Action.BUILD_VM.value, + Action.BUILD_VM_WITH_BOOTLOADER.value, ): parser_warn(f"--diff is a no-op with '{args.action}'") From 6ac558781e71c75d65c0f19f9501eebcfbcd60ba Mon Sep 17 00:00:00 2001 From: Elec3137 Date: Wed, 11 Feb 2026 08:44:51 -0800 Subject: [PATCH 5/8] nixos-rebuild-ng: print out diffed closures --- pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py | 6 ++++++ .../ni/nixos-rebuild-ng/src/nixos_rebuild/services.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py index 5726853dd8ba..b42af896d3c1 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py @@ -1,3 +1,4 @@ +import sys import json import logging import os @@ -538,6 +539,11 @@ def list_generations(profile: Profile) -> list[GenerationJson]: ) def diff_closures(current_config: Path, new_config: Path, target_host: Remote | None = None): + print( + f"<<< {current_config}" + f">>> {new_config}", + file=sys.stderr + ) run_wrapper( [ "nix", diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py index 2a49084c6f85..ba8effa134a1 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py @@ -321,12 +321,12 @@ def build_and_activate_system( grouped_nix_args=grouped_nix_args, ) - current_config = Path("/run/current-system") + current_config = Path("/run/current-system").readlink() if args.diff: if current_config.exists(): nix.diff_closures(current_config=current_config, new_config=path_to_config, target_host=target_host) else: - logger.warn(f"missing '{str(current_config)}', skipping configuration diff...") + logger.warning(f"missing '{str(current_config)}', skipping configuration diff...") _activate_system( path_to_config=path_to_config, From e3a7e3dcf53699e8b13f7e6007f51ce736ae376e Mon Sep 17 00:00:00 2001 From: Elec3137 Date: Wed, 11 Feb 2026 09:58:19 -0800 Subject: [PATCH 6/8] nixos-rebuild-ng: fix test failures due to early readlink() --- .../by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py index ba8effa134a1..933c2f8de10e 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/services.py @@ -321,10 +321,10 @@ def build_and_activate_system( grouped_nix_args=grouped_nix_args, ) - current_config = Path("/run/current-system").readlink() + current_config = Path("/run/current-system") if args.diff: if current_config.exists(): - nix.diff_closures(current_config=current_config, new_config=path_to_config, target_host=target_host) + nix.diff_closures(current_config=current_config.readlink(), new_config=path_to_config, target_host=target_host) else: logger.warning(f"missing '{str(current_config)}', skipping configuration diff...") From 5d230871220a27b8ee7c5b18d9a285c4d0789779 Mon Sep 17 00:00:00 2001 From: Elec3137 Date: Wed, 11 Feb 2026 10:24:04 -0800 Subject: [PATCH 7/8] nixos-rebuild-ng: add unit test for diff_closures --- .../ni/nixos-rebuild-ng/src/tests/test_nix.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py index c4b37bf0c32f..bcdefee20f77 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py @@ -550,6 +550,28 @@ def test_list_generations(mock_get_generations: Mock, tmp_path: Path) -> None: ] +@patch(get_qualified_name(n.run_wrapper, n), autospec=True) +def test_diff_closures(mock_run: Mock) -> None: + + assert n.diff_closures( + Path("/run/current-system"), + Path("/nix/var/nix/profiles/system"), + None + ) == None + mock_run.assert_called_with( + [ + "nix", + "--extra-experimental-features", + "nix-command flakes", + "store", + "diff-closures", + Path("/run/current-system"), + Path("/nix/var/nix/profiles/system"), + ], + remote=None + ) + + @patch(get_qualified_name(n.run_wrapper, n), autospec=True) def test_repl(mock_run: Mock) -> None: n.repl(m.BuildAttr("", None), {"nix_flag": True}) From 5bcffa5447893666ff2cfe8daecd412e5dbac9d6 Mon Sep 17 00:00:00 2001 From: Elec3137 Date: Wed, 11 Feb 2026 11:17:31 -0800 Subject: [PATCH 8/8] nixos-rebuild-ng: fix printing print user info (diffs) into stderr, add back missing newline --- pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py | 3 ++- pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py index b42af896d3c1..2804ea94a7b2 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py @@ -540,7 +540,7 @@ def list_generations(profile: Profile) -> list[GenerationJson]: def diff_closures(current_config: Path, new_config: Path, target_host: Remote | None = None): print( - f"<<< {current_config}" + f"<<< {current_config}\n" f">>> {new_config}", file=sys.stderr ) @@ -554,6 +554,7 @@ def diff_closures(current_config: Path, new_config: Path, target_host: Remote | new_config, ], remote=target_host, + stdout=sys.stderr ) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py index bcdefee20f77..69d29422337a 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py @@ -1,3 +1,4 @@ +import sys import textwrap import uuid from pathlib import Path @@ -568,7 +569,8 @@ def test_diff_closures(mock_run: Mock) -> None: Path("/run/current-system"), Path("/nix/var/nix/profiles/system"), ], - remote=None + remote=None, + stdout=sys.stderr )