nixos-rebuild-ng: validate NixOS configuration path
When `path://` or `git+file://` protocol is used in Flake mode (that is the most common case since we normalize the paths, see PR #375493) and the current working directory in a symlink pointing base store path to the Nix store (e.g., /run/opengl-driver/lib), there is a nasty bug where Nix resolves the path as the Nix store path of the current derivation instead of the target derivation. Since we blindly activate this path, this can corrupt the installation and break some other activation scripts, like `systemd-boot-builder.py`. While it is possible to recover this situation using `nix-env -p /nix/var/nix/profiles/system --delete-generations old`, this is far from ideal. This commit solves it by validating that the resolved NixOS configuration path includes at least `$out/nixos-version`. I am not sure if this is going to break some cases so there is a escape hatch in the form of the environment variable `NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM`, but in general it looks safe.
This commit is contained in:
@@ -5,6 +5,7 @@ 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
|
||||
@@ -329,6 +330,30 @@ def reexec(
|
||||
os.execve(current, argv, os.environ | {"_NIXOS_REBUILD_REEXEC": "1"})
|
||||
|
||||
|
||||
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()
|
||||
logger.error(msg)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def execute(argv: list[str]) -> None:
|
||||
args, args_groups = parse_args(argv)
|
||||
|
||||
@@ -488,6 +513,7 @@ 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,
|
||||
|
||||
@@ -213,7 +213,11 @@ def test_reexec_flake(
|
||||
)
|
||||
|
||||
|
||||
@patch.dict(os.environ, {}, clear=True)
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"},
|
||||
clear=True,
|
||||
)
|
||||
@patch("subprocess.run", autospec=True)
|
||||
def test_execute_nix_boot(mock_run: Mock, tmp_path: Path) -> None:
|
||||
nixpkgs_path = tmp_path / "nixpkgs"
|
||||
@@ -291,7 +295,15 @@ def test_execute_nix_boot(mock_run: Mock, tmp_path: Path) -> None:
|
||||
"boot",
|
||||
],
|
||||
check=True,
|
||||
**(DEFAULT_RUN_KWARGS | {"env": {"NIXOS_INSTALL_BOOTLOADER": "0"}}),
|
||||
**(
|
||||
DEFAULT_RUN_KWARGS
|
||||
| {
|
||||
"env": {
|
||||
"NIXOS_INSTALL_BOOTLOADER": "0",
|
||||
"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1",
|
||||
}
|
||||
}
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -421,7 +433,11 @@ def test_execute_nix_build_image_flake(mock_run: Mock, tmp_path: Path) -> None:
|
||||
)
|
||||
|
||||
|
||||
@patch.dict(os.environ, {}, clear=True)
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"},
|
||||
clear=True,
|
||||
)
|
||||
@patch("subprocess.run", autospec=True)
|
||||
def test_execute_nix_switch_flake(mock_run: Mock, tmp_path: Path) -> None:
|
||||
config_path = tmp_path / "test"
|
||||
@@ -498,13 +514,25 @@ def test_execute_nix_switch_flake(mock_run: Mock, tmp_path: Path) -> None:
|
||||
"switch",
|
||||
],
|
||||
check=True,
|
||||
**(DEFAULT_RUN_KWARGS | {"env": {"NIXOS_INSTALL_BOOTLOADER": "1"}}),
|
||||
**(
|
||||
DEFAULT_RUN_KWARGS
|
||||
| {
|
||||
"env": {
|
||||
"NIXOS_INSTALL_BOOTLOADER": "1",
|
||||
"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1",
|
||||
}
|
||||
}
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@patch.dict(os.environ, {}, clear=True)
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"},
|
||||
clear=True,
|
||||
)
|
||||
@patch("subprocess.run", autospec=True)
|
||||
@patch("uuid.uuid4", autospec=True)
|
||||
@patch(get_qualified_name(nr.cleanup_ssh), autospec=True)
|
||||
@@ -714,7 +742,11 @@ def test_execute_nix_switch_build_target_host(
|
||||
)
|
||||
|
||||
|
||||
@patch.dict(os.environ, {}, clear=True)
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"},
|
||||
clear=True,
|
||||
)
|
||||
@patch("subprocess.run", autospec=True)
|
||||
@patch(get_qualified_name(nr.cleanup_ssh), autospec=True)
|
||||
def test_execute_nix_switch_flake_target_host(
|
||||
@@ -817,7 +849,11 @@ def test_execute_nix_switch_flake_target_host(
|
||||
)
|
||||
|
||||
|
||||
@patch.dict(os.environ, {}, clear=True)
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"},
|
||||
clear=True,
|
||||
)
|
||||
@patch("subprocess.run", autospec=True)
|
||||
@patch(get_qualified_name(nr.cleanup_ssh), autospec=True)
|
||||
def test_execute_nix_switch_flake_build_host(
|
||||
|
||||
Reference in New Issue
Block a user