2023-08-17 00:55:32 +02:00
|
|
|
{ lib ? import ../. }:
|
|
|
|
let
|
|
|
|
|
|
|
|
inherit (builtins)
|
|
|
|
isAttrs
|
|
|
|
isPath
|
|
|
|
isString
|
|
|
|
pathExists
|
|
|
|
readDir
|
|
|
|
typeOf
|
|
|
|
split
|
|
|
|
;
|
|
|
|
|
|
|
|
inherit (lib.attrsets)
|
|
|
|
attrValues
|
|
|
|
mapAttrs
|
|
|
|
;
|
|
|
|
|
|
|
|
inherit (lib.filesystem)
|
|
|
|
pathType
|
|
|
|
;
|
|
|
|
|
|
|
|
inherit (lib.lists)
|
|
|
|
all
|
|
|
|
elemAt
|
2023-09-13 18:50:45 +02:00
|
|
|
foldl'
|
2023-08-17 00:55:32 +02:00
|
|
|
length
|
2023-09-13 18:50:45 +02:00
|
|
|
sublist
|
2023-08-17 00:55:32 +02:00
|
|
|
;
|
|
|
|
|
|
|
|
inherit (lib.path)
|
|
|
|
append
|
|
|
|
splitRoot
|
|
|
|
;
|
|
|
|
|
|
|
|
inherit (lib.path.subpath)
|
|
|
|
components
|
|
|
|
;
|
|
|
|
|
|
|
|
inherit (lib.strings)
|
|
|
|
isStringLike
|
|
|
|
concatStringsSep
|
|
|
|
substring
|
|
|
|
stringLength
|
|
|
|
;
|
|
|
|
|
|
|
|
in
|
|
|
|
# Rare case of justified usage of rec:
|
|
|
|
# - This file is internal, so the return value doesn't matter, no need to make things overridable
|
|
|
|
# - The functions depend on each other
|
|
|
|
# - We want to expose all of these functions for easy testing
|
|
|
|
rec {
|
|
|
|
|
|
|
|
# If you change the internal representation, make sure to:
|
2023-09-13 18:50:45 +02:00
|
|
|
# - Increment this version
|
|
|
|
# - Add an additional migration function below
|
2023-08-17 00:55:32 +02:00
|
|
|
# - Update the description of the internal representation in ./README.md
|
2023-09-13 18:50:45 +02:00
|
|
|
_currentVersion = 1;
|
|
|
|
|
|
|
|
# Migrations between versions. The 0th element converts from v0 to v1, and so on
|
|
|
|
migrations = [
|
|
|
|
# Convert v0 into v1: Add the _internalBase{Root,Components} attributes
|
|
|
|
(
|
|
|
|
filesetV0:
|
|
|
|
let
|
|
|
|
parts = splitRoot filesetV0._internalBase;
|
|
|
|
in
|
|
|
|
filesetV0 // {
|
|
|
|
_internalVersion = 1;
|
|
|
|
_internalBaseRoot = parts.root;
|
|
|
|
_internalBaseComponents = components parts.subpath;
|
|
|
|
}
|
|
|
|
)
|
|
|
|
];
|
2023-08-17 00:55:32 +02:00
|
|
|
|
|
|
|
# Create a fileset, see ./README.md#fileset
|
|
|
|
# Type: path -> filesetTree -> fileset
|
2023-09-13 18:50:45 +02:00
|
|
|
_create = base: tree:
|
|
|
|
let
|
|
|
|
# Decompose the base into its components
|
|
|
|
# See ../path/README.md for why we're not just using `toString`
|
|
|
|
parts = splitRoot base;
|
|
|
|
in
|
|
|
|
{
|
|
|
|
_type = "fileset";
|
2023-08-17 00:55:32 +02:00
|
|
|
|
2023-09-13 18:50:45 +02:00
|
|
|
_internalVersion = _currentVersion;
|
|
|
|
_internalBase = base;
|
|
|
|
_internalBaseRoot = parts.root;
|
|
|
|
_internalBaseComponents = components parts.subpath;
|
|
|
|
_internalTree = tree;
|
2023-08-17 00:55:32 +02:00
|
|
|
|
2023-09-13 18:50:45 +02:00
|
|
|
# Double __ to make it be evaluated and ordered first
|
|
|
|
__noEval = throw ''
|
|
|
|
lib.fileset: Directly evaluating a file set is not supported. Use `lib.fileset.toSource` to turn it into a usable source instead.'';
|
|
|
|
};
|
2023-08-17 00:55:32 +02:00
|
|
|
|
|
|
|
# Coerce a value to a fileset, erroring when the value cannot be coerced.
|
|
|
|
# The string gives the context for error messages.
|
|
|
|
# Type: String -> Path -> fileset
|
|
|
|
_coerce = context: value:
|
|
|
|
if value._type or "" == "fileset" then
|
|
|
|
if value._internalVersion > _currentVersion then
|
|
|
|
throw ''
|
|
|
|
${context} is a file set created from a future version of the file set library with a different internal representation:
|
|
|
|
- Internal version of the file set: ${toString value._internalVersion}
|
|
|
|
- Internal version of the library: ${toString _currentVersion}
|
|
|
|
Make sure to update your Nixpkgs to have a newer version of `lib.fileset`.''
|
2023-09-13 18:50:45 +02:00
|
|
|
else if value._internalVersion < _currentVersion then
|
|
|
|
let
|
|
|
|
# Get all the migration functions necessary to convert from the old to the current version
|
|
|
|
migrationsToApply = sublist value._internalVersion (_currentVersion - value._internalVersion) migrations;
|
|
|
|
in
|
|
|
|
foldl' (value: migration: migration value) value migrationsToApply
|
2023-08-17 00:55:32 +02:00
|
|
|
else
|
|
|
|
value
|
|
|
|
else if ! isPath value then
|
|
|
|
if isStringLike value then
|
|
|
|
throw ''
|
|
|
|
${context} "${toString value}" is a string-like value, but it should be a path instead.
|
|
|
|
Paths represented as strings are not supported by `lib.fileset`, use `lib.sources` or derivations instead.''
|
|
|
|
else
|
|
|
|
throw ''
|
|
|
|
${context} is of type ${typeOf value}, but it should be a path instead.''
|
|
|
|
else if ! pathExists value then
|
|
|
|
throw ''
|
|
|
|
${context} ${toString value} does not exist.''
|
|
|
|
else
|
|
|
|
_singleton value;
|
|
|
|
|
|
|
|
# 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
|
|
|
|
# This turns a file path ./default.nix into a fileset with
|
|
|
|
# - _internalBase: ./.
|
|
|
|
# - _internalTree: {
|
|
|
|
# "default.nix" = <type>;
|
|
|
|
# # Other directory entries
|
|
|
|
# <name> = null;
|
|
|
|
# }
|
|
|
|
# See ./README.md#single-files
|
|
|
|
_create (dirOf path)
|
|
|
|
(_nestTree
|
|
|
|
(dirOf path)
|
|
|
|
[ (baseNameOf path) ]
|
|
|
|
type
|
|
|
|
);
|
|
|
|
|
|
|
|
/*
|
|
|
|
Nest a filesetTree under some extra components, while filling out all the other directory entries that aren't included with null
|
|
|
|
|
|
|
|
_nestTree ./. [ "foo" "bar" ] tree == {
|
|
|
|
foo = {
|
|
|
|
bar = tree;
|
|
|
|
<other-entries> = null;
|
|
|
|
}
|
|
|
|
<other-entries> = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
Type: Path -> [ String ] -> filesetTree -> filesetTree
|
|
|
|
*/
|
|
|
|
_nestTree = targetBase: extraComponents: tree:
|
|
|
|
let
|
|
|
|
recurse = index: focusPath:
|
|
|
|
if index == length extraComponents then
|
|
|
|
tree
|
|
|
|
else
|
|
|
|
mapAttrs (_: _: null) (readDir focusPath)
|
|
|
|
// {
|
|
|
|
${elemAt extraComponents index} = recurse (index + 1) (append focusPath (elemAt extraComponents index));
|
|
|
|
};
|
|
|
|
in
|
|
|
|
recurse 0 targetBase;
|
|
|
|
|
|
|
|
# Expand "directory" filesetTree representation to the equivalent { <name> = filesetTree; }
|
|
|
|
# Type: Path -> filesetTree -> { <name> = filesetTree; }
|
|
|
|
_directoryEntries = path: value:
|
|
|
|
if isAttrs value then
|
|
|
|
value
|
|
|
|
else
|
|
|
|
readDir path;
|
|
|
|
|
|
|
|
/*
|
|
|
|
Simplify a filesetTree recursively:
|
|
|
|
- Replace all directories that have no files with `null`
|
|
|
|
This removes directories that would be empty
|
|
|
|
- Replace all directories with all files with `"directory"`
|
|
|
|
This speeds up the source filter function
|
|
|
|
|
|
|
|
Note that this function is strict, it evaluates the entire tree
|
|
|
|
|
|
|
|
Type: Path -> filesetTree -> filesetTree
|
|
|
|
*/
|
|
|
|
_simplifyTree = path: tree:
|
|
|
|
if tree == "directory" || isAttrs tree then
|
|
|
|
let
|
|
|
|
entries = _directoryEntries path tree;
|
|
|
|
simpleSubtrees = mapAttrs (name: _simplifyTree (path + "/${name}")) entries;
|
|
|
|
subtreeValues = attrValues simpleSubtrees;
|
|
|
|
in
|
|
|
|
# This triggers either when all files in a directory are filtered out
|
|
|
|
# Or when the directory doesn't contain any files at all
|
|
|
|
if all isNull subtreeValues then
|
|
|
|
null
|
|
|
|
# Triggers when we have the same as a `readDir path`, so we can turn it back into an equivalent "directory".
|
|
|
|
else if all isString subtreeValues then
|
|
|
|
"directory"
|
|
|
|
else
|
|
|
|
simpleSubtrees
|
|
|
|
else
|
|
|
|
tree;
|
|
|
|
|
|
|
|
# Turn a fileset into a source filter function suitable for `builtins.path`
|
|
|
|
# Only directories recursively containing at least one files are recursed into
|
|
|
|
# Type: Path -> fileset -> (String -> String -> Bool)
|
|
|
|
_toSourceFilter = fileset:
|
|
|
|
let
|
|
|
|
# Simplify the tree, necessary to make sure all empty directories are null
|
|
|
|
# which has the effect that they aren't included in the result
|
|
|
|
tree = _simplifyTree fileset._internalBase fileset._internalTree;
|
|
|
|
|
|
|
|
# The base path as a string with a single trailing slash
|
|
|
|
baseString =
|
2023-09-13 18:50:45 +02:00
|
|
|
if fileset._internalBaseComponents == [] then
|
2023-08-17 00:55:32 +02:00
|
|
|
# Need to handle the filesystem root specially
|
|
|
|
"/"
|
|
|
|
else
|
2023-09-13 18:50:45 +02:00
|
|
|
"/" + concatStringsSep "/" fileset._internalBaseComponents + "/";
|
2023-08-17 00:55:32 +02:00
|
|
|
|
|
|
|
baseLength = stringLength baseString;
|
|
|
|
|
|
|
|
# Check whether a list of path components under the base path exists in the tree.
|
|
|
|
# This function is called often, so it should be fast.
|
|
|
|
# Type: [ String ] -> Bool
|
|
|
|
inTree = components:
|
|
|
|
let
|
|
|
|
recurse = index: localTree:
|
|
|
|
if isAttrs localTree then
|
|
|
|
# We have an attribute set, meaning this is a directory with at least one file
|
|
|
|
if index >= length components then
|
|
|
|
# The path may have no more components though, meaning the filter is running on the directory itself,
|
|
|
|
# so we always include it, again because there's at least one file in it.
|
|
|
|
true
|
|
|
|
else
|
|
|
|
# If we do have more components, the filter runs on some entry inside this directory, so we need to recurse
|
|
|
|
# We do +2 because builtins.split is an interleaved list of the inbetweens and the matches
|
|
|
|
recurse (index + 2) localTree.${elemAt components index}
|
|
|
|
else
|
|
|
|
# If it's not an attribute set it can only be either null (in which case it's not included)
|
|
|
|
# or a string ("directory" or "regular", etc.) in which case it's included
|
|
|
|
localTree != null;
|
|
|
|
in recurse 0 tree;
|
|
|
|
|
|
|
|
# Filter suited when there's no files
|
|
|
|
empty = _: _: false;
|
|
|
|
|
|
|
|
# Filter suited when there's some files
|
|
|
|
# This can't be used for when there's no files, because the base directory is always included
|
|
|
|
nonEmpty =
|
|
|
|
path: _:
|
|
|
|
let
|
|
|
|
# Add a slash to the path string, turning "/foo" to "/foo/",
|
|
|
|
# making sure to not have any false prefix matches below.
|
|
|
|
# Note that this would produce "//" for "/",
|
|
|
|
# but builtins.path doesn't call the filter function on the `path` argument itself,
|
|
|
|
# meaning this function can never receive "/" as an argument
|
|
|
|
pathSlash = path + "/";
|
|
|
|
in
|
|
|
|
# Same as `hasPrefix pathSlash baseString`, but more efficient.
|
|
|
|
# With base /foo/bar we need to include /foo:
|
|
|
|
# hasPrefix "/foo/" "/foo/bar/"
|
|
|
|
if substring 0 (stringLength pathSlash) baseString == pathSlash then
|
|
|
|
true
|
|
|
|
# Same as `! hasPrefix baseString pathSlash`, but more efficient.
|
|
|
|
# With base /foo/bar we need to exclude /baz
|
|
|
|
# ! hasPrefix "/baz/" "/foo/bar/"
|
|
|
|
else if substring 0 baseLength pathSlash != baseString then
|
|
|
|
false
|
|
|
|
else
|
|
|
|
# Same as `removePrefix baseString path`, but more efficient.
|
|
|
|
# From the above code we know that hasPrefix baseString pathSlash holds, so this is safe.
|
|
|
|
# We don't use pathSlash here because we only needed the trailing slash for the prefix matching.
|
|
|
|
# With base /foo and path /foo/bar/baz this gives
|
|
|
|
# inTree (split "/" (removePrefix "/foo/" "/foo/bar/baz"))
|
|
|
|
# == inTree (split "/" "bar/baz")
|
|
|
|
# == inTree [ "bar" "baz" ]
|
|
|
|
inTree (split "/" (substring baseLength (-1) path));
|
|
|
|
in
|
|
|
|
# Special case because the code below assumes that the _internalBase is always included in the result
|
|
|
|
# which shouldn't be done when we have no files at all in the base
|
|
|
|
if tree == null then
|
|
|
|
empty
|
|
|
|
else
|
|
|
|
nonEmpty;
|
|
|
|
|
|
|
|
}
|