lix/fileset.nix

994 lines
34 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Add this to docs somewhere: If you pass the same arguments to the same functions, you will get the same result
{ lib }:
let
# TODO: Add builtins.traceVerbose or so to all the operations for debugging
# TODO: Document limitation that empty directories won't be included
# TODO: Point out that equality comparison using `==` doesn't quite work because there's multiple representations for all files in a directory: "directory" and `readDir path`.
# TODO: subset and superset check functions. Can easily be implemented with `difference` and `isEmpty`
# TODO: Write down complexity of each operation, most should be O(1)!
# TODO: Implement an operation for optionally including a file if it exists.
# TODO: Derive property tests from https://en.wikipedia.org/wiki/Algebra_of_sets
inherit (builtins)
isAttrs
isString
isPath
isList
typeOf
readDir
match
pathExists
seq
;
inherit (lib.trivial)
mapNullable
;
inherit (lib.lists)
head
tail
foldl'
range
length
elemAt
all
imap0
drop
commonPrefix
;
inherit (lib.strings)
isCoercibleToString
;
inherit (lib.attrsets)
mapAttrs
attrNames
attrValues
;
inherit (lib.filesystem)
pathType
;
inherit (lib.sources)
cleanSourceWith
;
inherit (lib.path)
append
deconstruct
construct
hasPrefix
removePrefix
;
inherit (lib.path.components)
fromSubpath
;
# Internal file set structure:
#
# # A set of files
# <fileset> = {
# _type = "fileset";
#
# # The base path, only files under this path can be represented
# # Always a directory
# _base = <path>;
#
# # A tree representation of all included files
# _tree = <tree>;
# };
#
# # A directory entry value
# <tree> =
# # A nested directory
# <directory>
#
# # A nested file
# | <file>
#
# # A removed file or directory
# # This is represented like this instead of removing the entry from the attribute set because:
# # - It improves laziness
# # - It allows keeping the attribute set as a `builtins.readDir` cache
# | null
#
# # A directory
# <directory> =
# # The inclusion state for every directory entry
# { <name> = <tree>; }
#
# # All files in a directory, recursively.
# # Semantically this is equivalent to `builtins.readDir path`, but lazier, because
# # operations that don't require the entry listing can avoid it.
# # This string is chosen to be compatible with `builtins.readDir` for a simpler implementation
# "directory";
#
# # A file
# <file> =
# # A file with this filetype
# # These strings match `builtins.readDir` for a simpler implementation
# "regular" | "symlink" | "unknown"
# Create a fileset structure
# Type: Path -> <tree> -> <fileset>
_create = base: tree: {
_type = "fileset";
# All attributes are internal
_base = base;
_tree = tree;
# Double __ to make it be evaluated and ordered first
__noEval = throw ''
File sets are not intended to be directly inspected or evaluated. Instead prefer:
- If you want to print a file set, use the `lib.fileset.trace` or `lib.fileset.pretty` function.
- If you want to check file sets for equality, use the `lib.fileset.equals` function.
'';
};
# Create a file set from a path
# Type: Path -> <fileset>
_singleton = path:
let
type = pathType path;
in
if type == "directory" then
_create path type
else
# Always coerce to a directory
# If we don't do this we run into problems like:
# - What should `toSource { base = ./default.nix; fileset = difference ./default.nix ./default.nix; }` do?
# - Importing an empty directory wouldn't make much sense because our `base` is a file
# - Neither can we create a store path containing nothing at all
# - The only option is to throw an error that `base` should be a directory
# - Should `fileFilter (file: file.name == "default.nix") ./default.nix` run the predicate on the ./default.nix file?
# - If no, should the result include or exclude ./default.nix? In any case, it would be confusing and inconsistent
# - If yes, it needs to consider ./. to have influence the filesystem result, because file names are part of the parent directory, so filter would change the necessary base
_create (dirOf path)
(_nestTree
(dirOf path)
[ (baseNameOf path) ]
type
);
# Turn a builtins.filterSource-based source filter on a root path into a file set containing only files included by the filter
# Type: Path -> (String -> String -> Bool) -> <fileset>
_fromSource = root: filter:
let
recurse = focusPath: type:
# FIXME: Generally we shouldn't use toString on paths, though it might be correct
# here since we're trying to mimic the impure behavior of `builtins.filterPath`
if ! filter (toString focusPath) type then
null
else if type == "directory" then
mapAttrs
(name: recurse (append focusPath name))
(readDir focusPath)
else
type;
rootPathType = pathType root;
tree =
if rootPathType == "directory" then
recurse root rootPathType
else
rootPathType;
in
_create root tree;
# Coerce a value to a fileset
# Type: String -> String -> Any -> <fileset>
_coerce = function: context: value:
if value._type or "" == "fileset" then
value
else if ! isPath value then
if value._isLibCleanSourceWith or false then
throw ''
lib.fileset.${function}: Expected ${context} to be a path, but it's a value produced by `lib.sources` instead.
Such a value is only supported when converted to a file set using `lib.fileset.fromSource`.''
else if isCoercibleToString value then
throw ''
lib.fileset.${function}: Expected ${context} to be a path, but it's a string-coercible value instead, possibly a Nix store path.
Such a value is not supported, `lib.fileset` only supports local file filtering.''
else
throw "lib.fileset.${function}: Expected ${context} to be a path, but got a ${typeOf value}."
else if ! pathExists value then
throw "lib.fileset.${function}: Expected ${context} \"${toString value}\" to be a path that exists, but it doesn't."
else
_singleton value;
# Nest a tree under some further components
# Type: Path -> [ String ] -> <tree> -> <tree>
_nestTree = targetBase: extraComponents: tree:
let
recurse = index: focusPath:
if index == length extraComponents then
tree
else
let
focusedName = elemAt extraComponents index;
in
mapAttrs
(name: _:
if name == focusedName then
recurse (index + 1) (append focusPath name)
else
null
)
(readDir focusPath);
in
recurse 0 targetBase;
# Expand "directory" to { <name> = <tree>; }
# Type: Path -> <directory> -> { <name> = <tree>; }
_directoryEntries = path: value:
if isAttrs value then
value
else
readDir path;
# The following tables are a bit complicated, but they nicely explain the
# corresponding implementations, here's the legend:
#
# lhs\rhs: The values for the left hand side and right hand side arguments
# null: null, an excluded file/directory
# attrs: satisfies `isAttrs value`, an explicitly listed directory containing nested trees
# dir: "directory", a recursively included directory
# str: "regular", "symlink" or "unknown", a filetype string
# rec: A result computed by recursing
# -: Can't occur because one argument is a directory while the other is a file
# <number>: Indicates that the result is computed by the branch with that number
# The union of two <tree>'s
# Type: <tree> -> <tree> -> <tree>
#
# lhs\rhs | null | attrs | dir | str |
# ------- | ------- | ------- | ----- | ----- |
# null | 2 null | 2 attrs | 2 dir | 2 str |
# attrs | 3 attrs | 1 rec | 2 dir | - |
# dir | 3 dir | 3 dir | 2 dir | - |
# str | 3 str | - | - | 2 str |
_unionTree = lhs: rhs:
# Branch 1
if isAttrs lhs && isAttrs rhs then
mapAttrs (name: _unionTree lhs.${name}) rhs
# Branch 2
else if lhs == null || isString rhs then
rhs
# Branch 3
else
lhs;
# The intersection of two <tree>'s
# Type: <tree> -> <tree> -> <tree>
#
# lhs\rhs | null | attrs | dir | str |
# ------- | ------- | ------- | ------- | ------ |
# null | 2 null | 2 null | 2 null | 2 null |
# attrs | 3 null | 1 rec | 2 attrs | - |
# dir | 3 null | 3 attrs | 2 dir | - |
# str | 3 null | - | - | 2 str |
_intersectTree = lhs: rhs:
# Branch 1
if isAttrs lhs && isAttrs rhs then
mapAttrs (name: _intersectTree lhs.${name}) rhs
# Branch 2
else if lhs == null || isString rhs then
lhs
# Branch 3
else
rhs;
# The difference between two <tree>'s
# Type: Path -> <tree> -> <tree> -> <tree>
#
# lhs\rhs | null | attrs | dir | str |
# ------- | ------- | ------- | ------ | ------ |
# null | 1 null | 1 null | 1 null | 1 null |
# attrs | 2 attrs | 3 rec | 1 null | - |
# dir | 2 dir | 3 rec | 1 null | - |
# str | 2 str | - | - | 1 null |
_differenceTree = path: lhs: rhs:
# Branch 1
if isString rhs || lhs == null then
null
# Branch 2
else if rhs == null then
lhs
# Branch 3
else
mapAttrs (name: lhsValue:
_differenceTree (append path name) lhsValue rhs.${name}
) (_directoryEntries path lhs);
# Whether two <tree>'s are equal
# Type: Path -> <tree> -> <tree> -> <tree>
#
# | lhs\rhs | null | attrs | dir | str |
# | ------- | ------- | ------- | ------ | ------- |
# | null | 1 true | 1 rec | 1 rec | 1 false |
# | attrs | 2 rec | 3 rec | 3 rec | - |
# | dir | 2 rec | 3 rec | 4 true | - |
# | str | 2 false | - | - | 4 true |
_equalsTree = path: lhs: rhs:
# Branch 1
if lhs == null then
_isEmptyTree path rhs
# Branch 2
else if rhs == null then
_isEmptyTree path lhs
# Branch 3
else if isAttrs lhs || isAttrs rhs then
let
lhs' = _directoryEntries path lhs;
rhs' = _directoryEntries path rhs;
in
all (name:
_equalsTree (append path name) lhs'.${name} rhs'.${name}
) (attrNames lhs')
# Branch 4
else
true;
# Whether a tree is empty, containing no files
# Type: Path -> <tree> -> Bool
_isEmptyTree = path: tree:
if isAttrs tree || tree == "directory" then
let
entries = _directoryEntries path tree;
in
all (name: _isEmptyTree (append path name) entries.${name}) (attrNames entries)
else
tree == null;
# Simplifies a tree, optionally expanding all "directory"'s into complete listings
# Type: Bool -> Path -> <tree> -> <tree>
_simplifyTree = expand: base: tree:
let
recurse = focusPath: tree:
if tree == "directory" && expand || isAttrs tree then
let
expanded = _directoryEntries focusPath tree;
transformedSubtrees = mapAttrs (name: recurse (append focusPath name)) expanded;
values = attrValues transformedSubtrees;
in
if all (value: value == "emptyDir") values then
"emptyDir"
else if all (value: isNull value || value == "emptyDir") values then
null
else if !expand && all (value: isString value || value == "emptyDir") values then
"directory"
else
mapAttrs (name: value: if value == "emptyDir" then null else value) transformedSubtrees
else
tree;
result = recurse base tree;
in
if result == "emptyDir" then
null
else
result;
_prettyTreeSuffix = tree:
if isAttrs tree then
""
else if tree == "directory" then
" (recursive directory)"
else
" (${tree})";
# Pretty-print all files included in the file set.
# Type: (b -> String -> b) -> b -> Path -> FileSet -> b
_prettyFoldl' = f: start: base: tree:
let
traceTreeAttrs = start: indent: tree:
# Nix should really be evaluating foldl''s second argument before starting the iteration
# See the same problem in Haskell:
# - https://stackoverflow.com/a/14282642
# - https://gitlab.haskell.org/ghc/ghc/-/issues/12173
# - https://well-typed.com/blog/90/#a-postscript-which-foldl
# - https://old.reddit.com/r/haskell/comments/21wvk7/foldl_is_broken/
seq start
(foldl' (prev: name:
let
subtree = tree.${name};
intermediate =
f prev "${indent}- ${name}${_prettyTreeSuffix subtree}";
in
if subtree == null then
# Don't print anything at all if this subtree is empty
prev
else if isAttrs subtree then
# A directory with explicit entries
# Do print this node, but also recurse
traceTreeAttrs intermediate "${indent} " subtree
else
# Either a file, or a recursively included directory
# Do print this node but no further recursion needed
intermediate
) start (attrNames tree));
intermediate =
if tree == null then
f start "${toString base} (empty)"
else
f start "${toString base}${_prettyTreeSuffix tree}";
in
if isAttrs tree then
traceTreeAttrs intermediate "" tree
else
intermediate;
# Coerce and normalise the bases of multiple file set values passed to user-facing functions
# Type: String -> [ { context :: String, value :: Any } ] -> { commonBase :: Path, trees :: [ <tree> ] }
_normaliseBase = function: list:
let
processed = map ({ context, value }:
let
fileset = _coerce function context value;
in {
inherit fileset context;
baseParts = deconstruct fileset._base;
}
) list;
first = head processed;
commonComponents = foldl' (components: el:
if first.baseParts.root != el.baseParts.root then
throw "lib.fileset.${function}: Expected file sets to have the same filesystem root, but ${first.context} has root \"${toString first.baseParts.root}\" while ${el.context} has root \"${toString el.baseParts.root}\"."
else
commonPrefix components el.baseParts.components
) first.baseParts.components (tail processed);
commonBase = construct {
root = first.baseParts.root;
components = commonComponents;
};
commonComponentsLength = length commonComponents;
trees = map (value:
_nestTree
commonBase
(drop commonComponentsLength value.baseParts.components)
value.fileset._tree
) processed;
in
{
inherit commonBase trees;
};
in {
/*
Import a file set into the Nix store, making it usable inside derivations.
Return a source-like value that can be coerced to a Nix store path.
This function takes an attribute set with these attributes as an argument:
- `root` (required): The local path that should be the root of the result.
`fileset` must not be influenceable by paths outside `root`, meaning `lib.fileset.getInfluenceBase fileset` must be under `root`.
Warning: Setting `root` to `lib.fileset.getInfluenceBase fileset` directly would make the resulting Nix store path file structure dependent on how `fileset` is declared.
This makes it non-trivial to predict where specific paths are located in the result.
- `fileset` (required): The set of files to import into the Nix store.
Use the other `lib.fileset` functions to define `fileset`.
Only directories containing at least one file are included in the result, unless `extraExistingDirs` is used to ensure the existence of specific directories even without any files.
- `extraExistingDirs` (optional, default `[]`): Additionally ensure the existence of these directory paths in the result, even they don't contain any files in `fileset`.
Type:
toSource :: {
root :: Path,
fileset :: FileSet,
extraExistingDirs :: [ Path ] ? [ ],
} -> SourceLike
*/
toSource = { root, fileset, extraExistingDirs ? [ ] }:
let
maybeFileset = fileset;
in
let
fileset = _coerce "toSource" "`fileset` attribute" maybeFileset;
# Directories that recursively have no files in them will always be `null`
sparseTree =
let
recurse = focusPath: tree:
if tree == "directory" || isAttrs tree then
let
entries = _directoryEntries focusPath tree;
sparseSubtrees = mapAttrs (name: recurse (append focusPath name)) entries;
values = attrValues sparseSubtrees;
in
if all isNull values then
null
else if all isString values then
"directory"
else
sparseSubtrees
else
tree;
resultingTree = recurse fileset._base fileset._tree;
# The fileset's _base might be below the root of the `toSource`, so we need to lift the tree up to `root`
extraRootNesting = removePrefix root fileset._base;
in _nestTree root extraRootNesting resultingTree;
sparseExtendedTree =
if ! isList extraExistingDirs then
throw "lib.fileset.toSource: Expected the `extraExistingDirs` attribute to be a list, but it's a ${typeOf extraExistingDirs} instead."
else
lib.foldl' (tree: i:
let
dir = elemAt extraExistingDirs i;
# We're slightly abusing the internal functions and structure to ensure that the extra directory is represented in the sparse tree.
value = mapAttrs (name: value: null) (readDir dir);
extraTree = _nestTree root (removePrefix root dir) value;
result = _unionTree tree extraTree;
in
if ! isPath dir then
throw "lib.fileset.toSource: Expected all elements of the `extraExistingDirs` attribute to be paths, but element at index ${toString i} is a ${typeOf dir} instead."
else if ! pathExists dir then
throw "lib.fileset.toSource: Expected all elements of the `extraExistingDirs` attribute to be paths that exist, but the path at index ${toString i} \"${toString dir}\" does not."
else if pathType dir != "directory" then
throw "lib.fileset.toSource: Expected all elements of the `extraExistingDirs` attribute to be paths pointing to directories, but the path at index ${toString i} \"${toString dir}\" points to a file instead."
else if ! hasPrefix root dir then
throw "lib.fileset.toSource: Expected all elements of the `extraExistingDirs` attribute to be paths under the `root` attribute \"${toString root}\", but the path at index ${toString i} \"${toString dir}\" is not."
else
result
) sparseTree (range 0 (length extraExistingDirs - 1));
rootComponentsLength = length (deconstruct root).components;
# This function is called often for the filter, so it should be fast
inSet = components:
let
recurse = index: localTree:
if index == length components then
localTree != null
else if localTree ? ${elemAt components index} then
recurse (index + 1) localTree.${elemAt components index}
else
localTree == "directory";
in recurse rootComponentsLength sparseExtendedTree;
in
if ! isPath root then
if root._isLibCleanSourceWith or false then
throw ''
lib.fileset.toSource: Expected attribute `root` to be a path, but it's a value produced by `lib.sources` instead.
Such a value is only supported when converted to a file set using `lib.fileset.fromSource` and passed to the `fileset` attribute, where it may also be combined using other functions from `lib.fileset`.''
else if isCoercibleToString root then
throw ''
lib.fileset.toSource: Expected attribute `root` to be a path, but it's a string-like value instead, possibly a Nix store path.
Such a value is not supported, `lib.fileset` only supports local file filtering.''
else
throw "lib.fileset.toSource: Expected attribute `root` to be a path, but it's a ${typeOf root} instead."
else if ! pathExists root then
throw "lib.fileset.toSource: Expected attribute `root` \"${toString root}\" to be a path that exists, but it doesn't."
else if pathType root != "directory" then
throw "lib.fileset.toSource: Expected attribute `root` \"${toString root}\" to be a path pointing to a directory, but it's pointing to a file instead."
else if ! hasPrefix root fileset._base then
throw "lib.fileset.toSource: Expected attribute `fileset` to not be influenceable by any paths outside `root`, but `lib.fileset.getInfluenceBase fileset` \"${toString fileset._base}\" is outside `root`."
else
cleanSourceWith {
name = "source";
src = root;
filter = pathString: _: inSet (fromSubpath "./${pathString}");
};
/*
Create a file set from a filtered local source as produced by the `lib.sources` functions.
This does not import anything into the store.
Type:
fromSource :: SourceLike -> FileSet
Example:
fromSource (lib.sources.cleanSource ./.)
*/
fromSource = source:
if ! source._isLibCleanSourceWith or false || ! source ? origSrc || ! source ? filter then
throw "lib.fileset.fromSource: Expected the argument to be a value produced from `lib.sources`, but got a ${typeOf source} instead."
else if ! isPath source.origSrc then
throw "lib.fileset.fromSource: Expected the argument to be source-like value of a local path."
else
_fromSource source.origSrc source.filter;
/*
Coerce a value to a file set:
- If the value is a file set already, return it directly
- If the value is a path pointing to a file, return a file set with that single file
- If the value is a path pointing to a directory, return a file set with all files contained in that directory
This function is mostly not needed because all functions in `lib.fileset` will implicitly apply it for arguments that are expected to be a file set.
Type:
coerce :: Any -> FileSet
*/
coerce = value: _coerce "coerce" "argument" value;
/*
Create a file set containing all files contained in a path (see `coerce`), or no files if the path doesn't exist.
This is useful when you want to include a file only if it actually exists.
Type:
optional :: Path -> FileSet
*/
optional = path:
if ! isPath path then
throw "lib.fileset.optional: Expected argument to be a path, but got a ${typeOf path}."
else if pathExists path then
_singleton path
else
_create path null;
/*
Return the common ancestor directory of all file set operations used to construct this file set, meaning that nothing outside the this directory can influence the set of files included.
Type:
getInfluenceBase :: FileSet -> Path
Example:
getInfluenceBase ./Makefile
=> ./.
getInfluenceBase ./src
=> ./src
getInfluenceBase (fileFilter (file: false) ./.)
=> ./.
getInfluenceBase (union ./Makefile ../default.nix)
=> ../.
*/
getInfluenceBase = maybeFileset:
let
fileset = _coerce "getInfluenceBase" "argument" maybeFileset;
in
fileset._base;
/*
Incrementally evaluate and trace a file set in a pretty way.
Functionally this is the same as splitting the result from `lib.fileset.pretty` into lines and tracing those.
However this function can do the same thing incrementally, so it can already start printing the result as the first lines are known.
The `expand` argument (false by default) controls whether all files should be printed individually.
Type:
trace :: { expand :: Bool ? false } -> FileSet -> Any -> Any
Example:
trace {} (unions [ ./foo.nix ./bar/baz.c ./qux ])
=>
trace: /home/user/src/myProject
trace: - bar
trace: - baz.c (regular)
trace: - foo.nix (regular)
trace: - qux (recursive directory)
null
trace { expand = true; } (unions [ ./foo.nix ./bar/baz.c ./qux ])
=>
trace: /home/user/src/myProject
trace: - bar
trace: - baz.c (regular)
trace: - foo.nix (regular)
trace: - qux
trace: - florp.c (regular)
trace: - florp.h (regular)
null
*/
trace = { expand ? false }: maybeFileset:
let
fileset = _coerce "trace" "second argument" maybeFileset;
simpleTree = _simplifyTree expand fileset._base fileset._tree;
in
_prettyFoldl' (acc: el: builtins.trace el acc) (x: x)
fileset._base
simpleTree;
/*
The same as `lib.fileset.trace`, but instead of taking an argument for the value to return, the given file set is returned instead.
Type:
traceVal :: { expand :: Bool ? false } -> FileSet -> FileSet
*/
traceVal = { expand ? false }: maybeFileset:
let
fileset = _coerce "traceVal" "second argument" maybeFileset;
simpleTree = _simplifyTree expand fileset._base fileset._tree;
in
_prettyFoldl' (acc: el: builtins.trace el acc) fileset
fileset._base
simpleTree;
/*
The same as `lib.fileset.trace`, but instead of tracing each line, the result is returned as a string.
Type:
pretty :: { expand :: Bool ? false } -> FileSet -> String
*/
pretty = { expand ? false }: maybeFileset:
let
fileset = _coerce "pretty" "second argument" maybeFileset;
simpleTree = _simplifyTree expand fileset._base fileset._tree;
in
_prettyFoldl' (acc: el: "${acc}\n${el}") ""
fileset._base
simpleTree;
/*
The file set containing all files that are in either of two given file sets.
Recursively, the first argument is evaluated first, only evaluating the second argument if necessary.
union a b = a b
Type:
union :: FileSet -> FileSet -> FileSet
*/
union = lhs: rhs:
let
normalised = _normaliseBase "union" [
{
context = "first argument";
value = lhs;
}
{
context = "second argument";
value = rhs;
}
];
in
_create normalised.commonBase
(_unionTree
(elemAt normalised.trees 0)
(elemAt normalised.trees 1)
);
/*
The file containing all files from that are in any of the given file sets.
Recursively, the elements are evaluated from left to right, only evaluating arguments on the right if necessary.
Type:
unions :: [FileSet] -> FileSet
*/
unions = list:
let
annotated = imap0 (i: el: {
context = "element ${toString i} of the argument";
value = el;
}) list;
normalised = _normaliseBase "unions" annotated;
tree = foldl' _unionTree (head normalised.trees) (tail normalised.trees);
in
if ! isList list then
throw "lib.fileset.unions: Expected argument to be a list, but got a ${typeOf list}."
else if length list == 0 then
throw "lib.fileset.unions: Expected argument to be a list with at least one element, but it contains no elements."
else
_create normalised.commonBase tree;
/*
The file set containing all files that are in both given sets.
Recursively, the first argument is evaluated first, only evaluating the second argument if necessary.
intersect a b == a b
Type:
intersect :: FileSet -> FileSet -> FileSet
*/
intersect = lhs: rhs:
let
normalised = _normaliseBase "intersect" [
{
context = "first argument";
value = lhs;
}
{
context = "second argument";
value = rhs;
}
];
in
_create normalised.commonBase
(_intersectTree
(elemAt normalised.trees 0)
(elemAt normalised.trees 1)
);
/*
The file set containing all files that are in all the given sets.
Recursively, the elements are evaluated from left to right, only evaluating arguments on the right if necessary.
Type:
intersects :: [FileSet] -> FileSet
*/
intersects = list:
let
annotated = imap0 (i: el: {
context = "element ${toString i} of the argument";
value = el;
}) list;
normalised = _normaliseBase "intersects" annotated;
tree = foldl' _intersectTree (head normalised.trees) (tail normalised.trees);
in
if ! isList list then
throw "lib.fileset.intersects: Expected argument to be a list, but got a ${typeOf list}."
else if length list == 0 then
throw "lib.fileset.intersects: Expected argument to be a list with at least one element, but it contains no elements."
else
_create normalised.commonBase tree;
/*
The file set containing all files that are in the first file set but not in the second.
Recursively, the second argument is evaluated first, only evaluating the first argument if necessary.
difference a b == a b
Type:
difference :: FileSet -> FileSet -> FileSet
*/
difference = lhs: rhs:
let
normalised = _normaliseBase "difference" [
{
context = "first argument";
value = lhs;
}
{
context = "second argument";
value = rhs;
}
];
in
_create normalised.commonBase
(_differenceTree normalised.commonBase
(elemAt normalised.trees 0)
(elemAt normalised.trees 1)
);
/*
Filter a file set to only contain files matching some predicate.
The predicate is called with an attribute set containing these attributes:
- `name`: The filename
- `type`: The type of the file, either "regular", "symlink" or "unknown"
- `ext`: The file extension or `null` if the file has none.
More formally:
- `ext` contains no `.`
- `.${ext}` is a suffix of the `name`
- Potentially other attributes in the future
Type:
fileFilter ::
({
name :: String,
type :: String,
ext :: String | Null,
...
} -> Bool)
-> FileSet
-> FileSet
*/
fileFilter = predicate: maybeFileset:
let
fileset = _coerce "fileFilter" "second argument" maybeFileset;
recurse = focusPath: tree:
mapAttrs (name: subtree:
if isAttrs subtree || subtree == "directory" then
recurse (append focusPath name) subtree
else if
predicate {
inherit name;
type = subtree;
ext = mapNullable head (match ".*\\.(.*)" name);
# To ensure forwards compatibility with more arguments being added in the future,
# adding an attribute which can't be deconstructed :)
"This attribute is passed to prevent `lib.fileset.fileFilter` predicate functions from breaking when more attributes are added in the future. Please add `...` to the function to handle this and future additional arguments." = null;
}
then
subtree
else
null
) (_directoryEntries focusPath tree);
in
_create fileset._base (recurse fileset._base fileset._tree);
/*
A file set containing all files that are contained in a directory whose name satisfies the given predicate.
Only directories under the given path are checked, this is to ensure that components outside of the given path cannot influence the result.
Consequently this function does not accept a file set as an argument.
If you need to filter files in a file set based on components, use `intersect myFileSet (directoryFilter myPredicate myPath)` instead.
Type:
directoryFilter :: (String -> Bool) -> Path -> FileSet
Example:
# Select all files in hidden directories within ./.
directoryFilter (hasPrefix ".") ./.
# Select all files in directories named `build` within ./src
directoryFilter (name: name == "build") ./src
*/
directoryFilter = predicate: path:
let
recurse = focusPath:
mapAttrs (name: type:
if type == "directory" then
if predicate name then
type
else
recurse (append focusPath name)
else
null
) (readDir focusPath);
in
if path._type or null == "fileset" then
throw ''
lib.fileset.directoryFilter: Expected second argument to be a path, but it's a file set.
If you need to filter files in a file set, use `intersect myFileSet (directoryFilter myPredicate myPath)` instead.''
else if ! isPath path then
throw "lib.fileset.directoryFilter: Expected second argument to be a path, but got a ${typeOf path}."
else if pathType path != "directory" then
throw "lib.fileset.directoryFilter: Expected second argument \"${toString path}\" to be a directory, but it's not."
else
_create path (recurse path);
/*
Check whether two file sets contain the same files.
Type:
equals :: FileSet -> FileSet -> Bool
*/
equals = lhs: rhs:
let
normalised = _normaliseBase "equals" [
{
context = "first argument";
value = lhs;
}
{
context = "second argument";
value = rhs;
}
];
in
_equalsTree normalised.commonBase
(elemAt normalised.trees 0)
(elemAt normalised.trees 1);
/*
Check whether a file set contains no files.
Type:
isEmpty :: FileSet -> Bool
*/
isEmpty = maybeFileset:
let
fileset = _coerce "isEmpty" "argument" maybeFileset;
in
_isEmptyTree fileset._base fileset._tree;
}