about summary refs log tree commit diff
path: root/nix
diff options
context:
space:
mode:
authorsterni <sternenseemann@systemli.org>2021-09-11T16·47+0200
committersterni <sternenseemann@systemli.org>2021-10-02T18·24+0000
commit66fa718cebb4808a95c17e7ee692cf8e5dc87653 (patch)
tree52f5409d179863d484de5bfa9695095562ca4d9a /nix
parent0eef0e343f6ac3b7afbe7f6895fce8b5d20d3b3a (diff)
feat(nix/utils): expose pathType of symlink target r/2947
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 <mail@profpatsch.de>
Reviewed-by: tazjin <mail@tazj.in>
Diffstat (limited to 'nix')
-rw-r--r--nix/utils/default.nix92
-rw-r--r--nix/utils/tests/default.nix25
2 files changed, 104 insertions, 13 deletions
diff --git a/nix/utils/default.nix b/nix/utils/default.nix
index bb28ca40a1..d90dee24af 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<string>
+     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 366ddd7c9c..1ba6848607 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
 ]