From 385085d8f01673335b2be2d127098077c8d53b06 Mon Sep 17 00:00:00 2001 From: Arnout Engelen Date: Tue, 18 Nov 2025 19:46:12 +0100 Subject: [PATCH 01/14] nixos-rebuild-ng: use remote journalctl instead of pipe for logs Notably, `--pipe` would cause `nixos-rebuild-ng` to panic when stderr closes. That's especially painful when there's changes to the systemd unit that manages the network connection, since that wouldn't come back up. This materially improves the reliability of 'nixos-rebuild switch' and while it adds a bit of complexity it doesn't seem too bad. NixOS test coverage of nixos-rebuild is also pretty OK so that gives confidence it won't break too much, except for use cases that rely on passing information from the 'deployer' to the 'target' machine via stdin, which seems like an obscure pattern. Fixes #462179 --- .../nixos-rebuild-ng/src/nixos_rebuild/nix.py | 116 +++++++-- .../src/nixos_rebuild/process.py | 78 +++++- .../nixos-rebuild-ng/src/tests/test_main.py | 103 ++++++-- .../ni/nixos-rebuild-ng/src/tests/test_nix.py | 230 ++++++++++-------- 4 files changed, 369 insertions(+), 158 deletions(-) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py index b1da9ef555dc..6598c69f259e 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py @@ -25,12 +25,14 @@ from .models import ( Profile, Remote, ) -from .process import PRESERVE_ENV, SSH_DEFAULT_OPTS, run_wrapper +from .process import PRESERVE_ENV, SSH_DEFAULT_OPTS, run_wrapper, run_wrapper_bg +from .process import Args as ProcessArgs from .utils import Args, dict_to_flags FLAKE_FLAGS: Final = ["--extra-experimental-features", "nix-command flakes"] FLAKE_REPL_TEMPLATE: Final = "repl.nix.template" -SWITCH_TO_CONFIGURATION_CMD_PREFIX: Final = [ +SYSTEMD_RUN_UNIT_PREFIX: Final = "nixos-rebuild-switch-to-configuration" +SYSTEMD_RUN_CMD_PREFIX: Final = [ "systemd-run", "-E", # Will be set to new value early in switch-to-configuration script, @@ -41,11 +43,10 @@ SWITCH_TO_CONFIGURATION_CMD_PREFIX: Final = [ "-E", "NIXOS_NO_CHECK", "--collect", + "--wait", "--no-ask-password", - "--pipe", "--quiet", "--service-type=exec", - "--unit=nixos-rebuild-switch-to-configuration", ] logger: Final = logging.getLogger(__name__) @@ -663,6 +664,78 @@ def set_profile( ) +def _has_systemd(target_host: Remote | None) -> bool: + r = run_wrapper( + ["test", "-d", "/run/systemd/system"], + remote=target_host, + check=False, + ) + return r.returncode == 0 + + +def _run_action( + action: Literal[Action.SWITCH, Action.BOOT, Action.TEST, Action.DRY_ACTIVATE], + path_to_config: Path, + install_bootloader: bool, + target_host: Remote | None, + sudo: bool, + prefix: ProcessArgs | None = None, +) -> None: + cmd: ProcessArgs = [path_to_config / "bin/switch-to-configuration", str(action)] + if prefix: + cmd = [*prefix, *cmd] + + run_wrapper( + cmd, + env={ + "LOCALE_ARCHIVE": PRESERVE_ENV, + "NIXOS_NO_CHECK": PRESERVE_ENV, + "NIXOS_INSTALL_BOOTLOADER": "1" if install_bootloader else "0", + }, + remote=target_host, + sudo=sudo, + ) + + +def _run_action_with_systemd( + action: Literal[Action.SWITCH, Action.BOOT, Action.TEST, Action.DRY_ACTIVATE], + path_to_config: Path, + install_bootloader: bool, + target_host: Remote | None, + sudo: bool, +) -> None: + unique_unit_name = SYSTEMD_RUN_UNIT_PREFIX + "-" + uuid.uuid4().hex[:8] + journalctl = run_wrapper_bg( + [ + "journalctl", + "-f", + f"--unit={unique_unit_name}", + "--output=cat", + ], + remote=target_host, + sudo=sudo, + ) + + try: + _run_action( + action, + path_to_config, + install_bootloader, + target_host, + sudo, + prefix=[*SYSTEMD_RUN_CMD_PREFIX, f"--unit={unique_unit_name}"], + ) + except KeyboardInterrupt: + run_wrapper( + ["systemctl", "stop", unique_unit_name], + remote=target_host, + sudo=sudo, + ) + raise + finally: + journalctl.terminate() + + def switch_to_configuration( path_to_config: Path, action: Literal[Action.SWITCH, Action.BOOT, Action.TEST, Action.DRY_ACTIVATE], @@ -686,29 +759,26 @@ def switch_to_configuration( if not path_to_config.exists(): raise NixOSRebuildError(f"specialisation not found: {specialisation}") - r = run_wrapper( - ["test", "-d", "/run/systemd/system"], - remote=target_host, - check=False, - ) - cmd = SWITCH_TO_CONFIGURATION_CMD_PREFIX - if r.returncode: + if _has_systemd(target_host): + _run_action_with_systemd( + action, + path_to_config, + install_bootloader, + target_host, + sudo, + ) + else: logger.debug( "skipping systemd-run to switch configuration since systemd is " "not working in target host" ) - cmd = [] - - run_wrapper( - [*cmd, path_to_config / "bin/switch-to-configuration", str(action)], - env={ - "LOCALE_ARCHIVE": PRESERVE_ENV, - "NIXOS_NO_CHECK": PRESERVE_ENV, - "NIXOS_INSTALL_BOOTLOADER": "1" if install_bootloader else "0", - }, - remote=target_host, - sudo=sudo, - ) + _run_action( + action, + path_to_config, + install_bootloader, + target_host, + sudo, + ) def upgrade_channels(all_channels: bool = False, sudo: bool = False) -> None: diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py index ac83d92039f7..aa32a20a7fac 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py @@ -9,7 +9,7 @@ from collections.abc import Mapping, Sequence from dataclasses import dataclass from enum import Enum from ipaddress import AddressValueError, IPv6Address -from typing import Final, Literal, Self, TextIO, TypedDict, Unpack, override +from typing import Final, Literal, NamedTuple, Self, TextIO, TypedDict, Unpack, override from . import tmpdir @@ -117,16 +117,18 @@ def cleanup_ssh() -> None: atexit.register(cleanup_ssh) -def run_wrapper( +class _RunParams(NamedTuple): + args: Args + popen_env: dict[str, str] | None + process_input: str | None + + +def _build_run_params( args: Args, - *, - check: bool = True, - env: Mapping[str, EnvValue] | None = None, - remote: Remote | None = None, - sudo: bool = False, - **kwargs: Unpack[RunKwargs], -) -> subprocess.CompletedProcess[str]: - "Wrapper around `subprocess.run` that supports extra functionality." + env: Mapping[str, EnvValue] | None, + remote: Remote | None, + sudo: bool, +) -> _RunParams: process_input = None run_args: list[Arg] = list(args) final_args: list[Arg] @@ -188,9 +190,63 @@ def run_wrapper( final_args = run_args popen_env = None if env is None else resolved_env + return _RunParams(final_args, popen_env, process_input) + + +def run_wrapper_bg( + args: Args, + env: Mapping[str, EnvValue] | None = None, + remote: Remote | None = None, + sudo: bool = False, +) -> subprocess.Popen[str]: + "Wrapper around `subprocess.Popen` that supports extra functionality." + (final_args, popen_env, process_input) = _build_run_params(args, env, remote, sudo) + + logger.debug( + "calling Popen with args=%r", + _sanitize_env_run_args(list(final_args)), + ) + + if process_input: + stdin = subprocess.PIPE + else: + stdin = None + + r = subprocess.Popen( + final_args, + env=popen_env, + stdin=stdin, + # Hope nobody is using NixOS with non-UTF8 encodings, but + # "surrogateescape" should still work in those systems. + text=True, + errors="surrogateescape", + ) + + if r.stdin and process_input: + r.stdin.write(process_input) + r.stdin.write("\n") + r.stdin.close() + + return r + + +def run_wrapper( + args: Args, + *, + check: bool = True, + env: Mapping[str, EnvValue] | None = None, + remote: Remote | None = None, + sudo: bool = False, + **kwargs: Unpack[RunKwargs], +) -> subprocess.CompletedProcess[str]: + "Wrapper around `subprocess.run` that supports extra functionality." + (final_args, popen_env, process_input) = _build_run_params( + list(args), env, remote, sudo + ) + logger.debug( "calling run with args=%r, kwargs=%r, env=%r", - _sanitize_env_run_args(remote_run_args if remote else run_args), + _sanitize_env_run_args(list(final_args)), kwargs, env, ) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py index 7f51c09d7497..986f6883a42d 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py @@ -166,8 +166,15 @@ def test_parse_args() -> None: {"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"}, clear=True, ) +@patch("uuid.uuid4") @patch("subprocess.run", autospec=True) -def test_execute_nix_boot(mock_run: Mock, tmp_path: Path) -> None: +@patch("subprocess.Popen") +def test_execute_nix_boot( + mock_popen: Mock, mock_run: Mock, mock_uuid4: Mock, tmp_path: Path +) -> None: + test_uuid = uuid.UUID("58fe9784-f60a-42bc-aa94-eb8f1a7e5c17") + mock_uuid4.return_value = test_uuid + nixpkgs_path = tmp_path / "nixpkgs" (nixpkgs_path / ".git").mkdir(parents=True) config_path = tmp_path / "test" @@ -238,7 +245,8 @@ def test_execute_nix_boot(mock_run: Mock, tmp_path: Path) -> None: ), call( [ - *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, + *nr.nix.SYSTEMD_RUN_CMD_PREFIX, + f"--unit={nr.nix.SYSTEMD_RUN_UNIT_PREFIX}-{test_uuid.hex[:8]}", config_path / "bin/switch-to-configuration", "boot", ], @@ -259,7 +267,8 @@ 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: +@patch("subprocess.Popen") +def test_execute_nix_build(mock_popen: Mock, mock_run: Mock, tmp_path: Path) -> None: config_path = tmp_path / "test" config_path.touch() @@ -301,7 +310,8 @@ def test_execute_nix_build(mock_run: Mock, tmp_path: Path) -> None: @patch.dict(os.environ, {}, clear=True) @patch("subprocess.run", autospec=True) -def test_execute_nix_build_vm(mock_run: Mock, tmp_path: Path) -> None: +@patch("subprocess.Popen") +def test_execute_nix_build_vm(mock_popen: Mock, mock_run: Mock, tmp_path: Path) -> None: config_path = tmp_path / "test" config_path.touch() @@ -350,7 +360,10 @@ def test_execute_nix_build_vm(mock_run: Mock, tmp_path: Path) -> None: @patch.dict(os.environ, {}, clear=True) @patch("subprocess.run", autospec=True) -def test_execute_nix_build_image_flake(mock_run: Mock, tmp_path: Path) -> None: +@patch("subprocess.Popen") +def test_execute_nix_build_image_flake( + mock_popen: Mock, mock_run: Mock, tmp_path: Path +) -> None: config_path = tmp_path / "test" config_path.touch() @@ -432,11 +445,18 @@ def test_execute_nix_build_image_flake(mock_run: Mock, tmp_path: Path) -> None: {"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"}, clear=True, ) +@patch("uuid.uuid4") @patch("subprocess.run", autospec=True) -def test_execute_nix_switch_flake(mock_run: Mock, tmp_path: Path) -> None: +@patch("subprocess.Popen") +def test_execute_nix_switch_flake( + mock_popen: Mock, mock_run: Mock, mock_uuid4: Mock, tmp_path: Path +) -> None: config_path = tmp_path / "test" config_path.touch() + test_uuid = uuid.UUID("58fe9784-f60a-42bc-aa94-eb8f1a7e5c17") + mock_uuid4.return_value = test_uuid + def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]: if args[0] == "nix": return CompletedProcess([], 0, str(config_path)) @@ -506,7 +526,8 @@ def test_execute_nix_switch_flake(mock_run: Mock, tmp_path: Path) -> None: "env", "-i", "NIXOS_INSTALL_BOOTLOADER=1", - *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, + *nr.nix.SYSTEMD_RUN_CMD_PREFIX, + f"--unit={nr.nix.SYSTEMD_RUN_UNIT_PREFIX}-{test_uuid.hex[:8]}", config_path / "bin/switch-to-configuration", "switch", ], @@ -523,11 +544,13 @@ def test_execute_nix_switch_flake(mock_run: Mock, tmp_path: Path) -> None: clear=True, ) @patch("subprocess.run", autospec=True) +@patch("subprocess.Popen") @patch("uuid.uuid4", autospec=True) @patch(get_qualified_name(nr.services.cleanup_ssh), autospec=True) def test_execute_nix_switch_build_target_host( mock_cleanup_ssh: Mock, mock_uuid4: Mock, + mock_popen: Mock, mock_run: Mock, tmp_path: Path, ) -> None: @@ -551,7 +574,8 @@ def test_execute_nix_switch_build_target_host( return CompletedProcess([], 0) mock_run.side_effect = run_side_effect - mock_uuid4.return_value = uuid.UUID(int=0) + test_uuid = uuid.UUID("58fe9784-f60a-42bc-aa94-eb8f1a7e5c17") + mock_uuid4.side_effect = [uuid.UUID(int=0), uuid.UUID(int=1), test_uuid] nr.execute( [ @@ -644,7 +668,7 @@ def test_execute_nix_switch_build_target_host( "--realise", str(config_path), "--add-root", - "/tmp/tmpdir/00000000000000000000000000000000", + "/tmp/tmpdir/00000000000000000000000000000001", ], check=True, stdout=PIPE, @@ -748,7 +772,8 @@ def test_execute_nix_switch_build_target_host( "-c", """'exec env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""", "sh", - *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, + *nr.nix.SYSTEMD_RUN_CMD_PREFIX, + f"--unit={nr.nix.SYSTEMD_RUN_UNIT_PREFIX}-{test_uuid.hex[:8]}", str(config_path / "bin/switch-to-configuration"), "switch", ], @@ -764,13 +789,20 @@ def test_execute_nix_switch_build_target_host( {"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"}, clear=True, ) +@patch("uuid.uuid4") @patch("subprocess.run", autospec=True) +@patch("subprocess.Popen") @patch(get_qualified_name(nr.services.cleanup_ssh), autospec=True) def test_execute_nix_switch_flake_target_host( mock_cleanup_ssh: Mock, + mock_popen: Mock, mock_run: Mock, + mock_uuid4: Mock, tmp_path: Path, ) -> None: + test_uuid = uuid.UUID("58fe9784-f60a-42bc-aa94-eb8f1a7e5c17") + mock_uuid4.return_value = test_uuid + config_path = tmp_path / "test" config_path.touch() @@ -865,7 +897,8 @@ def test_execute_nix_switch_flake_target_host( "-c", """'exec env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""", "sh", - *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, + *nr.nix.SYSTEMD_RUN_CMD_PREFIX, + f"--unit={nr.nix.SYSTEMD_RUN_UNIT_PREFIX}-{test_uuid.hex[:8]}", str(config_path / "bin/switch-to-configuration"), "switch", ], @@ -881,13 +914,20 @@ def test_execute_nix_switch_flake_target_host( {"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"}, clear=True, ) +@patch("uuid.uuid4") @patch("subprocess.run", autospec=True) +@patch("subprocess.Popen") @patch(get_qualified_name(nr.services.cleanup_ssh), autospec=True) def test_execute_nix_switch_flake_build_host( mock_cleanup_ssh: Mock, + mock_popen: Mock, mock_run: Mock, + mock_uuid4: Mock, tmp_path: Path, ) -> None: + test_uuid = uuid.UUID("58fe9784-f60a-42bc-aa94-eb8f1a7e5c17") + mock_uuid4.return_value = test_uuid + config_path = tmp_path / "test" config_path.touch() @@ -984,7 +1024,8 @@ def test_execute_nix_switch_flake_build_host( ), call( [ - *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, + *nr.nix.SYSTEMD_RUN_CMD_PREFIX, + f"--unit={nr.nix.SYSTEMD_RUN_UNIT_PREFIX}-{test_uuid.hex[:8]}", config_path / "bin/switch-to-configuration", "switch", ], @@ -996,7 +1037,10 @@ def test_execute_nix_switch_flake_build_host( @patch("subprocess.run", autospec=True) -def test_execute_switch_rollback(mock_run: Mock, tmp_path: Path) -> None: +@patch("subprocess.Popen") +def test_execute_switch_rollback( + mock_popen: Mock, mock_run: Mock, tmp_path: Path +) -> None: nixpkgs_path = tmp_path / "nixpkgs" (nixpkgs_path / ".git").mkdir(parents=True) @@ -1073,7 +1117,8 @@ def test_execute_switch_rollback(mock_run: Mock, tmp_path: Path) -> None: @patch("subprocess.run", autospec=True) -def test_execute_build(mock_run: Mock, tmp_path: Path) -> None: +@patch("subprocess.Popen") +def test_execute_build(mock_popen: Mock, mock_run: Mock, tmp_path: Path) -> None: config_path = tmp_path / "test" config_path.touch() mock_run.side_effect = [ @@ -1102,8 +1147,9 @@ def test_execute_build(mock_run: Mock, tmp_path: Path) -> None: @patch("subprocess.run", autospec=True) +@patch("subprocess.Popen") def test_execute_build_dry_run_build_and_target_remote( - mock_run: Mock, tmp_path: Path + mock_popen: Mock, mock_run: Mock, tmp_path: Path ) -> None: config_path = tmp_path / "test" config_path.touch() @@ -1174,7 +1220,8 @@ def test_execute_build_dry_run_build_and_target_remote( @patch("subprocess.run", autospec=True) -def test_execute_test_flake(mock_run: Mock, tmp_path: Path) -> None: +@patch("subprocess.Popen") +def test_execute_test_flake(mock_popen: Mock, mock_run: Mock, tmp_path: Path) -> None: config_path = tmp_path / "test" config_path.touch() @@ -1224,11 +1271,13 @@ def test_execute_test_flake(mock_run: Mock, tmp_path: Path) -> None: @patch("subprocess.run", autospec=True) +@patch("subprocess.Popen") @patch("pathlib.Path.exists", autospec=True, return_value=True) @patch("pathlib.Path.mkdir", autospec=True) def test_execute_test_rollback( mock_path_mkdir: Mock, mock_path_exists: Mock, + mock_popen: Mock, mock_run: Mock, ) -> None: def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]: @@ -1291,8 +1340,15 @@ def test_execute_test_rollback( {"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"}, clear=True, ) +@patch("uuid.uuid4", autospec=True) @patch("subprocess.run", autospec=True) -def test_execute_switch_store_path(mock_run: Mock, tmp_path: Path) -> None: +@patch("subprocess.Popen") +def test_execute_switch_store_path( + mock_popen: Mock, mock_run: Mock, mock_uuid4: Mock, tmp_path: Path +) -> None: + test_uuid = uuid.UUID("58fe9784-f60a-42bc-aa94-eb8f1a7e5c17") + mock_uuid4.return_value = test_uuid + config_path = tmp_path / "test-system" config_path.mkdir() @@ -1330,7 +1386,8 @@ def test_execute_switch_store_path(mock_run: Mock, tmp_path: Path) -> None: ), call( [ - *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, + *nr.nix.SYSTEMD_RUN_CMD_PREFIX, + f"--unit={nr.nix.SYSTEMD_RUN_UNIT_PREFIX}-{test_uuid.hex[:8]}", config_path / "bin/switch-to-configuration", "switch", ], @@ -1349,13 +1406,20 @@ def test_execute_switch_store_path(mock_run: Mock, tmp_path: Path) -> None: @patch.dict(os.environ, {}, clear=True) +@patch("uuid.uuid4") @patch("subprocess.run", autospec=True) @patch(get_qualified_name(nr.services.cleanup_ssh), autospec=True) +@patch("subprocess.Popen") def test_execute_switch_store_path_target_host( + mock_popen: Mock, mock_cleanup_ssh: Mock, mock_run: Mock, + mock_uuid4: Mock, tmp_path: Path, ) -> None: + test_uuid = uuid.UUID("58fe9784-f60a-42bc-aa94-eb8f1a7e5c17") + mock_uuid4.return_value = test_uuid + config_path = tmp_path / "test-system" config_path.mkdir() @@ -1448,7 +1512,8 @@ def test_execute_switch_store_path_target_host( "-c", """'exec env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""", "sh", - *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, + *nr.nix.SYSTEMD_RUN_CMD_PREFIX, + f"--unit={nr.nix.SYSTEMD_RUN_UNIT_PREFIX}-{test_uuid.hex[:8]}", str(config_path / "bin/switch-to-configuration"), "switch", ], diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py index 8a69fd89fad7..60c6afa59c9d 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py @@ -2,7 +2,7 @@ import sys import textwrap import uuid from pathlib import Path -from subprocess import PIPE, CompletedProcess +from subprocess import PIPE, CompletedProcess, Popen from typing import Any from unittest.mock import ANY, Mock, call, patch @@ -695,139 +695,159 @@ def test_set_profile(mock_run: Mock) -> None: @patch(get_qualified_name(n.run_wrapper, n), autospec=True) +@patch(get_qualified_name(n.run_wrapper_bg, n), autospec=True) def test_switch_to_configuration_without_systemd_run( - mock_run: Any, monkeypatch: MonkeyPatch + mock_run_bg: Mock, mock_run: Mock, monkeypatch: MonkeyPatch ) -> None: profile_path = Path("/path/to/profile") config_path = Path("/path/to/config") - mock_run.return_value = CompletedProcess([], 1) - with monkeypatch.context() as mp: - mp.setenv("LOCALE_ARCHIVE", "") + proc = Popen(["echo"]) + try: + mock_run_bg.return_value = p + mock_run.return_value = CompletedProcess([], 1) - n.switch_to_configuration( - profile_path, - m.Action.SWITCH, + with monkeypatch.context() as mp: + mp.setenv("LOCALE_ARCHIVE", "") + + n.switch_to_configuration( + profile_path, + m.Action.SWITCH, + sudo=False, + target_host=None, + specialisation=None, + install_bootloader=False, + ) + mock_run.assert_called_with( + [profile_path / "bin/switch-to-configuration", "switch"], + env={ + "LOCALE_ARCHIVE": p.PRESERVE_ENV, + "NIXOS_NO_CHECK": p.PRESERVE_ENV, + "NIXOS_INSTALL_BOOTLOADER": "0", + }, sudo=False, - target_host=None, - specialisation=None, - install_bootloader=False, + remote=None, ) - mock_run.assert_called_with( - [profile_path / "bin/switch-to-configuration", "switch"], - env={ - "LOCALE_ARCHIVE": p.PRESERVE_ENV, - "NIXOS_NO_CHECK": p.PRESERVE_ENV, - "NIXOS_INSTALL_BOOTLOADER": "0", - }, - sudo=False, - remote=None, - ) - with pytest.raises(m.NixOSRebuildError) as e: - n.switch_to_configuration( - config_path, - m.Action.BOOT, - sudo=False, - target_host=None, - specialisation="special", + with pytest.raises(m.NixOSRebuildError) as e: + n.switch_to_configuration( + config_path, + m.Action.BOOT, + sudo=False, + target_host=None, + specialisation="special", + ) + assert ( + str(e.value) + == "error: '--specialisation' can only be used with 'switch' and 'test'" ) - assert ( - str(e.value) - == "error: '--specialisation' can only be used with 'switch' and 'test'" - ) - target_host = m.Remote("user@localhost", [], None) - with monkeypatch.context() as mp: - mp.setenv("LOCALE_ARCHIVE", "/path/to/locale") - mp.setenv("PATH", "/path/to/bin") - mp.setattr(Path, Path.exists.__name__, lambda self: True) + target_host = m.Remote("user@localhost", [], None) + with monkeypatch.context() as mp: + mp.setenv("LOCALE_ARCHIVE", "/path/to/locale") + mp.setenv("PATH", "/path/to/bin") + mp.setattr(Path, Path.exists.__name__, lambda self: True) - n.switch_to_configuration( - Path("/path/to/config"), - m.Action.TEST, + n.switch_to_configuration( + Path("/path/to/config"), + m.Action.TEST, + sudo=True, + target_host=target_host, + install_bootloader=True, + specialisation="special", + ) + mock_run.assert_called_with( + [ + config_path / "specialisation/special/bin/switch-to-configuration", + "test", + ], + env={ + "LOCALE_ARCHIVE": p.PRESERVE_ENV, + "NIXOS_NO_CHECK": p.PRESERVE_ENV, + "NIXOS_INSTALL_BOOTLOADER": "1", + }, sudo=True, - target_host=target_host, - install_bootloader=True, - specialisation="special", + remote=target_host, ) - mock_run.assert_called_with( - [ - config_path / "specialisation/special/bin/switch-to-configuration", - "test", - ], - env={ - "LOCALE_ARCHIVE": p.PRESERVE_ENV, - "NIXOS_NO_CHECK": p.PRESERVE_ENV, - "NIXOS_INSTALL_BOOTLOADER": "1", - }, - sudo=True, - remote=target_host, - ) + finally: + proc.communicate(timeout=1) +@patch("uuid.uuid4") @patch(get_qualified_name(n.run_wrapper, n), autospec=True) +@patch(get_qualified_name(n.run_wrapper_bg, n), autospec=True) def test_switch_to_configuration_with_systemd_run( - mock_run: Mock, monkeypatch: MonkeyPatch + mock_run_bg: Mock, mock_run: Mock, mock_uuid4: Mock, monkeypatch: MonkeyPatch ) -> None: + test_uuid = uuid.UUID("58fe9784-f60a-42bc-aa94-eb8f1a7e5c17") + mock_uuid4.return_value = test_uuid + profile_path = Path("/path/to/profile") config_path = Path("/path/to/config") - mock_run.return_value = CompletedProcess([], 0) - with monkeypatch.context() as mp: - mp.setenv("LOCALE_ARCHIVE", "") + proc = Popen(["echo"]) + try: + mock_run_bg.return_value = proc + mock_run.return_value = CompletedProcess([], 0) - n.switch_to_configuration( - profile_path, - m.Action.SWITCH, + with monkeypatch.context() as mp: + mp.setenv("LOCALE_ARCHIVE", "") + + n.switch_to_configuration( + profile_path, + m.Action.SWITCH, + sudo=False, + target_host=None, + specialisation=None, + install_bootloader=False, + ) + mock_run.assert_called_with( + [ + *n.SYSTEMD_RUN_CMD_PREFIX, + f"--unit={n.SYSTEMD_RUN_UNIT_PREFIX}-{test_uuid.hex[:8]}", + profile_path / "bin/switch-to-configuration", + "switch", + ], + env={ + "LOCALE_ARCHIVE": p.PRESERVE_ENV, + "NIXOS_NO_CHECK": p.PRESERVE_ENV, + "NIXOS_INSTALL_BOOTLOADER": "0", + }, sudo=False, - target_host=None, - specialisation=None, - install_bootloader=False, + remote=None, ) - mock_run.assert_called_with( - [ - *n.SWITCH_TO_CONFIGURATION_CMD_PREFIX, - profile_path / "bin/switch-to-configuration", - "switch", - ], - env={ - "LOCALE_ARCHIVE": p.PRESERVE_ENV, - "NIXOS_NO_CHECK": p.PRESERVE_ENV, - "NIXOS_INSTALL_BOOTLOADER": "0", - }, - sudo=False, - remote=None, - ) - target_host = m.Remote("user@localhost", [], None) - with monkeypatch.context() as mp: - mp.setenv("LOCALE_ARCHIVE", "/path/to/locale") - mp.setenv("PATH", "/path/to/bin") - mp.setattr(Path, Path.exists.__name__, lambda self: True) + target_host = m.Remote("user@localhost", [], None) + with monkeypatch.context() as mp: + mp.setenv("LOCALE_ARCHIVE", "/path/to/locale") + mp.setenv("PATH", "/path/to/bin") + mp.setattr(Path, Path.exists.__name__, lambda self: True) - n.switch_to_configuration( - Path("/path/to/config"), - m.Action.TEST, + n.switch_to_configuration( + Path("/path/to/config"), + m.Action.TEST, + sudo=True, + target_host=target_host, + install_bootloader=True, + specialisation="special", + ) + mock_run.assert_called_with( + [ + *n.SYSTEMD_RUN_CMD_PREFIX, + f"--unit={n.SYSTEMD_RUN_UNIT_PREFIX}-{test_uuid.hex[:8]}", + config_path / "specialisation/special/bin/switch-to-configuration", + "test", + ], + env={ + "LOCALE_ARCHIVE": p.PRESERVE_ENV, + "NIXOS_NO_CHECK": p.PRESERVE_ENV, + "NIXOS_INSTALL_BOOTLOADER": "1", + }, sudo=True, - target_host=target_host, - install_bootloader=True, - specialisation="special", + remote=target_host, ) - mock_run.assert_called_with( - [ - *n.SWITCH_TO_CONFIGURATION_CMD_PREFIX, - config_path / "specialisation/special/bin/switch-to-configuration", - "test", - ], - env={ - "LOCALE_ARCHIVE": p.PRESERVE_ENV, - "NIXOS_NO_CHECK": p.PRESERVE_ENV, - "NIXOS_INSTALL_BOOTLOADER": "1", - }, - sudo=True, - remote=target_host, - ) + finally: + proc.communicate(timeout=1) @patch( From 15acaca6c64e3aa7328a286825f2091c82384c3b Mon Sep 17 00:00:00 2001 From: Arnout Engelen Date: Wed, 17 Dec 2025 17:42:33 +0100 Subject: [PATCH 02/14] nixos/tests/nixos-rebuild-target-host-interrupted: introduced Nixos test for the scenario that the connection breaks during the update --- nixos/tests/all-tests.nix | 3 + .../nixos-rebuild-target-host-interrupted.nix | 237 ++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 nixos/tests/nixos-rebuild-target-host-interrupted.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 34cc5e601125..623400a21787 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1128,6 +1128,9 @@ in nixos-rebuild-target-host = runTest { imports = [ ./nixos-rebuild-target-host.nix ]; }; + nixos-rebuild-target-host-interrupted = runTest { + imports = [ ./nixos-rebuild-target-host-interrupted.nix ]; + }; nixpkgs = pkgs.callPackage ../modules/misc/nixpkgs/test.nix { inherit evalMinimalConfig; }; nixpkgs-config-allow-unfree = pkgs.callPackage ../modules/misc/nixpkgs/test-nixpkgs-config-allow-unfree.nix diff --git a/nixos/tests/nixos-rebuild-target-host-interrupted.nix b/nixos/tests/nixos-rebuild-target-host-interrupted.nix new file mode 100644 index 000000000000..e780019cc68d --- /dev/null +++ b/nixos/tests/nixos-rebuild-target-host-interrupted.nix @@ -0,0 +1,237 @@ +{ hostPkgs, ... }: + +# This test recreates a remote deployment scenario where the connection +# between deployer and target is closed during the deployment - in this +# case because the connection goes over a 'reverse ssh' tunnel service +# that has changes that are being deployed. + +# This is not seamless (the deployer doesn't get to see the logs after +# the disconnect), but is a lot better than the old behaviour, where +# the switch was aborted and the connection never restored. + +{ + name = "nixos-rebuild-target-host-interrupted"; + + # TODO: remove overlay from nixos/modules/profiles/installation-device.nix + # make it a _small package instead, then remove pkgsReadOnly = false;. + node.pkgsReadOnly = false; + + nodes = { + deployer = + { + nodes, + lib, + pkgs, + ... + }: + let + inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey; + in + { + imports = [ + ../modules/profiles/installation-device.nix + ]; + + nix.settings = { + substituters = lib.mkForce [ ]; + hashed-mirrors = null; + connect-timeout = 1; + }; + + system.includeBuildDependencies = true; + + virtualisation = { + cores = 2; + memorySize = 3072; + }; + + services.openssh.enable = true; + users.users.root.openssh.authorizedKeys.keys = [ nodes.target.system.build.publicKey ]; + system.extraDependencies = [ + # so that it doesn't need to be built inside the test + pkgs.nixVersions.latest + ]; + + system.build.privateKey = snakeOilPrivateKey; + system.build.publicKey = snakeOilPublicKey; + system.switch.enable = true; + + services.getty.autologinUser = lib.mkForce "root"; + }; + + target = + { + nodes, + lib, + pkgs, + ... + }: + let + inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey; + targetConfig = { + documentation.enable = false; + services.openssh.enable = true; + system.build.privateKey = snakeOilPrivateKey; + system.build.publicKey = snakeOilPublicKey; + + users.users.root.openssh.authorizedKeys.keys = [ nodes.deployer.system.build.publicKey ]; + users.users.alice.openssh.authorizedKeys.keys = [ nodes.deployer.system.build.publicKey ]; + users.users.bob.openssh.authorizedKeys.keys = [ nodes.deployer.system.build.publicKey ]; + + users.users.alice.extraGroups = [ "wheel" ]; + users.users.bob.extraGroups = [ "wheel" ]; + + # Disable sudo for root to ensure sudo isn't called without `--sudo` + security.sudo.extraRules = lib.mkForce [ + { + groups = [ "wheel" ]; + commands = [ { command = "ALL"; } ]; + } + { + users = [ "alice" ]; + commands = [ + { + command = "ALL"; + options = [ "NOPASSWD" ]; + } + ]; + } + ]; + + nix.settings.trusted-users = [ "@wheel" ]; + + systemd.services."autossh-ng" = { + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + User = "root"; + Restart = "always"; + RestartSec = "10s"; + ExecStart = "${pkgs.openssh}/bin/ssh -o \"ServerAliveInterval 30\" -o \"ServerAliveCountMax 3\" -o ExitOnForwardFailure=yes -N -R2222:localhost:22 deployer"; + }; + }; + + }; + in + { + imports = [ ./common/user-account.nix ]; + + config = lib.mkMerge [ + targetConfig + { + system.build = { + inherit targetConfig; + }; + system.switch.enable = true; + + networking.hostName = "target"; + } + ]; + }; + }; + + testScript = + { nodes, ... }: + let + sshConfig = builtins.toFile "ssh.conf" '' + UserKnownHostsFile=/dev/null + StrictHostKeyChecking=no + ''; + + targetConfigJSON = hostPkgs.writeText "target-configuration.json" ( + builtins.toJSON nodes.target.system.build.targetConfig + ); + + targetNetworkJSON = hostPkgs.writeText "target-network.json" ( + builtins.toJSON nodes.target.system.build.networkConfig + ); + + configFile = + hostname: + hostPkgs.writeText "configuration.nix" # nix + '' + { lib, pkgs, modulesPath, ... }: { + imports = [ + (modulesPath + "/virtualisation/qemu-vm.nix") + (modulesPath + "/testing/test-instrumentation.nix") + (modulesPath + "/../tests/common/user-account.nix") + (lib.modules.importJSON ./target-configuration.json) + (lib.modules.importJSON ./target-network.json) + ./hardware-configuration.nix + ]; + + boot.loader.grub = { + enable = true; + device = "/dev/vda"; + forceInstall = true; + }; + + # needed to make NIX_SSHOPTS work for nix-copy-closure + nix.package = pkgs.nixVersions.latest; + + # We're changing the '-E' parameter to the new hostname here, + # not because we care about the logs, but because we want to + # force the scenario where the connection is broken during the + # deployment (because the autossh-ng service is stopped and + # started): + systemd.services."autossh-ng".serviceConfig.ExecStart = + lib.mkForce "''${pkgs.openssh}/bin/ssh -o \"ServerAliveInterval 30\" -o \"ServerAliveCountMax 3\" -o ExitOnForwardFailure=yes -N -R2222:localhost:22 -E ${hostname} deployer"; + + # this will be asserted to validate the switch happened: + networking.hostName = "${hostname}"; + } + ''; + in + # python + '' + start_all() + target.wait_for_open_port(22) + + deployer.wait_until_succeeds("ping -c1 target") + deployer.succeed("install -Dm 600 ${nodes.deployer.system.build.privateKey} ~root/.ssh/id_ecdsa") + deployer.succeed("install ${sshConfig} ~root/.ssh/config") + + target.succeed("nixos-generate-config") + target.succeed("install -Dm 600 ${nodes.target.system.build.privateKey} ~root/.ssh/id_ecdsa") + target.succeed("install ${sshConfig} ~root/.ssh/config") + deployer.succeed("scp alice@target:/etc/nixos/hardware-configuration.nix /root/hardware-configuration.nix") + target.wait_for_unit("autossh-ng.service") + + deployer.copy_from_host("${configFile "config-1-deployed"}", "/root/configuration-1.nix") + deployer.copy_from_host("${configFile "config-2-deployed"}", "/root/configuration-2.nix") + deployer.copy_from_host("${targetNetworkJSON}", "/root/target-network.json") + deployer.copy_from_host("${targetConfigJSON}", "/root/target-configuration.json") + + with subtest("Deploy to alice@target via reverse ssh"): + deployer.wait_for_unit("multi-user.target") + # Uses TTY/send_chars instead of deployer.succeed to set NIX_SSHOPTS + deployer.send_chars("NIX_SSHOPTS=\"-p 2222\" nixos-rebuild switch -I nixos-config=/root/configuration-1.nix --target-host alice@localhost --sudo\n") + + # the connection breaks, but the 'switch' should now continue in the background: + deployer.wait_until_tty_matches("1", "error: while running command with remote sudo") + + def deployed(last_try: bool) -> bool: + target_hostname = deployer.succeed("ssh alice@target cat /etc/hostname", timeout=20).rstrip() + if last_try: + print(f"Still seeing hostname {target_hostname}") + return target_hostname == "config-1-deployed" + retry(deployed) + + with subtest("Deploy to bob@target via reverse ssh with password-based sudo"): + deployer.wait_for_unit("multi-user.target") + # Uses TTY/send_chars instead of deployer.succeed to set NIX_SSHOPTS and for ask-sudo-password + deployer.send_chars("NIX_SSHOPTS=\"-p 2222\" nixos-rebuild switch -I nixos-config=/root/configuration-2.nix --target-host bob@localhost --ask-sudo-password\n") + deployer.wait_until_tty_matches("1", "password for bob") + deployer.send_chars("${nodes.target.users.users.bob.password}\n") + + # the connection breaks, but the 'switch' should now continue in the background: + deployer.wait_until_tty_matches("1", "error: while running command with remote sudo") + + def deployed(last_try: bool) -> bool: + target_hostname = deployer.succeed("ssh alice@target cat /etc/hostname", timeout=20).rstrip() + if last_try: + print(f"Still seeing hostname {target_hostname}") + return target_hostname == "config-2-deployed" + retry(deployed) + ''; +} From 426750caaa1725b2cb6ddcae9b515dfaf4bd99d1 Mon Sep 17 00:00:00 2001 From: Jost Alemann Date: Thu, 5 Mar 2026 23:36:53 +0100 Subject: [PATCH 03/14] ruff: 0.15.4 -> 0.15.5 Changelog: https://github.com/astral-sh/ruff/releases/tag/0.15.5 Diff: https://github.com/astral-sh/ruff/compare/0.15.4...0.15.5 --- pkgs/by-name/ru/ruff/package.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkgs/by-name/ru/ruff/package.nix b/pkgs/by-name/ru/ruff/package.nix index 402fbe80d2d0..0ff8cbb186a7 100644 --- a/pkgs/by-name/ru/ruff/package.nix +++ b/pkgs/by-name/ru/ruff/package.nix @@ -16,18 +16,18 @@ rustPlatform.buildRustPackage (finalAttrs: { pname = "ruff"; - version = "0.15.4"; + version = "0.15.5"; src = fetchFromGitHub { owner = "astral-sh"; repo = "ruff"; tag = finalAttrs.version; - hash = "sha256-Vj/4Iod9aFuDK5B6cVe03/3VI5gltoWwWH/0NWrldaw="; + hash = "sha256-bemgVXV/Bkp0aXmWX+R6Aas2/naOx0XEGp0ofh+vyyM="; }; cargoBuildFlags = [ "--package=ruff" ]; - cargoHash = "sha256-yZeBev0vRsNXTdjQdLIkGAYIu66Yuzf6Pjct4xswXME="; + cargoHash = "sha256-NaWWX6EAVkEg/KQ+Up0t2fh/24fnTo6i5dDZoOWErjg="; nativeBuildInputs = [ installShellFiles ]; From 45d6b680ece33c3f9d178d04e67573f5ecafdb1c Mon Sep 17 00:00:00 2001 From: Scandiravian Date: Wed, 18 Feb 2026 12:00:14 +0100 Subject: [PATCH 04/14] maintainers: add scandiravian --- maintainers/maintainer-list.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix index 731e0b0c7967..ab118308b439 100644 --- a/maintainers/maintainer-list.nix +++ b/maintainers/maintainer-list.nix @@ -23952,6 +23952,12 @@ githubId = 15986681; name = "Simon Bruder"; }; + scandiravian = { + email = "nixos@scandiravian.com"; + github = "scandiravian"; + githubId = 13556969; + name = "Scandiravian"; + }; scd31 = { name = "scd31"; github = "scd31"; From 5b372639a62991e8b9ba74f52c500fea204db994 Mon Sep 17 00:00:00 2001 From: Scandiravian Date: Wed, 18 Feb 2026 11:56:30 +0100 Subject: [PATCH 05/14] mdbook-linkcheck2: init at 0.11 --- pkgs/by-name/md/mdbook-linkcheck2/package.nix | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 pkgs/by-name/md/mdbook-linkcheck2/package.nix diff --git a/pkgs/by-name/md/mdbook-linkcheck2/package.nix b/pkgs/by-name/md/mdbook-linkcheck2/package.nix new file mode 100644 index 000000000000..d7375d7ded59 --- /dev/null +++ b/pkgs/by-name/md/mdbook-linkcheck2/package.nix @@ -0,0 +1,34 @@ +{ + lib, + fetchFromGitHub, + rustPlatform, + testers, + mdbook-linkcheck2, +}: +rustPlatform.buildRustPackage (finalAttrs: { + pname = "mdbook-linkcheck2"; + version = "0.12.0"; + + src = fetchFromGitHub { + owner = "marxin"; + repo = "mdbook-linkcheck2"; + tag = "v${finalAttrs.version}"; + sha256 = "sha256-SvheBEIWiL1zdYeMQalbBeAQC86DycqV1/PTA+0S7Gg="; + }; + + cargoHash = "sha256-s4nvVHl/bViIxZfqc4SxSnCCYIY/hxy0C7f2/9ztqts="; + + doCheck = false; # tries to access network to test broken web link functionality + + passthru.tests.version = testers.testVersion { package = mdbook-linkcheck2; }; + + meta = { + description = "Backend for mdbook which will check your links for you"; + mainProgram = "mdbook-linkcheck2"; + homepage = "https://github.com/marxin/mdbook-linkcheck2"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ + scandiravian + ]; + }; +}) From 7cd7d7e116e56b62beeae4300d06da2706514b90 Mon Sep 17 00:00:00 2001 From: Christian Ravn Date: Tue, 3 Mar 2026 11:35:45 +0100 Subject: [PATCH 06/14] mdbook-linkcheck: drop As the package is unmaintained upstream and incompatible with the version 0.5+ of mdbook it has been dropped --- .../manual/release-notes/rl-2605.section.md | 2 + pkgs/by-name/md/mdbook-linkcheck/package.nix | 45 ------------------- pkgs/top-level/aliases.nix | 1 + 3 files changed, 3 insertions(+), 45 deletions(-) delete mode 100644 pkgs/by-name/md/mdbook-linkcheck/package.nix diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index 151f532bada9..12c8cd950c45 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -243,6 +243,8 @@ See . `component.settings`. The unix module now supports using SSH keys from Kanidm via `services.kanidm.unix.sshIntegration = true`. +- `mdbook-linkcheck` has been removed as it is unmaintained and incompatible with the latest version of `mdbook`. Users can instead migrate to `mdbook-linkcheck2`. + - `glibc` has been updated to version 2.42. This version no longer makes the stack executable when a shared library requires this. A symptom diff --git a/pkgs/by-name/md/mdbook-linkcheck/package.nix b/pkgs/by-name/md/mdbook-linkcheck/package.nix deleted file mode 100644 index 22e4c90ab63e..000000000000 --- a/pkgs/by-name/md/mdbook-linkcheck/package.nix +++ /dev/null @@ -1,45 +0,0 @@ -{ - lib, - stdenv, - fetchFromGitHub, - rustPlatform, - pkg-config, - openssl, - testers, - mdbook-linkcheck, -}: - -rustPlatform.buildRustPackage (finalAttrs: { - pname = "mdbook-linkcheck"; - version = "0.7.7"; - - src = fetchFromGitHub { - owner = "Michael-F-Bryan"; - repo = "mdbook-linkcheck"; - rev = "v${finalAttrs.version}"; - sha256 = "sha256-ZbraChBHuKAcUA62EVHZ1RygIotNEEGv24nhSPAEj00="; - }; - - cargoHash = "sha256-Tt7ljjWv2CMtP/ELZNgSH/ifmBk/42+E0r9ZXQEJNP8="; - - buildInputs = lib.optionals (!stdenv.hostPlatform.isDarwin) [ openssl ]; - - nativeBuildInputs = lib.optionals (!stdenv.hostPlatform.isDarwin) [ pkg-config ]; - - env.OPENSSL_NO_VENDOR = 1; - - doCheck = false; # tries to access network to test broken web link functionality - - passthru.tests.version = testers.testVersion { package = mdbook-linkcheck; }; - - meta = { - description = "Backend for `mdbook` which will check your links for you"; - mainProgram = "mdbook-linkcheck"; - homepage = "https://github.com/Michael-F-Bryan/mdbook-linkcheck"; - license = lib.licenses.mit; - maintainers = with lib.maintainers; [ - zhaofengli - matthiasbeyer - ]; - }; -}) diff --git a/pkgs/top-level/aliases.nix b/pkgs/top-level/aliases.nix index b6dcba8642a3..e3bb2f39b4e9 100644 --- a/pkgs/top-level/aliases.nix +++ b/pkgs/top-level/aliases.nix @@ -1296,6 +1296,7 @@ mapAliases { matrix-synapse-tools.synadm = throw "'matrix-synapse-tools.synadm' has been renamed to/replaced by 'synadm'"; # Converted to throw 2025-10-27 mcomix3 = throw "'mcomix3' has been renamed to/replaced by 'mcomix'"; # Converted to throw 2025-10-27 mdbook-alerts = throw "'mdbook-alerts' has been removed because it is deprecated and natively supported by mdbook since version 0.5.0"; # Added 2026-01-07 + mdbook-linkcheck = throw "'mdbook-linkcheck' has been removed and replaced by 'mdbook-linkcheck2' due to incompatibility with mdbook version 0.5.0+"; # Added 2026-03-03 mdt = throw "'mdt' has been renamed to/replaced by 'md-tui'"; # Converted to throw 2025-10-27 mediastreamer = throw "'mediastreamer' has been moved to 'linphonePackages.mediastreamer2'"; # Added 2025-09-20 mediastreamer-openh264 = throw "'mediastreamer-openh264' has been moved to 'linphonePackages.msopenh264'"; # Added 2025-09-20 From 95578b12ec89daf1171668174d65cc26f1bba84a Mon Sep 17 00:00:00 2001 From: phaer Date: Wed, 4 Mar 2026 11:17:07 +0100 Subject: [PATCH 07/14] nix: drop mdbook-linkcheck dependency Upstream nix removed mdbook-linkcheck in commit 2636f50dd4. The mdbook-0.5-support patches applied to nix 2.28 and 2.30 already remove [output.linkcheck] from book.toml.in, so no linkcheck binary is needed on PATH. --- pkgs/tools/package-management/nix/common-meson.nix | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkgs/tools/package-management/nix/common-meson.nix b/pkgs/tools/package-management/nix/common-meson.nix index 1524571859eb..4a5b765a4131 100644 --- a/pkgs/tools/package-management/nix/common-meson.nix +++ b/pkgs/tools/package-management/nix/common-meson.nix @@ -45,7 +45,6 @@ assert (hash == null) -> (src != null); meson, ninja, mdbook, - mdbook-linkcheck, nlohmann_json, nixosTests, openssl, @@ -116,7 +115,6 @@ stdenv.mkDerivation (finalAttrs: { ++ lib.optionals enableDocumentation [ (lib.getBin lowdown-unsandboxed) mdbook - mdbook-linkcheck ] ++ lib.optionals stdenv.hostPlatform.isLinux [ util-linuxMinimal From 8ddce743556b3f008e7be7cfd2df947fdbb4a7ca Mon Sep 17 00:00:00 2001 From: phaer Date: Wed, 4 Mar 2026 11:17:11 +0100 Subject: [PATCH 08/14] nix-manual: drop mdbook-linkcheck dependency Nix 2.31+ upstream already dropped [output.linkcheck] from book.toml, so the linkcheck binary is not invoked during the manual build. --- .../tools/package-management/nix/modular/doc/manual/package.nix | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkgs/tools/package-management/nix/modular/doc/manual/package.nix b/pkgs/tools/package-management/nix/modular/doc/manual/package.nix index c8254f56afab..886911137bc8 100644 --- a/pkgs/tools/package-management/nix/modular/doc/manual/package.nix +++ b/pkgs/tools/package-management/nix/modular/doc/manual/package.nix @@ -6,7 +6,6 @@ ninja, lowdown-unsandboxed, mdbook, - mdbook-linkcheck, jq, python3, rsync, @@ -35,7 +34,6 @@ mkMesonDerivation (finalAttrs: { ninja (lib.getBin lowdown-unsandboxed) mdbook - mdbook-linkcheck jq python3 rsync From 2790cfc4dabe5d2bc2a00074fb948a8cc1b2c9fc Mon Sep 17 00:00:00 2001 From: phaer Date: Wed, 4 Mar 2026 11:17:17 +0100 Subject: [PATCH 09/14] lix: drop mdbook-linkcheck dependency Lix 2.93/2.94 still reference [output.linkcheck] in book.toml, but mdbook-linkcheck2 installs as binary 'mdbook-linkcheck2' which mdbook cannot discover via [output.linkcheck]. Substituting mdbook-linkcheck2 would not work due to this binary name mismatch. Dropping it entirely for now seems safe, lix upstream commented it out in 54df89f601b3. And the build still passes. --- pkgs/tools/package-management/lix/common-lix.nix | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkgs/tools/package-management/lix/common-lix.nix b/pkgs/tools/package-management/lix/common-lix.nix index c111fb6e93d8..8fd3b27e3551 100644 --- a/pkgs/tools/package-management/lix/common-lix.nix +++ b/pkgs/tools/package-management/lix/common-lix.nix @@ -50,7 +50,6 @@ assert lib.assertMsg ( lsof, mercurial, mdbook, - mdbook-linkcheck, nlohmann_json, ninja, openssl, @@ -179,7 +178,6 @@ stdenv.mkDerivation (finalAttrs: { ++ lib.optionals enableDocumentation [ (lib.getBin lowdown-unsandboxed) mdbook - mdbook-linkcheck doxygen ] ++ lib.optionals (hasDtraceSupport && withDtrace) [ systemtap-sdt ] From 2262713c9b90985749490f6e7c8eea5fcd11ccc9 Mon Sep 17 00:00:00 2001 From: K900 Date: Sat, 7 Mar 2026 20:18:08 +0300 Subject: [PATCH 10/14] linux_testing: 6.19-rc8 -> 7.0-rc2 --- pkgs/os-specific/linux/kernel/kernels-org.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/os-specific/linux/kernel/kernels-org.json b/pkgs/os-specific/linux/kernel/kernels-org.json index 51c85bacf3f6..bfbc16d95176 100644 --- a/pkgs/os-specific/linux/kernel/kernels-org.json +++ b/pkgs/os-specific/linux/kernel/kernels-org.json @@ -1,7 +1,7 @@ { "testing": { - "version": "6.19-rc8", - "hash": "sha256:1vwlhsk3cry48p9v2kqqxkmrsw2sjgspfylv3c7r5430yk7fhyg1", + "version": "7.0-rc2", + "hash": "sha256:11zw5rck5hg1w8jjjgc8hjmswzdv4k1wqnkg4zmzxg0ds447cd0b", "lts": false }, "6.1": { From 38123e3ea3018574b8906ca55e83d3cb51e6289a Mon Sep 17 00:00:00 2001 From: K900 Date: Sat, 7 Mar 2026 20:18:12 +0300 Subject: [PATCH 11/14] linux_6_12: 6.12.75 -> 6.12.76 --- pkgs/os-specific/linux/kernel/kernels-org.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/os-specific/linux/kernel/kernels-org.json b/pkgs/os-specific/linux/kernel/kernels-org.json index bfbc16d95176..1d63ad45fc6a 100644 --- a/pkgs/os-specific/linux/kernel/kernels-org.json +++ b/pkgs/os-specific/linux/kernel/kernels-org.json @@ -25,8 +25,8 @@ "lts": true }, "6.12": { - "version": "6.12.75", - "hash": "sha256:0xy1q4x18z6ghaw481hqvkmnkcgxn00nb0n4224amwbgalkpkvh6", + "version": "6.12.76", + "hash": "sha256:15nq7agr492b9lh57xp10hs48dx9g7k253y2lm4vvrj69j1kxd5v", "lts": true }, "6.18": { From 8de310b1a984e9b2e678d0e66a058f7e9aaf8428 Mon Sep 17 00:00:00 2001 From: K900 Date: Sat, 7 Mar 2026 20:18:15 +0300 Subject: [PATCH 12/14] linux_6_6: 6.6.128 -> 6.6.129 --- pkgs/os-specific/linux/kernel/kernels-org.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/os-specific/linux/kernel/kernels-org.json b/pkgs/os-specific/linux/kernel/kernels-org.json index 1d63ad45fc6a..4a13564ff155 100644 --- a/pkgs/os-specific/linux/kernel/kernels-org.json +++ b/pkgs/os-specific/linux/kernel/kernels-org.json @@ -20,8 +20,8 @@ "lts": true }, "6.6": { - "version": "6.6.128", - "hash": "sha256:0j4f6imsxm5pyqk2yn4snv47q272dwpzp0w8qcaiy0l0hjxk75k6", + "version": "6.6.129", + "hash": "sha256:12j42awg44w97zq8fzifpm300jm9q9ya7qkpn7xbnkr2480qz86a", "lts": true }, "6.12": { From b938d814da6b4e3ae249cdd25489206d5dfd1f02 Mon Sep 17 00:00:00 2001 From: K900 Date: Sat, 7 Mar 2026 20:18:18 +0300 Subject: [PATCH 13/14] linux_6_1: 6.1.165 -> 6.1.166 --- pkgs/os-specific/linux/kernel/kernels-org.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/os-specific/linux/kernel/kernels-org.json b/pkgs/os-specific/linux/kernel/kernels-org.json index 4a13564ff155..eb6bfae41550 100644 --- a/pkgs/os-specific/linux/kernel/kernels-org.json +++ b/pkgs/os-specific/linux/kernel/kernels-org.json @@ -5,8 +5,8 @@ "lts": false }, "6.1": { - "version": "6.1.165", - "hash": "sha256:1x1fsaax7yd5p3vwcgp04hbcafmwfqiw1bz0qxsip45yy62730as", + "version": "6.1.166", + "hash": "sha256:0jcl12gjlfdf9pwqg1m84rzwnrj3grxxgk5blrq8zlaq45sgr3c1", "lts": true }, "5.15": { From b215ccdf1cc7a8d87167fb775097ffa738a888fe Mon Sep 17 00:00:00 2001 From: K900 Date: Sat, 7 Mar 2026 20:36:43 +0300 Subject: [PATCH 14/14] linux/common-config: update for 7.0, tweak preemption settings --- pkgs/os-specific/linux/kernel/common-config.nix | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pkgs/os-specific/linux/kernel/common-config.nix b/pkgs/os-specific/linux/kernel/common-config.nix index 23b8aabf4e9a..66859facc6d9 100644 --- a/pkgs/os-specific/linux/kernel/common-config.nix +++ b/pkgs/os-specific/linux/kernel/common-config.nix @@ -575,7 +575,7 @@ let # Enable Hyper-V Synthetic DRM Driver DRM_HYPERV = whenAtLeast "5.14" module; # And disable the legacy framebuffer driver when we have the new one - FB_HYPERV = whenAtLeast "5.14" no; + FB_HYPERV = whenBetween "5.14" "7.0" no; } // lib.optionalAttrs (stdenv.hostPlatform.system == "x86_64-linux") { # Intel GVT-g graphics virtualization supports 64-bit only @@ -739,7 +739,9 @@ let NFS_FSCACHE = yes; NFS_SWAP = yes; NFS_V3_ACL = yes; - NFS_V4_1 = yes; # NFSv4.1 client support + # NFSv4.1 is enabled unconditionally on 7.0 and up + # see: https://github.com/torvalds/linux/commit/7537db24806fdc3d3ec4fef53babdc22c9219e75 + NFS_V4_1 = whenOlder "7.0" yes; NFS_V4_2 = yes; NFS_V4_SECURITY_LABEL = yes; NFS_LOCALIO = whenAtLeast "6.12" yes; @@ -1168,7 +1170,7 @@ let ACCESSIBILITY = yes; # Accessibility support AUXDISPLAY = yes; # Auxiliary Display support - HIPPI = yes; + HIPPI = whenOlder "7.0" yes; MTD_COMPLEX_MAPPINGS = yes; # needed for many devices SCSI_LOWLEVEL = yes; # enable lots of SCSI devices @@ -1378,8 +1380,14 @@ let HMM_MIRROR = yes; DRM_AMDGPU_USERPTR = yes; + # We want to prefer PREEMPT_LAZY when available, and fall back on PREEMPT_VOLUNTARY. + # It just so happens that kconfig asks for PREEMPT_LAZY first, so doing it like this + # does what we want. + # FIXME: This is stupid and bad. + # See: https://github.com/torvalds/linux/commit/7dadeaa6e851e7d67733f3e24fc53ee107781d0f PREEMPT = no; - PREEMPT_VOLUNTARY = yes; + PREEMPT_LAZY = option yes; + PREEMPT_VOLUNTARY = option yes; X86_AMD_PLATFORM_DEVICE = lib.mkIf stdenv.hostPlatform.isx86 yes; X86_PLATFORM_DRIVERS_DELL = lib.mkIf stdenv.hostPlatform.isx86 (whenAtLeast "5.12" yes);