···55import sys
66from pathlib import Path
77from subprocess import CalledProcessError, run
88+from textwrap import dedent
89from typing import Final, assert_never
9101011from . import nix, tmpdir
1112from .constants import EXECUTABLE, WITH_NIX_2_18, WITH_REEXEC, WITH_SHELL_FILES
1212-from .models import Action, BuildAttr, Flake, ImageVariants, NRError, Profile
1313+from .models import Action, BuildAttr, Flake, ImageVariants, NixOSRebuildError, Profile
1314from .process import Remote, cleanup_ssh
1415from .utils import Args, LogFormatter, tabulate
1516···99100 "--attr",
100101 "-A",
101102 help="Enable and build the NixOS system from nix file and use the "
102102- + "specified attribute path from file specified by the --file option",
103103+ "specified attribute path from file specified by the --file option",
103104 )
104105 main_parser.add_argument(
105106 "--flake",
···117118 "--install-bootloader",
118119 action="store_true",
119120 help="Causes the boot loader to be (re)installed on the device specified "
120120- + "by the relevant configuration options",
121121+ "by the relevant configuration options",
121122 )
122123 main_parser.add_argument(
123124 "--install-grub",
···142143 "--upgrade",
143144 action="store_true",
144145 help="Update the root user's channel named 'nixos' before rebuilding "
145145- + "the system and channels which have a file named '.update-on-nixos-rebuild'",
146146+ "the system and channels which have a file named '.update-on-nixos-rebuild'",
146147 )
147148 main_parser.add_argument(
148149 "--upgrade-all",
···186187 main_parser.add_argument(
187188 "--image-variant",
188189 help="Selects an image variant to build from the "
189189- + "config.system.build.images attribute of the given configuration",
190190+ "config.system.build.images attribute of the given configuration",
190191 )
191192 main_parser.add_argument("action", choices=Action.values(), nargs="?")
192193···321322 # - Exec format error (e.g.: another OS/CPU arch)
322323 logger.warning(
323324 "could not re-exec in a newer version of nixos-rebuild, "
324324- + "using current version",
325325+ "using current version",
325326 exc_info=logger.isEnabledFor(logging.DEBUG),
326327 )
327328 # We already run clean-up, let's re-exec in the current version
···329330 os.execve(current, argv, os.environ | {"_NIXOS_REBUILD_REEXEC": "1"})
330331331332333333+def validate_image_variant(image_variant: str, variants: ImageVariants) -> None:
334334+ if image_variant not in variants:
335335+ raise NixOSRebuildError(
336336+ "please specify one of the following supported image variants via "
337337+ "--image-variant:\n" + "\n".join(f"- {v}" for v in variants)
338338+ )
339339+340340+341341+def validate_nixos_config(path_to_config: Path) -> None:
342342+ if not (path_to_config / "nixos-version").exists() and not os.environ.get(
343343+ "NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM"
344344+ ):
345345+ msg = dedent(
346346+ # the lowercase for the first letter below is proposital
347347+ f"""
348348+ your NixOS configuration path seems to be missing essential files.
349349+ To avoid corrupting your current NixOS installation, the activation will abort.
350350+351351+ This could be caused by Nix bug: https://github.com/NixOS/nix/issues/13367.
352352+ This is the evaluated NixOS configuration path: {path_to_config}.
353353+ Change the directory to somewhere else (e.g., `cd $HOME`) before trying again.
354354+355355+ If you think this is a mistake, you can set the environment variable
356356+ NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM to 1
357357+ and re-run the command to continue.
358358+ Please open an issue if this is the case.
359359+ """
360360+ ).strip()
361361+ raise NixOSRebuildError(msg)
362362+363363+332364def execute(argv: list[str]) -> None:
333365 args, args_groups = parse_args(argv)
334366···393425 no_link = action in (Action.SWITCH, Action.BOOT)
394426 rollback = bool(args.rollback)
395427396396- def validate_image_variant(variants: ImageVariants) -> None:
397397- if args.image_variant not in variants:
398398- raise NRError(
399399- "please specify one of the following "
400400- + "supported image variants via --image-variant:\n"
401401- + "\n".join(f"- {v}" for v in variants)
402402- )
403403-404428 match action:
405429 case Action.BUILD_IMAGE if flake:
406430 variants = nix.get_build_image_variants_flake(
407431 flake,
408432 eval_flags=flake_common_flags,
409433 )
410410- validate_image_variant(variants)
434434+ validate_image_variant(args.image_variant, variants)
411435 attr = f"config.system.build.images.{args.image_variant}"
412436 case Action.BUILD_IMAGE:
413437 variants = nix.get_build_image_variants(
414438 build_attr,
415439 instantiate_flags=common_flags,
416440 )
417417- validate_image_variant(variants)
441441+ validate_image_variant(args.image_variant, variants)
418442 attr = f"config.system.build.images.{args.image_variant}"
419443 case Action.BUILD_VM:
420444 attr = "config.system.build.vm"
···435459 if maybe_path_to_config: # kinda silly but this makes mypy happy
436460 path_to_config = maybe_path_to_config
437461 else:
438438- raise NRError("could not find previous generation")
462462+ raise NixOSRebuildError("could not find previous generation")
439463 case (_, True, _, _):
440440- raise NRError(f"--rollback is incompatible with '{action}'")
464464+ raise NixOSRebuildError(
465465+ f"--rollback is incompatible with '{action}'"
466466+ )
441467 case (_, False, Remote(_), Flake(_)):
442468 path_to_config = nix.build_remote_flake(
443469 attr,
···488514 copy_flags=copy_flags,
489515 )
490516 if action in (Action.SWITCH, Action.BOOT):
517517+ validate_nixos_config(path_to_config)
491518 nix.set_profile(
492519 profile,
493520 path_to_config,
···2020 Generation,
2121 GenerationJson,
2222 ImageVariants,
2323- NRError,
2323+ NixOSRebuildError,
2424 Profile,
2525 Remote,
2626)
···256256 )
257257 else:
258258 if flake_flags:
259259- raise NRError("'edit' does not support extra Nix flags")
259259+ raise NixOSRebuildError("'edit' does not support extra Nix flags")
260260 nixos_config = Path(
261261 os.getenv("NIXOS_CONFIG") or find_file("nixos-config") or "/etc/nixos"
262262 )
···266266 if nixos_config.exists():
267267 run_wrapper([os.getenv("EDITOR", "nano"), nixos_config], check=False)
268268 else:
269269- raise NRError("cannot find NixOS config file")
269269+ raise NixOSRebuildError("cannot find NixOS config file")
270270271271272272def find_file(file: str, nix_flags: Args | None = None) -> Path | None:
···424424 and if this is the current active profile or not.
425425 """
426426 if not profile.path.exists():
427427- raise NRError(f"no profile '{profile.name}' found")
427427+ raise NixOSRebuildError(f"no profile '{profile.name}' found")
428428429429 def parse_path(path: Path, profile: Profile) -> Generation:
430430 entry_id = path.name.split("-")[1]
···456456 and if this is the current active profile or not.
457457 """
458458 if not profile.path.exists():
459459- raise NRError(f"no profile '{profile.name}' found")
459459+ raise NixOSRebuildError(f"no profile '{profile.name}' found")
460460461461 # Using `nix-env --list-generations` needs root to lock the profile
462462 r = run_wrapper(
···635635 """
636636 if specialisation:
637637 if action not in (Action.SWITCH, Action.TEST):
638638- raise NRError(
638638+ raise NixOSRebuildError(
639639 "'--specialisation' can only be used with 'switch' and 'test'"
640640 )
641641 path_to_config = path_to_config / f"specialisation/{specialisation}"
642642643643 if not path_to_config.exists():
644644- raise NRError(f"specialisation not found: {specialisation}")
644644+ raise NixOSRebuildError(f"specialisation not found: {specialisation}")
645645646646 r = run_wrapper(
647647 ["test", "-d", "/run/systemd/system"],
···652652 if r.returncode:
653653 logger.debug(
654654 "skipping systemd-run to switch configuration since systemd is "
655655- + "not working in target host"
655655+ "not working in target host"
656656 )
657657 cmd = []
658658
···5555 if o in ["-t", "-tt", "RequestTTY=yes", "RequestTTY=force"]:
5656 logger.warning(
5757 f"detected option '{o}' in NIX_SSHOPTS. SSH's TTY may "
5858- + "cause issues, it is recommended to remove this option"
5858+ "cause issues, it is recommended to remove this option"
5959 )
6060 if not ask_sudo_password:
6161 logger.warning(
6262 "if you want to prompt for sudo password use "
6363- + "'--ask-sudo-password' option instead"
6363+ "'--ask-sudo-password' option instead"
6464 )
65656666···161161 if sudo and remote and remote.sudo_password is None:
162162 logger.error(
163163 "while running command with remote sudo, did you forget to use "
164164- + "--ask-sudo-password?"
164164+ "--ask-sudo-password?"
165165 )
166166 raise
167167
···714714 remote=None,
715715 )
716716717717- with pytest.raises(m.NRError) as e:
717717+ with pytest.raises(m.NixOSRebuildError) as e:
718718 n.switch_to_configuration(
719719 config_path,
720720 m.Action.BOOT,