From 1b14797343497de3aaf4ed104a2be5fe63d891b3 Mon Sep 17 00:00:00 2001 From: Thiago Kenji Okada Date: Wed, 5 Nov 2025 15:51:02 +0000 Subject: [PATCH 01/17] nixos-rebuild-ng: also consider --no-build-output in Flakes --- .../src/nixos_rebuild/__init__.py | 2 + .../src/nixos_rebuild/services.py | 10 ++-- .../nixos-rebuild-ng/src/tests/test_main.py | 47 ++++++++++++++++++- 3 files changed, 52 insertions(+), 7 deletions(-) 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 6d7ad01baebd..539816cabeba 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 @@ -275,6 +275,8 @@ def execute(argv: list[str]) -> None: flake_common_flags = common_flags | vars(args_groups["flake_common_flags"]) flake_build_flags = common_build_flags | flake_common_flags copy_flags = common_flags | vars(args_groups["copy_flags"]) + if build_flags.get("no_build_output"): + flake_build_flags = flake_build_flags | {"no_link": True} if args.upgrade or args.upgrade_all: nix.upgrade_channels(args.upgrade_all, args.sudo) 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 2d46c417580d..4034c9c8378a 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 @@ -163,16 +163,16 @@ def _build_system( flake, build_host, eval_flags=flake_common_flags, - flake_build_flags=flake_build_flags - | {"no_link": no_link, "dry_run": dry_run}, + flake_build_flags={"no_link": no_link, "dry_run": dry_run} + | flake_build_flags, copy_flags=copy_flags, ) case (None, Flake(_)): path_to_config = nix.build_flake( attr, flake, - flake_build_flags=flake_build_flags - | {"no_link": no_link, "dry_run": dry_run}, + flake_build_flags={"no_link": no_link, "dry_run": dry_run} + | flake_build_flags, ) case (Remote(_), None): path_to_config = nix.build_remote( @@ -187,7 +187,7 @@ def _build_system( path_to_config = nix.build( attr, build_attr, - build_flags=build_flags | {"no_out_link": no_link, "dry_run": dry_run}, + build_flags={"no_out_link": no_link, "dry_run": dry_run} | build_flags, ) # In dry_run mode there is nothing to copy diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py index cdd4e65b17e6..73187861c780 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py @@ -178,8 +178,8 @@ def test_execute_nix_boot(mock_run: Mock, tmp_path: Path) -> None: "", "--attr", "config.system.build.toplevel", - "-vvv", "--no-out-link", + "-vvv", ], check=True, stdout=PIPE, @@ -222,6 +222,49 @@ def test_execute_nix_boot(mock_run: Mock, tmp_path: Path) -> None: ) +# https://github.com/NixOS/nixpkgs/issues/437872 +@patch.dict(os.environ, {}, clear=True) +@patch("subprocess.run", autospec=True) +def test_execute_nix_build(mock_run: Mock, tmp_path: Path) -> None: + config_path = tmp_path / "test" + config_path.touch() + + def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]: + return CompletedProcess([], 0, str(config_path)) + + mock_run.side_effect = run_side_effect + + nr.execute( + [ + "nixos-rebuild", + "build", + "--flake", + "/path/to/config#hostname", + "--no-build-output", + ] + ) + + assert mock_run.call_count == 1 + mock_run.assert_has_calls( + [ + call( + [ + "nix", + "--extra-experimental-features", + "nix-command flakes", + "build", + "--print-out-paths", + '/path/to/config#nixosConfigurations."hostname".config.system.build.toplevel', + "--no-link", + ], + check=True, + stdout=PIPE, + **DEFAULT_RUN_KWARGS, + ), + ] + ) + + @patch.dict(os.environ, {}, clear=True) @patch("subprocess.run", autospec=True) def test_execute_nix_build_vm(mock_run: Mock, tmp_path: Path) -> None: @@ -392,11 +435,11 @@ def test_execute_nix_switch_flake(mock_run: Mock, tmp_path: Path) -> None: "build", "--print-out-paths", '/path/to/config#nixosConfigurations."hostname".config.system.build.toplevel', + "--no-link", "-v", "--option", "narinfo-cache-negative-ttl", "1200", - "--no-link", ], check=True, stdout=PIPE, From b2bdf0b379e0de7c96999c197170238ef1796c75 Mon Sep 17 00:00:00 2001 From: Thiago Kenji Okada Date: Wed, 5 Nov 2025 16:10:17 +0000 Subject: [PATCH 02/17] nixos-rebuild-ng: refactor code using GroupedNixArgs --- .../src/nixos_rebuild/__init__.py | 27 ++---- .../src/nixos_rebuild/models.py | 31 +++++++ .../src/nixos_rebuild/services.py | 91 +++++++++---------- .../src/tests/test_services.py | 37 ++++++-- 4 files changed, 108 insertions(+), 78 deletions(-) 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 539816cabeba..f17de4ae96f3 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 @@ -6,7 +6,7 @@ from typing import Final, assert_never from . import nix, services from .constants import EXECUTABLE, WITH_REEXEC, WITH_SHELL_FILES -from .models import Action, BuildAttr, Flake, Profile +from .models import Action, BuildAttr, Flake, GroupedNixArgs, Profile from .process import Remote from .utils import LogFormatter @@ -268,15 +268,7 @@ def parse_args( def execute(argv: list[str]) -> None: args, args_groups = parse_args(argv) - - common_flags = vars(args_groups["common_flags"]) - common_build_flags = common_flags | vars(args_groups["common_build_flags"]) - build_flags = common_build_flags | vars(args_groups["classic_build_flags"]) - flake_common_flags = common_flags | vars(args_groups["flake_common_flags"]) - flake_build_flags = common_build_flags | flake_common_flags - copy_flags = common_flags | vars(args_groups["copy_flags"]) - if build_flags.get("no_build_output"): - flake_build_flags = flake_build_flags | {"no_link": True} + grouped_nix_args = GroupedNixArgs.from_parsed_args_groups(args_groups) if args.upgrade or args.upgrade_all: nix.upgrade_channels(args.upgrade_all, args.sudo) @@ -292,7 +284,7 @@ def execute(argv: list[str]) -> None: # Re-exec to a newer version of the script before building to ensure we get # the latest fixes if WITH_REEXEC and can_run and not args.no_reexec: - services.reexec(argv, args, build_flags, flake_build_flags) + services.reexec(argv, args, grouped_nix_args) profile = Profile.from_arg(args.profile_name) target_host = Remote.from_arg(args.target_host, args.ask_sudo_password) @@ -301,7 +293,7 @@ def execute(argv: list[str]) -> None: flake = Flake.from_arg(args.flake, target_host) if can_run and not flake: - services.write_version_suffix(build_flags) + services.write_version_suffix(grouped_nix_args) match action: case ( @@ -323,15 +315,11 @@ def execute(argv: list[str]) -> None: profile=profile, flake=flake, build_attr=build_attr, - build_flags=build_flags, - common_flags=common_flags, - copy_flags=copy_flags, - flake_build_flags=flake_build_flags, - flake_common_flags=flake_common_flags, + grouped_nix_args=grouped_nix_args, ) case Action.EDIT: - services.edit(flake=flake, flake_build_flags=flake_build_flags) + services.edit(flake=flake, grouped_nix_args=grouped_nix_args) case Action.DRY_RUN: raise AssertionError("DRY_RUN should be a DRY_BUILD alias") @@ -343,8 +331,7 @@ def execute(argv: list[str]) -> None: services.repl( flake=flake, build_attr=build_attr, - flake_build_flags=flake_build_flags, - build_flags=build_flags, + grouped_nix_args=grouped_nix_args, ) case _: diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/models.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/models.py index ef249b9a718a..8cfe53f8e77a 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/models.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/models.py @@ -1,12 +1,14 @@ import platform import re import subprocess +from argparse import Namespace from dataclasses import dataclass from enum import Enum from pathlib import Path from typing import Any, ClassVar, Self, TypedDict, override from .process import Remote, run_wrapper +from .utils import Args type ImageVariants = dict[str, str] @@ -143,6 +145,35 @@ class GenerationJson(TypedDict): current: bool +@dataclass(frozen=True) +class GroupedNixArgs: + build_flags: Args + common_flags: Args + copy_flags: Args + flake_build_flags: Args + flake_common_flags: Args + + @classmethod + def from_parsed_args_groups(cls, args_groups: dict[str, Namespace]) -> Self: + common_flags = vars(args_groups["common_flags"]) + common_build_flags = common_flags | vars(args_groups["common_build_flags"]) + build_flags = common_build_flags | vars(args_groups["classic_build_flags"]) + flake_common_flags = common_flags | vars(args_groups["flake_common_flags"]) + flake_build_flags = common_build_flags | flake_common_flags + copy_flags = common_flags | vars(args_groups["copy_flags"]) + # --no-build-output -> --no-link + if build_flags.get("no_build_output"): + flake_build_flags["no_link"] = True + + return cls( + build_flags=build_flags, + common_flags=common_flags, + copy_flags=copy_flags, + flake_build_flags=flake_build_flags, + flake_common_flags=flake_common_flags, + ) + + @dataclass(frozen=True) class Profile: name: str 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 4034c9c8378a..f1902bd30fb8 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 @@ -8,9 +8,17 @@ from typing import Final from . import nix, tmpdir from .constants import EXECUTABLE -from .models import Action, BuildAttr, Flake, ImageVariants, NixOSRebuildError, Profile +from .models import ( + Action, + BuildAttr, + Flake, + GroupedNixArgs, + ImageVariants, + NixOSRebuildError, + Profile, +) from .process import Remote, cleanup_ssh -from .utils import Args, tabulate +from .utils import tabulate NIXOS_REBUILD_ATTR: Final = "config.system.build.nixos-rebuild" NIXOS_REBUILD_REEXEC_ENV: Final = "_NIXOS_REBUILD_REEXEC" @@ -21,8 +29,7 @@ logger: Final = logging.getLogger(__name__) def reexec( argv: list[str], args: argparse.Namespace, - build_flags: Args, - flake_build_flags: Args, + grouped_nix_args: GroupedNixArgs, ) -> None: if os.environ.get(NIXOS_REBUILD_REEXEC_ENV): return @@ -36,14 +43,14 @@ def reexec( drv = nix.build_flake( NIXOS_REBUILD_ATTR, flake, - flake_build_flags | {"no_link": True}, + grouped_nix_args.flake_build_flags | {"no_link": True}, ) else: build_attr = BuildAttr.from_arg(args.attr, args.file) drv = nix.build( NIXOS_REBUILD_ATTR, build_attr, - build_flags | {"no_out_link": True}, + grouped_nix_args.build_flags | {"no_out_link": True}, ) if drv: @@ -88,21 +95,20 @@ def _get_system_attr( args: argparse.Namespace, flake: Flake | None, build_attr: BuildAttr, - common_flags: Args, - flake_common_flags: Args, + grouped_nix_args: GroupedNixArgs, ) -> str: match action: case Action.BUILD_IMAGE if flake: variants = nix.get_build_image_variants_flake( flake, - eval_flags=flake_common_flags, + eval_flags=grouped_nix_args.flake_common_flags, ) _validate_image_variant(args.image_variant, variants) attr = f"config.system.build.images.{args.image_variant}" case Action.BUILD_IMAGE: variants = nix.get_build_image_variants( build_attr, - instantiate_flags=common_flags, + instantiate_flags=grouped_nix_args.common_flags, ) _validate_image_variant(args.image_variant, variants) attr = f"config.system.build.images.{args.image_variant}" @@ -146,11 +152,7 @@ def _build_system( target_host: Remote | None, flake: Flake | None, build_attr: BuildAttr, - build_flags: Args, - common_flags: Args, - copy_flags: Args, - flake_build_flags: Args, - flake_common_flags: Args, + grouped_nix_args: GroupedNixArgs, ) -> Path: dry_run = action == Action.DRY_BUILD # actions that we will not add a /result symlink in CWD @@ -162,32 +164,33 @@ def _build_system( attr, flake, build_host, - eval_flags=flake_common_flags, + eval_flags=grouped_nix_args.flake_common_flags, flake_build_flags={"no_link": no_link, "dry_run": dry_run} - | flake_build_flags, - copy_flags=copy_flags, + | grouped_nix_args.flake_build_flags, + copy_flags=grouped_nix_args.copy_flags, ) case (None, Flake(_)): path_to_config = nix.build_flake( attr, flake, flake_build_flags={"no_link": no_link, "dry_run": dry_run} - | flake_build_flags, + | grouped_nix_args.flake_build_flags, ) case (Remote(_), None): path_to_config = nix.build_remote( attr, build_attr, build_host, - realise_flags=common_flags, - instantiate_flags=build_flags, - copy_flags=copy_flags, + realise_flags=grouped_nix_args.common_flags, + instantiate_flags=grouped_nix_args.build_flags, + copy_flags=grouped_nix_args.copy_flags, ) case (None, None): path_to_config = nix.build( attr, build_attr, - build_flags={"no_out_link": no_link, "dry_run": dry_run} | build_flags, + build_flags={"no_out_link": no_link, "dry_run": dry_run} + | grouped_nix_args.build_flags, ) # In dry_run mode there is nothing to copy @@ -197,7 +200,7 @@ def _build_system( path_to_config, to_host=target_host, from_host=build_host, - copy_flags=copy_flags, + copy_flags=grouped_nix_args.copy_flags, ) return path_to_config @@ -211,8 +214,7 @@ def _activate_system( profile: Profile, flake: Flake | None, build_attr: BuildAttr, - flake_common_flags: Args, - common_flags: Args, + grouped_nix_args: GroupedNixArgs, ) -> None: # Print only the result to stdout to make it easier to script def print_result(msg: str, result: str | Path) -> None: @@ -257,13 +259,13 @@ def _activate_system( image_name = nix.get_build_image_name_flake( flake, args.image_variant, - eval_flags=flake_common_flags, + eval_flags=grouped_nix_args.flake_common_flags, ) else: image_name = nix.get_build_image_name( build_attr, args.image_variant, - instantiate_flags=common_flags, + instantiate_flags=grouped_nix_args.common_flags, ) disk_path = path_to_config / image_name print_result("Done. The disk image can be found in", disk_path) @@ -277,11 +279,7 @@ def build_and_activate_system( profile: Profile, flake: Flake | None, build_attr: BuildAttr, - build_flags: Args, - common_flags: Args, - copy_flags: Args, - flake_build_flags: Args, - flake_common_flags: Args, + grouped_nix_args: GroupedNixArgs, ) -> None: logger.info("building the system configuration...") attr = _get_system_attr( @@ -289,8 +287,7 @@ def build_and_activate_system( args=args, flake=flake, build_attr=build_attr, - common_flags=common_flags, - flake_common_flags=flake_common_flags, + grouped_nix_args=grouped_nix_args, ) if args.rollback: @@ -308,11 +305,7 @@ def build_and_activate_system( target_host=target_host, flake=flake, build_attr=build_attr, - build_flags=build_flags, - common_flags=common_flags, - copy_flags=copy_flags, - flake_build_flags=flake_build_flags, - flake_common_flags=flake_common_flags, + grouped_nix_args=grouped_nix_args, ) _activate_system( @@ -323,14 +316,13 @@ def build_and_activate_system( profile=profile, flake=flake, build_attr=build_attr, - common_flags=common_flags, - flake_common_flags=flake_common_flags, + grouped_nix_args=grouped_nix_args, ) -def edit(flake: Flake | None, flake_build_flags: Args | None = None) -> None: +def edit(flake: Flake | None, grouped_nix_args: GroupedNixArgs) -> None: if flake: - nix.edit_flake(flake, flake_build_flags) + nix.edit_flake(flake, grouped_nix_args.flake_build_flags) else: nix.edit() @@ -358,17 +350,16 @@ def list_generations( def repl( flake: Flake | None, build_attr: BuildAttr, - flake_build_flags: Args, - build_flags: Args, + grouped_nix_args: GroupedNixArgs, ) -> None: if flake: - nix.repl_flake(flake, flake_build_flags) + nix.repl_flake(flake, grouped_nix_args.flake_build_flags) else: - nix.repl(build_attr, build_flags) + nix.repl(build_attr, grouped_nix_args.build_flags) -def write_version_suffix(build_flags: Args) -> None: - nixpkgs_path = nix.find_file("nixpkgs", build_flags) +def write_version_suffix(grouped_nix_args: GroupedNixArgs) -> None: + nixpkgs_path = nix.find_file("nixpkgs", grouped_nix_args.build_flags) rev = nix.get_nixpkgs_rev(nixpkgs_path) if nixpkgs_path and rev: try: diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_services.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_services.py index 655c127c2744..7fec0818954e 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_services.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_services.py @@ -19,7 +19,14 @@ def test_reexec(mock_build: Mock, mock_execve: Mock, monkeypatch: MonkeyPatch) - args, _ = n.parse_args(argv) mock_build.return_value = Path("/path") - s.reexec(argv, args, {"build": True}, {"flake": True}) + grouped_nix_args = n.models.GroupedNixArgs( + build_flags={"build": True}, + common_flags={"common": True}, + copy_flags={"copy": True}, + flake_build_flags={"flake_build": True}, + flake_common_flags={"flake_common": True}, + ) + s.reexec(argv, args, grouped_nix_args) mock_build.assert_has_calls( [ call( @@ -34,7 +41,7 @@ def test_reexec(mock_build: Mock, mock_execve: Mock, monkeypatch: MonkeyPatch) - mock_build.return_value = Path("/path/new") - s.reexec(argv, args, {}, {}) + s.reexec(argv, args, grouped_nix_args) # exec in the new version successfully mock_execve.assert_called_once_with( Path("/path/new/bin/nixos-rebuild-ng"), @@ -45,7 +52,7 @@ def test_reexec(mock_build: Mock, mock_execve: Mock, monkeypatch: MonkeyPatch) - mock_execve.reset_mock() mock_execve.side_effect = [OSError("BOOM"), None] - s.reexec(argv, args, {}, {}) + s.reexec(argv, args, grouped_nix_args) # exec in the previous version if the new version fails mock_execve.assert_any_call( Path("/path/bin/nixos-rebuild-ng"), @@ -65,18 +72,25 @@ def test_reexec_flake( args, _ = n.parse_args(argv) mock_build.return_value = Path("/path") - s.reexec(argv, args, {"build": True}, {"flake": True}) + grouped_nix_args = n.models.GroupedNixArgs( + build_flags={"build": True}, + common_flags={"common": True}, + copy_flags={"copy": True}, + flake_build_flags={"flake_build": True}, + flake_common_flags={"flake_common": True}, + ) + s.reexec(argv, args, grouped_nix_args) mock_build.assert_called_once_with( s.NIXOS_REBUILD_ATTR, n.models.Flake(ANY, ANY), - {"flake": True, "no_link": True}, + {"flake_build": True, "no_link": True}, ) # do not exec if there is no new version mock_execve.assert_not_called() mock_build.return_value = Path("/path/new") - s.reexec(argv, args, {}, {}) + s.reexec(argv, args, grouped_nix_args) # exec in the new version successfully mock_execve.assert_called_once_with( Path("/path/new/bin/nixos-rebuild-ng"), @@ -87,7 +101,7 @@ def test_reexec_flake( mock_execve.reset_mock() mock_execve.side_effect = [OSError("BOOM"), None] - s.reexec(argv, args, {}, {}) + s.reexec(argv, args, grouped_nix_args) # exec in the previous version if the new version fails mock_execve.assert_any_call( Path("/path/bin/nixos-rebuild-ng"), @@ -104,6 +118,13 @@ def test_reexec_skip_if_already_reexec(mock_build: Mock, mock_execve: Mock) -> N args, _ = n.parse_args(argv) mock_build.return_value = Path("/path") - s.reexec(argv, args, {"build": True}, {"flake": True}) + grouped_nix_args = n.models.GroupedNixArgs( + build_flags={"build": True}, + common_flags={"common": True}, + copy_flags={"copy": True}, + flake_build_flags={"flake_build": True}, + flake_common_flags={"flake_common": True}, + ) + s.reexec(argv, args, grouped_nix_args) mock_build.assert_not_called() mock_execve.assert_not_called() From ae569ef294344e1da3db3e762ac4e076a5e46e0c Mon Sep 17 00:00:00 2001 From: Thiago Kenji Okada Date: Wed, 5 Nov 2025 17:58:40 +0000 Subject: [PATCH 03/17] nixos-rebuild-ng: also support --no-link as an alias for --no-build-output --- .../by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py | 4 +++- 1 file changed, 3 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 f17de4ae96f3..a67e28caab52 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 @@ -55,7 +55,9 @@ def get_parser() -> tuple[argparse.ArgumentParser, dict[str, argparse.ArgumentPa flake_common_flags.add_argument("--override-input", nargs=2, action="append") classic_build_flags = argparse.ArgumentParser(add_help=False, allow_abbrev=False) - classic_build_flags.add_argument("--no-build-output", "-Q", action="store_true") + classic_build_flags.add_argument( + "--no-build-output", "--no-link", "-Q", action="store_true" + ) copy_flags = argparse.ArgumentParser(add_help=False, allow_abbrev=False) copy_flags.add_argument( From 69b747f82e16931b234a9c55b3b1ad802a24202f Mon Sep 17 00:00:00 2001 From: Thiago Kenji Okada Date: Wed, 5 Nov 2025 18:00:27 +0000 Subject: [PATCH 04/17] nixos-rebuild-ng: add -S as alias for --ask-sudo-password --- pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py | 1 + 1 file changed, 1 insertion(+) 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 a67e28caab52..d6b719faa300 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 @@ -153,6 +153,7 @@ def get_parser() -> tuple[argparse.ArgumentParser, dict[str, argparse.ArgumentPa ) main_parser.add_argument( "--ask-sudo-password", + "-S", action="store_true", help="Asks for sudo password for remote activation, implies --sudo", ) From 85bc1a4305399c70fff673795ee93532c9be22be Mon Sep 17 00:00:00 2001 From: Thiago Kenji Okada Date: Wed, 5 Nov 2025 18:06:26 +0000 Subject: [PATCH 05/17] nixos-rebuild-ng: update manual --- .../ni/nixos-rebuild-ng/nixos-rebuild.8.scd | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) 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 fdb3b1382d99..610549f68204 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 @@ -18,13 +18,11 @@ nixos-rebuild - reconfigure a NixOS machine # SYNOPSIS ; document here only non-deprecated flags -_nixos-rebuild_ \[--verbose] [--max-jobs MAX_JOBS] [--cores CORES] [--log-format LOG_FORMAT] [--keep-going] [--keep-failed] [--fallback] [--repair] [--option OPTION OPTION] [--builders BUILDERS]++ - \[--include INCLUDE] [--quiet] [--print-build-logs] [--show-trace] [--accept-flake-config] [--refresh] [--impure] [--offline] [--no-net] [--recreate-lock-file]++ - \[--no-update-lock-file] [--no-write-lock-file] [--no-registries] [--commit-lock-file] [--update-input UPDATE_INPUT] [--override-input OVERRIDE_INPUT OVERRIDE_INPUT]++ - \[--no-build-output] [--use-substitutes] [--help] [--file FILE] [--attr ATTR] [--flake [FLAKE]] [--no-flake] [--install-bootloader] [--profile-name PROFILE_NAME]++ - \[--specialisation SPECIALISATION] [--rollback] [--upgrade] [--upgrade-all] [--json] [--ask-sudo-password] [--sudo] [--no-reexec]++ - \[--image-variant VARIANT]++ - \[--build-host BUILD_HOST] [--target-host TARGET_HOST]++ +_nixos-rebuild_ \[--verbose] [--quiet] [--max-jobs MAX_JOBS] [--cores CORES] [--log-format LOG_FORMAT] [--keep-going] [--keep-failed] [--fallback] [--repair] [--option OPTION OPTION] [--builders BUILDERS] [--include INCLUDE]++ + \[--print-build-logs] [--show-trace] [--accept-flake-config] [--refresh] [--impure] [--offline] [--no-net] [--recreate-lock-file] [--no-update-lock-file] [--no-write-lock-file] [--no-registries] [--commit-lock-file]++ + \[--update-input UPDATE_INPUT] [--override-input OVERRIDE_INPUT OVERRIDE_INPUT] [--no-build-output] [--use-substitutes] [--help] [--debug] [--file FILE] [--attr ATTR] [--flake [FLAKE]] [--no-flake] [--install-bootloader]++ + \[--profile-name PROFILE_NAME] [--specialisation SPECIALISATION] [--rollback] [--upgrade] [--upgrade-all] [--json] [--ask-sudo-password] [--sudo] [--no-reexec]++ + \[--build-host BUILD_HOST] [--target-host TARGET_HOST] [--no-build-nix] [--image-variant IMAGE_VARIANT]++ \[{switch,boot,test,build,edit,repl,dry-build,dry-run,dry-activate,build-image,build-vm,build-vm-with-bootloader,list-generations}] # DESCRIPTION @@ -260,9 +258,10 @@ It must be one of the following: When set, *nixos-rebuild* prefixes activation commands with sudo. Setting this option allows deploying as a non-root user. -*--ask-sudo-password* +*--ask-sudo-password*, *-S* When set, *nixos-rebuild* will ask for sudo password for remote activation (i.e.: on *--target-host*) at the start of the build process. + Implies *--sudo*. *--file* _path_, *-f* _path_ Enable and build the NixOS system from the specified file. The file must @@ -305,9 +304,9 @@ Flake-related options: Builder options: *--verbose,* *-v*, *--quiet*, *--log-format*, *--no-build-output*, *-Q*, -*--max-jobs*, *-j*, *--cores*, *--keep-going*, *-k*, *--keep-failed*, *-K*, -*--fallback*, *--include*, *-I*, *--option*, *--repair*, *--builders*, -*--print-build-logs*, *-L*, *--show-trace* +*--no-link*, *--max-jobs*, *-j*, *--cores*, *--keep-going*, *-k*, +*--keep-failed*, *-K*, *--fallback*, *--include*, *-I*, *--option*, *--repair*, +*--builders*, *--print-build-logs*, *-L*, *--show-trace* See the Nix manual, *nix flake lock --help* or *nix-build --help* for details. From d48645aa660a34a0edd139fbb236fca4ebadb7db Mon Sep 17 00:00:00 2001 From: Thiago Kenji Okada Date: Wed, 5 Nov 2025 18:24:03 +0000 Subject: [PATCH 06/17] nixos-rebuild-ng: move grouping of nix args to parse_args --- .../src/nixos_rebuild/__init__.py | 17 +++++++++-------- .../nixos-rebuild-ng/src/tests/test_main.py | 19 +++++++++++++------ 2 files changed, 22 insertions(+), 14 deletions(-) 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 d6b719faa300..a0e0ebed2f6e 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 @@ -198,13 +198,15 @@ def get_main_parser() -> argparse.ArgumentParser: def parse_args( argv: list[str], -) -> tuple[argparse.Namespace, dict[str, argparse.Namespace]]: +) -> tuple[argparse.Namespace, GroupedNixArgs]: parser, sub_parsers = get_parser() args = parser.parse_args(argv[1:]) - args_groups = { - group: parser.parse_known_args(argv[1:])[0] - for group, parser in sub_parsers.items() - } + grouped_nix_args = GroupedNixArgs.from_parsed_args_groups( + { + group: parser.parse_known_args(argv[1:])[0] + for group, parser in sub_parsers.items() + } + ) if args.help or args.action is None: if WITH_SHELL_FILES: @@ -266,12 +268,11 @@ def parse_args( if args.flake and (args.file or args.attr): parser.error("--flake cannot be used with --file or --attr") - return args, args_groups + return args, grouped_nix_args def execute(argv: list[str]) -> None: - args, args_groups = parse_args(argv) - grouped_nix_args = GroupedNixArgs.from_parsed_args_groups(args_groups) + args, grouped_nix_args = parse_args(argv) if args.upgrade or args.upgrade_all: nix.upgrade_channels(args.upgrade_all, args.sudo) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py index 73187861c780..4c5f67196234 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py @@ -65,8 +65,7 @@ def test_parse_args() -> None: assert r1.install_grub is True assert r1.profile_name == "system" assert r1.action == "switch" - # round-trip test (ensure that we have the same flags as parsed) - assert nr.utils.dict_to_flags(vars(g1["common_flags"])) == [ + assert nr.utils.dict_to_flags(g1.common_flags) == [ "--option", "foo1", "bar1", @@ -74,7 +73,13 @@ def test_parse_args() -> None: "foo2", "bar2", ] - assert nr.utils.dict_to_flags(vars(g1["flake_common_flags"])) == [ + assert nr.utils.dict_to_flags(g1.flake_common_flags) == [ + "--option", + "foo1", + "bar1", + "--option", + "foo2", + "bar2", "--update-input", "input1", "--update-input", @@ -112,13 +117,15 @@ def test_parse_args() -> None: assert r2.action == "dry-build" assert r2.file == "foo" assert r2.attr == "bar" - # round-trip test (ensure that we have the same flags as parsed) - assert nr.utils.dict_to_flags(vars(g2["common_flags"])) == [ + assert nr.utils.dict_to_flags(g2.common_flags) == [ "-vvv", "--quiet", "--quiet", ] - assert nr.utils.dict_to_flags(vars(g2["common_build_flags"])) == [ + assert nr.utils.dict_to_flags(g2.build_flags) == [ + "-vvv", + "--quiet", + "--quiet", "--include", "include1", "--include", From a490811513e962c0d235e06cabc4546461ac3d4b Mon Sep 17 00:00:00 2001 From: Gaetan Lepage Date: Thu, 6 Nov 2025 23:55:51 +0000 Subject: [PATCH 07/17] ruff: 0.14.3 -> 0.14.4 Diff: https://github.com/astral-sh/ruff/compare/0.14.3...0.14.4 Changelog: https://github.com/astral-sh/ruff/releases/tag/0.14.4 --- pkgs/by-name/ru/ruff/package.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkgs/by-name/ru/ruff/package.nix b/pkgs/by-name/ru/ruff/package.nix index c56b26a73cce..78619c4d6bee 100644 --- a/pkgs/by-name/ru/ruff/package.nix +++ b/pkgs/by-name/ru/ruff/package.nix @@ -16,18 +16,18 @@ rustPlatform.buildRustPackage (finalAttrs: { pname = "ruff"; - version = "0.14.3"; + version = "0.14.4"; src = fetchFromGitHub { owner = "astral-sh"; repo = "ruff"; tag = finalAttrs.version; - hash = "sha256-iYXZyB0s3rlGV3HQLN1fuAohFUm/53VLAwA3Ahj6HzM="; + hash = "sha256-jRH7OOT03MDomZAJM20+J4y5+xjN1ZAV27Z44O1qCEQ="; }; cargoBuildFlags = [ "--package=ruff" ]; - cargoHash = "sha256-dYXFNe+nglKelgzi2Afo0AJyt53qfCAJ7reTMMfjWOI="; + cargoHash = "sha256-eY7QnKVrkXaNRWMaTxigNo0kf0oK9DQU4z9x4wC3Npw="; nativeBuildInputs = [ installShellFiles ]; From 3734842aa1c0a3bd85e5a448d0b7b2bf6a24316a Mon Sep 17 00:00:00 2001 From: Adam Dinwoodie Date: Sun, 8 Dec 2024 13:04:52 +0000 Subject: [PATCH 08/17] linger-users: fix shellcheck warnings Running with systemd.enableStrictShellChecks with lingering users causes failures due to parsing the output from `ls`. Rewrite the script to avoid parsing ls, and instead rely on loginctl enable-linger and disable-linger commands being idempotent and run them unconditionally. This also fixes a bug where the systemd unit for adding and removing lingering user configuration is only enabled if there are users configured with lingering in the NixOS configuration. This means that if a NixOS system is built with some lingering users, then the linger configuration is removed from all those users, the script to disable lingering won't be run, and those users will incorrectly continue to have lingering enabled. Fixes #418101. --- nixos/modules/config/users-groups.nix | 41 ++++++++++++++++++--------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix index 0399962418ad..6cde632c3c52 100644 --- a/nixos/modules/config/users-groups.nix +++ b/nixos/modules/config/users-groups.nix @@ -661,8 +661,6 @@ let shells = mapAttrsToList (_: u: u.shell) cfg.users; in filter types.shellPackage.check shells; - - lingeringUsers = map (u: u.name) (attrValues (flip filterAttrs cfg.users (n: u: u.linger))); in { imports = [ @@ -894,7 +892,7 @@ in else ""; # keep around for backwards compatibility - systemd.services.linger-users = lib.mkIf ((length lingeringUsers) > 0) { + systemd.services.linger-users = { wantedBy = [ "multi-user.target" ]; after = [ "systemd-logind.service" ]; requires = [ "systemd-logind.service" ]; @@ -902,21 +900,38 @@ in script = let lingerDir = "/var/lib/systemd/linger"; - lingeringUsersFile = builtins.toFile "lingering-users" ( - concatStrings (map (s: "${s}\n") (sort (a: b: a < b) lingeringUsers)) - ); # this sorting is important for `comm` to work correctly + userPartition = lib.lists.partition (u: u.linger) (builtins.attrValues cfg.users); + lingeringUserNames = map (u: u.name) userPartition.right; + nonLingeringUserNames = map (u: u.name) userPartition.wrong; in '' + ${lib.strings.toShellVars { inherit lingeringUserNames nonLingeringUserNames; }} + + user_configured () { + # Use `id` to check if the user exists rather than checking the + # NixOS configuration, as it may be that the user has been + # manually configured, which is permitted if users.mutableUsers + # is true (the default). + id "$1" >/dev/null + } + mkdir -vp ${lingerDir} cd ${lingerDir} - for user in $(ls); do - if ! id "$user" >/dev/null; then - echo "Removing linger for missing user $user" - rm --force -- "$user" - fi + shopt -s dotglob nullglob + for user in *; do + if ! user_configured "$user"; then + # systemd has this user configured to linger despite them not + # existing. + rm -- "$user" + fi done - ls | sort | comm -3 -1 ${lingeringUsersFile} - | xargs -r ${pkgs.systemd}/bin/loginctl disable-linger - ls | sort | comm -3 -2 ${lingeringUsersFile} - | xargs -r ${pkgs.systemd}/bin/loginctl enable-linger + + if (( ''${#nonLingeringUserNames[*]} > 0 )); then + ${pkgs.systemd}/bin/loginctl disable-linger "''${nonLingeringUserNames[@]}" + fi + if (( ''${#lingeringUserNames[*]} > 0 )); then + ${pkgs.systemd}/bin/loginctl enable-linger "''${lingeringUserNames[@]}" + fi ''; serviceConfig.Type = "oneshot"; From 337c8f6a06d2173dc3c0292a2278257c57b50167 Mon Sep 17 00:00:00 2001 From: Adam Dinwoodie Date: Sun, 8 Dec 2024 14:33:59 +0000 Subject: [PATCH 09/17] tests/systemd-user-linger: correct uid Make the UIDs for test user "bob" consistent to avoid potential false positive test results. This test previously set up a user with UID 10001 then checked to make sure no user slice for a user with UID 1001 existed. That would always be true, since there is no user with UID 1001. --- nixos/tests/systemd-user-linger.nix | 37 ++++++++++++++--------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/nixos/tests/systemd-user-linger.nix b/nixos/tests/systemd-user-linger.nix index 32094c74fa56..eef53a3419fe 100644 --- a/nixos/tests/systemd-user-linger.nix +++ b/nixos/tests/systemd-user-linger.nix @@ -1,33 +1,32 @@ -{ lib, ... }: -{ +rec { name = "systemd-user-linger"; - nodes.machine = - { ... }: - { - users.users = { - alice = { - isNormalUser = true; - linger = true; - uid = 1000; - }; + nodes.machine = { + users.users = { + alice = { + isNormalUser = true; + linger = true; + uid = 1000; + }; - bob = { - isNormalUser = true; - linger = false; - uid = 10001; - }; + bob = { + isNormalUser = true; + linger = false; + uid = 1001; }; }; + }; testScript = - { ... }: + let + uidStrings = builtins.mapAttrs (k: v: builtins.toString v.uid) nodes.machine.users.users; + in '' machine.wait_for_file("/var/lib/systemd/linger/alice") - machine.succeed("systemctl status user-1000.slice") + machine.succeed("systemctl status user-${uidStrings.alice}.slice") machine.fail("test -e /var/lib/systemd/linger/bob") - machine.fail("systemctl status user-1001.slice") + machine.fail("systemctl status user-${uidStrings.bob}.slice") with subtest("missing users have linger purged"): machine.succeed("touch /var/lib/systemd/linger/missing") From 6eb3c6281e7f3964db0d46cdb07c57282d18a9df Mon Sep 17 00:00:00 2001 From: Adam Dinwoodie Date: Sun, 8 Dec 2024 14:54:30 +0000 Subject: [PATCH 10/17] tests/systemd-user-linger: test clearing lingering users Split the existing system-user-linger tests into two: one which tests lingering is set up for the correct users and only the correct users, and one which checks that lingering is cleared for users that don't exist. This means that the latter test now also ensures the lingering configuration is handled even if there are no users configured who should be lingering, where previously such lingering configuration wouldn't be touched. --- nixos/tests/all-tests.nix | 1 + nixos/tests/systemd-user-linger-purge.nix | 29 +++++++++++++++++++++++ nixos/tests/systemd-user-linger.nix | 5 ---- 3 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 nixos/tests/systemd-user-linger-purge.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 511e2101fa7b..76184504a05a 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1510,6 +1510,7 @@ in systemd-sysusers-password-option-override-ordering = runTest ./systemd-sysusers-password-option-override-ordering.nix; systemd-timesyncd-nscd-dnssec = runTest ./systemd-timesyncd-nscd-dnssec.nix; systemd-user-linger = runTest ./systemd-user-linger.nix; + systemd-user-linger-purge = runTest ./systemd-user-linger-purge.nix; systemd-user-tmpfiles-rules = runTest ./systemd-user-tmpfiles-rules.nix; systemd-userdbd = runTest ./systemd-userdbd.nix; systemtap = handleTest ./systemtap.nix { }; diff --git a/nixos/tests/systemd-user-linger-purge.nix b/nixos/tests/systemd-user-linger-purge.nix new file mode 100644 index 000000000000..21a779950e88 --- /dev/null +++ b/nixos/tests/systemd-user-linger-purge.nix @@ -0,0 +1,29 @@ +# This test checks #418101, where lingering users would not be cleared up if +# the configuration is updated to remove lingering from all users. +rec { + name = "systemd-user-linger-purge"; + + nodes.machine = { + users.users = { + bob = { + isNormalUser = true; + linger = false; + uid = 1001; + }; + }; + }; + + testScript = + let + uidStrings = builtins.mapAttrs (k: v: builtins.toString v.uid) nodes.machine.users.users; + in + '' + machine.fail("test -e /var/lib/systemd/linger/bob") + machine.fail("systemctl status user-${uidStrings.bob}.slice") + + with subtest("missing users have linger purged"): + machine.succeed("touch /var/lib/systemd/linger/alice") + machine.systemctl("restart linger-users") + machine.succeed("test ! -e /var/lib/systemd/linger/alice") + ''; +} diff --git a/nixos/tests/systemd-user-linger.nix b/nixos/tests/systemd-user-linger.nix index eef53a3419fe..c1acd8534dde 100644 --- a/nixos/tests/systemd-user-linger.nix +++ b/nixos/tests/systemd-user-linger.nix @@ -27,10 +27,5 @@ rec { machine.fail("test -e /var/lib/systemd/linger/bob") machine.fail("systemctl status user-${uidStrings.bob}.slice") - - with subtest("missing users have linger purged"): - machine.succeed("touch /var/lib/systemd/linger/missing") - machine.systemctl("restart linger-users") - machine.succeed("test ! -e /var/lib/systemd/linger/missing") ''; } From 0a72d5772490a635eff783eea35ee009b99c6142 Mon Sep 17 00:00:00 2001 From: Adam Dinwoodie Date: Thu, 19 Jun 2025 10:25:57 +0100 Subject: [PATCH 11/17] tests/systemd-user-linger-purge: check mutable user handling Mutable users configured to linger shouldn't have their lingering deconfigured by the systemd unit for managing lingering users. Test in both the scenario where there are no users configured in NixOS to linger, and where there are such users, to catch the case at https://github.com/NixOS/nixpkgs/pull/363209#issuecomment-2987448905 where mutable lingering users would be incorrectly cleared if there were any immutable lingering users. --- nixos/tests/systemd-user-linger-purge.nix | 8 ++++++++ nixos/tests/systemd-user-linger.nix | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/nixos/tests/systemd-user-linger-purge.nix b/nixos/tests/systemd-user-linger-purge.nix index 21a779950e88..90cdda3d87e7 100644 --- a/nixos/tests/systemd-user-linger-purge.nix +++ b/nixos/tests/systemd-user-linger-purge.nix @@ -25,5 +25,13 @@ rec { machine.succeed("touch /var/lib/systemd/linger/alice") machine.systemctl("restart linger-users") machine.succeed("test ! -e /var/lib/systemd/linger/alice") + + with subtest("mutable users can linger"): + machine.succeed("useradd alice") + machine.succeed("test ! -e /var/lib/systemd/linger/alice") + machine.succeed("loginctl enable-linger alice") + machine.succeed("test -e /var/lib/systemd/linger/alice") + machine.systemctl("restart linger-users") + machine.succeed("test -e /var/lib/systemd/linger/alice") ''; } diff --git a/nixos/tests/systemd-user-linger.nix b/nixos/tests/systemd-user-linger.nix index c1acd8534dde..357bf632e50f 100644 --- a/nixos/tests/systemd-user-linger.nix +++ b/nixos/tests/systemd-user-linger.nix @@ -27,5 +27,13 @@ rec { machine.fail("test -e /var/lib/systemd/linger/bob") machine.fail("systemctl status user-${uidStrings.bob}.slice") + + with subtest("mutable users can linger"): + machine.succeed("useradd clare") + machine.succeed("test ! -e /var/lib/systemd/linger/clare") + machine.succeed("loginctl enable-linger clare") + machine.succeed("test -e /var/lib/systemd/linger/clare") + machine.systemctl("restart linger-users") + machine.succeed("test -e /var/lib/systemd/linger/clare") ''; } From ac476b9cbcd009c7396163a63cf4b125c78e236c Mon Sep 17 00:00:00 2001 From: Adam Dinwoodie Date: Thu, 19 Jun 2025 10:36:32 +0100 Subject: [PATCH 12/17] users-groups: use loginctl from configured systemctl If a user has configured a different systemd package, linger-users.service should respect that and use the provided loginctl executable rather than the one from the default nixpkgs package. --- nixos/modules/config/users-groups.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix index 6cde632c3c52..98966fc1a7cb 100644 --- a/nixos/modules/config/users-groups.nix +++ b/nixos/modules/config/users-groups.nix @@ -927,10 +927,10 @@ in done if (( ''${#nonLingeringUserNames[*]} > 0 )); then - ${pkgs.systemd}/bin/loginctl disable-linger "''${nonLingeringUserNames[@]}" + ${config.systemd.package}/bin/loginctl disable-linger "''${nonLingeringUserNames[@]}" fi if (( ''${#lingeringUserNames[*]} > 0 )); then - ${pkgs.systemd}/bin/loginctl enable-linger "''${lingeringUserNames[@]}" + ${config.systemd.package}/bin/loginctl enable-linger "''${lingeringUserNames[@]}" fi ''; From e3cb0fe2ca984caf2acbc45bf28a9d2e7d271be0 Mon Sep 17 00:00:00 2001 From: Adam Dinwoodie Date: Sat, 8 Nov 2025 17:49:47 +0000 Subject: [PATCH 13/17] linger-users: use systemd directory options Using systemd properties avoids the need for manually running mkdir and cd commands, and helps systemd clean up properly when appropriate. Suggested-By: Grimmauld --- nixos/modules/config/users-groups.nix | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix index 98966fc1a7cb..ae866c915a6f 100644 --- a/nixos/modules/config/users-groups.nix +++ b/nixos/modules/config/users-groups.nix @@ -899,7 +899,6 @@ in script = let - lingerDir = "/var/lib/systemd/linger"; userPartition = lib.lists.partition (u: u.linger) (builtins.attrValues cfg.users); lingeringUserNames = map (u: u.name) userPartition.right; nonLingeringUserNames = map (u: u.name) userPartition.wrong; @@ -915,8 +914,6 @@ in id "$1" >/dev/null } - mkdir -vp ${lingerDir} - cd ${lingerDir} shopt -s dotglob nullglob for user in *; do if ! user_configured "$user"; then @@ -934,7 +931,11 @@ in fi ''; - serviceConfig.Type = "oneshot"; + serviceConfig = { + Type = "oneshot"; + StateDirectory = "systemd/linger"; + WorkingDirectory = "/var/lib/systemd/linger"; + }; }; # Warn about user accounts with deprecated password hashing schemes From 2130c0a63ef1c630019ae6ae10202aa6246ae8f7 Mon Sep 17 00:00:00 2001 From: Adam Dinwoodie Date: Sat, 8 Nov 2025 17:51:13 +0000 Subject: [PATCH 14/17] linger-users: log when deconfiguring linger If systemd has recorded that a user should be lingering despite them not having an account on the system, that record is removed. When that happens, log for the sake of future debugging and investigations. Suggested-By: Grimmauld --- nixos/modules/config/users-groups.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix index ae866c915a6f..d059bbc43391 100644 --- a/nixos/modules/config/users-groups.nix +++ b/nixos/modules/config/users-groups.nix @@ -919,6 +919,7 @@ in if ! user_configured "$user"; then # systemd has this user configured to linger despite them not # existing. + echo "Removing linger for missing user $user" >&2 rm -- "$user" fi done From fbaced1f27079163bba4c39817325dbba78c3a23 Mon Sep 17 00:00:00 2001 From: Sergei Trofimovich Date: Sat, 8 Nov 2025 22:39:28 +0000 Subject: [PATCH 15/17] btrfs-progs: 6.17 -> 6.17.1 Changes: https://github.com/kdave/btrfs-progs/releases/tag/v6.17.1 --- pkgs/by-name/bt/btrfs-progs/package.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/by-name/bt/btrfs-progs/package.nix b/pkgs/by-name/bt/btrfs-progs/package.nix index ffa5dac48db6..f521f193042a 100644 --- a/pkgs/by-name/bt/btrfs-progs/package.nix +++ b/pkgs/by-name/bt/btrfs-progs/package.nix @@ -21,11 +21,11 @@ stdenv.mkDerivation rec { pname = "btrfs-progs"; - version = "6.17"; + version = "6.17.1"; src = fetchurl { url = "mirror://kernel/linux/kernel/people/kdave/btrfs-progs/btrfs-progs-v${version}.tar.xz"; - hash = "sha256-J31pbJ15cT/1r7U8fv69zq0uamAHeJsXQuxBH05Moik="; + hash = "sha256-pL4Kbrs8R2Qn+12Xss8CewzNtrDFX/FjIzIMHoy3dlg="; }; nativeBuildInputs = [ From 531d3a9a45e82ca5dbb1192934a9f28e674d32d9 Mon Sep 17 00:00:00 2001 From: Adam Dinwoodie Date: Sun, 9 Nov 2025 19:12:05 +0000 Subject: [PATCH 16/17] linger-users: allow disabling for bashless profile The linger-users systemd unit runs a Bash script. To allow this to be avoided for the bashless profile, provide an option to have NixOS not manage lingering for any users. To make this feasible, add the possibility for each individual user account to not have its lingering configuration managed by NixOS at all, and make this the default from 26.05. In practice, this won't result in a change of behaviour except for people who manually use `loginctl enable-linger` commands to add lingering for some user accounts, then rely on NixOS to disable lingering the next time the systemd units are restarted. --- .../manual/release-notes/rl-2511.section.md | 2 + nixos/modules/config/users-groups.nix | 60 +++++++++++++++---- nixos/modules/profiles/bashless.nix | 1 + 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/nixos/doc/manual/release-notes/rl-2511.section.md b/nixos/doc/manual/release-notes/rl-2511.section.md index a398bba4bff5..6f6eb32bbe95 100644 --- a/nixos/doc/manual/release-notes/rl-2511.section.md +++ b/nixos/doc/manual/release-notes/rl-2511.section.md @@ -379,6 +379,8 @@ and [release notes for v18](https://goteleport.com/docs/changelog/#1800-070325). - In all other cases, you'll need to set this option to `true` yourself. - `boot.isNspawnContainer` being `true` implies [](#opt-boot.isContainer) being `true`. +- `users.users.*.linger` now defaults to `null` rather than `false`, meaning NixOS will not attempt to enable or disable lingering for that user account. In practice, this is unlikely to make a difference for most people, as new users are created without lingering configured, but it means users who use `loginctl` commands to manage lingering imperatively will not have their changes overridden by default. There is a new, related option, `users.manageLingering`, which can be used to prevent NixOS attempting to manage lingering entirely. + - Due to [deprecation of gnome-session X11 support](https://blogs.gnome.org/alatiera/2025/06/08/the-x11-session-removal/), `services.desktopManager.pantheon` now defaults to pantheon-wayland session. The X11 session has been removed, see [this issue](https://github.com/elementary/session-settings/issues/91) for details. - `bcachefs` file systems will now use the out-of-tree module for supported kernels. The in-tree module has been removed, and users will need to switch to kernels that support the out-of-tree module. diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix index d059bbc43391..add0e9159231 100644 --- a/nixos/modules/config/users-groups.nix +++ b/nixos/modules/config/users-groups.nix @@ -11,6 +11,7 @@ let any attrNames attrValues + boolToString concatMap concatMapStringsSep concatStrings @@ -43,6 +44,7 @@ let stringLength trace types + versionOlder xor ; @@ -128,6 +130,10 @@ let ''; userOpts = + let + # Pass state version through despite config being overwritten in the inner module + inherit (config.system) stateVersion; + in { name, config, ... }: { @@ -455,16 +461,22 @@ let }; linger = mkOption { - type = types.bool; - default = false; + type = types.nullOr types.bool; + example = true; + default = if versionOlder stateVersion "26.11" then false else null; + defaultText = literalExpression "if lib.versionOlder config.system.stateVersion \"25.11\" then false else null"; description = '' - Whether to enable lingering for this user. If true, systemd user - units will start at boot, rather than starting at login and stopping - at logout. This is the declarative equivalent of running - `loginctl enable-linger` for this user. + Whether to enable or disable lingering for this user. Without + lingering, user units will not be started until the user logs in, + and may be stopped on logout depending on the settings in + `logind.conf`. - If false, user units will not be started until the user logs in, and - may be stopped on logout depending on the settings in `logind.conf`. + By default, NixOS will not manage lingering, new users will default + to not lingering, and you can change the linger setting using + `loginctl enable-linger` or `loginctl disable-linger`. Setting + this option to `true` or `false` is the declarative equivalent of + running `loginctl enable-linger` or `loginctl disable-linger` + respectively. ''; }; }; @@ -708,6 +720,13 @@ in ''; }; + users.manageLingering = mkOption { + type = types.bool; + default = true; + description = "Whether to manage whether users linger or not."; + example = false; + }; + users.users = mkOption { default = { }; type = with types; attrsOf (submodule userOpts); @@ -892,16 +911,17 @@ in else ""; # keep around for backwards compatibility - systemd.services.linger-users = { + systemd.services.linger-users = lib.mkIf cfg.manageLingering { wantedBy = [ "multi-user.target" ]; after = [ "systemd-logind.service" ]; requires = [ "systemd-logind.service" ]; script = let - userPartition = lib.lists.partition (u: u.linger) (builtins.attrValues cfg.users); - lingeringUserNames = map (u: u.name) userPartition.right; - nonLingeringUserNames = map (u: u.name) userPartition.wrong; + lingeringUsers = filterAttrs (n: v: v.linger == true) cfg.users; + nonLingeringUsers = filterAttrs (n: v: v.linger == false) cfg.users; + lingeringUserNames = mapAttrsToList (n: v: v.name) lingeringUsers; + nonLingeringUserNames = mapAttrsToList (n: v: v.name) nonLingeringUsers; in '' ${lib.strings.toShellVars { inherit lingeringUserNames nonLingeringUserNames; }} @@ -1180,6 +1200,22 @@ in users.groups.${user.name} = {}; ''; } + { + assertion = user.linger != null -> cfg.manageLingering; + message = '' + users.manageLingering is set to false, but + users.users.${user.name}.linger is configured. + + If you want NixOS to manage whether user accounts linger or + not, you must set users.manageLingering to true. This is the + default setting. + + If you do not want NixOS to manage whether user accounts linger + or not, you must set users.users.${user.name}.linger to null. + This is the default setting provided system.stateVersion is at + least "25.11". + ''; + } ] ++ (map (shell: { diff --git a/nixos/modules/profiles/bashless.nix b/nixos/modules/profiles/bashless.nix index a658a5977709..572f665bfede 100644 --- a/nixos/modules/profiles/bashless.nix +++ b/nixos/modules/profiles/bashless.nix @@ -32,6 +32,7 @@ boot.kexec.enable = lib.mkDefault false; # Relies on bash scripts powerManagement.enable = lib.mkDefault false; + users.manageLingering = lib.mkDefault false; # Relies on the gzip command which depends on bash services.logrotate.enable = lib.mkDefault false; From 1d198094028896ff57b8357fbc9e2ed0e56fffc4 Mon Sep 17 00:00:00 2001 From: Grimmauld Date: Mon, 10 Nov 2025 11:36:01 +0100 Subject: [PATCH 17/17] linger-users: default to null, be explicit about null = imperative --- nixos/doc/manual/release-notes/rl-2511.section.md | 2 +- nixos/modules/config/users-groups.nix | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/nixos/doc/manual/release-notes/rl-2511.section.md b/nixos/doc/manual/release-notes/rl-2511.section.md index 0ee33b960154..fa7194cf4b8a 100644 --- a/nixos/doc/manual/release-notes/rl-2511.section.md +++ b/nixos/doc/manual/release-notes/rl-2511.section.md @@ -385,7 +385,7 @@ and [release notes for v18](https://goteleport.com/docs/changelog/#1800-070325). - In all other cases, you'll need to set this option to `true` yourself. - `boot.isNspawnContainer` being `true` implies [](#opt-boot.isContainer) being `true`. -- `users.users.*.linger` now defaults to `null` rather than `false`, meaning NixOS will not attempt to enable or disable lingering for that user account. In practice, this is unlikely to make a difference for most people, as new users are created without lingering configured, but it means users who use `loginctl` commands to manage lingering imperatively will not have their changes overridden by default. There is a new, related option, `users.manageLingering`, which can be used to prevent NixOS attempting to manage lingering entirely. +- `users.users.*.linger` now defaults to `null` rather than `false`, meaning NixOS will not attempt to enable or disable lingering for that user account, instead allowing for imperative control over lingering using the `loginctl` commands. In practice, this is unlikely to make a difference for most people, as new users are created without lingering configured. There is a new, related option, `users.manageLingering`, which can be used to prevent NixOS attempting to manage lingering entirely. - Due to [deprecation of gnome-session X11 support](https://blogs.gnome.org/alatiera/2025/06/08/the-x11-session-removal/), `services.desktopManager.pantheon` now defaults to pantheon-wayland session. The X11 session has been removed, see [this issue](https://github.com/elementary/session-settings/issues/91) for details. diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix index add0e9159231..defde2707ae7 100644 --- a/nixos/modules/config/users-groups.nix +++ b/nixos/modules/config/users-groups.nix @@ -130,10 +130,6 @@ let ''; userOpts = - let - # Pass state version through despite config being overwritten in the inner module - inherit (config.system) stateVersion; - in { name, config, ... }: { @@ -463,8 +459,7 @@ let linger = mkOption { type = types.nullOr types.bool; example = true; - default = if versionOlder stateVersion "26.11" then false else null; - defaultText = literalExpression "if lib.versionOlder config.system.stateVersion \"25.11\" then false else null"; + default = null; description = '' Whether to enable or disable lingering for this user. Without lingering, user units will not be started until the user logs in, @@ -472,8 +467,8 @@ let `logind.conf`. By default, NixOS will not manage lingering, new users will default - to not lingering, and you can change the linger setting using - `loginctl enable-linger` or `loginctl disable-linger`. Setting + to not lingering, and lingering can be configured imperatively using + `loginctl enable-linger` or `loginctl disable-linger`. Setting this option to `true` or `false` is the declarative equivalent of running `loginctl enable-linger` or `loginctl disable-linger` respectively.