Unverified Commit 5c562a35 authored by rnhmjoj's avatar rnhmjoj
Browse files

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 <nixos-system> 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.
parent bdd34aca
Loading
Loading
Loading
Loading
+15 −8
Original line number Diff line number Diff line
@@ -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 _<nixos-system>_ 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
+26 −3
Original line number Diff line number Diff line
@@ -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("<nixpkgs/nixos>", 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 <nixos-system> Nix path entry
        #   4. /etc/nixos/system.nix
        #   5. the <nixpkgs/nixos> 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("<nixos-system>", attr)
        elif Path("/etc/nixos/system.nix").exists():
            return cls(Path("/etc/nixos/system.nix"), attr)
        return cls("<nixpkgs/nixos>", attr)


@dataclass(frozen=True)
+3 −1
Original line number Diff line number Diff line
@@ -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],
+122 −24
Original line number Diff line number Diff line
@@ -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",
                    "<nixpkgs/nixos>",
                    "<nixos-system>",
                    "--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(
+44 −6
Original line number Diff line number Diff line
@@ -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("<nixpkgs/nixos>", 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(
        # <nixos-system> 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(
            "<nixos-system>", "attr"
        )

    with (
        # <nixos-system> 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(
        # <nixos-system> not defined
        get_qualified_name(n.find_file),
        autospec=True,
        return_value=None,
    ):
        assert m.BuildAttr.from_arg(None, None) == m.BuildAttr("<nixpkgs/nixos>", None)


def test_build_attr_to_attr() -> None:
Loading