nixos-rebuild-ng: validate NixOS config path in target host

In #418243 we started to validate NixOS config path to avoid a nasty bug
in Nix, but this doesn't work in the `--build-host` and `--target-host`
case because the configuration will not be available in the local host
to check if it contains a `nixos-version`. This moves the check to
`--target-host` instead, that is probably the correct choice anyway.

Fix #418868.
This commit is contained in:
Thiago Kenji Okada
2025-06-22 15:43:30 +01:00
parent 7c796b5aff
commit 0f6624e21a
3 changed files with 43 additions and 25 deletions
@@ -5,7 +5,6 @@ import os
import sys
from pathlib import Path
from subprocess import CalledProcessError, run
from textwrap import dedent
from typing import Final, assert_never
from . import nix, tmpdir
@@ -338,29 +337,6 @@ def validate_image_variant(image_variant: str, variants: ImageVariants) -> None:
)
def validate_nixos_config(path_to_config: Path) -> None:
if not (path_to_config / "nixos-version").exists() and not os.environ.get(
"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM"
):
msg = dedent(
# the lowercase for the first letter below is proposital
f"""
your NixOS configuration path seems to be missing essential files.
To avoid corrupting your current NixOS installation, the activation will abort.
This could be caused by Nix bug: https://github.com/NixOS/nix/issues/13367.
This is the evaluated NixOS configuration path: {path_to_config}.
Change the directory to somewhere else (e.g., `cd $HOME`) before trying again.
If you think this is a mistake, you can set the environment variable
NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM to 1
and re-run the command to continue.
Please open an issue if this is the case.
"""
).strip()
raise NixOSRebuildError(msg)
def execute(argv: list[str]) -> None:
args, args_groups = parse_args(argv)
@@ -514,7 +490,6 @@ def execute(argv: list[str]) -> None:
copy_flags=copy_flags,
)
if action in (Action.SWITCH, Action.BOOT):
validate_nixos_config(path_to_config)
nix.set_profile(
profile,
path_to_config,
@@ -9,6 +9,7 @@ from importlib.resources import files
from pathlib import Path
from string import Template
from subprocess import PIPE, CalledProcessError
from textwrap import dedent
from typing import Final, Literal
from . import tmpdir
@@ -613,6 +614,33 @@ def set_profile(
sudo: bool,
) -> None:
"Set a path as the current active Nix profile."
if not os.environ.get(
"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM"
):
r = run_wrapper(
["test", "-f", path_to_config / "nixos-version"],
remote=target_host,
check=False,
)
if r.returncode:
msg = dedent(
# the lowercase for the first letter below is proposital
f"""
your NixOS configuration path seems to be missing essential files.
To avoid corrupting your current NixOS installation, the activation will abort.
This could be caused by Nix bug: https://github.com/NixOS/nix/issues/13367.
This is the evaluated NixOS configuration path: {path_to_config}.
Change the directory to somewhere else (e.g., `cd $HOME`) before trying again.
If you think this is a mistake, you can set the environment variable
NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM to 1
and re-run the command to continue.
Please open an issue if this is the case.
"""
).strip()
raise NixOSRebuildError(msg)
run_wrapper(
["nix-env", "-p", profile.path, "--set", path_to_config],
remote=target_host,
@@ -674,6 +674,8 @@ def test_rollback_temporary_profile(tmp_path: Path) -> None:
def test_set_profile(mock_run: Mock) -> None:
profile_path = Path("/path/to/profile")
config_path = Path("/path/to/config")
mock_run.return_value = CompletedProcess([], 0)
n.set_profile(
m.Profile("system", profile_path),
config_path,
@@ -687,6 +689,19 @@ def test_set_profile(mock_run: Mock) -> None:
sudo=False,
)
mock_run.return_value = CompletedProcess([], 1)
with pytest.raises(m.NixOSRebuildError) as e:
n.set_profile(
m.Profile("system", profile_path),
config_path,
target_host=None,
sudo=False,
)
assert str(e.value).startswith(
"error: your NixOS configuration path seems to be missing essential files."
)
@patch(get_qualified_name(n.run_wrapper, n), autospec=True)
def test_switch_to_configuration_without_systemd_run(