forked from lix-project/lix
994 lines
34 KiB
Nix
994 lines
34 KiB
Nix
|
# 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;
|
|||
|
}
|