From 94c35115220d4a4353f0e3b9107d9f470c1e3759 Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Sat, 12 Oct 2024 03:59:58 +0200 Subject: [PATCH 1/5] nixos-install: use associative array for build method selection Use similar mechanism to what nixos-rebuild uses to evaluate which configuration file, and format, to build Amended-by: rnhmjoj --- .../by-name/ni/nixos-install/nixos-install.sh | 115 ++++++++++++++---- 1 file changed, 89 insertions(+), 26 deletions(-) diff --git a/pkgs/by-name/ni/nixos-install/nixos-install.sh b/pkgs/by-name/ni/nixos-install/nixos-install.sh index 02ec8d66bd33..bc582cf777a2 100755 --- a/pkgs/by-name/ni/nixos-install/nixos-install.sh +++ b/pkgs/by-name/ni/nixos-install/nixos-install.sh @@ -19,7 +19,8 @@ system= verbosity=() attr= buildFile= -buildingAttribute=1 +# keys: module, by-attrset, flake, system +declare -A requestedBuildMethods while [ "$#" -gt 0 ]; do i="$1"; shift 1 @@ -38,10 +39,12 @@ while [ "$#" -gt 0 ]; do ;; --system|--closure|--store-path) system="$1"; shift 1 + requestedBuildMethods["system"]=1 ;; --flake) flake="$1" flakeFlags=(--extra-experimental-features 'nix-command flakes') + requestedBuildMethods["flake"]=1 shift 1 ;; --file|-f) @@ -49,8 +52,14 @@ while [ "$#" -gt 0 ]; do log "$0: '$i' requires an argument" exit 1 fi - buildFile="$1" - buildingAttribute= + if [ -f "$1" ]; then + buildFile="$1" + elif [ -f "$1/system.nix" ]; then + buildFile="${1%/}/system.nix" + else + buildFile="${1%/}/default.nix" + fi + requestedBuildMethods["by-attrset"]=1 shift 1 ;; --attr|-A) @@ -59,7 +68,7 @@ while [ "$#" -gt 0 ]; do exit 1 fi attr="$1" - buildingAttribute= + requestedBuildMethods["by-attrset"]=1 shift 1 ;; --recreate-lock-file|--no-update-lock-file|--no-write-lock-file|--no-registries|--commit-lock-file) @@ -106,6 +115,20 @@ while [ "$#" -gt 0 ]; do esac done +# Finds a specific file in a directory or its parents +findInParents() { + local dir=$1 + local filename=$2 + while [[ ! -f "$dir/$filename" ]] && [[ "$dir" != / ]]; do + dir=$(dirname "$dir") + done + if [[ -f "$dir/$filename" ]]; then + echo "$dir/$filename" + else + return 1 + fi +} + if ! test -e "$mountPoint"; then echo "mount point $mountPoint doesn't exist" exit 1 @@ -122,30 +145,61 @@ while [[ "$checkPath" != "/" ]]; do checkPath="$(dirname "$checkPath")" done -# Verify that user is not trying to use attribute building and flake -# at the same time -if [[ -z $buildingAttribute && -n $flake ]]; then - echo "$0: '--flake' cannot be used with '--file' or '--attr'" +# If user requested multiple build methods, abort +if [[ ${#requestedBuildMethods[@]} -gt 1 ]]; then + echo "error: multiple build methods requested: ${!requestedBuildMethods[@]}" exit 1 fi # Get the path of the NixOS configuration file. -if [[ -z $flake && -n $buildingAttribute ]]; then - if [[ -z $NIXOS_CONFIG ]]; then - NIXOS_CONFIG=$mountPoint/etc/nixos/configuration.nix +findByAttrset() { + # - From system.nix up from the current directory + # - Hardcoded to $mountPoint/etc/nixos/system.nix + # - Hardcoded to /etc/nixos/system.nix + # (default.nix is also searched for backward compatibility) + if resolvedBuildFile="$(findInParents "$PWD" system.nix)"; then + buildFile=$resolvedBuildFile + return 0 + elif resolvedBuildFile="$(findInParents "$PWD" default.nix)"; then + buildFile=$resolvedBuildFile + return 0 + elif [[ -f "/etc/nixos/system.nix" ]]; then + buildFile="/etc/nixos/system.nix" + return 0 + elif [[ -f "/etc/nixos/default.nix" ]]; then + buildFile="/etc/nixos/default.nix" + return 0 fi + return 1 +} - if [[ ${NIXOS_CONFIG:0:1} != / ]]; then - echo "$0: \$NIXOS_CONFIG is not an absolute path" +findModule() { + # - From NIXOS_CONFIG environment variable + # - hardcoded to $mountPoint/etc/nixos/configuration.nix + if [[ -n $NIXOS_CONFIG ]]; then + return 0 + elif [[ -f "$mountPoint/etc/nixos/configuration.nix" ]]; then + NIXOS_CONFIG="$mountPoint/etc/nixos/configuration.nix" + return 0 + fi + return 1 +} + +# If user didn't request a specific build method, try to find one +if [[ ${#requestedBuildMethods[@]} -eq 0 ]]; then + # Flakes cannot be resolved without knowing hostname + if findByAttrset; then + requestedBuildMethods["by-attrset"]=1 + elif findModule; then + requestedBuildMethods["module"]=1 + else + echo "error: no build method found" exit 1 fi -elif [[ -z $buildingAttribute ]]; then - if [[ -z $buildFile ]]; then - buildFile="$mountPoint/etc/nixos/default.nix" - elif [[ -d $buildFile ]]; then - buildFile="$buildFile/default.nix" - fi -elif [[ -n $flake ]]; then +fi + +# Find configuration files if not already found +if [[ -n "${requestedBuildMethods["flake"]}" ]]; then if [[ $flake =~ ^(.*)\#([^\#\"]*)$ ]]; then flake="${BASH_REMATCH[1]}" flakeAttr="${BASH_REMATCH[2]}" @@ -156,19 +210,28 @@ elif [[ -n $flake ]]; then exit 1 fi flakeAttr="nixosConfigurations.\"$flakeAttr\"" +elif [[ -n "${requestedBuildMethods["by-attrset"]}" && -z $buildFile ]]; then + findByAttrset +elif [[ -n "${requestedBuildMethods["module"]}" && -z $NIXOS_CONFIG ]]; then + findModule + + if [[ ${NIXOS_CONFIG:0:1} != / ]]; then + echo "$0: \$NIXOS_CONFIG is not an absolute path" + exit 1 + fi fi # Resolve the flake. -if [[ -n $flake ]]; then +if [[ -n "${requestedBuildMethods["flake"]}" ]]; then flake=$(nix "${flakeFlags[@]}" flake metadata --json "${extraBuildFlags[@]}" "${lockFlags[@]}" -- "$flake" | jq -r .url) fi -if [[ ! -e $NIXOS_CONFIG && -z $system && -z $flake && -n $buildingAttribute ]]; then +if [[ ! -e $NIXOS_CONFIG && -n "${requestedBuildMethods["module"]}" ]]; then echo "configuration file $NIXOS_CONFIG doesn't exist" exit 1 fi -if [[ ! -z $buildingAttribute && -e $buildFile && -z $system ]]; then +if [[ ! -e $buildFile && -n "${requestedBuildMethods["by-attrset"]}" ]]; then echo "configuration file $buildFile doesn't exist" exit 1 fi @@ -202,12 +265,12 @@ fi # Build the system configuration in the target filesystem. if [[ -z $system ]]; then outLink="$tmpdir/system" - if [[ -z $flake && -n $buildingAttribute ]]; then + if [[ -n "${requestedBuildMethods["module"]}" ]]; then echo "building the configuration in $NIXOS_CONFIG..." nix-build --out-link "$outLink" --store "$mountPoint" "${extraBuildFlags[@]}" \ --extra-substituters "$sub" \ '' -A system -I "nixos-config=$NIXOS_CONFIG" "${verbosity[@]}" - elif [[ -z $buildingAttribute ]]; then + elif [[ -n "${requestedBuildMethods["by-attrset"]}" ]]; then if [[ -n $attr ]]; then echo "building the configuration in $buildFile and attribute $attr..." else @@ -216,7 +279,7 @@ if [[ -z $system ]]; then nix-build --out-link "$outLink" --store "$mountPoint" "${extraBuildFlags[@]}" \ --extra-substituters "$sub" \ "$buildFile" -A "${attr:+$attr.}config.system.build.toplevel" "${verbosity[@]}" - else + else # [[ -n "${requestedBuildMethods["flake"]}" ]] echo "building the flake in $flake..." nix "${flakeFlags[@]}" build "$flake#$flakeAttr.config.system.build.toplevel" \ --store "$mountPoint" --extra-substituters "$sub" "${verbosity[@]}" \ From 7d537b0dfc07ecc4e958f95e6dfded1cfc77319f Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Sat, 12 Oct 2024 06:58:50 +0200 Subject: [PATCH 2/5] nixos-install: Update manpage nixos-install now detects by-attrset configurations differently, and uses system.nix filename instead of default.nix Amended-by: rnhmjoj --- pkgs/by-name/ni/nixos-install/nixos-install.8 | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/pkgs/by-name/ni/nixos-install/nixos-install.8 b/pkgs/by-name/ni/nixos-install/nixos-install.8 index 5b4f8c936720..186c85566416 100644 --- a/pkgs/by-name/ni/nixos-install/nixos-install.8 +++ b/pkgs/by-name/ni/nixos-install/nixos-install.8 @@ -33,9 +33,19 @@ .Sh DESCRIPTION This command installs NixOS in the file system mounted on .Pa /mnt Ns -, based on the NixOS configuration specified in -.Pa /mnt/etc/nixos/configuration.nix Ns -\&. It performs the following steps: +, or defined through the +.Fl -root +option, based on the NixOS configuration specified in +.Pa /mnt/etc/nixos/system.nix Ns +, +.Pa /mnt/etc/nixos/configuration.nix +or specified through the +.Fl -file Ns +, +.Fl -attr Ns +or +.Fl -flake Ns +options. It performs the following steps: . .Bl -enum .It @@ -46,9 +56,7 @@ It copies Nix and its dependencies to .It It runs Nix in .Pa /mnt -to build the NixOS configuration specified in -.Pa /mnt/etc/nixos/configuration.nix Ns -\&. +to build the given NixOS configuration. . .It It installs the current channel @@ -114,7 +122,7 @@ output named \&. . .It Fl -file Ar path , Fl f Ar path -Enable and build the NixOS system from the specified file. The file must +Build the NixOS system from the specified file. The file must evaluate to an attribute set, and it must contain a valid NixOS configuration at attribute .Va attrPath Ns @@ -127,17 +135,19 @@ function in nixpkgs or importing and calling from nixpkgs. If specified without .Fl -attr option, builds the configuration from the top-level -attribute of the file. +attribute set of the file. . .It Fl -attr Ar attrPath , Fl A Ar attrPath -Enable and build the NixOS system from nix file and use the specified attribute +Build the NixOS system from nix file and use the specified attribute path from file specified by the .Fl -file option. If specified without .Fl -file -option, uses -.Va [root] Ns Pa /etc/nixos/default.nix Ns -\&. +option, it tires to find +.Pa system.nix +in +.Va root Ns Pa /etc/nixos Ns +, in current directory and iteratively in parent directories. . .It Fl -channel Ar channel If this option is provided, do not copy the current From bdd34aca82cd3a6c14b4f29dd7cae02b26697a63 Mon Sep 17 00:00:00 2001 From: rnhmjoj Date: Fri, 20 Mar 2026 19:04:24 +0100 Subject: [PATCH 3/5] nixos-install-tools: fix nixos-install-help test --- pkgs/by-name/ni/nixos-install-tools/package.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/by-name/ni/nixos-install-tools/package.nix b/pkgs/by-name/ni/nixos-install-tools/package.nix index 03be3c9ea39b..98588f4abc54 100644 --- a/pkgs/by-name/ni/nixos-install-tools/package.nix +++ b/pkgs/by-name/ni/nixos-install-tools/package.nix @@ -56,9 +56,9 @@ in ''; } '' - nixos-install --help | grep -F 'NixOS Reference Pages' + nixos-install --help | grep -F "System Manager's Manual" nixos-install --help | grep -F 'configuration.nix' - nixos-generate-config --help | grep -F 'NixOS Reference Pages' + nixos-generate-config --help | grep -F "System Manager's Manual" nixos-generate-config --help | grep -F 'hardware-configuration.nix' # FIXME: Tries to call unshare, which it must not do for --help From 5c562a3587bb0ff4232af21fe8e70ec1b06f63f7 Mon Sep 17 00:00:00 2001 From: rnhmjoj Date: Mon, 16 Mar 2026 08:25:51 +0100 Subject: [PATCH 4/5] nixos-rebuild-ng: add support for system.nix - Support loading a system.nix from the cwd if --attr is used or when --file points to a directory. default.nix is still supported, but system.nix should be preferred to avoid loading files unrelated to NixOS systems - Introduce a system-wide file, defaulting to /etc/nixos/system.nix, which is loaded even if neither --file nor --attr are used These changes allow to build a completely self-contained system configuration (NIX_PATH can be empty) with just `nixos-rebuild`, similarly to flakes. --- .../ni/nixos-rebuild-ng/nixos-rebuild.8.scd | 23 ++- .../src/nixos_rebuild/models.py | 29 +++- .../nixos-rebuild-ng/src/nixos_rebuild/nix.py | 4 +- .../nixos-rebuild-ng/src/tests/test_main.py | 146 +++++++++++++++--- .../nixos-rebuild-ng/src/tests/test_models.py | 50 +++++- .../src/tests/test_services.py | 8 +- 6 files changed, 217 insertions(+), 43 deletions(-) diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd b/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd index b2298ce10639..d4f83ac9d803 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd +++ b/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd @@ -28,11 +28,12 @@ _nixos-rebuild_ \[--verbose] [--quiet] [--max-jobs MAX_JOBS] [--cores CORES] [-- # DESCRIPTION This command updates the system so that it corresponds to the configuration -specified in /etc/nixos/configuration.nix, /etc/nixos/flake.nix or the file and -attribute specified by the *--file* and/or *--attr* options. Thus, every +specified in /etc/nixos/configuration.nix, /etc/nixos/flake.nix, +/etc/nixos/system.nix or the file and attribute specified by the *--file*, +*--attr* or *--flake* options. Thus, every time you modify the configuration or any other NixOS module, you must run *nixos-rebuild* to make the changes take effect. It builds the new system in -/nix/store, runs its activation script, and stop and (re)starts any system +/nix/store, runs its activation script, stops and (re)starts any system services if needed. Please note that user services need to be started manually as they aren't detected by the activation script at the moment. @@ -281,20 +282,21 @@ It must be one of the following: Implies *--sudo*. *--file* _path_, *-f* _path_ - Enable and build the NixOS system from the specified file. The file must + Build the NixOS system from the specified file. The file must evaluate to an attribute set, and it must contain a valid NixOS configuration at attribute _attrPath_. This is useful for building a NixOS system from a nix file that is not a flake or a NixOS configuration module. Attribute set a with valid NixOS configuration can be made using _nixos_ function in nixpkgs or importing and calling nixos/lib/eval-config.nix from nixpkgs. If specified without *--attr* - option, builds the configuration from the top-level attribute of the + option, builds the configuration from the top-level attribute set of the file. *--attr* _attrPath_, *-A* _attrPath_ - Enable and build the NixOS system from nix file and use the specified - attribute path from file specified by the *--file* option. If specified - without *--file* option, uses _default.nix_ in current directory. + Build the NixOS system from a nix file and use the specified + attribute path from the file specified by the *--file* option. + If specified without *--file* option, uses _system.nix_ in current directory, + the system-wide __ file, or finally, /etc/nixos/system.nix. *--flake* _flake-uri[#name]_, *-F* _flake-uri[#name]_ Build the NixOS system from the specified flake. It defaults to the @@ -355,6 +357,11 @@ NIX_SUDOOPTS # FILES +/etc/nixos/system.nix + If this file exists, then *nixos-rebuild* will use it as if the + *--file* option was given. This allows to build a self-contained + system configuration, without requiring ‘nixos’ channel. + /etc/nixos/flake.nix If this file exists, then *nixos-rebuild* will use it as if the *--flake* option was given. This file may be a symlink to a diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/models.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/models.py index 7fb37e8f97a9..ad28e368c2f5 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/models.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/models.py @@ -7,6 +7,7 @@ from enum import Enum from pathlib import Path from typing import Any, ClassVar, Self, TypedDict, override +from . import nix from .process import Remote, run_wrapper from .utils import Args @@ -58,9 +59,31 @@ class BuildAttr: @classmethod def from_arg(cls, attr: str | None, file: str | None) -> Self: - if not (attr or file): - return cls("", None) - return cls(Path(file or "default.nix"), attr) + # We use, in this order: + # 1. the --file argument (can be a directory, implying /system.nix) + # 2. system.nix in the cwd, but only if --attr is used + # 3. the Nix path entry + # 4. /etc/nixos/system.nix + # 5. the Nix path entry (uses configuration.nix) + + if file: + fpath = Path(file) + if fpath.is_dir() and (fpath / "system.nix").exists(): + return cls(fpath / "system.nix", attr) + # Backward compatibility + elif fpath.is_dir() and (fpath / "default.nix").exists(): + return cls(fpath / "default.nix", attr) + return cls(fpath, attr) + elif attr and Path("system.nix").exists(): + return cls(Path("system.nix"), attr) + elif attr and Path("default.nix").exists(): + # Backward compatibility + return cls(Path("default.nix"), attr) + elif nix.find_file("nixos-system"): + return cls("", attr) + elif Path("/etc/nixos/system.nix").exists(): + return cls(Path("/etc/nixos/system.nix"), attr) + return cls("", attr) @dataclass(frozen=True) 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 6699cc1778e1..95ebb3c9c0c2 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 @@ -270,8 +270,8 @@ def find_file(file: str, nix_flags: Args | None = None) -> Path | None: "Find classic Nix file location." r = run_wrapper( ["nix-instantiate", "--find-file", file, *dict_to_flags(nix_flags)], - stdout=PIPE, check=False, + capture_output=True, ) if r.returncode: return None @@ -670,6 +670,8 @@ def set_profile( remote=target_host, sudo=sudo, ) + + def switch_to_configuration( path_to_config: Path, action: Literal[Action.SWITCH, Action.BOOT, Action.TEST, Action.DRY_ACTIVATE], 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..456a41f46eac 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 @@ -187,12 +187,18 @@ def test_execute_nix_boot(mock_run: Mock, tmp_path: Path) -> None: nr.execute(["nixos-rebuild", "boot", "--no-flake", "-vvv", "--no-reexec"]) - assert mock_run.call_count == 7 + assert mock_run.call_count == 8 mock_run.assert_has_calls( [ + call( + ["nix-instantiate", "--find-file", "nixos-system"], + capture_output=True, + check=False, + **DEFAULT_RUN_KWARGS, + ), call( ["nix-instantiate", "--find-file", "nixpkgs", "-vvv"], - stdout=PIPE, + capture_output=True, check=False, **DEFAULT_RUN_KWARGS, ), @@ -210,7 +216,7 @@ def test_execute_nix_boot(mock_run: Mock, tmp_path: Path) -> None: call( [ "nix-build", - "", + "", "--attr", "config.system.build.toplevel", "--no-out-link", @@ -278,9 +284,15 @@ def test_execute_nix_build(mock_run: Mock, tmp_path: Path) -> None: ] ) - assert mock_run.call_count == 1 + assert mock_run.call_count == 2 mock_run.assert_has_calls( [ + call( + ["nix-instantiate", "--find-file", "nixos-system"], + check=False, + capture_output=True, + **DEFAULT_RUN_KWARGS, + ), call( [ "nix", @@ -308,6 +320,8 @@ def test_execute_nix_build_vm(mock_run: Mock, tmp_path: Path) -> None: def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]: if args[0] == "nix-build": return CompletedProcess([], 0, str(config_path)) + elif args[0] == "nix-instantiate": + return CompletedProcess([], 1) else: return CompletedProcess([], 0) @@ -326,9 +340,15 @@ def test_execute_nix_build_vm(mock_run: Mock, tmp_path: Path) -> None: ] ) - assert mock_run.call_count == 1 + assert mock_run.call_count == 2 mock_run.assert_has_calls( [ + call( + ["nix-instantiate", "--find-file", "nixos-system"], + check=False, + capture_output=True, + **DEFAULT_RUN_KWARGS, + ), call( [ "nix-build", @@ -343,7 +363,7 @@ def test_execute_nix_build_vm(mock_run: Mock, tmp_path: Path) -> None: check=True, stdout=PIPE, **DEFAULT_RUN_KWARGS, - ) + ), ] ) @@ -363,6 +383,8 @@ def test_execute_nix_build_image_flake(mock_run: Mock, tmp_path: Path) -> None: ) elif args[0] == "nix": return CompletedProcess([], 0, str(config_path)) + elif args[0] == "nix-instantiate": + return CompletedProcess([], 1) else: return CompletedProcess([], 0) @@ -379,9 +401,15 @@ def test_execute_nix_build_image_flake(mock_run: Mock, tmp_path: Path) -> None: ] ) - assert mock_run.call_count == 3 + assert mock_run.call_count == 4 mock_run.assert_has_calls( [ + call( + ["nix-instantiate", "--find-file", "nixos-system"], + check=False, + capture_output=True, + **DEFAULT_RUN_KWARGS, + ), call( [ "nix", @@ -440,6 +468,8 @@ def test_execute_nix_switch_flake(mock_run: Mock, tmp_path: Path) -> None: def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]: if args[0] == "nix": return CompletedProcess([], 0, str(config_path)) + elif args[0] == "nix-instantiate": + return CompletedProcess([], 1) else: return CompletedProcess([], 0) @@ -462,9 +492,15 @@ def test_execute_nix_switch_flake(mock_run: Mock, tmp_path: Path) -> None: ] ) - assert mock_run.call_count == 4 + assert mock_run.call_count == 5 mock_run.assert_has_calls( [ + call( + ["nix-instantiate", "--find-file", "nixos-system"], + check=False, + capture_output=True, + **DEFAULT_RUN_KWARGS, + ), call( [ "nix", @@ -572,9 +608,15 @@ def test_execute_nix_switch_build_target_host( ] ) - assert mock_run.call_count == 11 + assert mock_run.call_count == 12 mock_run.assert_has_calls( [ + call( + ["nix-instantiate", "--find-file", "nixos-system"], + check=False, + capture_output=True, + **DEFAULT_RUN_KWARGS, + ), call( [ "nix-instantiate", @@ -586,7 +628,7 @@ def test_execute_nix_switch_build_target_host( "nixpkgs=$HOME/.nix-defexpr/channels/pinned_nixpkgs", ], check=False, - stdout=PIPE, + capture_output=True, **DEFAULT_RUN_KWARGS, ), call( @@ -777,6 +819,8 @@ def test_execute_nix_switch_flake_target_host( def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]: if args[0] == "nix": return CompletedProcess([], 0, str(config_path)) + elif args[0] == "nix-instantiate": + return CompletedProcess([], 1) else: return CompletedProcess([], 0) @@ -795,9 +839,15 @@ def test_execute_nix_switch_flake_target_host( ] ) - assert mock_run.call_count == 5 + assert mock_run.call_count == 6 mock_run.assert_has_calls( [ + call( + ["nix-instantiate", "--find-file", "nixos-system"], + check=False, + capture_output=True, + **DEFAULT_RUN_KWARGS, + ), call( [ "nix", @@ -896,6 +946,8 @@ def test_execute_nix_switch_flake_build_host( return CompletedProcess([], 0, str(config_path)) elif args[0] == "ssh" and "nix" in args: return CompletedProcess([], 0, str(config_path)) + elif args[0] == "nix-instantiate": + return CompletedProcess([], 1) else: return CompletedProcess([], 0) @@ -913,9 +965,15 @@ def test_execute_nix_switch_flake_build_host( ] ) - assert mock_run.call_count == 7 + assert mock_run.call_count == 8 mock_run.assert_has_calls( [ + call( + ["nix-instantiate", "--find-file", "nixos-system"], + check=False, + capture_output=True, + **DEFAULT_RUN_KWARGS, + ), call( [ "nix", @@ -1001,7 +1059,9 @@ def test_execute_switch_rollback(mock_run: Mock, tmp_path: Path) -> None: (nixpkgs_path / ".git").mkdir(parents=True) def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]: - if args[0] == "nix-instantiate": + if args[0] == "nix-instantiate" and "nixos-system" in args: + return CompletedProcess([], 1) + elif args[0] == "nix-instantiate": return CompletedProcess([], 0, str(nixpkgs_path)) elif args[0] == "git": return CompletedProcess([], 0, "") @@ -1023,13 +1083,19 @@ def test_execute_switch_rollback(mock_run: Mock, tmp_path: Path) -> None: ] ) - assert mock_run.call_count == 5 + assert mock_run.call_count == 6 mock_run.assert_has_calls( [ + call( + ["nix-instantiate", "--find-file", "nixos-system"], + capture_output=True, + check=False, + **DEFAULT_RUN_KWARGS, + ), call( ["nix-instantiate", "--find-file", "nixpkgs"], check=False, - stdout=PIPE, + capture_output=True, **DEFAULT_RUN_KWARGS, ), call( @@ -1077,15 +1143,23 @@ def test_execute_build(mock_run: Mock, tmp_path: Path) -> None: config_path = tmp_path / "test" config_path.touch() mock_run.side_effect = [ + # nix-instantiate --find-file nixos-system + CompletedProcess([], 1), # nixos_build_flake CompletedProcess([], 0, str(config_path)), ] nr.execute(["nixos-rebuild", "build", "--no-flake", "--no-reexec"]) - assert mock_run.call_count == 1 + assert mock_run.call_count == 2 mock_run.assert_has_calls( [ + call( + ["nix-instantiate", "--find-file", "nixos-system"], + capture_output=True, + check=False, + **DEFAULT_RUN_KWARGS, + ), call( [ "nix-build", @@ -1096,7 +1170,7 @@ def test_execute_build(mock_run: Mock, tmp_path: Path) -> None: check=True, stdout=PIPE, **DEFAULT_RUN_KWARGS, - ) + ), ] ) @@ -1108,6 +1182,8 @@ def test_execute_build_dry_run_build_and_target_remote( config_path = tmp_path / "test" config_path.touch() mock_run.side_effect = [ + # nix-instantiate --find-file nixos-system + CompletedProcess([], 1), CompletedProcess([], 0, str(config_path)), CompletedProcess([], 0), CompletedProcess([], 0, str(config_path)), @@ -1126,9 +1202,15 @@ def test_execute_build_dry_run_build_and_target_remote( ] ) - assert mock_run.call_count == 3 + assert mock_run.call_count == 4 mock_run.assert_has_calls( [ + call( + ["nix-instantiate", "--find-file", "nixos-system"], + capture_output=True, + check=False, + **DEFAULT_RUN_KWARGS, + ), call( [ "nix", @@ -1181,6 +1263,8 @@ def test_execute_test_flake(mock_run: Mock, tmp_path: Path) -> None: def run_side_effect(args: list[str], **kwargs: Any) -> CompletedProcess[str]: if args[0] == "nix": return CompletedProcess([], 0, str(config_path)) + elif args[0] == "nix-instantiate": + return CompletedProcess([], 1) elif args[0] == "test": return CompletedProcess([], 1) else: @@ -1192,9 +1276,15 @@ def test_execute_test_flake(mock_run: Mock, tmp_path: Path) -> None: ["nixos-rebuild", "test", "--flake", "github:user/repo#hostname", "--no-reexec"] ) - assert mock_run.call_count == 3 + assert mock_run.call_count == 4 mock_run.assert_has_calls( [ + call( + ["nix-instantiate", "--find-file", "nixos-system"], + capture_output=True, + check=False, + **DEFAULT_RUN_KWARGS, + ), call( [ "nix", @@ -1242,6 +1332,8 @@ def test_execute_test_rollback( 2084 2024-11-07 23:54:17 (current) """), ) + elif args[0] == "nix-instantiate" and "nixos-system" in args: + return CompletedProcess([], 1) elif args[0] == "test": return CompletedProcess([], 1) else: @@ -1253,7 +1345,7 @@ def test_execute_test_rollback( ["nixos-rebuild", "test", "--rollback", "--profile-name", "foo", "--no-reexec"] ) - assert mock_run.call_count == 3 + assert mock_run.call_count == 4 mock_run.assert_has_calls( [ call( @@ -1296,7 +1388,7 @@ def test_execute_switch_store_path(mock_run: Mock, tmp_path: Path) -> None: config_path = tmp_path / "test-system" config_path.mkdir() - mock_run.return_value = CompletedProcess([], 0) + mock_run.return_value = CompletedProcess([], 0, stdout="") nr.execute( [ @@ -1309,9 +1401,15 @@ def test_execute_switch_store_path(mock_run: Mock, tmp_path: Path) -> None: ) # --store-path skips build and write_version_suffix, so only activation calls - assert mock_run.call_count == 3 + assert mock_run.call_count == 4 mock_run.assert_has_calls( [ + call( + ["nix-instantiate", "--find-file", "nixos-system"], + capture_output=True, + check=False, + **DEFAULT_RUN_KWARGS, + ), call( [ "nix-env", @@ -1359,7 +1457,7 @@ def test_execute_switch_store_path_target_host( config_path = tmp_path / "test-system" config_path.mkdir() - mock_run.return_value = CompletedProcess([], 0) + mock_run.return_value = CompletedProcess([], 0, stdout="") nr.execute( [ @@ -1375,7 +1473,7 @@ def test_execute_switch_store_path_target_host( ) # --store-path skips build and write_version_suffix, so only copy/activation calls - assert mock_run.call_count == 5 + assert mock_run.call_count == 6 mock_run.assert_has_calls( [ call( diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_models.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_models.py index 21c957b9d821..64ffce686976 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_models.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_models.py @@ -5,19 +5,57 @@ from unittest.mock import Mock, patch from pytest import MonkeyPatch import nixos_rebuild.models as m +import nixos_rebuild.nix as n from .helpers import get_qualified_name -def test_build_attr_from_arg() -> None: - assert m.BuildAttr.from_arg(None, None) == m.BuildAttr("", None) - assert m.BuildAttr.from_arg("attr", None) == m.BuildAttr( - Path("default.nix"), "attr" - ) +def test_build_attr_from_arg(tmp_path: Path) -> None: assert m.BuildAttr.from_arg("attr", "file.nix") == m.BuildAttr( Path("file.nix"), "attr" ) - assert m.BuildAttr.from_arg(None, "file.nix") == m.BuildAttr(Path("file.nix"), None) + + with patch( + # system.nix exists + "pathlib.Path.exists", + autospec=True, + side_effect=[True], + ): + assert m.BuildAttr.from_arg("attr", None) == m.BuildAttr( + Path("system.nix"), "attr" + ) + + with patch( + # is defined + get_qualified_name(n.find_file), + autospec=True, + return_value=Path("/some/file.nix"), + ): + assert m.BuildAttr.from_arg("attr", None) == m.BuildAttr( + "", "attr" + ) + + with ( + # not defined + patch(get_qualified_name(n.find_file), autospec=True, return_value=None), + # system.nix does not exist, but /etc/nixos/system.nix does + patch( + "pathlib.Path.exists", + autospec=True, + side_effect=[True], + ), + ): + assert m.BuildAttr.from_arg(None, None) == m.BuildAttr( + Path("/etc/nixos/system.nix"), None + ) + + with patch( + # not defined + get_qualified_name(n.find_file), + autospec=True, + return_value=None, + ): + assert m.BuildAttr.from_arg(None, None) == m.BuildAttr("", None) def test_build_attr_to_attr() -> None: diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_services.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_services.py index 529073e62464..6bd823b1d48d 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_services.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_services.py @@ -1,5 +1,6 @@ import os from pathlib import Path +from subprocess import CompletedProcess from unittest.mock import ANY, Mock, call, patch from pytest import MonkeyPatch @@ -12,8 +13,13 @@ from .helpers import get_qualified_name @patch.dict(os.environ, {}, clear=True) @patch("os.execve", autospec=True) +@patch(get_qualified_name(n.nix.run_wrapper, n.nix), autospec=True) @patch(get_qualified_name(s.nix.build), autospec=True) -def test_reexec(mock_build: Mock, mock_execve: Mock, monkeypatch: MonkeyPatch) -> None: +def test_reexec( + mock_build: Mock, mock_run: Mock, mock_execve: Mock, monkeypatch: MonkeyPatch +) -> None: + mock_run.return_value = CompletedProcess([], 0, stdout="") + monkeypatch.setattr(s, "EXECUTABLE", "nixos-rebuild-ng") argv = ["/path/bin/nixos-rebuild-ng", "switch", "--no-flake"] args, _ = n.parse_args(argv) From fadc273dd040dacb210971909589cdc77ac118db Mon Sep 17 00:00:00 2001 From: rnhmjoj Date: Mon, 23 Feb 2026 07:56:56 +0100 Subject: [PATCH 5/5] nixos/release-notes: explain the new system.nix file --- .../manual/release-notes/rl-2605.section.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index cd59164990eb..622ca41db4e8 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -4,6 +4,32 @@ +- The system.nix file has been added has an alternative entry point to configuration.nix (and flake.nix) that allows to configure NixOS without using `nix-channel`. + This file must evaluate to a NixOS system derivation or an attribute set of such derivations, in which case the attribute to build has to be selected with the `--attr` option of `nixos-rebuild` or `nixos-install`. + For example, + ```nix + # system.nix + let + # Pinned Nixpkgs archive + # + # Use `curl -I https://channels.nixos.org/nixos-26.05` to get the + # latest commit of the stable channel and `nix-prefetch-url --unpack` + # to compute its sha256 hash. + nixpkgs = builtins.fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/c217913993d6.tar.gz"; + sha256 = "026mprs324330pfazlgbw987qmsa8ligglarvqbcxzig2kgw0lqg"; + }; + in + import "${nixpkgs}/nixos" { + # Build NixOS using an external configuration.nix file, + # or directly set your options here + configuration = ./configuration.nix; + } + ``` + + The default location of system.nix is `/etc/nixos/system.nix` and can be changed by setting the `` search path. + `nixos-rebuild` and `nixos-install` can now also load a system.nix file in the current directory (only if `--attr` is used) or from a directory specified with `--file`. + - The default kernel package has been updated from 6.12 to 6.18. All supported kernels remain available. ## New Modules {#sec-release-26.05-new-modules}