From 66fa718cebb4808a95c17e7ee692cf8e5dc87653 Mon Sep 17 00:00:00 2001 From: sterni Date: Sat, 11 Sep 2021 18:47:18 +0200 Subject: [PATCH] feat(nix/utils): expose pathType of symlink target In order to make readTree import symlinked directories I've been looking into how to detect if a symlink points to a directory (since this would allow us to use symlinks for //nix/sparseTree). I've found a hack for this: symlinkPointsToDir = path: isSymlink path && builtins.pathExists (toString path + "/.") Unfortunately it doesn't seem to be possible to distinguish whether the symlink target does not exist or is a regular file. Since it's possible, I thought might as well add this to `pathType`. To make returning the extra information workable, I've elected to use the attribute set layout used by `//nix/tag`. This doesn't require us to depend anything (as opposed to yants), but gives us pattern matching (via `nix.tag.match`) and also quite idiomatic checking of pathTypes: pathType ./foo ? file (pathType ./foo).symlink or null == "symlink-directory" Nonexistent paths are encoded like this: pathType ./foo ? missing Of course we can't use this in readTree (since it must be zero dependency), but we can easily inline this hack at some point. Change-Id: I15b64a1ea69953c95dc3239ef5860623652b3089 Reviewed-on: https://cl.tvl.fyi/c/depot/+/3535 Tested-by: BuildkiteCI Reviewed-by: Profpatsch Reviewed-by: tazjin --- nix/utils/default.nix | 92 +++++++++++++++++++++++++++++++------ nix/utils/tests/default.nix | 25 ++++++++++ 2 files changed, 104 insertions(+), 13 deletions(-) diff --git a/nix/utils/default.nix b/nix/utils/default.nix index bb28ca40a..d90dee24a 100644 --- a/nix/utils/default.nix +++ b/nix/utils/default.nix @@ -58,23 +58,62 @@ let else builtins.throw "Don't know how to get (base)name of " + lib.generators.toPretty {} p; - /* Get the type of a path itself as it would be returned for a - directory child by builtins.readDir. + /* Query the type of a path exposing the same information as would be by + `builtins.readDir`, but for a single, specific target path. - Type: path(-like) -> option + The information is returned as a tagged value, i. e. an attribute set with + exactly one attribute where the type of the path is encoded in the name + of the single attribute. The allowed tags and values are as follows: + + * `regular`: is a regular file, always `true` if returned + * `directory`: is a directory, always `true` if returned + * `missing`: path does not exist, always `true` if returned + * `symlink`: path is a symlink, value is a string describing the type + of its realpath which may be either: + + * `"directory"`: realpath of the symlink is a directory + * `"regular-or-missing`": realpath of the symlink is either a regular + file or does not exist. Due to limitations of the Nix expression + language, we can't tell which. + + Type: path(-like) -> tag + + `tag` refers to the attribute set format of `//nix/tag`. Example: pathType ./foo.c - => "regular" + => { regular = true; } pathType /home/lukas - => "directory" + => { directory = true; } pathType ./result - => "symlink" + => { symlink = "directory"; } + + pathType ./link-to-file + => { symlink = "regular-or-missing"; } pathType /does/not/exist - => null + => { missing = true; } + + # Check if a path exists + !(pathType /file ? missing) + + # Check if a path is a directory or a symlink to a directory + # A handy shorthand for this is provided as `realPathIsDirectory`. + pathType /path ? directory || (pathType /path).symlink or null == "directory" + + # Match on the result using //nix/tag + nix.tag.match (nix.utils.pathType ./result) { + symlink = v: "symlink to ${v}"; + directory = _: "directory"; + regular = _: "regular"; + missing = _: "path does not exist"; + } + => "symlink to directory" + + # Query path type + nix.tag.tagName (pathType /path) */ pathType = path: let @@ -87,14 +126,27 @@ let # to keep the string context, otherwise a derivation # would not be realized before our check (at eval time) containingDir = builtins.readDir (builtins.dirOf path); - in - containingDir.${builtins.baseNameOf path'} or null; + # Construct tag to use for the value + thisPathType = containingDir.${builtins.baseNameOf path'} or "missing"; + # Trick to check if the symlink target exists and is a directory: + # if we append a "/." to the string version of the path, Nix won't + # canocalize it (which would strip any "/." in the path), so if + # path' + "/." exists, we know that the symlink points to an existing + # directory. If not, either the target doesn't exist or is a regular file. + # TODO(sterni): is there a way to check reliably if the symlink target exists? + isSymlinkDir = builtins.pathExists (path' + "/."); + in { + ${thisPathType} = + /**/ if thisPathType != "symlink" then true + else if isSymlinkDir then "directory" + else "regular-or-missing"; + }; pathType' = path: let p = pathType path; in - if p == null + if p ? missing then builtins.throw "${lib.generators.toPretty {} path} does not exist" else p; @@ -103,21 +155,34 @@ let Type: path(-like) -> bool */ - isDirectory = path: pathType' path == "directory"; + isDirectory = path: pathType' path ? directory; + + /* Checks whether the given path is a directory or + a symlink to a directory. Throws if the path in + question doesn't exist. + + Warning: Does not throw if the target file or + directory doesn't exist, but the symlink does. + + Type: path(-like) -> bool + */ + realPathIsDirectory = path: let + pt = pathType' path; + in pt ? directory || pt.symlink or null == "directory"; /* Check whether the given path is a regular file. Throws if the path in question doesn't exist. Type: path(-like) -> bool */ - isRegularFile = path: pathType' path == "regular"; + isRegularFile = path: pathType' path ? regular; /* Check whether the given path is a symbolic link. Throws if the path in question doesn't exist. Type: path(-like) -> bool */ - isSymlink = path: pathType' path == "symlink"; + isSymlink = path: pathType' path ? symlink; in { inherit @@ -125,6 +190,7 @@ in { storePathName pathType isDirectory + realPathIsDirectory isRegularFile isSymlink ; diff --git a/nix/utils/tests/default.nix b/nix/utils/tests/default.nix index 366ddd7c9..1ba684860 100644 --- a/nix/utils/tests/default.nix +++ b/nix/utils/tests/default.nix @@ -11,8 +11,10 @@ let inherit (depot.nix.utils) isDirectory + realPathIsDirectory isRegularFile isSymlink + pathType storePathName ; @@ -29,6 +31,13 @@ let (isDirectory ./symlink-directory) false) (assertUtilsPred "file not isDirectory" (isDirectory ./directory/file) false) + # realPathIsDirectory + (assertUtilsPred "directory realPathIsDirectory" + (realPathIsDirectory ./directory) true) + (assertUtilsPred "symlink to directory realPathIsDirectory" + (realPathIsDirectory ./symlink-directory) true) + (assertUtilsPred "realPathIsDirectory resolves chained symlinks" + (realPathIsDirectory ./symlink-symlink-directory) true) # isRegularFile (assertUtilsPred "file isRegularFile" (isRegularFile ./directory/file) true) @@ -52,12 +61,27 @@ let # missing files throw (assertThrows "isDirectory throws on missing file" (isDirectory ./does-not-exist)) + (assertThrows "realPathIsDirectory throws on missing file" + (realPathIsDirectory ./does-not-exist)) (assertThrows "isRegularFile throws on missing file" (isRegularFile ./does-not-exist)) (assertThrows "isSymlink throws on missing file" (isSymlink ./does-not-exist)) ]); + symlinkPathTypeTests = it "correctly judges symlinks" [ + (assertEq "symlinks to directories are detected correcty" + ((pathType ./symlink-directory).symlink or null) "directory") + (assertEq "symlinks to symlinks to directories are detected correctly" + ((pathType ./symlink-symlink-directory).symlink or null) "directory") + (assertEq "symlinks to files are detected-ish" + ((pathType ./symlink-file).symlink or null) "regular-or-missing") + (assertEq "symlinks to symlinks to files are detected-ish" + ((pathType ./symlink-symlink-file).symlink or null) "regular-or-missing") + (assertEq "symlinks to nowhere are not distinguished from files" + ((pathType ./missing).symlink or null) "regular-or-missing") + ]; + cheddarStorePath = builtins.unsafeDiscardStringContext depot.tools.cheddar.outPath; @@ -75,5 +99,6 @@ in runTestsuite "nix.utils" [ pathPredicates + symlinkPathTypeTests storePathNameTests ]