nixos-rebuild-ng: fix repl behaving differently on git flakes than build commands

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.
This commit is contained in:
Jörg Thalheim
2025-01-21 22:47:52 +01:00
parent d21198f27d
commit 8e2bc8eed7
3 changed files with 64 additions and 16 deletions
@@ -61,6 +61,25 @@ class BuildAttr:
return cls(Path(file or "default.nix"), attr)
def discover_git(location: Path) -> str | None:
current = location.resolve()
previous = None
while current.is_dir() and current != previous:
dotgit = current / ".git"
if dotgit.is_dir():
return str(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 dotgit_content.split("gitdir: ")[1]
previous = current
current = current.parent
return None
@dataclass(frozen=True)
class Flake:
path: Path | str
@@ -84,11 +103,15 @@ class Flake:
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'}"
path = m.group("path")
if ":" in path:
return cls(path, nixos_attr)
path_str = m.group("path")
if ":" in path_str:
return cls(path_str, nixos_attr)
else:
return cls(Path(path), nixos_attr)
path = Path(path_str)
git_repo = discover_git(path)
if git_repo is not None:
return cls(f"git+file://{git_repo}", nixos_attr)
return cls(path, nixos_attr)
@classmethod
def from_arg(cls, flake_arg: Any, target_host: Remote | None) -> Self | None:
@@ -4,6 +4,8 @@ from pathlib import Path
from typing import Any
from unittest.mock import patch
from pytest import MonkeyPatch
import nixos_rebuild.models as m
from .helpers import get_qualified_name
@@ -30,7 +32,7 @@ def test_build_attr_to_attr() -> None:
)
def test_flake_parse() -> None:
def test_flake_parse(tmpdir: Path, monkeypatch: MonkeyPatch) -> None:
assert m.Flake.parse("/path/to/flake#attr") == m.Flake(
Path("/path/to/flake"), "nixosConfigurations.attr"
)
@@ -40,15 +42,31 @@ def test_flake_parse() -> None:
assert m.Flake.parse("/path/to/flake", lambda: "hostname") == m.Flake(
Path("/path/to/flake"), "nixosConfigurations.hostname"
)
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")
# 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"
)
def test_flake_to_attr() -> None:
@@ -61,7 +79,7 @@ def test_flake_to_attr() -> None:
@patch(get_qualified_name(platform.node), autospec=True)
def test_flake_from_arg(mock_node: Any) -> None:
def test_flake_from_arg(mock_node: Any, monkeypatch: MonkeyPatch, tmpdir: Path) -> None:
mock_node.return_value = "hostname"
# Flake string
@@ -73,9 +91,11 @@ def test_flake_from_arg(mock_node: Any) -> None:
assert m.Flake.from_arg(False, None) is None
# True
assert m.Flake.from_arg(True, None) == m.Flake(
Path("."), "nixosConfigurations.hostname"
)
with monkeypatch.context() as patch_context:
patch_context.chdir(tmpdir)
assert m.Flake.from_arg(True, None) == m.Flake(
Path("."), "nixosConfigurations.hostname"
)
# None when we do not have /etc/nixos/flake.nix
with patch(
@@ -6,6 +6,7 @@ from typing import Any
from unittest.mock import ANY, call, patch
import pytest
from pytest import MonkeyPatch
import nixos_rebuild.models as m
import nixos_rebuild.nix as n
@@ -51,7 +52,8 @@ def test_build(mock_run: Any, monkeypatch: Any) -> None:
autospec=True,
return_value=CompletedProcess([], 0, stdout=" \n/path/to/file\n "),
)
def test_build_flake(mock_run: Any) -> None:
def test_build_flake(mock_run: Any, monkeypatch: MonkeyPatch, tmpdir: Path) -> None:
monkeypatch.chdir(tmpdir)
flake = m.Flake.parse(".#hostname")
assert n.build_flake(
@@ -165,7 +167,10 @@ def test_build_remote(mock_uuid4: Any, mock_run: Any, monkeypatch: Any) -> None:
autospec=True,
return_value=CompletedProcess([], 0, stdout=" \n/path/to/file\n "),
)
def test_build_remote_flake(mock_run: Any, monkeypatch: Any) -> None:
def test_build_remote_flake(
mock_run: Any, monkeypatch: MonkeyPatch, tmpdir: Path
) -> None:
monkeypatch.chdir(tmpdir)
flake = m.Flake.parse(".#hostname")
build_host = m.Remote("user@host", [], None)
monkeypatch.setenv("NIX_SSHOPTS", "--ssh opts")
@@ -287,7 +292,7 @@ def test_copy_closure(monkeypatch: Any) -> None:
@patch(get_qualified_name(n.run_wrapper, n), autospec=True)
def test_edit(mock_run: Any, monkeypatch: Any, tmpdir: Any) -> None:
# Flake
flake = m.Flake.parse(".#attr")
flake = m.Flake.parse(f"{tmpdir}#attr")
n.edit(flake, {"commit_lock_file": True})
mock_run.assert_called_with(
[
@@ -297,7 +302,7 @@ def test_edit(mock_run: Any, monkeypatch: Any, tmpdir: Any) -> None:
"edit",
"--commit-lock-file",
"--",
".#nixosConfigurations.attr",
f"{tmpdir}#nixosConfigurations.attr",
],
check=False,
)