From f5d63ddc958596ff7344f88fb1e28ab240786187 Mon Sep 17 00:00:00 2001 From: Some Guy <253672337+JustAGuyTryingHisBest@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:49:57 -0800 Subject: [PATCH 1/3] feat: builtins.readSymlink primop --- src/libexpr/primops.cc | 18 ++++++++++++++++++ ...eval-fail-readSymlink-not-a-symlink.err.exp | 1 + .../eval-fail-readSymlink-not-a-symlink.nix | 1 + ...-fail-readSymlink-not-a-symlink.postprocess | 10 ++++++++++ .../functional/lang/eval-okay-readSymlink.exp | 1 + .../functional/lang/eval-okay-readSymlink.nix | 4 ++++ 6 files changed, 35 insertions(+) create mode 100644 tests/functional/lang/eval-fail-readSymlink-not-a-symlink.err.exp create mode 100644 tests/functional/lang/eval-fail-readSymlink-not-a-symlink.nix create mode 100644 tests/functional/lang/eval-fail-readSymlink-not-a-symlink.postprocess create mode 100644 tests/functional/lang/eval-okay-readSymlink.exp create mode 100644 tests/functional/lang/eval-okay-readSymlink.nix diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index b3116856613..ed003adce5c 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -2376,6 +2376,24 @@ static RegisterPrimOp primop_readFileType({ .fun = prim_readFileType, }); +/* Read the target of a symlink. */ +static void prim_readSymlink(EvalState & state, const PosIdx pos, Value ** args, Value & v) +{ + auto path = state.realisePath(pos, *args[0], SymlinkResolution::Ancestors); + v.mkString(path.readLink(), state.mem); +} + +static RegisterPrimOp primop_readSymlink({ + .name = "__readSymlink", + .args = {"path"}, + .doc = R"( + Return the target of the symlink at *path*. + + If *path* does not refer to a symlink, an error is thrown. + )", + .fun = prim_readSymlink, +}); + /* Read a directory (without . or ..) */ static void prim_readDir(EvalState & state, const PosIdx pos, Value ** args, Value & v) { diff --git a/tests/functional/lang/eval-fail-readSymlink-not-a-symlink.err.exp b/tests/functional/lang/eval-fail-readSymlink-not-a-symlink.err.exp new file mode 100644 index 00000000000..7d689c8bff1 --- /dev/null +++ b/tests/functional/lang/eval-fail-readSymlink-not-a-symlink.err.exp @@ -0,0 +1 @@ +error: filesystem error: read_symlink: not a symlink ["/pwd/lang/readDir/bar"] diff --git a/tests/functional/lang/eval-fail-readSymlink-not-a-symlink.nix b/tests/functional/lang/eval-fail-readSymlink-not-a-symlink.nix new file mode 100644 index 00000000000..a2a04dfb101 --- /dev/null +++ b/tests/functional/lang/eval-fail-readSymlink-not-a-symlink.nix @@ -0,0 +1 @@ +builtins.readSymlink ./readDir/bar diff --git a/tests/functional/lang/eval-fail-readSymlink-not-a-symlink.postprocess b/tests/functional/lang/eval-fail-readSymlink-not-a-symlink.postprocess new file mode 100644 index 00000000000..5466d6120ff --- /dev/null +++ b/tests/functional/lang/eval-fail-readSymlink-not-a-symlink.postprocess @@ -0,0 +1,10 @@ +# shellcheck shell=bash +set -euo pipefail +testcaseBasename=$1 + +# Normalize platform-specific filesystem error messages +sed -i "$testcaseBasename.err" \ + -e 's/filesystem error: in read_symlink:/filesystem error: read_symlink:/' \ + -e 's/Invalid argument/not a symlink/' \ + -e 's/Not a symbolic link/not a symlink/' \ + ; diff --git a/tests/functional/lang/eval-okay-readSymlink.exp b/tests/functional/lang/eval-okay-readSymlink.exp new file mode 100644 index 00000000000..8a72759b8d6 --- /dev/null +++ b/tests/functional/lang/eval-okay-readSymlink.exp @@ -0,0 +1 @@ +{ ldir = "foo"; linked = "foo/git-hates-directories"; } diff --git a/tests/functional/lang/eval-okay-readSymlink.nix b/tests/functional/lang/eval-okay-readSymlink.nix new file mode 100644 index 00000000000..da19a51e0e3 --- /dev/null +++ b/tests/functional/lang/eval-okay-readSymlink.nix @@ -0,0 +1,4 @@ +{ + ldir = builtins.readSymlink ./readDir/ldir; + linked = builtins.readSymlink ./readDir/linked; +} From 048edd40f8dce7db22e880b350812bbb4fec8560 Mon Sep 17 00:00:00 2001 From: Some Guy <253672337+JustAGuyTryingHisBest@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:29:30 -0800 Subject: [PATCH 2/3] fix quotes --- .../functional/lang/eval-fail-readSymlink-not-a-symlink.err.exp | 2 +- .../lang/eval-fail-readSymlink-not-a-symlink.postprocess | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/functional/lang/eval-fail-readSymlink-not-a-symlink.err.exp b/tests/functional/lang/eval-fail-readSymlink-not-a-symlink.err.exp index 7d689c8bff1..1610db4536b 100644 --- a/tests/functional/lang/eval-fail-readSymlink-not-a-symlink.err.exp +++ b/tests/functional/lang/eval-fail-readSymlink-not-a-symlink.err.exp @@ -1 +1 @@ -error: filesystem error: read_symlink: not a symlink ["/pwd/lang/readDir/bar"] +error: filesystem error: read_symlink: not a symlink [/pwd/lang/readDir/bar] diff --git a/tests/functional/lang/eval-fail-readSymlink-not-a-symlink.postprocess b/tests/functional/lang/eval-fail-readSymlink-not-a-symlink.postprocess index 5466d6120ff..644651dadbd 100644 --- a/tests/functional/lang/eval-fail-readSymlink-not-a-symlink.postprocess +++ b/tests/functional/lang/eval-fail-readSymlink-not-a-symlink.postprocess @@ -7,4 +7,5 @@ sed -i "$testcaseBasename.err" \ -e 's/filesystem error: in read_symlink:/filesystem error: read_symlink:/' \ -e 's/Invalid argument/not a symlink/' \ -e 's/Not a symbolic link/not a symlink/' \ + -e 's/\["\([^"]*\)"\]/[\1]/' \ ; From cd0537e1eb86d278b7acd628cc15543547bc77fb Mon Sep 17 00:00:00 2001 From: Some Guy <253672337+JustAGuyTryingHisBest@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:18:35 -0800 Subject: [PATCH 3/3] builtins.readSymlink return Path --- src/libexpr/primops.cc | 6 ++++-- tests/functional/lang/eval-okay-readSymlink.exp | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index ed003adce5c..58001c6142f 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -2379,8 +2379,10 @@ static RegisterPrimOp primop_readFileType({ /* Read the target of a symlink. */ static void prim_readSymlink(EvalState & state, const PosIdx pos, Value ** args, Value & v) { - auto path = state.realisePath(pos, *args[0], SymlinkResolution::Ancestors); - v.mkString(path.readLink(), state.mem); + const auto path = state.realisePath(pos, *args[0], SymlinkResolution::Ancestors); + const auto target = path.readLink(); + const auto parent = path.parent(); + v.mkPath(SourcePath(path.accessor, CanonPath(target, parent.path)), state.mem); } static RegisterPrimOp primop_readSymlink({ diff --git a/tests/functional/lang/eval-okay-readSymlink.exp b/tests/functional/lang/eval-okay-readSymlink.exp index 8a72759b8d6..f2c5cdd1d10 100644 --- a/tests/functional/lang/eval-okay-readSymlink.exp +++ b/tests/functional/lang/eval-okay-readSymlink.exp @@ -1 +1 @@ -{ ldir = "foo"; linked = "foo/git-hates-directories"; } +{ ldir = /pwd/lang/readDir/foo; linked = /pwd/lang/readDir/foo/git-hates-directories; }