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 <mail@profpatsch.de> Reviewed-by: tazjin <mail@tazj.in>
This commit is contained in:
parent
0eef0e343f
commit
66fa718ceb
2 changed files with 104 additions and 13 deletions
|
@ -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
|
||||
;
|
||||
|
|
|
@ -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
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue