about summary refs log tree commit diff
path: root/nix/utils
diff options
context:
space:
mode:
Diffstat (limited to 'nix/utils')
-rw-r--r--nix/utils/OWNERS3
-rw-r--r--nix/utils/default.nix186
-rw-r--r--nix/utils/tests/.skip-subtree1
-rw-r--r--nix/utils/tests/default.nix126
-rw-r--r--nix/utils/tests/directory/file0
l---------nix/utils/tests/missing1
l---------nix/utils/tests/symlink-directory1
l---------nix/utils/tests/symlink-file1
l---------nix/utils/tests/symlink-symlink-directory1
l---------nix/utils/tests/symlink-symlink-file1
10 files changed, 321 insertions, 0 deletions
diff --git a/nix/utils/OWNERS b/nix/utils/OWNERS
new file mode 100644
index 000000000000..f16dd105d761
--- /dev/null
+++ b/nix/utils/OWNERS
@@ -0,0 +1,3 @@
+inherited: true
+owners:
+  - sterni
diff --git a/nix/utils/default.nix b/nix/utils/default.nix
new file mode 100644
index 000000000000..cabea5bbeeb3
--- /dev/null
+++ b/nix/utils/default.nix
@@ -0,0 +1,186 @@
+{ depot, lib, ... }:
+
+let
+  /* Get the basename of a store path without
+     the leading hash.
+
+     Type: (path | drv | string) -> string
+
+     Example:
+       storePathName ./foo.c
+       => "foo.c"
+
+       storePathName (writeText "foo.c" "int main() { return 0; }")
+       => "foo.c"
+
+       storePathName "${hello}/bin/hello"
+       => "hello"
+  */
+  storePathName = p:
+    if lib.isDerivation p
+    then p.name
+    else if builtins.isPath p
+    then builtins.baseNameOf p
+    else if builtins.isString p || (builtins.isAttrs p && (p ? outPath || p ? __toString))
+    then
+      let
+        strPath = toString p;
+        # strip leading storeDir and trailing slashes
+        noStoreDir = lib.removeSuffix "/"
+          (lib.removePrefix "${builtins.storeDir}/" strPath);
+        # a basename of a child of a store path isn't really
+        # referring to a store path, so removing the string
+        # context is safe (e. g. "hello" for "${hello}/bin/hello").
+        basename = builtins.unsafeDiscardStringContext
+          (builtins.baseNameOf strPath);
+      in
+      # If p is a direct child of storeDir, we need to remove
+        # the leading hash as well to make sure that:
+        # `storePathName drv == storePathName (toString drv)`.
+      if noStoreDir == basename
+      then builtins.substring 33 (-1) basename
+      else basename
+    else builtins.throw "Don't know how to get (base)name of "
+      + lib.generators.toPretty { } p;
+
+  /* Query the type of a path exposing the same information as would be by
+     `builtins.readDir`, but for a single, specific target path.
+
+     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 = true; }
+
+       pathType /home/lukas
+       => { directory = true; }
+
+       pathType ./result
+       => { symlink = "directory"; }
+
+       pathType ./link-to-file
+       => { symlink = "regular-or-missing"; }
+
+       pathType /does/not/exist
+       => { 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
+      # baseNameOf is very annoyed if we proceed with string context.
+      # We need to call toString to prevent unsafeDiscardStringContext
+      # from importing a path into store which messes with base- and
+      # dirname of course.
+      path' = builtins.unsafeDiscardStringContext (toString path);
+      # To read the containing directory we absolutely need
+      # to keep the string context, otherwise a derivation
+      # would not be realized before our check (at eval time)
+      containingDir = builtins.readDir (builtins.dirOf path);
+      # 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 ? missing
+    then builtins.throw "${lib.generators.toPretty {} path} does not exist"
+    else p;
+
+  /* Check whether the given path is a directory.
+     Throws if the path in question doesn't exist.
+
+     Type: path(-like) -> bool
+  */
+  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;
+
+  /* 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;
+
+in
+{
+  inherit
+    storePathName
+    pathType
+    isDirectory
+    realPathIsDirectory
+    isRegularFile
+    isSymlink
+    ;
+}
diff --git a/nix/utils/tests/.skip-subtree b/nix/utils/tests/.skip-subtree
new file mode 100644
index 000000000000..1efb1b9476ba
--- /dev/null
+++ b/nix/utils/tests/.skip-subtree
@@ -0,0 +1 @@
+subdirectories are just test cases
diff --git a/nix/utils/tests/default.nix b/nix/utils/tests/default.nix
new file mode 100644
index 000000000000..52b7ca41d215
--- /dev/null
+++ b/nix/utils/tests/default.nix
@@ -0,0 +1,126 @@
+{ depot, lib, ... }:
+
+let
+  inherit (depot.nix.runTestsuite)
+    runTestsuite
+    it
+    assertEq
+    assertThrows
+    assertDoesNotThrow
+    ;
+
+  inherit (depot.nix.utils)
+    isDirectory
+    realPathIsDirectory
+    isRegularFile
+    isSymlink
+    pathType
+    storePathName
+    ;
+
+  assertUtilsPred = msg: act: exp: [
+    (assertDoesNotThrow "${msg} does not throw" act)
+    (assertEq msg (builtins.tryEval act).value exp)
+  ];
+
+  pathPredicates = it "judges paths correctly" (lib.flatten [
+    # isDirectory
+    (assertUtilsPred "directory isDirectory"
+      (isDirectory ./directory)
+      true)
+    (assertUtilsPred "symlink not isDirectory"
+      (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)
+    (assertUtilsPred "symlink not isRegularFile"
+      (isRegularFile ./symlink-file)
+      false)
+    (assertUtilsPred "directory not isRegularFile"
+      (isRegularFile ./directory)
+      false)
+    # isSymlink
+    (assertUtilsPred "symlink to file isSymlink"
+      (isSymlink ./symlink-file)
+      true)
+    (assertUtilsPred "symlink to directory isSymlink"
+      (isSymlink ./symlink-directory)
+      true)
+    (assertUtilsPred "symlink to symlink isSymlink"
+      (isSymlink ./symlink-symlink-file)
+      true)
+    (assertUtilsPred "symlink to missing file isSymlink"
+      (isSymlink ./missing)
+      true)
+    (assertUtilsPred "directory not isSymlink"
+      (isSymlink ./directory)
+      false)
+    (assertUtilsPred "file not isSymlink"
+      (isSymlink ./directory/file)
+      false)
+    # 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;
+
+  cleanedSource = lib.cleanSource ./.;
+
+  storePathNameTests = it "correctly gets the basename of a store path" [
+    (assertEq "base name of a derivation"
+      (storePathName depot.tools.cheddar)
+      depot.tools.cheddar.name)
+    (assertEq "base name of a store path string"
+      (storePathName cheddarStorePath)
+      depot.tools.cheddar.name)
+    (assertEq "base name of a path within a store path"
+      (storePathName "${cheddarStorePath}/bin/cheddar") "cheddar")
+    (assertEq "base name of a path"
+      (storePathName ../default.nix) "default.nix")
+    (assertEq "base name of a cleanSourced path"
+      (storePathName cleanedSource)
+      cleanedSource.name)
+  ];
+in
+
+runTestsuite "nix.utils" [
+  pathPredicates
+  symlinkPathTypeTests
+  storePathNameTests
+]
diff --git a/nix/utils/tests/directory/file b/nix/utils/tests/directory/file
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/nix/utils/tests/directory/file
diff --git a/nix/utils/tests/missing b/nix/utils/tests/missing
new file mode 120000
index 000000000000..cfa0a46515b5
--- /dev/null
+++ b/nix/utils/tests/missing
@@ -0,0 +1 @@
+does-not-exist
\ No newline at end of file
diff --git a/nix/utils/tests/symlink-directory b/nix/utils/tests/symlink-directory
new file mode 120000
index 000000000000..6d0450cc2421
--- /dev/null
+++ b/nix/utils/tests/symlink-directory
@@ -0,0 +1 @@
+directory
\ No newline at end of file
diff --git a/nix/utils/tests/symlink-file b/nix/utils/tests/symlink-file
new file mode 120000
index 000000000000..cd5d01792aa4
--- /dev/null
+++ b/nix/utils/tests/symlink-file
@@ -0,0 +1 @@
+directory/file
\ No newline at end of file
diff --git a/nix/utils/tests/symlink-symlink-directory b/nix/utils/tests/symlink-symlink-directory
new file mode 120000
index 000000000000..5d6ba5247eff
--- /dev/null
+++ b/nix/utils/tests/symlink-symlink-directory
@@ -0,0 +1 @@
+symlink-directory
\ No newline at end of file
diff --git a/nix/utils/tests/symlink-symlink-file b/nix/utils/tests/symlink-symlink-file
new file mode 120000
index 000000000000..f8d9ce3659f4
--- /dev/null
+++ b/nix/utils/tests/symlink-symlink-file
@@ -0,0 +1 @@
+symlink-file
\ No newline at end of file