aboutsummaryrefslogtreecommitdiff
path: root/fileset.nix
diff options
context:
space:
mode:
Diffstat (limited to 'fileset.nix')
-rw-r--r--fileset.nix993
1 files changed, 993 insertions, 0 deletions
diff --git a/fileset.nix b/fileset.nix
new file mode 100644
index 000000000..3eb909162
--- /dev/null
+++ b/fileset.nix
@@ -0,0 +1,993 @@
+# 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;
+}