staging-nixos merge for 2025-11-12 (#460995)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
${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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 { };
|
||||
|
||||
37
nixos/tests/systemd-user-linger-purge.nix
Normal file
37
nixos/tests/systemd-user-linger-purge.nix
Normal 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")
|
||||
'';
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
{ lib, ... }:
|
||||
{
|
||||
rec {
|
||||
name = "systemd-user-linger";
|
||||
|
||||
nodes.machine =
|
||||
{ ... }:
|
||||
{
|
||||
nodes.machine = {
|
||||
users.users = {
|
||||
alice = {
|
||||
isNormalUser = true;
|
||||
@@ -15,23 +12,28 @@
|
||||
bob = {
|
||||
isNormalUser = true;
|
||||
linger = false;
|
||||
uid = 10001;
|
||||
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")
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
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 _:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 ];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user