···41- `_type` (constant string `"fileset"`):
42 Tag to indicate this value is a file set.
4344-- `_internalVersion` (constant `2`, the current version):
45 Version of the representation.
00000004647- `_internalBase` (path):
48 Any files outside of this path cannot influence the set of files.
···110 only removing files explicitly referenced by paths can break a file set expression.
111- (+) This can be removed later, if we discover it's too restrictive
112- (-) It leads to errors when a sensible result could sometimes be returned, such as in the above example.
00000000000000000000113114### Empty directories
115
···41- `_type` (constant string `"fileset"`):
42 Tag to indicate this value is a file set.
4344+- `_internalVersion` (constant `3`, the current version):
45 Version of the representation.
46+47+- `_internalIsEmptyWithoutBase` (bool):
48+ Whether this file set is the empty file set without a base path.
49+ If `true`, `_internalBase*` and `_internalTree` are not set.
50+ This is the only way to represent an empty file set without needing a base path.
51+52+ Such a value can be used as the identity element for `union` and the return value of `unions []` and co.
5354- `_internalBase` (path):
55 Any files outside of this path cannot influence the set of files.
···117 only removing files explicitly referenced by paths can break a file set expression.
118- (+) This can be removed later, if we discover it's too restrictive
119- (-) It leads to errors when a sensible result could sometimes be returned, such as in the above example.
120+121+### Empty file set without a base
122+123+There is a special representation for an empty file set without a base path.
124+This is used for return values that should be empty but when there's no base path that would makes sense.
125+126+Arguments:
127+- Alternative: This could also be represented using `_internalBase = /.` and `_internalTree = null`.
128+ - (+) Removes the need for a special representation.
129+ - (-) Due to [influence tracking](#influence-tracking),
130+ `union empty ./.` would have `/.` as the base path,
131+ which would then prevent `toSource { root = ./.; fileset = union empty ./.; }` from working,
132+ which is not as one would expect.
133+ - (-) With the assumption that there can be multiple filesystem roots (as established with the [path library](../path/README.md)),
134+ this would have to cause an error with `union empty pathWithAnotherFilesystemRoot`,
135+ which is not as one would expect.
136+- Alternative: Do not have such a value and error when it would be needed as a return value
137+ - (+) Removes the need for a special representation.
138+ - (-) Leaves us with no identity element for `union` and no reasonable return value for `unions []`.
139+ From a set theory perspective, which has a well-known notion of empty sets, this is unintuitive.
140141### Empty directories
142
+2-5
lib/fileset/default.nix
···156 lib.fileset.toSource: `root` is of type ${typeOf root}, but it should be a path instead.''
157 # Currently all Nix paths have the same filesystem root, but this could change in the future.
158 # See also ../path/README.md
159- else if rootFilesystemRoot != filesetFilesystemRoot then
160 throw ''
161 lib.fileset.toSource: Filesystem roots are not the same for `fileset` and `root` ("${toString root}"):
162 `root`: root "${toString rootFilesystemRoot}"
···170 lib.fileset.toSource: `root` (${toString root}) is a file, but it should be a directory instead. Potential solutions:
171 - If you want to import the file into the store _without_ a containing directory, use string interpolation or `builtins.path` instead of this function.
172 - 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.''
173- else if ! hasPrefix root fileset._internalBase then
174 throw ''
175 lib.fileset.toSource: `fileset` could contain files in ${toString fileset._internalBase}, which is not under the `root` (${toString root}). Potential solutions:
176 - Set `root` to ${toString fileset._internalBase} or any directory higher up. This changes the layout of the resulting store path.
···264 filesets:
265 if ! isList filesets then
266 throw "lib.fileset.unions: Expected argument to be a list, but got a ${typeOf filesets}."
267- else if filesets == [ ] then
268- # 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.
269- throw "lib.fileset.unions: Expected argument to be a list with at least one element, but it contains no elements."
270 else
271 pipe filesets [
272 # Annotate the elements with context, used by _coerceMany for better errors
···156 lib.fileset.toSource: `root` is of type ${typeOf root}, but it should be a path instead.''
157 # Currently all Nix paths have the same filesystem root, but this could change in the future.
158 # See also ../path/README.md
159+ else if ! fileset._internalIsEmptyWithoutBase && rootFilesystemRoot != filesetFilesystemRoot then
160 throw ''
161 lib.fileset.toSource: Filesystem roots are not the same for `fileset` and `root` ("${toString root}"):
162 `root`: root "${toString rootFilesystemRoot}"
···170 lib.fileset.toSource: `root` (${toString root}) is a file, but it should be a directory instead. Potential solutions:
171 - If you want to import the file into the store _without_ a containing directory, use string interpolation or `builtins.path` instead of this function.
172 - 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.''
173+ else if ! fileset._internalIsEmptyWithoutBase && ! hasPrefix root fileset._internalBase then
174 throw ''
175 lib.fileset.toSource: `fileset` could contain files in ${toString fileset._internalBase}, which is not under the `root` (${toString root}). Potential solutions:
176 - Set `root` to ${toString fileset._internalBase} or any directory higher up. This changes the layout of the resulting store path.
···264 filesets:
265 if ! isList filesets then
266 throw "lib.fileset.unions: Expected argument to be a list, but got a ${typeOf filesets}."
000267 else
268 pipe filesets [
269 # Annotate the elements with context, used by _coerceMany for better errors
+56-13
lib/fileset/internal.nix
···28 drop
29 elemAt
30 filter
031 findFirstIndex
32 foldl'
33 head
···64 # - Increment this version
65 # - Add an additional migration function below
66 # - Update the description of the internal representation in ./README.md
67- _currentVersion = 2;
6869 # Migrations between versions. The 0th element converts from v0 to v1, and so on
70 migrations = [
···89 _internalVersion = 2;
90 }
91 )
000000000092 ];
93000000000000000094 # Create a fileset, see ./README.md#fileset
95 # Type: path -> filesetTree -> fileset
96 _create = base: tree:
···103 _type = "fileset";
104105 _internalVersion = _currentVersion;
00106 _internalBase = base;
107 _internalBaseRoot = parts.root;
108 _internalBaseComponents = components parts.subpath;
109 _internalTree = tree;
110111 # Double __ to make it be evaluated and ordered first
112- __noEval = throw ''
113- lib.fileset: Directly evaluating a file set is not supported. Use `lib.fileset.toSource` to turn it into a usable source instead.'';
114 };
115116 # Coerce a value to a fileset, erroring when the value cannot be coerced.
···155 _coerce "${functionContext}: ${context}" value
156 ) list;
157158- firstBaseRoot = (head filesets)._internalBaseRoot;
000159160 # Finds the first element with a filesystem root different than the first element, if any
161 differentIndex = findFirstIndex (fileset:
162- firstBaseRoot != fileset._internalBaseRoot
00163 ) null filesets;
164 in
165- if differentIndex != null then
0166 throw ''
167 ${functionContext}: Filesystem roots are not the same:
168 ${(head list).context}: root "${toString firstBaseRoot}"
···311 # Special case because the code below assumes that the _internalBase is always included in the result
312 # which shouldn't be done when we have no files at all in the base
313 # This also forces the tree before returning the filter, leads to earlier error messages
314- if tree == null then
315 empty
316 else
317 nonEmpty;
···321 # Type: [ Fileset ] -> Fileset
322 _unionMany = filesets:
323 let
324- first = head filesets;
00000325326 # To be able to union filesetTree's together, they need to have the same base path.
327 # Base paths can be unioned by taking their common prefix,
···332 # so this cannot cause a stack overflow due to a build-up of unevaluated thunks.
333 commonBaseComponents = foldl'
334 (components: el: commonPrefix components el._internalBaseComponents)
335- first._internalBaseComponents
336 # We could also not do the `tail` here to avoid a list allocation,
337 # but then we'd have to pay for a potentially expensive
338 # but unnecessary `commonPrefix` call
339- (tail filesets);
340341 # The common base path assembled from a filesystem root and the common components
342- commonBase = append first._internalBaseRoot (join commonBaseComponents);
343344 # A list of filesetTree's that all have the same base path
345 # This is achieved by nesting the trees into the components they have over the common base path
···351 setAttrByPath
352 (drop (length commonBaseComponents) fileset._internalBaseComponents)
353 fileset._internalTree
354- ) filesets;
355356 # Folds all trees together into a single one using _unionTree
357 # We do not use a fold here because it would cause a thunk build-up
358 # which could cause a stack overflow for a large number of trees
359 resultTree = _unionTrees trees;
360 in
361- _create commonBase resultTree;
0000362363 # The union of multiple filesetTree's with the same base path.
364 # Later elements are only evaluated if necessary.
···28 drop
29 elemAt
30 filter
31+ findFirst
32 findFirstIndex
33 foldl'
34 head
···65 # - Increment this version
66 # - Add an additional migration function below
67 # - Update the description of the internal representation in ./README.md
68+ _currentVersion = 3;
6970 # Migrations between versions. The 0th element converts from v0 to v1, and so on
71 migrations = [
···90 _internalVersion = 2;
91 }
92 )
93+94+ # Convert v2 into v3: filesetTree's now have a representation for an empty file set without a base path
95+ (
96+ filesetV2:
97+ filesetV2 // {
98+ # All v1 file sets are not the new empty file set
99+ _internalIsEmptyWithoutBase = false;
100+ _internalVersion = 3;
101+ }
102+ )
103 ];
104105+ _noEvalMessage = ''
106+ lib.fileset: Directly evaluating a file set is not supported. Use `lib.fileset.toSource` to turn it into a usable source instead.'';
107+108+ # The empty file set without a base path
109+ _emptyWithoutBase = {
110+ _type = "fileset";
111+112+ _internalVersion = _currentVersion;
113+114+ # The one and only!
115+ _internalIsEmptyWithoutBase = true;
116+117+ # Double __ to make it be evaluated and ordered first
118+ __noEval = throw _noEvalMessage;
119+ };
120+121 # Create a fileset, see ./README.md#fileset
122 # Type: path -> filesetTree -> fileset
123 _create = base: tree:
···130 _type = "fileset";
131132 _internalVersion = _currentVersion;
133+134+ _internalIsEmptyWithoutBase = false;
135 _internalBase = base;
136 _internalBaseRoot = parts.root;
137 _internalBaseComponents = components parts.subpath;
138 _internalTree = tree;
139140 # Double __ to make it be evaluated and ordered first
141+ __noEval = throw _noEvalMessage;
0142 };
143144 # Coerce a value to a fileset, erroring when the value cannot be coerced.
···183 _coerce "${functionContext}: ${context}" value
184 ) list;
185186+ # Find the first value with a base, there may be none!
187+ firstWithBase = findFirst (fileset: ! fileset._internalIsEmptyWithoutBase) null filesets;
188+ # This value is only accessed if first != null
189+ firstBaseRoot = firstWithBase._internalBaseRoot;
190191 # Finds the first element with a filesystem root different than the first element, if any
192 differentIndex = findFirstIndex (fileset:
193+ # The empty value without a base doesn't have a base path
194+ ! fileset._internalIsEmptyWithoutBase
195+ && firstBaseRoot != fileset._internalBaseRoot
196 ) null filesets;
197 in
198+ # Only evaluates `differentIndex` if there are any elements with a base
199+ if firstWithBase != null && differentIndex != null then
200 throw ''
201 ${functionContext}: Filesystem roots are not the same:
202 ${(head list).context}: root "${toString firstBaseRoot}"
···345 # Special case because the code below assumes that the _internalBase is always included in the result
346 # which shouldn't be done when we have no files at all in the base
347 # This also forces the tree before returning the filter, leads to earlier error messages
348+ if fileset._internalIsEmptyWithoutBase || tree == null then
349 empty
350 else
351 nonEmpty;
···355 # Type: [ Fileset ] -> Fileset
356 _unionMany = filesets:
357 let
358+ # All filesets that have a base, aka not the ones that are the empty value without a base
359+ filesetsWithBase = filter (fileset: ! fileset._internalIsEmptyWithoutBase) filesets;
360+361+ # The first fileset that has a base.
362+ # This value is only accessed if there are at all.
363+ firstWithBase = head filesetsWithBase;
364365 # To be able to union filesetTree's together, they need to have the same base path.
366 # Base paths can be unioned by taking their common prefix,
···371 # so this cannot cause a stack overflow due to a build-up of unevaluated thunks.
372 commonBaseComponents = foldl'
373 (components: el: commonPrefix components el._internalBaseComponents)
374+ firstWithBase._internalBaseComponents
375 # We could also not do the `tail` here to avoid a list allocation,
376 # but then we'd have to pay for a potentially expensive
377 # but unnecessary `commonPrefix` call
378+ (tail filesetsWithBase);
379380 # The common base path assembled from a filesystem root and the common components
381+ commonBase = append firstWithBase._internalBaseRoot (join commonBaseComponents);
382383 # A list of filesetTree's that all have the same base path
384 # This is achieved by nesting the trees into the components they have over the common base path
···390 setAttrByPath
391 (drop (length commonBaseComponents) fileset._internalBaseComponents)
392 fileset._internalTree
393+ ) filesetsWithBase;
394395 # Folds all trees together into a single one using _unionTree
396 # We do not use a fold here because it would cause a thunk build-up
397 # which could cause a stack overflow for a large number of trees
398 resultTree = _unionTrees trees;
399 in
400+ # If there's no values with a base, we have no files
401+ if filesetsWithBase == [ ] then
402+ _emptyWithoutBase
403+ else
404+ _create commonBase resultTree;
405406 # The union of multiple filesetTree's with the same base path.
407 # Later elements are only evaluated if necessary.
+33-7
lib/fileset/tests.sh
···282283# File sets cannot be evaluated directly
284expectFailure 'union ./. ./.' 'lib.fileset: Directly evaluating a file set is not supported. Use `lib.fileset.toSource` to turn it into a usable source instead.'
0285286# Past versions of the internal representation are supported
287expectEqual '_coerce "<tests>: value" { _type = "fileset"; _internalVersion = 0; _internalBase = ./.; }' \
288- '{ _internalBase = ./.; _internalBaseComponents = path.subpath.components (path.splitRoot ./.).subpath; _internalBaseRoot = /.; _internalVersion = 2; _type = "fileset"; }'
289expectEqual '_coerce "<tests>: value" { _type = "fileset"; _internalVersion = 1; }' \
290- '{ _type = "fileset"; _internalVersion = 2; }'
00291292# Future versions of the internal representation are unsupported
293-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:
294-\s*- Internal version of the file set: 3
295-\s*- Internal version of the library: 2
296\s*Make sure to update your Nixpkgs to have a newer version of `lib.fileset`.'
297298# _create followed by _coerce should give the inputs back without any validation
299expectEqual '{
300 inherit (_coerce "<test>" (_create ./. "directory"))
301 _internalVersion _internalBase _internalTree;
302-}' '{ _internalBase = ./.; _internalTree = "directory"; _internalVersion = 2; }'
303304#### Resulting store path ####
305···310tree=(
311)
312checkFileset './.'
000000313314# Directories recursively containing no files are not included
315tree=(
···408409# unions needs a list with at least 1 element
410expectFailure 'toSource { root = ./.; fileset = unions null; }' 'lib.fileset.unions: Expected argument to be a list, but got a null.'
411-expectFailure 'toSource { root = ./.; fileset = unions [ ]; }' 'lib.fileset.unions: Expected argument to be a list with at least one element, but it contains no elements.'
412413# The tree of later arguments should not be evaluated if a former argument already includes all files
414tree=()
415checkFileset 'union ./. (_create ./. (abort "This should not be used!"))'
416checkFileset 'unions [ ./. (_create ./. (abort "This should not be used!")) ]'
000000000000000000417418# union doesn't include files that weren't specified
419tree=(
···282283# File sets cannot be evaluated directly
284expectFailure 'union ./. ./.' 'lib.fileset: Directly evaluating a file set is not supported. Use `lib.fileset.toSource` to turn it into a usable source instead.'
285+expectFailure '_emptyWithoutBase' 'lib.fileset: Directly evaluating a file set is not supported. Use `lib.fileset.toSource` to turn it into a usable source instead.'
286287# Past versions of the internal representation are supported
288expectEqual '_coerce "<tests>: value" { _type = "fileset"; _internalVersion = 0; _internalBase = ./.; }' \
289+ '{ _internalBase = ./.; _internalBaseComponents = path.subpath.components (path.splitRoot ./.).subpath; _internalBaseRoot = /.; _internalIsEmptyWithoutBase = false; _internalVersion = 3; _type = "fileset"; }'
290expectEqual '_coerce "<tests>: value" { _type = "fileset"; _internalVersion = 1; }' \
291+ '{ _type = "fileset"; _internalIsEmptyWithoutBase = false; _internalVersion = 3; }'
292+expectEqual '_coerce "<tests>: value" { _type = "fileset"; _internalVersion = 2; }' \
293+ '{ _type = "fileset"; _internalIsEmptyWithoutBase = false; _internalVersion = 3; }'
294295# Future versions of the internal representation are unsupported
296+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:
297+\s*- Internal version of the file set: 4
298+\s*- Internal version of the library: 3
299\s*Make sure to update your Nixpkgs to have a newer version of `lib.fileset`.'
300301# _create followed by _coerce should give the inputs back without any validation
302expectEqual '{
303 inherit (_coerce "<test>" (_create ./. "directory"))
304 _internalVersion _internalBase _internalTree;
305+}' '{ _internalBase = ./.; _internalTree = "directory"; _internalVersion = 3; }'
306307#### Resulting store path ####
308···313tree=(
314)
315checkFileset './.'
316+317+# The empty value without a base should also result in an empty result
318+tree=(
319+ [a]=0
320+)
321+checkFileset '_emptyWithoutBase'
322323# Directories recursively containing no files are not included
324tree=(
···417418# unions needs a list with at least 1 element
419expectFailure 'toSource { root = ./.; fileset = unions null; }' 'lib.fileset.unions: Expected argument to be a list, but got a null.'
0420421# The tree of later arguments should not be evaluated if a former argument already includes all files
422tree=()
423checkFileset 'union ./. (_create ./. (abort "This should not be used!"))'
424checkFileset 'unions [ ./. (_create ./. (abort "This should not be used!")) ]'
425+426+# unions doesn't include any files for an empty list or only empty values without a base
427+tree=(
428+ [x]=0
429+ [y/z]=0
430+)
431+checkFileset 'unions [ ]'
432+checkFileset 'unions [ _emptyWithoutBase ]'
433+checkFileset 'unions [ _emptyWithoutBase _emptyWithoutBase ]'
434+checkFileset 'union _emptyWithoutBase _emptyWithoutBase'
435+436+# The empty value without a base is the left and right identity of union
437+tree=(
438+ [x]=1
439+ [y/z]=0
440+)
441+checkFileset 'union ./x _emptyWithoutBase'
442+checkFileset 'union _emptyWithoutBase ./x'
443444# union doesn't include files that weren't specified
445tree=(