From 4d67200b802b90b54c97e4be92a8f78282fcac4e Mon Sep 17 00:00:00 2001 From: Steve Walker <65963536+etherswangel@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:52:51 +0800 Subject: [PATCH] flake: Fix follow symlinks Contributes towards #106. (cherry picked from commit 24d11d22c31dd7773cbcef6ea077e62ea114e2a3) Upstream-PR: https://github.com/NixOS/nix/pull/12286 Change-Id: I523c76e31f27fac5d73eff76b22894adcd6ee135 --- doc/manual/change-authors.yml | 7 ++ doc/manual/rl-next/flake-follow-symlinks.md | 10 +++ lix/libexpr/flake/flakeref.cc | 10 +++ tests/functional2/flakes/assets/flake1.nix | 7 ++ tests/functional2/flakes/assets/flake2.nix | 7 ++ tests/functional2/flakes/assets/flake_a.nix | 7 ++ tests/functional2/flakes/assets/flake_b.nix | 7 ++ .../functional2/flakes/test_symlink_paths.py | 75 +++++++++++++++++++ 8 files changed, 130 insertions(+) create mode 100644 doc/manual/rl-next/flake-follow-symlinks.md create mode 100644 tests/functional2/flakes/assets/flake1.nix create mode 100644 tests/functional2/flakes/assets/flake2.nix create mode 100644 tests/functional2/flakes/assets/flake_a.nix create mode 100644 tests/functional2/flakes/assets/flake_b.nix create mode 100644 tests/functional2/flakes/test_symlink_paths.py diff --git a/doc/manual/change-authors.yml b/doc/manual/change-authors.yml index 5e69bb105..fd0e37bae 100644 --- a/doc/manual/change-authors.yml +++ b/doc/manual/change-authors.yml @@ -247,6 +247,9 @@ roberth: seppel3210: github: Seppel3210 +stevalkr: + github: stevalkr + teofilc: forgejo: teofilc github: TeofilC @@ -288,6 +291,10 @@ xanderio: xokdvium: github: xokdvium +xyenon: + forgejo: xyenon + github: xyenon + yorickvp: github: yorickvp diff --git a/doc/manual/rl-next/flake-follow-symlinks.md b/doc/manual/rl-next/flake-follow-symlinks.md new file mode 100644 index 000000000..51fc8bce9 --- /dev/null +++ b/doc/manual/rl-next/flake-follow-symlinks.md @@ -0,0 +1,10 @@ +--- +synopsis: Fix resolving of symlinks in flake paths +issues: [fj#106] +prs: [12286] +cls: [4783] +category: Fixes +credits: [stevalkr, xyenon] +--- + +Flake paths are now canonicalized to resolve symlinks. This ensures that when a flake is accessed via a symlink, paths are resolved relative to the target directory, not the symlink's location. diff --git a/lix/libexpr/flake/flakeref.cc b/lix/libexpr/flake/flakeref.cc index 1e1a789d5..b5c2366c7 100644 --- a/lix/libexpr/flake/flakeref.cc +++ b/lix/libexpr/flake/flakeref.cc @@ -7,6 +7,8 @@ #include "lix/libfetchers/fetchers.hh" #include "lix/libfetchers/registry.hh" +#include + namespace nix { #if 0 @@ -150,6 +152,14 @@ std::pair parseFlakeRefWithFragment( throw BadURL("could not find a flake.nix file"); } + try { + path = absPath(path, std::nullopt, true); + } catch (SysError & e) { + if (e.errNo != ENOENT && e.errNo != ENOTDIR) { + throw; + } + } + if (!S_ISDIR(lstat(path).st_mode)) throw BadURL("path '%s' is not a flake (because it's not a directory)", path); diff --git a/tests/functional2/flakes/assets/flake1.nix b/tests/functional2/flakes/assets/flake1.nix new file mode 100644 index 000000000..5bf57bf66 --- /dev/null +++ b/tests/functional2/flakes/assets/flake1.nix @@ -0,0 +1,7 @@ +{ + outputs = + { self }: + { + x = 2; + }; +} diff --git a/tests/functional2/flakes/assets/flake2.nix b/tests/functional2/flakes/assets/flake2.nix new file mode 100644 index 000000000..7ebb829b3 --- /dev/null +++ b/tests/functional2/flakes/assets/flake2.nix @@ -0,0 +1,7 @@ +{ + outputs = + { self }: + { + x = builtins.readFile ../file; + }; +} diff --git a/tests/functional2/flakes/assets/flake_a.nix b/tests/functional2/flakes/assets/flake_a.nix new file mode 100644 index 000000000..d8ee9f02f --- /dev/null +++ b/tests/functional2/flakes/assets/flake_a.nix @@ -0,0 +1,7 @@ +{ + outputs = + { self }: + { + x = "a"; + }; +} diff --git a/tests/functional2/flakes/assets/flake_b.nix b/tests/functional2/flakes/assets/flake_b.nix new file mode 100644 index 000000000..f361e2825 --- /dev/null +++ b/tests/functional2/flakes/assets/flake_b.nix @@ -0,0 +1,7 @@ +{ + outputs = + { self }: + { + x = "b"; + }; +} diff --git a/tests/functional2/flakes/test_symlink_paths.py b/tests/functional2/flakes/test_symlink_paths.py new file mode 100644 index 000000000..b6e2ae1b1 --- /dev/null +++ b/tests/functional2/flakes/test_symlink_paths.py @@ -0,0 +1,75 @@ +from pathlib import Path + +from functional2.testlib.fixtures.nix import Nix +from functional2.testlib.fixtures.file_helper import CopyFile, Symlink, with_files, File +from functional2.testlib.utils import get_global_asset_pack +from functional2.testlib.fixtures.command import Command +from functional2.testlib.fixtures.env import ManagedEnv + + +@with_files( + {"flake1": {"flake.nix": CopyFile("assets/flake1.nix")}, "flake1_sym": Symlink("flake1")} +) +def test_symlink_points_to_flake(nix: Nix, files: Path) -> None: + result = nix.nix(["eval", f"{files}/flake1_sym#x"], flake=True).run().ok() + assert result.stdout_plain == "2" + + +@with_files( + { + "subdir": {"flake1": {"flake.nix": CopyFile("assets/flake1.nix")}}, + "subdir_sym": Symlink("subdir"), + } +) +def test_symlink_points_to_flake_in_subdir(nix: Nix, files: Path) -> None: + result = nix.nix(["eval", f"{files}/subdir_sym/flake1#x"], flake=True).run().ok() + assert result.stdout_plain == "2" + + +@with_files( + { + "flake1": {"subdir": {"flake.nix": CopyFile("assets/flake1.nix")}}, + "flake1_sym": Symlink("flake1/subdir"), + } +) +def test_symlink_points_to_dir_in_repo(nix: Nix, files: Path) -> None: + result = nix.nix(["eval", f"{files}/flake1_sym#x"], flake=True).run().ok() + assert result.stdout_plain == "2" + + +@with_files( + { + "repo1": get_global_asset_pack(".git") + | {"file": File("Hello"), "subdir": {"flake.nix": CopyFile("assets/flake2.nix")}}, + "repo2": get_global_asset_pack(".git") + | {"file": File("World"), "flake1_sym": Symlink("../repo1/subdir")}, + } +) +def test_symlink_from_repo_to_another(nix: Nix, files: Path, env: ManagedEnv) -> None: + env.path.add_program("git") + + Command(["git", "add", "subdir/flake.nix", "file"], _env=env, cwd=files / "repo1").run().ok() + result = nix.nix(["eval", f"{files}/repo1/subdir#x"], flake=True).run().ok() + assert result.stdout_plain == '"Hello"' + + Command(["git", "add", "flake1_sym", "file"], _env=env, cwd=files / "repo2").run().ok() + result = nix.nix(["eval", f"{files}/repo2/flake1_sym#x"], flake=True).run().ok() + assert result.stdout_plain == '"Hello"' + + +@with_files( + { + "repo1": get_global_asset_pack(".git") + | {"flake.nix": CopyFile("assets/flake_a.nix"), "subdir": {}}, + "repo2": get_global_asset_pack(".git") + | {"flake.nix": CopyFile("assets/flake_b.nix"), "subdir_sym": Symlink("../repo1/subdir")}, + } +) +def test_symlink_to_subdir_without_flake(nix: Nix, files: Path, env: ManagedEnv) -> None: + env.path.add_program("git") + + Command(["git", "add", "flake.nix", "subdir"], _env=env, cwd=files / "repo1").run().ok() + Command(["git", "add", "flake.nix", "subdir_sym"], _env=env, cwd=files / "repo2").run().ok() + + result = nix.nix(["eval", f"{files}/repo2/subdir_sym#x"], flake=True).run().ok() + assert result.stdout_plain == '"b"'