···4141- `_type` (constant string `"fileset"`):
4242 Tag to indicate this value is a file set.
43434444-- `_internalVersion` (constant `2`, the current version):
4444+- `_internalVersion` (constant `3`, the current version):
4545 Version of the representation.
4646+4747+- `_internalIsEmptyWithoutBase` (bool):
4848+ Whether this file set is the empty file set without a base path.
4949+ If `true`, `_internalBase*` and `_internalTree` are not set.
5050+ This is the only way to represent an empty file set without needing a base path.
5151+5252+ Such a value can be used as the identity element for `union` and the return value of `unions []` and co.
46534754- `_internalBase` (path):
4855 Any files outside of this path cannot influence the set of files.
···110117 only removing files explicitly referenced by paths can break a file set expression.
111118- (+) This can be removed later, if we discover it's too restrictive
112119- (-) It leads to errors when a sensible result could sometimes be returned, such as in the above example.
120120+121121+### Empty file set without a base
122122+123123+There is a special representation for an empty file set without a base path.
124124+This is used for return values that should be empty but when there's no base path that would makes sense.
125125+126126+Arguments:
127127+- Alternative: This could also be represented using `_internalBase = /.` and `_internalTree = null`.
128128+ - (+) Removes the need for a special representation.
129129+ - (-) Due to [influence tracking](#influence-tracking),
130130+ `union empty ./.` would have `/.` as the base path,
131131+ which would then prevent `toSource { root = ./.; fileset = union empty ./.; }` from working,
132132+ which is not as one would expect.
133133+ - (-) With the assumption that there can be multiple filesystem roots (as established with the [path library](../path/README.md)),
134134+ this would have to cause an error with `union empty pathWithAnotherFilesystemRoot`,
135135+ which is not as one would expect.
136136+- Alternative: Do not have such a value and error when it would be needed as a return value
137137+ - (+) Removes the need for a special representation.
138138+ - (-) Leaves us with no identity element for `union` and no reasonable return value for `unions []`.
139139+ From a set theory perspective, which has a well-known notion of empty sets, this is unintuitive.
113140114141### Empty directories
115142
+2-5
lib/fileset/default.nix
···156156 lib.fileset.toSource: `root` is of type ${typeOf root}, but it should be a path instead.''
157157 # Currently all Nix paths have the same filesystem root, but this could change in the future.
158158 # See also ../path/README.md
159159- else if rootFilesystemRoot != filesetFilesystemRoot then
159159+ else if ! fileset._internalIsEmptyWithoutBase && rootFilesystemRoot != filesetFilesystemRoot then
160160 throw ''
161161 lib.fileset.toSource: Filesystem roots are not the same for `fileset` and `root` ("${toString root}"):
162162 `root`: root "${toString rootFilesystemRoot}"
···170170 lib.fileset.toSource: `root` (${toString root}) is a file, but it should be a directory instead. Potential solutions:
171171 - If you want to import the file into the store _without_ a containing directory, use string interpolation or `builtins.path` instead of this function.
172172 - If you want to import the file into the store _with_ a containing directory, set `root` to the containing directory, such as ${toString (dirOf root)}, and set `fileset` to the file path.''
173173- else if ! hasPrefix root fileset._internalBase then
173173+ else if ! fileset._internalIsEmptyWithoutBase && ! hasPrefix root fileset._internalBase then
174174 throw ''
175175 lib.fileset.toSource: `fileset` could contain files in ${toString fileset._internalBase}, which is not under the `root` (${toString root}). Potential solutions:
176176 - Set `root` to ${toString fileset._internalBase} or any directory higher up. This changes the layout of the resulting store path.
···264264 filesets:
265265 if ! isList filesets then
266266 throw "lib.fileset.unions: Expected argument to be a list, but got a ${typeOf filesets}."
267267- else if filesets == [ ] then
268268- # TODO: This could be supported, but requires an extra internal representation for the empty file set, which would be special for not having a base path.
269269- throw "lib.fileset.unions: Expected argument to be a list with at least one element, but it contains no elements."
270267 else
271268 pipe filesets [
272269 # Annotate the elements with context, used by _coerceMany for better errors
+56-13
lib/fileset/internal.nix
···2828 drop
2929 elemAt
3030 filter
3131+ findFirst
3132 findFirstIndex
3233 foldl'
3334 head
···6465 # - Increment this version
6566 # - Add an additional migration function below
6667 # - Update the description of the internal representation in ./README.md
6767- _currentVersion = 2;
6868+ _currentVersion = 3;
68696970 # Migrations between versions. The 0th element converts from v0 to v1, and so on
7071 migrations = [
···8990 _internalVersion = 2;
9091 }
9192 )
9393+9494+ # Convert v2 into v3: filesetTree's now have a representation for an empty file set without a base path
9595+ (
9696+ filesetV2:
9797+ filesetV2 // {
9898+ # All v1 file sets are not the new empty file set
9999+ _internalIsEmptyWithoutBase = false;
100100+ _internalVersion = 3;
101101+ }
102102+ )
92103 ];
93104105105+ _noEvalMessage = ''
106106+ lib.fileset: Directly evaluating a file set is not supported. Use `lib.fileset.toSource` to turn it into a usable source instead.'';
107107+108108+ # The empty file set without a base path
109109+ _emptyWithoutBase = {
110110+ _type = "fileset";
111111+112112+ _internalVersion = _currentVersion;
113113+114114+ # The one and only!
115115+ _internalIsEmptyWithoutBase = true;
116116+117117+ # Double __ to make it be evaluated and ordered first
118118+ __noEval = throw _noEvalMessage;
119119+ };
120120+94121 # Create a fileset, see ./README.md#fileset
95122 # Type: path -> filesetTree -> fileset
96123 _create = base: tree:
···103130 _type = "fileset";
104131105132 _internalVersion = _currentVersion;
133133+134134+ _internalIsEmptyWithoutBase = false;
106135 _internalBase = base;
107136 _internalBaseRoot = parts.root;
108137 _internalBaseComponents = components parts.subpath;
109138 _internalTree = tree;
110139111140 # Double __ to make it be evaluated and ordered first
112112- __noEval = throw ''
113113- lib.fileset: Directly evaluating a file set is not supported. Use `lib.fileset.toSource` to turn it into a usable source instead.'';
141141+ __noEval = throw _noEvalMessage;
114142 };
115143116144 # Coerce a value to a fileset, erroring when the value cannot be coerced.
···155183 _coerce "${functionContext}: ${context}" value
156184 ) list;
157185158158- firstBaseRoot = (head filesets)._internalBaseRoot;
186186+ # Find the first value with a base, there may be none!
187187+ firstWithBase = findFirst (fileset: ! fileset._internalIsEmptyWithoutBase) null filesets;
188188+ # This value is only accessed if first != null
189189+ firstBaseRoot = firstWithBase._internalBaseRoot;
159190160191 # Finds the first element with a filesystem root different than the first element, if any
161192 differentIndex = findFirstIndex (fileset:
162162- firstBaseRoot != fileset._internalBaseRoot
193193+ # The empty value without a base doesn't have a base path
194194+ ! fileset._internalIsEmptyWithoutBase
195195+ && firstBaseRoot != fileset._internalBaseRoot
163196 ) null filesets;
164197 in
165165- if differentIndex != null then
198198+ # Only evaluates `differentIndex` if there are any elements with a base
199199+ if firstWithBase != null && differentIndex != null then
166200 throw ''
167201 ${functionContext}: Filesystem roots are not the same:
168202 ${(head list).context}: root "${toString firstBaseRoot}"
···311345 # Special case because the code below assumes that the _internalBase is always included in the result
312346 # which shouldn't be done when we have no files at all in the base
313347 # This also forces the tree before returning the filter, leads to earlier error messages
314314- if tree == null then
348348+ if fileset._internalIsEmptyWithoutBase || tree == null then
315349 empty
316350 else
317351 nonEmpty;
···321355 # Type: [ Fileset ] -> Fileset
322356 _unionMany = filesets:
323357 let
324324- first = head filesets;
358358+ # All filesets that have a base, aka not the ones that are the empty value without a base
359359+ filesetsWithBase = filter (fileset: ! fileset._internalIsEmptyWithoutBase) filesets;
360360+361361+ # The first fileset that has a base.
362362+ # This value is only accessed if there are at all.
363363+ firstWithBase = head filesetsWithBase;
325364326365 # To be able to union filesetTree's together, they need to have the same base path.
327366 # Base paths can be unioned by taking their common prefix,
···332371 # so this cannot cause a stack overflow due to a build-up of unevaluated thunks.
333372 commonBaseComponents = foldl'
334373 (components: el: commonPrefix components el._internalBaseComponents)
335335- first._internalBaseComponents
374374+ firstWithBase._internalBaseComponents
336375 # We could also not do the `tail` here to avoid a list allocation,
337376 # but then we'd have to pay for a potentially expensive
338377 # but unnecessary `commonPrefix` call
339339- (tail filesets);
378378+ (tail filesetsWithBase);
340379341380 # The common base path assembled from a filesystem root and the common components
342342- commonBase = append first._internalBaseRoot (join commonBaseComponents);
381381+ commonBase = append firstWithBase._internalBaseRoot (join commonBaseComponents);
343382344383 # A list of filesetTree's that all have the same base path
345384 # This is achieved by nesting the trees into the components they have over the common base path
···351390 setAttrByPath
352391 (drop (length commonBaseComponents) fileset._internalBaseComponents)
353392 fileset._internalTree
354354- ) filesets;
393393+ ) filesetsWithBase;
355394356395 # Folds all trees together into a single one using _unionTree
357396 # We do not use a fold here because it would cause a thunk build-up
358397 # which could cause a stack overflow for a large number of trees
359398 resultTree = _unionTrees trees;
360399 in
361361- _create commonBase resultTree;
400400+ # If there's no values with a base, we have no files
401401+ if filesetsWithBase == [ ] then
402402+ _emptyWithoutBase
403403+ else
404404+ _create commonBase resultTree;
362405363406 # The union of multiple filesetTree's with the same base path.
364407 # Later elements are only evaluated if necessary.
+33-7
lib/fileset/tests.sh
···282282283283# File sets cannot be evaluated directly
284284expectFailure 'union ./. ./.' 'lib.fileset: Directly evaluating a file set is not supported. Use `lib.fileset.toSource` to turn it into a usable source instead.'
285285+expectFailure '_emptyWithoutBase' 'lib.fileset: Directly evaluating a file set is not supported. Use `lib.fileset.toSource` to turn it into a usable source instead.'
285286286287# Past versions of the internal representation are supported
287288expectEqual '_coerce "<tests>: value" { _type = "fileset"; _internalVersion = 0; _internalBase = ./.; }' \
288288- '{ _internalBase = ./.; _internalBaseComponents = path.subpath.components (path.splitRoot ./.).subpath; _internalBaseRoot = /.; _internalVersion = 2; _type = "fileset"; }'
289289+ '{ _internalBase = ./.; _internalBaseComponents = path.subpath.components (path.splitRoot ./.).subpath; _internalBaseRoot = /.; _internalIsEmptyWithoutBase = false; _internalVersion = 3; _type = "fileset"; }'
289290expectEqual '_coerce "<tests>: value" { _type = "fileset"; _internalVersion = 1; }' \
290290- '{ _type = "fileset"; _internalVersion = 2; }'
291291+ '{ _type = "fileset"; _internalIsEmptyWithoutBase = false; _internalVersion = 3; }'
292292+expectEqual '_coerce "<tests>: value" { _type = "fileset"; _internalVersion = 2; }' \
293293+ '{ _type = "fileset"; _internalIsEmptyWithoutBase = false; _internalVersion = 3; }'
291294292295# Future versions of the internal representation are unsupported
293293-expectFailure '_coerce "<tests>: value" { _type = "fileset"; _internalVersion = 3; }' '<tests>: value is a file set created from a future version of the file set library with a different internal representation:
294294-\s*- Internal version of the file set: 3
295295-\s*- Internal version of the library: 2
296296+expectFailure '_coerce "<tests>: value" { _type = "fileset"; _internalVersion = 4; }' '<tests>: value is a file set created from a future version of the file set library with a different internal representation:
297297+\s*- Internal version of the file set: 4
298298+\s*- Internal version of the library: 3
296299\s*Make sure to update your Nixpkgs to have a newer version of `lib.fileset`.'
297300298301# _create followed by _coerce should give the inputs back without any validation
299302expectEqual '{
300303 inherit (_coerce "<test>" (_create ./. "directory"))
301304 _internalVersion _internalBase _internalTree;
302302-}' '{ _internalBase = ./.; _internalTree = "directory"; _internalVersion = 2; }'
305305+}' '{ _internalBase = ./.; _internalTree = "directory"; _internalVersion = 3; }'
303306304307#### Resulting store path ####
305308···310313tree=(
311314)
312315checkFileset './.'
316316+317317+# The empty value without a base should also result in an empty result
318318+tree=(
319319+ [a]=0
320320+)
321321+checkFileset '_emptyWithoutBase'
313322314323# Directories recursively containing no files are not included
315324tree=(
···408417409418# unions needs a list with at least 1 element
410419expectFailure 'toSource { root = ./.; fileset = unions null; }' 'lib.fileset.unions: Expected argument to be a list, but got a null.'
411411-expectFailure 'toSource { root = ./.; fileset = unions [ ]; }' 'lib.fileset.unions: Expected argument to be a list with at least one element, but it contains no elements.'
412420413421# The tree of later arguments should not be evaluated if a former argument already includes all files
414422tree=()
415423checkFileset 'union ./. (_create ./. (abort "This should not be used!"))'
416424checkFileset 'unions [ ./. (_create ./. (abort "This should not be used!")) ]'
425425+426426+# unions doesn't include any files for an empty list or only empty values without a base
427427+tree=(
428428+ [x]=0
429429+ [y/z]=0
430430+)
431431+checkFileset 'unions [ ]'
432432+checkFileset 'unions [ _emptyWithoutBase ]'
433433+checkFileset 'unions [ _emptyWithoutBase _emptyWithoutBase ]'
434434+checkFileset 'union _emptyWithoutBase _emptyWithoutBase'
435435+436436+# The empty value without a base is the left and right identity of union
437437+tree=(
438438+ [x]=1
439439+ [y/z]=0
440440+)
441441+checkFileset 'union ./x _emptyWithoutBase'
442442+checkFileset 'union _emptyWithoutBase ./x'
417443418444# union doesn't include files that weren't specified
419445tree=(