staging-nixos merge for 2025-11-12 (#460995)

This commit is contained in:
Vladimír Čunát
2025-11-12 18:09:33 +00:00
committed by GitHub
14 changed files with 333 additions and 157 deletions

View File

@@ -397,6 +397,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. - In all other cases, you'll need to set this option to `true` yourself.
- `boot.isNspawnContainer` being `true` implies [](#opt-boot.isContainer) being `true`. - `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. - 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. - `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.

View File

@@ -11,6 +11,7 @@ let
any any
attrNames attrNames
attrValues attrValues
boolToString
concatMap concatMap
concatMapStringsSep concatMapStringsSep
concatStrings concatStrings
@@ -43,6 +44,7 @@ let
stringLength stringLength
trace trace
types types
versionOlder
xor xor
; ;
@@ -455,16 +457,21 @@ let
}; };
linger = mkOption { linger = mkOption {
type = types.bool; type = types.nullOr types.bool;
default = false; example = true;
default = null;
description = '' description = ''
Whether to enable lingering for this user. If true, systemd user Whether to enable or disable lingering for this user. Without
units will start at boot, rather than starting at login and stopping lingering, user units will not be started until the user logs in,
at logout. This is the declarative equivalent of running and may be stopped on logout depending on the settings in
`loginctl enable-linger` for this user. `logind.conf`.
If false, user units will not be started until the user logs in, and By default, NixOS will not manage lingering, new users will default
may be stopped on logout depending on the settings in `logind.conf`. 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; shells = mapAttrsToList (_: u: u.shell) cfg.users;
in in
filter types.shellPackage.check shells; filter types.shellPackage.check shells;
lingeringUsers = map (u: u.name) (attrValues (flip filterAttrs cfg.users (n: u: u.linger)));
in in
{ {
imports = [ 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 { users.users = mkOption {
default = { }; default = { };
type = with types; attrsOf (submodule userOpts); type = with types; attrsOf (submodule userOpts);
@@ -894,32 +906,52 @@ in
else else
""; # keep around for backwards compatibility ""; # 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" ]; wantedBy = [ "multi-user.target" ];
after = [ "systemd-logind.service" ]; after = [ "systemd-logind.service" ];
requires = [ "systemd-logind.service" ]; requires = [ "systemd-logind.service" ];
script = script =
let let
lingerDir = "/var/lib/systemd/linger"; lingeringUsers = filterAttrs (n: v: v.linger == true) cfg.users;
lingeringUsersFile = builtins.toFile "lingering-users" ( nonLingeringUsers = filterAttrs (n: v: v.linger == false) cfg.users;
concatStrings (map (s: "${s}\n") (sort (a: b: a < b) lingeringUsers)) lingeringUserNames = mapAttrsToList (n: v: v.name) lingeringUsers;
); # this sorting is important for `comm` to work correctly nonLingeringUserNames = mapAttrsToList (n: v: v.name) nonLingeringUsers;
in in
'' ''
mkdir -vp ${lingerDir} ${lib.strings.toShellVars { inherit lingeringUserNames nonLingeringUserNames; }}
cd ${lingerDir}
for user in $(ls); do user_configured () {
if ! id "$user" >/dev/null; then # Use `id` to check if the user exists rather than checking the
echo "Removing linger for missing user $user" # NixOS configuration, as it may be that the user has been
rm --force -- "$user" # manually configured, which is permitted if users.mutableUsers
fi # 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 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 # Warn about user accounts with deprecated password hashing schemes
@@ -1163,6 +1195,22 @@ in
users.groups.${user.name} = {}; 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 ++ (map
(shell: { (shell: {

View File

@@ -32,6 +32,7 @@
boot.kexec.enable = lib.mkDefault false; boot.kexec.enable = lib.mkDefault false;
# Relies on bash scripts # Relies on bash scripts
powerManagement.enable = lib.mkDefault false; powerManagement.enable = lib.mkDefault false;
users.manageLingering = lib.mkDefault false;
# Relies on the gzip command which depends on bash # Relies on the gzip command which depends on bash
services.logrotate.enable = lib.mkDefault false; services.logrotate.enable = lib.mkDefault false;

View File

@@ -1522,6 +1522,7 @@ in
systemd-sysusers-password-option-override-ordering = runTest ./systemd-sysusers-password-option-override-ordering.nix; 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-timesyncd-nscd-dnssec = runTest ./systemd-timesyncd-nscd-dnssec.nix;
systemd-user-linger = runTest ./systemd-user-linger.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-user-tmpfiles-rules = runTest ./systemd-user-tmpfiles-rules.nix;
systemd-userdbd = runTest ./systemd-userdbd.nix; systemd-userdbd = runTest ./systemd-userdbd.nix;
systemtap = handleTest ./systemtap.nix { }; systemtap = handleTest ./systemtap.nix { };

View File

@@ -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")
'';
}

View File

@@ -1,37 +1,39 @@
{ lib, ... }: rec {
{
name = "systemd-user-linger"; name = "systemd-user-linger";
nodes.machine = nodes.machine = {
{ ... }: users.users = {
{ alice = {
users.users = { isNormalUser = true;
alice = { linger = true;
isNormalUser = true; uid = 1000;
linger = true; };
uid = 1000;
};
bob = { bob = {
isNormalUser = true; isNormalUser = true;
linger = false; linger = false;
uid = 10001; uid = 1001;
};
}; };
}; };
};
testScript = 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.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("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"): with subtest("mutable users can linger"):
machine.succeed("touch /var/lib/systemd/linger/missing") 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.systemctl("restart linger-users")
machine.succeed("test ! -e /var/lib/systemd/linger/missing") machine.succeed("test -e /var/lib/systemd/linger/clare")
''; '';
} }

View File

@@ -21,11 +21,11 @@
stdenv.mkDerivation rec { stdenv.mkDerivation rec {
pname = "btrfs-progs"; pname = "btrfs-progs";
version = "6.17"; version = "6.17.1";
src = fetchurl { src = fetchurl {
url = "mirror://kernel/linux/kernel/people/kdave/btrfs-progs/btrfs-progs-v${version}.tar.xz"; 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 = [ nativeBuildInputs = [

View File

@@ -18,13 +18,11 @@ nixos-rebuild - reconfigure a NixOS machine
# SYNOPSIS # SYNOPSIS
; document here only non-deprecated flags ; 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]++ _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]++
\[--include INCLUDE] [--quiet] [--print-build-logs] [--show-trace] [--accept-flake-config] [--refresh] [--impure] [--offline] [--no-net] [--recreate-lock-file]++ \[--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]++
\[--no-update-lock-file] [--no-write-lock-file] [--no-registries] [--commit-lock-file] [--update-input UPDATE_INPUT] [--override-input OVERRIDE_INPUT OVERRIDE_INPUT]++ \[--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]++
\[--no-build-output] [--use-substitutes] [--help] [--file FILE] [--attr ATTR] [--flake [FLAKE]] [--no-flake] [--install-bootloader] [--profile-name PROFILE_NAME]++ \[--profile-name PROFILE_NAME] [--specialisation SPECIALISATION] [--rollback] [--upgrade] [--upgrade-all] [--json] [--ask-sudo-password] [--sudo] [--no-reexec]++
\[--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]++
\[--image-variant VARIANT]++
\[--build-host BUILD_HOST] [--target-host TARGET_HOST]++
\[{switch,boot,test,build,edit,repl,dry-build,dry-run,dry-activate,build-image,build-vm,build-vm-with-bootloader,list-generations}] \[{switch,boot,test,build,edit,repl,dry-build,dry-run,dry-activate,build-image,build-vm,build-vm-with-bootloader,list-generations}]
# DESCRIPTION # DESCRIPTION
@@ -260,9 +258,10 @@ It must be one of the following:
When set, *nixos-rebuild* prefixes activation commands with sudo. When set, *nixos-rebuild* prefixes activation commands with sudo.
Setting this option allows deploying as a non-root user. 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 When set, *nixos-rebuild* will ask for sudo password for remote
activation (i.e.: on *--target-host*) at the start of the build process. activation (i.e.: on *--target-host*) at the start of the build process.
Implies *--sudo*.
*--file* _path_, *-f* _path_ *--file* _path_, *-f* _path_
Enable and build the NixOS system from the specified file. The file must Enable and build the NixOS system from the specified file. The file must
@@ -305,9 +304,9 @@ Flake-related options:
Builder options: Builder options:
*--verbose,* *-v*, *--quiet*, *--log-format*, *--no-build-output*, *-Q*, *--verbose,* *-v*, *--quiet*, *--log-format*, *--no-build-output*, *-Q*,
*--max-jobs*, *-j*, *--cores*, *--keep-going*, *-k*, *--keep-failed*, *-K*, *--no-link*, *--max-jobs*, *-j*, *--cores*, *--keep-going*, *-k*,
*--fallback*, *--include*, *-I*, *--option*, *--repair*, *--builders*, *--keep-failed*, *-K*, *--fallback*, *--include*, *-I*, *--option*, *--repair*,
*--print-build-logs*, *-L*, *--show-trace* *--builders*, *--print-build-logs*, *-L*, *--show-trace*
See the Nix manual, *nix flake lock --help* or *nix-build --help* for details. See the Nix manual, *nix flake lock --help* or *nix-build --help* for details.

View File

@@ -6,7 +6,7 @@ from typing import Final, assert_never
from . import nix, services from . import nix, services
from .constants import EXECUTABLE, WITH_REEXEC, WITH_SHELL_FILES 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 .process import Remote
from .utils import LogFormatter 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") 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 = 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 = argparse.ArgumentParser(add_help=False, allow_abbrev=False)
copy_flags.add_argument( copy_flags.add_argument(
@@ -151,6 +153,7 @@ def get_parser() -> tuple[argparse.ArgumentParser, dict[str, argparse.ArgumentPa
) )
main_parser.add_argument( main_parser.add_argument(
"--ask-sudo-password", "--ask-sudo-password",
"-S",
action="store_true", action="store_true",
help="Asks for sudo password for remote activation, implies --sudo", help="Asks for sudo password for remote activation, implies --sudo",
) )
@@ -195,13 +198,15 @@ def get_main_parser() -> argparse.ArgumentParser:
def parse_args( def parse_args(
argv: list[str], argv: list[str],
) -> tuple[argparse.Namespace, dict[str, argparse.Namespace]]: ) -> tuple[argparse.Namespace, GroupedNixArgs]:
parser, sub_parsers = get_parser() parser, sub_parsers = get_parser()
args = parser.parse_args(argv[1:]) args = parser.parse_args(argv[1:])
args_groups = { grouped_nix_args = GroupedNixArgs.from_parsed_args_groups(
group: parser.parse_known_args(argv[1:])[0] {
for group, parser in sub_parsers.items() group: parser.parse_known_args(argv[1:])[0]
} for group, parser in sub_parsers.items()
}
)
if args.help or args.action is None: if args.help or args.action is None:
if WITH_SHELL_FILES: if WITH_SHELL_FILES:
@@ -263,18 +268,11 @@ def parse_args(
if args.flake and (args.file or args.attr): if args.flake and (args.file or args.attr):
parser.error("--flake cannot be used with --file or --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: def execute(argv: list[str]) -> None:
args, args_groups = parse_args(argv) args, grouped_nix_args = 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 args.upgrade or args.upgrade_all: if args.upgrade or args.upgrade_all:
nix.upgrade_channels(args.upgrade_all, args.sudo) 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 # Re-exec to a newer version of the script before building to ensure we get
# the latest fixes # the latest fixes
if WITH_REEXEC and can_run and not args.no_reexec: 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) profile = Profile.from_arg(args.profile_name)
target_host = Remote.from_arg(args.target_host, args.ask_sudo_password) 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) flake = Flake.from_arg(args.flake, target_host)
if can_run and not flake: if can_run and not flake:
services.write_version_suffix(build_flags) services.write_version_suffix(grouped_nix_args)
match action: match action:
case ( case (
@@ -321,15 +319,11 @@ def execute(argv: list[str]) -> None:
profile=profile, profile=profile,
flake=flake, flake=flake,
build_attr=build_attr, build_attr=build_attr,
build_flags=build_flags, grouped_nix_args=grouped_nix_args,
common_flags=common_flags,
copy_flags=copy_flags,
flake_build_flags=flake_build_flags,
flake_common_flags=flake_common_flags,
) )
case Action.EDIT: 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: case Action.DRY_RUN:
raise AssertionError("DRY_RUN should be a DRY_BUILD alias") raise AssertionError("DRY_RUN should be a DRY_BUILD alias")
@@ -341,8 +335,7 @@ def execute(argv: list[str]) -> None:
services.repl( services.repl(
flake=flake, flake=flake,
build_attr=build_attr, build_attr=build_attr,
flake_build_flags=flake_build_flags, grouped_nix_args=grouped_nix_args,
build_flags=build_flags,
) )
case _: case _:

View File

@@ -1,12 +1,14 @@
import platform import platform
import re import re
import subprocess import subprocess
from argparse import Namespace
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from typing import Any, ClassVar, Self, TypedDict, override from typing import Any, ClassVar, Self, TypedDict, override
from .process import Remote, run_wrapper from .process import Remote, run_wrapper
from .utils import Args
type ImageVariants = dict[str, str] type ImageVariants = dict[str, str]
@@ -143,6 +145,35 @@ class GenerationJson(TypedDict):
current: bool 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) @dataclass(frozen=True)
class Profile: class Profile:
name: str name: str

View File

@@ -8,9 +8,17 @@ from typing import Final
from . import nix, tmpdir from . import nix, tmpdir
from .constants import EXECUTABLE 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 .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_ATTR: Final = "config.system.build.nixos-rebuild"
NIXOS_REBUILD_REEXEC_ENV: Final = "_NIXOS_REBUILD_REEXEC" NIXOS_REBUILD_REEXEC_ENV: Final = "_NIXOS_REBUILD_REEXEC"
@@ -21,8 +29,7 @@ logger: Final = logging.getLogger(__name__)
def reexec( def reexec(
argv: list[str], argv: list[str],
args: argparse.Namespace, args: argparse.Namespace,
build_flags: Args, grouped_nix_args: GroupedNixArgs,
flake_build_flags: Args,
) -> None: ) -> None:
if os.environ.get(NIXOS_REBUILD_REEXEC_ENV): if os.environ.get(NIXOS_REBUILD_REEXEC_ENV):
return return
@@ -36,14 +43,14 @@ def reexec(
drv = nix.build_flake( drv = nix.build_flake(
NIXOS_REBUILD_ATTR, NIXOS_REBUILD_ATTR,
flake, flake,
flake_build_flags | {"no_link": True}, grouped_nix_args.flake_build_flags | {"no_link": True},
) )
else: else:
build_attr = BuildAttr.from_arg(args.attr, args.file) build_attr = BuildAttr.from_arg(args.attr, args.file)
drv = nix.build( drv = nix.build(
NIXOS_REBUILD_ATTR, NIXOS_REBUILD_ATTR,
build_attr, build_attr,
build_flags | {"no_out_link": True}, grouped_nix_args.build_flags | {"no_out_link": True},
) )
if drv: if drv:
@@ -88,21 +95,20 @@ def _get_system_attr(
args: argparse.Namespace, args: argparse.Namespace,
flake: Flake | None, flake: Flake | None,
build_attr: BuildAttr, build_attr: BuildAttr,
common_flags: Args, grouped_nix_args: GroupedNixArgs,
flake_common_flags: Args,
) -> str: ) -> str:
match action: match action:
case Action.BUILD_IMAGE if flake: case Action.BUILD_IMAGE if flake:
variants = nix.get_build_image_variants_flake( variants = nix.get_build_image_variants_flake(
flake, flake,
eval_flags=flake_common_flags, eval_flags=grouped_nix_args.flake_common_flags,
) )
_validate_image_variant(args.image_variant, variants) _validate_image_variant(args.image_variant, variants)
attr = f"config.system.build.images.{args.image_variant}" attr = f"config.system.build.images.{args.image_variant}"
case Action.BUILD_IMAGE: case Action.BUILD_IMAGE:
variants = nix.get_build_image_variants( variants = nix.get_build_image_variants(
build_attr, build_attr,
instantiate_flags=common_flags, instantiate_flags=grouped_nix_args.common_flags,
) )
_validate_image_variant(args.image_variant, variants) _validate_image_variant(args.image_variant, variants)
attr = f"config.system.build.images.{args.image_variant}" attr = f"config.system.build.images.{args.image_variant}"
@@ -146,11 +152,7 @@ def _build_system(
target_host: Remote | None, target_host: Remote | None,
flake: Flake | None, flake: Flake | None,
build_attr: BuildAttr, build_attr: BuildAttr,
build_flags: Args, grouped_nix_args: GroupedNixArgs,
common_flags: Args,
copy_flags: Args,
flake_build_flags: Args,
flake_common_flags: Args,
) -> Path: ) -> Path:
dry_run = action == Action.DRY_BUILD dry_run = action == Action.DRY_BUILD
# actions that we will not add a /result symlink in CWD # actions that we will not add a /result symlink in CWD
@@ -162,32 +164,33 @@ def _build_system(
attr, attr,
flake, flake,
build_host, build_host,
eval_flags=flake_common_flags, eval_flags=grouped_nix_args.flake_common_flags,
flake_build_flags=flake_build_flags flake_build_flags={"no_link": no_link, "dry_run": dry_run}
| {"no_link": no_link, "dry_run": dry_run}, | grouped_nix_args.flake_build_flags,
copy_flags=copy_flags, copy_flags=grouped_nix_args.copy_flags,
) )
case (None, Flake(_)): case (None, Flake(_)):
path_to_config = nix.build_flake( path_to_config = nix.build_flake(
attr, attr,
flake, flake,
flake_build_flags=flake_build_flags flake_build_flags={"no_link": no_link, "dry_run": dry_run}
| {"no_link": no_link, "dry_run": dry_run}, | grouped_nix_args.flake_build_flags,
) )
case (Remote(_), None): case (Remote(_), None):
path_to_config = nix.build_remote( path_to_config = nix.build_remote(
attr, attr,
build_attr, build_attr,
build_host, build_host,
realise_flags=common_flags, realise_flags=grouped_nix_args.common_flags,
instantiate_flags=build_flags, instantiate_flags=grouped_nix_args.build_flags,
copy_flags=copy_flags, copy_flags=grouped_nix_args.copy_flags,
) )
case (None, None): case (None, None):
path_to_config = nix.build( path_to_config = nix.build(
attr, attr,
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 # In dry_run mode there is nothing to copy
@@ -197,7 +200,7 @@ def _build_system(
path_to_config, path_to_config,
to_host=target_host, to_host=target_host,
from_host=build_host, from_host=build_host,
copy_flags=copy_flags, copy_flags=grouped_nix_args.copy_flags,
) )
return path_to_config return path_to_config
@@ -211,8 +214,7 @@ def _activate_system(
profile: Profile, profile: Profile,
flake: Flake | None, flake: Flake | None,
build_attr: BuildAttr, build_attr: BuildAttr,
flake_common_flags: Args, grouped_nix_args: GroupedNixArgs,
common_flags: Args,
) -> None: ) -> None:
# Print only the result to stdout to make it easier to script # Print only the result to stdout to make it easier to script
def print_result(msg: str, result: str | Path) -> None: def print_result(msg: str, result: str | Path) -> None:
@@ -257,13 +259,13 @@ def _activate_system(
image_name = nix.get_build_image_name_flake( image_name = nix.get_build_image_name_flake(
flake, flake,
args.image_variant, args.image_variant,
eval_flags=flake_common_flags, eval_flags=grouped_nix_args.flake_common_flags,
) )
else: else:
image_name = nix.get_build_image_name( image_name = nix.get_build_image_name(
build_attr, build_attr,
args.image_variant, args.image_variant,
instantiate_flags=common_flags, instantiate_flags=grouped_nix_args.common_flags,
) )
disk_path = path_to_config / image_name disk_path = path_to_config / image_name
print_result("Done. The disk image can be found in", disk_path) print_result("Done. The disk image can be found in", disk_path)
@@ -277,11 +279,7 @@ def build_and_activate_system(
profile: Profile, profile: Profile,
flake: Flake | None, flake: Flake | None,
build_attr: BuildAttr, build_attr: BuildAttr,
build_flags: Args, grouped_nix_args: GroupedNixArgs,
common_flags: Args,
copy_flags: Args,
flake_build_flags: Args,
flake_common_flags: Args,
) -> None: ) -> None:
logger.info("building the system configuration...") logger.info("building the system configuration...")
attr = _get_system_attr( attr = _get_system_attr(
@@ -289,8 +287,7 @@ def build_and_activate_system(
args=args, args=args,
flake=flake, flake=flake,
build_attr=build_attr, build_attr=build_attr,
common_flags=common_flags, grouped_nix_args=grouped_nix_args,
flake_common_flags=flake_common_flags,
) )
if args.rollback: if args.rollback:
@@ -308,11 +305,7 @@ def build_and_activate_system(
target_host=target_host, target_host=target_host,
flake=flake, flake=flake,
build_attr=build_attr, build_attr=build_attr,
build_flags=build_flags, grouped_nix_args=grouped_nix_args,
common_flags=common_flags,
copy_flags=copy_flags,
flake_build_flags=flake_build_flags,
flake_common_flags=flake_common_flags,
) )
_activate_system( _activate_system(
@@ -323,14 +316,13 @@ def build_and_activate_system(
profile=profile, profile=profile,
flake=flake, flake=flake,
build_attr=build_attr, build_attr=build_attr,
common_flags=common_flags, grouped_nix_args=grouped_nix_args,
flake_common_flags=flake_common_flags,
) )
def edit(flake: Flake | None, flake_build_flags: Args | None = None) -> None: def edit(flake: Flake | None, grouped_nix_args: GroupedNixArgs) -> None:
if flake: if flake:
nix.edit_flake(flake, flake_build_flags) nix.edit_flake(flake, grouped_nix_args.flake_build_flags)
else: else:
nix.edit() nix.edit()
@@ -358,17 +350,16 @@ def list_generations(
def repl( def repl(
flake: Flake | None, flake: Flake | None,
build_attr: BuildAttr, build_attr: BuildAttr,
flake_build_flags: Args, grouped_nix_args: GroupedNixArgs,
build_flags: Args,
) -> None: ) -> None:
if flake: if flake:
nix.repl_flake(flake, flake_build_flags) nix.repl_flake(flake, grouped_nix_args.flake_build_flags)
else: else:
nix.repl(build_attr, build_flags) nix.repl(build_attr, grouped_nix_args.build_flags)
def write_version_suffix(build_flags: Args) -> None: def write_version_suffix(grouped_nix_args: GroupedNixArgs) -> None:
nixpkgs_path = nix.find_file("nixpkgs", build_flags) nixpkgs_path = nix.find_file("nixpkgs", grouped_nix_args.build_flags)
rev = nix.get_nixpkgs_rev(nixpkgs_path) rev = nix.get_nixpkgs_rev(nixpkgs_path)
if nixpkgs_path and rev: if nixpkgs_path and rev:
try: try:

View File

@@ -65,8 +65,7 @@ def test_parse_args() -> None:
assert r1.install_grub is True assert r1.install_grub is True
assert r1.profile_name == "system" assert r1.profile_name == "system"
assert r1.action == "switch" assert r1.action == "switch"
# round-trip test (ensure that we have the same flags as parsed) assert nr.utils.dict_to_flags(g1.common_flags) == [
assert nr.utils.dict_to_flags(vars(g1["common_flags"])) == [
"--option", "--option",
"foo1", "foo1",
"bar1", "bar1",
@@ -74,7 +73,13 @@ def test_parse_args() -> None:
"foo2", "foo2",
"bar2", "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", "--update-input",
"input1", "input1",
"--update-input", "--update-input",
@@ -112,13 +117,15 @@ def test_parse_args() -> None:
assert r2.action == "dry-build" assert r2.action == "dry-build"
assert r2.file == "foo" assert r2.file == "foo"
assert r2.attr == "bar" assert r2.attr == "bar"
# round-trip test (ensure that we have the same flags as parsed) assert nr.utils.dict_to_flags(g2.common_flags) == [
assert nr.utils.dict_to_flags(vars(g2["common_flags"])) == [
"-vvv", "-vvv",
"--quiet", "--quiet",
"--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", "--include",
"include1", "include1",
"--include", "--include",
@@ -178,8 +185,8 @@ def test_execute_nix_boot(mock_run: Mock, tmp_path: Path) -> None:
"<nixpkgs/nixos>", "<nixpkgs/nixos>",
"--attr", "--attr",
"config.system.build.toplevel", "config.system.build.toplevel",
"-vvv",
"--no-out-link", "--no-out-link",
"-vvv",
], ],
check=True, check=True,
stdout=PIPE, stdout=PIPE,
@@ -222,6 +229,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.dict(os.environ, {}, clear=True)
@patch("subprocess.run", autospec=True) @patch("subprocess.run", autospec=True)
def test_execute_nix_build_vm(mock_run: Mock, tmp_path: Path) -> None: def test_execute_nix_build_vm(mock_run: Mock, tmp_path: Path) -> None:
@@ -392,11 +442,11 @@ def test_execute_nix_switch_flake(mock_run: Mock, tmp_path: Path) -> None:
"build", "build",
"--print-out-paths", "--print-out-paths",
'/path/to/config#nixosConfigurations."hostname".config.system.build.toplevel', '/path/to/config#nixosConfigurations."hostname".config.system.build.toplevel',
"--no-link",
"-v", "-v",
"--option", "--option",
"narinfo-cache-negative-ttl", "narinfo-cache-negative-ttl",
"1200", "1200",
"--no-link",
], ],
check=True, check=True,
stdout=PIPE, stdout=PIPE,

View File

@@ -19,7 +19,14 @@ def test_reexec(mock_build: Mock, mock_execve: Mock, monkeypatch: MonkeyPatch) -
args, _ = n.parse_args(argv) args, _ = n.parse_args(argv)
mock_build.return_value = Path("/path") 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( mock_build.assert_has_calls(
[ [
call( call(
@@ -34,7 +41,7 @@ def test_reexec(mock_build: Mock, mock_execve: Mock, monkeypatch: MonkeyPatch) -
mock_build.return_value = Path("/path/new") mock_build.return_value = Path("/path/new")
s.reexec(argv, args, {}, {}) s.reexec(argv, args, grouped_nix_args)
# exec in the new version successfully # exec in the new version successfully
mock_execve.assert_called_once_with( mock_execve.assert_called_once_with(
Path("/path/new/bin/nixos-rebuild-ng"), 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.reset_mock()
mock_execve.side_effect = [OSError("BOOM"), None] 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 # exec in the previous version if the new version fails
mock_execve.assert_any_call( mock_execve.assert_any_call(
Path("/path/bin/nixos-rebuild-ng"), Path("/path/bin/nixos-rebuild-ng"),
@@ -65,18 +72,25 @@ def test_reexec_flake(
args, _ = n.parse_args(argv) args, _ = n.parse_args(argv)
mock_build.return_value = Path("/path") 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( mock_build.assert_called_once_with(
s.NIXOS_REBUILD_ATTR, s.NIXOS_REBUILD_ATTR,
n.models.Flake(ANY, ANY), 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 # do not exec if there is no new version
mock_execve.assert_not_called() mock_execve.assert_not_called()
mock_build.return_value = Path("/path/new") mock_build.return_value = Path("/path/new")
s.reexec(argv, args, {}, {}) s.reexec(argv, args, grouped_nix_args)
# exec in the new version successfully # exec in the new version successfully
mock_execve.assert_called_once_with( mock_execve.assert_called_once_with(
Path("/path/new/bin/nixos-rebuild-ng"), Path("/path/new/bin/nixos-rebuild-ng"),
@@ -87,7 +101,7 @@ def test_reexec_flake(
mock_execve.reset_mock() mock_execve.reset_mock()
mock_execve.side_effect = [OSError("BOOM"), None] 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 # exec in the previous version if the new version fails
mock_execve.assert_any_call( mock_execve.assert_any_call(
Path("/path/bin/nixos-rebuild-ng"), 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) args, _ = n.parse_args(argv)
mock_build.return_value = Path("/path") 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_build.assert_not_called()
mock_execve.assert_not_called() mock_execve.assert_not_called()

View File

@@ -16,18 +16,18 @@
rustPlatform.buildRustPackage (finalAttrs: { rustPlatform.buildRustPackage (finalAttrs: {
pname = "ruff"; pname = "ruff";
version = "0.14.3"; version = "0.14.4";
src = fetchFromGitHub { src = fetchFromGitHub {
owner = "astral-sh"; owner = "astral-sh";
repo = "ruff"; repo = "ruff";
tag = finalAttrs.version; tag = finalAttrs.version;
hash = "sha256-iYXZyB0s3rlGV3HQLN1fuAohFUm/53VLAwA3Ahj6HzM="; hash = "sha256-jRH7OOT03MDomZAJM20+J4y5+xjN1ZAV27Z44O1qCEQ=";
}; };
cargoBuildFlags = [ "--package=ruff" ]; cargoBuildFlags = [ "--package=ruff" ];
cargoHash = "sha256-dYXFNe+nglKelgzi2Afo0AJyt53qfCAJ7reTMMfjWOI="; cargoHash = "sha256-eY7QnKVrkXaNRWMaTxigNo0kf0oK9DQU4z9x4wC3Npw=";
nativeBuildInputs = [ installShellFiles ]; nativeBuildInputs = [ installShellFiles ];