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.
- `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.

View File

@@ -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.
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 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: {

View File

@@ -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;

View File

@@ -1522,6 +1522,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 { };

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

View File

@@ -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 = [

View File

@@ -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.

View File

@@ -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
@@ -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 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)
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 _:

View File

@@ -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

View File

@@ -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,
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 _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:

View File

@@ -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 test_execute_nix_boot(mock_run: Mock, tmp_path: Path) -> None:
"<nixpkgs/nixos>",
"--attr",
"config.system.build.toplevel",
"-vvv",
"--no-out-link",
"-vvv",
],
check=True,
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("subprocess.run", autospec=True)
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",
"--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,

View File

@@ -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()

View File

@@ -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 ];