diff --git a/nixos/doc/manual/release-notes/rl-2511.section.md b/nixos/doc/manual/release-notes/rl-2511.section.md index 96659c6cbdd08..fa7194cf4b8aa 100644 --- a/nixos/doc/manual/release-notes/rl-2511.section.md +++ b/nixos/doc/manual/release-notes/rl-2511.section.md @@ -385,6 +385,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, 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. - `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 0399962418ad2..defde2707ae7b 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 ; @@ -455,16 +457,21 @@ let }; linger = mkOption { - type = types.bool; - default = false; + type = types.nullOr types.bool; + example = true; + default = 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. - - 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`. + 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`. + + By default, NixOS will not manage lingering, new users will default + 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. ''; }; }; @@ -661,8 +668,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 = [ @@ -710,6 +715,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); @@ -894,32 +906,52 @@ in else ""; # keep around for backwards compatibility - systemd.services.linger-users = lib.mkIf ((length lingeringUsers) > 0) { + systemd.services.linger-users = lib.mkIf cfg.manageLingering { wantedBy = [ "multi-user.target" ]; after = [ "systemd-logind.service" ]; requires = [ "systemd-logind.service" ]; 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 + 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 '' - 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 + ${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 + } + + shopt -s dotglob nullglob + for user in *; do + 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 - 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 + ${config.systemd.package}/bin/loginctl disable-linger "''${nonLingeringUserNames[@]}" + fi + if (( ''${#lingeringUserNames[*]} > 0 )); then + ${config.systemd.package}/bin/loginctl enable-linger "''${lingeringUserNames[@]}" + fi ''; - serviceConfig.Type = "oneshot"; + serviceConfig = { + Type = "oneshot"; + StateDirectory = "systemd/linger"; + WorkingDirectory = "/var/lib/systemd/linger"; + }; }; # Warn about user accounts with deprecated password hashing schemes @@ -1163,6 +1195,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 a658a5977709d..572f665bfedef 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; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 15f15e2fc2123..5693d560c3a8b 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1519,6 +1519,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 0000000000000..90cdda3d87e7c --- /dev/null +++ b/nixos/tests/systemd-user-linger-purge.nix @@ -0,0 +1,37 @@ +# 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") + + 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 32094c74fa566..357bf632e50fe 100644 --- a/nixos/tests/systemd-user-linger.nix +++ b/nixos/tests/systemd-user-linger.nix @@ -1,37 +1,39 @@ -{ 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") + 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/missing") + machine.succeed("test -e /var/lib/systemd/linger/clare") ''; } diff --git a/pkgs/by-name/bt/btrfs-progs/package.nix b/pkgs/by-name/bt/btrfs-progs/package.nix index ffa5dac48db6b..f521f193042ae 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 = [ 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 fdb3b1382d996..610549f68204e 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. 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 6d7ad01baebdc..a0e0ebed2f6e1 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 . 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 @@ -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( @@ -151,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", ) @@ -195,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: @@ -263,18 +268,11 @@ def parser_warn(msg: str) -> None: 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) - - 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"]) + args, grouped_nix_args = parse_args(argv) if args.upgrade or args.upgrade_all: nix.upgrade_channels(args.upgrade_all, args.sudo) @@ -290,7 +288,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) @@ -299,7 +297,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 ( @@ -321,15 +319,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") @@ -341,8 +335,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 ef249b9a718ac..8cfe53f8e77ad 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 2d46c417580d4..f1902bd30fb8b 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 . 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 @@ 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, - flake_build_flags=flake_build_flags - | {"no_link": no_link, "dry_run": dry_run}, - copy_flags=copy_flags, + eval_flags=grouped_nix_args.flake_common_flags, + flake_build_flags={"no_link": no_link, "dry_run": dry_run} + | 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=flake_build_flags - | {"no_link": no_link, "dry_run": dry_run}, + flake_build_flags={"no_link": no_link, "dry_run": dry_run} + | 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=build_flags | {"no_out_link": no_link, "dry_run": dry_run}, + 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 print_result(msg: str, result: str | Path) -> None: 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_main.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py index cdd4e65b17e6f..4c5f671962349 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", @@ -178,8 +185,8 @@ def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]: "", "--attr", "config.system.build.toplevel", - "-vvv", "--no-out-link", + "-vvv", ], check=True, stdout=PIPE, @@ -222,6 +229,49 @@ def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]: ) +# 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 +442,11 @@ def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]: "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, 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 655c127c27441..7fec0818954e2 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() diff --git a/pkgs/by-name/ru/ruff/package.nix b/pkgs/by-name/ru/ruff/package.nix index c56b26a73ccef..78619c4d6bee6 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 ];