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 0d67bc81aed97..67947d6420fcf 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 @@ -4,7 +4,7 @@ from dataclasses import dataclass from enum import Enum from pathlib import Path -from typing import Any, Callable, ClassVar, Self, TypedDict, override +from typing import Any, ClassVar, Self, TypedDict, override from .process import Remote, run_wrapper @@ -61,43 +61,18 @@ def from_arg(cls, attr: str | None, file: str | None) -> Self: return cls(Path(file or "default.nix"), attr) -def discover_git(location: Path) -> Path | None: - """ - Discover the current git repository in the given location. - """ - current = location.resolve() - previous = None - - while current.is_dir() and current != previous: - dotgit = current / ".git" - if dotgit.is_dir(): - return current - elif dotgit.is_file(): # this is a worktree - with dotgit.open() as f: - dotgit_content = f.read().strip() - if dotgit_content.startswith("gitdir: "): - return Path(dotgit_content.split("gitdir: ")[1]) - previous = current - current = current.parent - - return None - - -def discover_closest_flake(location: Path) -> Path | None: - """ - Discover the closest flake.nix file starting from the given location upwards. - """ - current = location.resolve() - previous = None - - while current.is_dir() and current != previous: - flake_file = current / "flake.nix" - if flake_file.is_file(): - return current - previous = current - current = current.parent - - return None +def _get_hostname(target_host: Remote | None) -> str | None: + if target_host: + try: + return run_wrapper( + ["uname", "-n"], + stdout=subprocess.PIPE, + remote=target_host, + ).stdout.strip() + except (AttributeError, subprocess.CalledProcessError): + return None + else: + return platform.node() @dataclass(frozen=True) @@ -114,53 +89,41 @@ def __str__(self) -> str: return f"{self.path}#{self.attr}" @classmethod - def parse( - cls, - flake_str: str, - hostname_fn: Callable[[], str | None] = lambda: None, - ) -> Self: + def parse(cls, flake_str: str, target_host: Remote | None = None) -> Self: m = cls._re.match(flake_str) assert m is not None, f"got no matches for {flake_str}" attr = m.group("attr") - nixos_attr = f'nixosConfigurations."{attr or hostname_fn() or "default"}"' + nixos_attr = ( + f'nixosConfigurations."{attr or _get_hostname(target_host) or "default"}"' + ) path_str = m.group("path") if ":" in path_str: return cls(path_str, nixos_attr) else: - path = Path(path_str) - git_repo = discover_git(path) - if git_repo is not None: - url = f"git+file://{git_repo}" - flake_path = discover_closest_flake(path) - if ( - flake_path is not None - and flake_path != git_repo - and flake_path.is_relative_to(git_repo) - ): - url += f"?dir={flake_path.relative_to(git_repo)}" - return cls(url, nixos_attr) - return cls(path, nixos_attr) + # Since we use builtins.getFlake we have behavior differences + # between normal nix build and the nix repl because + # builtins.getFlake won't pick up local flakes as git+file + # but assumes path:// flakes instead. + # This can have surprising effects such as beeing able to access + # untracked files that would lead to build failures otherwise + # or copying large files to the nix store. + # We could force git+file:// protocol to fix this, but thanks to + # another issue in nix this causes a nasty issue where + # nixos-rebuild is run inside a symlink to a nix store path + # (e.g. /run/opengl-driver/lib). + # See: + # - https://github.com/NixOS/nixpkgs/pull/375493 + # - https://github.com/NixOS/nixpkgs/pull/410498 + # - https://github.com/NixOS/nixpkgs/issues/93694 + return cls(Path(path_str), nixos_attr) @classmethod def from_arg(cls, flake_arg: Any, target_host: Remote | None) -> Self | None: - def get_hostname() -> str | None: - if target_host: - try: - return run_wrapper( - ["uname", "-n"], - stdout=subprocess.PIPE, - remote=target_host, - ).stdout.strip() - except (AttributeError, subprocess.CalledProcessError): - return None - else: - return platform.node() - match flake_arg: case str(s): - return cls.parse(s, get_hostname) + return cls.parse(s, target_host) case True: - return cls.parse(".", get_hostname) + return cls.parse(".", target_host) case False: return None case _: @@ -169,7 +132,7 @@ def get_hostname() -> str | None: if default_path.exists(): # It can be a symlink to the actual flake. default_path = default_path.resolve() - return cls.parse(str(default_path.parent), get_hostname) + return cls.parse(str(default_path.parent), target_host) else: return None 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 2cc450e00a363..2f00db70c88af 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 @@ -2,8 +2,6 @@ from pathlib import Path from unittest.mock import Mock, patch -from pytest import MonkeyPatch - import nixos_rebuild.models as m from .helpers import get_qualified_name @@ -30,45 +28,32 @@ def test_build_attr_to_attr() -> None: ) -def test_flake_parse(tmpdir: Path, monkeypatch: MonkeyPatch) -> None: +@patch("platform.node", autospec=True, return_value="hostname") +def test_flake_parse(mock_node: Mock) -> None: assert m.Flake.parse("/path/to/flake#attr") == m.Flake( Path("/path/to/flake"), 'nixosConfigurations."attr"' ) - assert m.Flake.parse("/path/ to /flake", lambda: "hostname") == m.Flake( + assert m.Flake.parse("/path/ to /flake") == m.Flake( Path("/path/ to /flake"), 'nixosConfigurations."hostname"' ) - assert m.Flake.parse("/path/to/flake", lambda: "hostname") == m.Flake( + assert m.Flake.parse("/path/to/flake") == m.Flake( Path("/path/to/flake"), 'nixosConfigurations."hostname"' ) - # change directory to tmpdir - with monkeypatch.context() as patch_context: - patch_context.chdir(tmpdir) - assert m.Flake.parse(".#attr") == m.Flake( - Path("."), 'nixosConfigurations."attr"' - ) - assert m.Flake.parse("#attr") == m.Flake( - Path("."), 'nixosConfigurations."attr"' - ) - assert m.Flake.parse(".") == m.Flake(Path("."), 'nixosConfigurations."default"') assert m.Flake.parse("path:/to/flake#attr") == m.Flake( "path:/to/flake", 'nixosConfigurations."attr"' ) assert m.Flake.parse("github:user/repo/branch") == m.Flake( - "github:user/repo/branch", 'nixosConfigurations."default"' - ) - git_root = tmpdir / "git_root" - git_root.mkdir() - (git_root / ".git").mkdir() - assert m.Flake.parse(str(git_root)) == m.Flake( - f"git+file://{git_root}", 'nixosConfigurations."default"' - ) - - work_tree = tmpdir / "work_tree" - work_tree.mkdir() - (work_tree / ".git").write_text("gitdir: /path/to/git", "utf-8") - assert m.Flake.parse(str(work_tree)) == m.Flake( - "git+file:///path/to/git", 'nixosConfigurations."default"' + "github:user/repo/branch", 'nixosConfigurations."hostname"' ) + with patch( + get_qualified_name(m.run_wrapper, m), + autospec=True, + return_value=subprocess.CompletedProcess([], 0, stdout="remote\n"), + ): + target_host = m.Remote("remote", [], None) + assert m.Flake.parse("/path/to/flake", target_host) == m.Flake( + Path("/path/to/flake"), 'nixosConfigurations."remote"' + ) def test_flake_to_attr() -> None: @@ -80,12 +65,8 @@ def test_flake_to_attr() -> None: ) -@patch("platform.node", autospec=True) -def test_flake_from_arg( - mock_node: Mock, monkeypatch: MonkeyPatch, tmpdir: Path -) -> None: - mock_node.return_value = "hostname" - +@patch("platform.node", autospec=True, return_value="hostname") +def test_flake_from_arg(mock_node: Mock) -> None: # Flake string assert m.Flake.from_arg("/path/to/flake#attr", None) == m.Flake( Path("/path/to/flake"), 'nixosConfigurations."attr"' @@ -95,11 +76,9 @@ def test_flake_from_arg( assert m.Flake.from_arg(False, None) is None # True - with monkeypatch.context() as patch_context: - patch_context.chdir(tmpdir) - assert m.Flake.from_arg(True, None) == m.Flake( - Path("."), 'nixosConfigurations."hostname"' - ) + assert m.Flake.from_arg(True, None) == m.Flake( + Path("."), 'nixosConfigurations."hostname"' + ) # None when we do not have /etc/nixos/flake.nix with patch( @@ -109,28 +88,6 @@ def test_flake_from_arg( ): assert m.Flake.from_arg(None, None) is None - # None when we have a file in /etc/nixos/flake.nix - with ( - patch( - "pathlib.Path.exists", - autospec=True, - return_value=True, - ), - patch( - "pathlib.Path.is_symlink", - autospec=True, - return_value=False, - ), - patch( - get_qualified_name(m.discover_git), - autospec=True, - return_value="/etc/nixos", - ), - ): - assert m.Flake.from_arg(None, None) == m.Flake( - "git+file:///etc/nixos", 'nixosConfigurations."hostname"' - ) - with ( patch( "pathlib.Path.exists", diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py index 195f929aaf534..c50839b78fc4d 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py @@ -56,8 +56,7 @@ def test_build(mock_run: Mock) -> None: autospec=True, return_value=CompletedProcess([], 0, stdout=" \n/path/to/file\n "), ) -def test_build_flake(mock_run: Mock, monkeypatch: MonkeyPatch, tmpdir: Path) -> None: - monkeypatch.chdir(tmpdir) +def test_build_flake(mock_run: Mock) -> None: flake = m.Flake.parse(".#hostname") assert n.build_flake( @@ -193,10 +192,7 @@ def run_wrapper_side_effect( autospec=True, return_value=CompletedProcess([], 0, stdout=" \n/path/to/file\n "), ) -def test_build_remote_flake( - mock_run: Mock, monkeypatch: MonkeyPatch, tmpdir: Path -) -> None: - monkeypatch.chdir(tmpdir) +def test_build_remote_flake(mock_run: Mock, monkeypatch: MonkeyPatch) -> None: flake = m.Flake.parse(".#hostname") build_host = m.Remote("user@host", [], None) monkeypatch.setenv("NIX_SSHOPTS", "--ssh opts") @@ -318,7 +314,7 @@ def test_copy_closure(monkeypatch: MonkeyPatch) -> None: @patch(get_qualified_name(n.run_wrapper, n), autospec=True) def test_edit(mock_run: Mock, monkeypatch: MonkeyPatch, tmpdir: Path) -> None: # Flake - flake = m.Flake.parse(f"{tmpdir}#attr") + flake = m.Flake.parse("#attr") n.edit(flake, {"commit_lock_file": True}) mock_run.assert_called_with( [ @@ -328,7 +324,7 @@ def test_edit(mock_run: Mock, monkeypatch: MonkeyPatch, tmpdir: Path) -> None: "edit", "--commit-lock-file", "--", - f'{tmpdir}#nixosConfigurations."attr"', + '.#nixosConfigurations."attr"', ], check=False, )