Merge pull request #257351 from tweag/fileset.empty

`lib.fileset`: Representation for empty file sets without a base path

authored by

Robert Hensing and committed by
GitHub
812887f1 c0838e12

+119 -26
+28 -1
lib/fileset/README.md
··· 41 41 - `_type` (constant string `"fileset"`): 42 42 Tag to indicate this value is a file set. 43 43 44 - - `_internalVersion` (constant `2`, the current version): 44 + - `_internalVersion` (constant `3`, the current version): 45 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. 46 53 47 54 - `_internalBase` (path): 48 55 Any files outside of this path cannot influence the set of files. ··· 110 117 only removing files explicitly referenced by paths can break a file set expression. 111 118 - (+) This can be removed later, if we discover it's too restrictive 112 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. 113 140 114 141 ### Empty directories 115 142
+2 -5
lib/fileset/default.nix
··· 156 156 lib.fileset.toSource: `root` is of type ${typeOf root}, but it should be a path instead.'' 157 157 # Currently all Nix paths have the same filesystem root, but this could change in the future. 158 158 # See also ../path/README.md 159 - else if rootFilesystemRoot != filesetFilesystemRoot then 159 + else if ! fileset._internalIsEmptyWithoutBase && rootFilesystemRoot != filesetFilesystemRoot then 160 160 throw '' 161 161 lib.fileset.toSource: Filesystem roots are not the same for `fileset` and `root` ("${toString root}"): 162 162 `root`: root "${toString rootFilesystemRoot}" ··· 170 170 lib.fileset.toSource: `root` (${toString root}) is a file, but it should be a directory instead. Potential solutions: 171 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 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 173 + else if ! fileset._internalIsEmptyWithoutBase && ! hasPrefix root fileset._internalBase then 174 174 throw '' 175 175 lib.fileset.toSource: `fileset` could contain files in ${toString fileset._internalBase}, which is not under the `root` (${toString root}). Potential solutions: 176 176 - Set `root` to ${toString fileset._internalBase} or any directory higher up. This changes the layout of the resulting store path. ··· 264 264 filesets: 265 265 if ! isList filesets then 266 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 267 else 271 268 pipe filesets [ 272 269 # Annotate the elements with context, used by _coerceMany for better errors
+56 -13
lib/fileset/internal.nix
··· 28 28 drop 29 29 elemAt 30 30 filter 31 + findFirst 31 32 findFirstIndex 32 33 foldl' 33 34 head ··· 64 65 # - Increment this version 65 66 # - Add an additional migration function below 66 67 # - Update the description of the internal representation in ./README.md 67 - _currentVersion = 2; 68 + _currentVersion = 3; 68 69 69 70 # Migrations between versions. The 0th element converts from v0 to v1, and so on 70 71 migrations = [ ··· 89 90 _internalVersion = 2; 90 91 } 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 + ) 92 103 ]; 93 104 105 + _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 + 94 121 # Create a fileset, see ./README.md#fileset 95 122 # Type: path -> filesetTree -> fileset 96 123 _create = base: tree: ··· 103 130 _type = "fileset"; 104 131 105 132 _internalVersion = _currentVersion; 133 + 134 + _internalIsEmptyWithoutBase = false; 106 135 _internalBase = base; 107 136 _internalBaseRoot = parts.root; 108 137 _internalBaseComponents = components parts.subpath; 109 138 _internalTree = tree; 110 139 111 140 # 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.''; 141 + __noEval = throw _noEvalMessage; 114 142 }; 115 143 116 144 # Coerce a value to a fileset, erroring when the value cannot be coerced. ··· 155 183 _coerce "${functionContext}: ${context}" value 156 184 ) list; 157 185 158 - firstBaseRoot = (head filesets)._internalBaseRoot; 186 + # 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; 159 190 160 191 # Finds the first element with a filesystem root different than the first element, if any 161 192 differentIndex = findFirstIndex (fileset: 162 - firstBaseRoot != fileset._internalBaseRoot 193 + # The empty value without a base doesn't have a base path 194 + ! fileset._internalIsEmptyWithoutBase 195 + && firstBaseRoot != fileset._internalBaseRoot 163 196 ) null filesets; 164 197 in 165 - if differentIndex != null then 198 + # Only evaluates `differentIndex` if there are any elements with a base 199 + if firstWithBase != null && differentIndex != null then 166 200 throw '' 167 201 ${functionContext}: Filesystem roots are not the same: 168 202 ${(head list).context}: root "${toString firstBaseRoot}" ··· 311 345 # Special case because the code below assumes that the _internalBase is always included in the result 312 346 # which shouldn't be done when we have no files at all in the base 313 347 # This also forces the tree before returning the filter, leads to earlier error messages 314 - if tree == null then 348 + if fileset._internalIsEmptyWithoutBase || tree == null then 315 349 empty 316 350 else 317 351 nonEmpty; ··· 321 355 # Type: [ Fileset ] -> Fileset 322 356 _unionMany = filesets: 323 357 let 324 - first = head filesets; 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; 325 364 326 365 # To be able to union filesetTree's together, they need to have the same base path. 327 366 # Base paths can be unioned by taking their common prefix, ··· 332 371 # so this cannot cause a stack overflow due to a build-up of unevaluated thunks. 333 372 commonBaseComponents = foldl' 334 373 (components: el: commonPrefix components el._internalBaseComponents) 335 - first._internalBaseComponents 374 + firstWithBase._internalBaseComponents 336 375 # We could also not do the `tail` here to avoid a list allocation, 337 376 # but then we'd have to pay for a potentially expensive 338 377 # but unnecessary `commonPrefix` call 339 - (tail filesets); 378 + (tail filesetsWithBase); 340 379 341 380 # The common base path assembled from a filesystem root and the common components 342 - commonBase = append first._internalBaseRoot (join commonBaseComponents); 381 + commonBase = append firstWithBase._internalBaseRoot (join commonBaseComponents); 343 382 344 383 # A list of filesetTree's that all have the same base path 345 384 # This is achieved by nesting the trees into the components they have over the common base path ··· 351 390 setAttrByPath 352 391 (drop (length commonBaseComponents) fileset._internalBaseComponents) 353 392 fileset._internalTree 354 - ) filesets; 393 + ) filesetsWithBase; 355 394 356 395 # Folds all trees together into a single one using _unionTree 357 396 # We do not use a fold here because it would cause a thunk build-up 358 397 # which could cause a stack overflow for a large number of trees 359 398 resultTree = _unionTrees trees; 360 399 in 361 - _create commonBase resultTree; 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; 362 405 363 406 # The union of multiple filesetTree's with the same base path. 364 407 # Later elements are only evaluated if necessary.
+33 -7
lib/fileset/tests.sh
··· 282 282 283 283 # File sets cannot be evaluated directly 284 284 expectFailure '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.' 285 286 286 287 # Past versions of the internal representation are supported 287 288 expectEqual '_coerce "<tests>: value" { _type = "fileset"; _internalVersion = 0; _internalBase = ./.; }' \ 288 - '{ _internalBase = ./.; _internalBaseComponents = path.subpath.components (path.splitRoot ./.).subpath; _internalBaseRoot = /.; _internalVersion = 2; _type = "fileset"; }' 289 + '{ _internalBase = ./.; _internalBaseComponents = path.subpath.components (path.splitRoot ./.).subpath; _internalBaseRoot = /.; _internalIsEmptyWithoutBase = false; _internalVersion = 3; _type = "fileset"; }' 289 290 expectEqual '_coerce "<tests>: value" { _type = "fileset"; _internalVersion = 1; }' \ 290 - '{ _type = "fileset"; _internalVersion = 2; }' 291 + '{ _type = "fileset"; _internalIsEmptyWithoutBase = false; _internalVersion = 3; }' 292 + expectEqual '_coerce "<tests>: value" { _type = "fileset"; _internalVersion = 2; }' \ 293 + '{ _type = "fileset"; _internalIsEmptyWithoutBase = false; _internalVersion = 3; }' 291 294 292 295 # 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 + 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 296 299 \s*Make sure to update your Nixpkgs to have a newer version of `lib.fileset`.' 297 300 298 301 # _create followed by _coerce should give the inputs back without any validation 299 302 expectEqual '{ 300 303 inherit (_coerce "<test>" (_create ./. "directory")) 301 304 _internalVersion _internalBase _internalTree; 302 - }' '{ _internalBase = ./.; _internalTree = "directory"; _internalVersion = 2; }' 305 + }' '{ _internalBase = ./.; _internalTree = "directory"; _internalVersion = 3; }' 303 306 304 307 #### Resulting store path #### 305 308 ··· 310 313 tree=( 311 314 ) 312 315 checkFileset './.' 316 + 317 + # The empty value without a base should also result in an empty result 318 + tree=( 319 + [a]=0 320 + ) 321 + checkFileset '_emptyWithoutBase' 313 322 314 323 # Directories recursively containing no files are not included 315 324 tree=( ··· 408 417 409 418 # unions needs a list with at least 1 element 410 419 expectFailure '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.' 412 420 413 421 # The tree of later arguments should not be evaluated if a former argument already includes all files 414 422 tree=() 415 423 checkFileset 'union ./. (_create ./. (abort "This should not be used!"))' 416 424 checkFileset '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' 417 443 418 444 # union doesn't include files that weren't specified 419 445 tree=(