···99 _fileFilter
1010 _printFileset
1111 _intersection
1212+ _difference
1213 ;
13141415 inherit (builtins)
···365366 ];
366367 in
367368 _intersection
369369+ (elemAt filesets 0)
370370+ (elemAt filesets 1);
371371+372372+ /*
373373+ The file set containing all files from the first file set that are not in the second file set.
374374+ See also [Difference (set theory)](https://en.wikipedia.org/wiki/Complement_(set_theory)#Relative_complement).
375375+376376+ The given file sets are evaluated as lazily as possible,
377377+ with the first argument being evaluated first if needed.
378378+379379+ Type:
380380+ union :: FileSet -> FileSet -> FileSet
381381+382382+ Example:
383383+ # Create a file set containing all files from the current directory,
384384+ # except ones under ./tests
385385+ difference ./. ./tests
386386+387387+ let
388388+ # A set of Nix-related files
389389+ nixFiles = unions [ ./default.nix ./nix ./tests/default.nix ];
390390+ in
391391+ # Create a file set containing all files under ./tests, except ones in `nixFiles`,
392392+ # meaning only without ./tests/default.nix
393393+ difference ./tests nixFiles
394394+ */
395395+ difference =
396396+ # The positive file set.
397397+ # The result can only contain files that are also in this file set.
398398+ #
399399+ # This argument can also be a path,
400400+ # which gets [implicitly coerced to a file set](#sec-fileset-path-coercion).
401401+ positive:
402402+ # The negative file set.
403403+ # The result will never contain files that are also in this file set.
404404+ #
405405+ # This argument can also be a path,
406406+ # which gets [implicitly coerced to a file set](#sec-fileset-path-coercion).
407407+ negative:
408408+ let
409409+ filesets = _coerceMany "lib.fileset.difference" [
410410+ {
411411+ context = "first argument (positive set)";
412412+ value = positive;
413413+ }
414414+ {
415415+ context = "second argument (negative set)";
416416+ value = negative;
417417+ }
418418+ ];
419419+ in
420420+ _difference
368421 (elemAt filesets 0)
369422 (elemAt filesets 1);
370423
+80
lib/fileset/internal.nix
···651651 # In all other cases it's the rhs
652652 rhs;
653653654654+ # Compute the set difference between two file sets.
655655+ # The filesets must already be coerced and validated to be in the same filesystem root.
656656+ # Type: Fileset -> Fileset -> Fileset
657657+ _difference = positive: negative:
658658+ let
659659+ # The common base components prefix, e.g.
660660+ # (/foo/bar, /foo/bar/baz) -> /foo/bar
661661+ # (/foo/bar, /foo/baz) -> /foo
662662+ commonBaseComponentsLength =
663663+ # TODO: Have a `lib.lists.commonPrefixLength` function such that we don't need the list allocation from commonPrefix here
664664+ length (
665665+ commonPrefix
666666+ positive._internalBaseComponents
667667+ negative._internalBaseComponents
668668+ );
669669+670670+ # We need filesetTree's with the same base to be able to compute the difference between them
671671+ # This here is the filesetTree from the negative file set, but for a base path that matches the positive file set.
672672+ # Examples:
673673+ # For `difference /foo /foo/bar`, `negativeTreeWithPositiveBase = { bar = "directory"; }`
674674+ # because under the base path of `/foo`, only `bar` from the negative file set is included
675675+ # For `difference /foo/bar /foo`, `negativeTreeWithPositiveBase = "directory"`
676676+ # because under the base path of `/foo/bar`, everything from the negative file set is included
677677+ # For `difference /foo /bar`, `negativeTreeWithPositiveBase = null`
678678+ # because under the base path of `/foo`, nothing from the negative file set is included
679679+ negativeTreeWithPositiveBase =
680680+ if commonBaseComponentsLength == length positive._internalBaseComponents then
681681+ # The common prefix is the same as the positive base path, so the second path is equal or longer.
682682+ # We need to _shorten_ the negative filesetTree to the same base path as the positive one
683683+ # E.g. for `difference /foo /foo/bar` the common prefix is /foo, equal to the positive file set's base
684684+ # So we need to shorten the base of the tree for the negative argument from /foo/bar to just /foo
685685+ _shortenTreeBase positive._internalBaseComponents negative
686686+ else if commonBaseComponentsLength == length negative._internalBaseComponents then
687687+ # The common prefix is the same as the negative base path, so the first path is longer.
688688+ # We need to lengthen the negative filesetTree to the same base path as the positive one.
689689+ # E.g. for `difference /foo/bar /foo` the common prefix is /foo, equal to the negative file set's base
690690+ # So we need to lengthen the base of the tree for the negative argument from /foo to /foo/bar
691691+ _lengthenTreeBase positive._internalBaseComponents negative
692692+ else
693693+ # The common prefix is neither the first nor the second path.
694694+ # This means there's no overlap between the two file sets,
695695+ # and nothing from the negative argument should get removed from the positive one
696696+ # E.g for `difference /foo /bar`, we remove nothing to get the same as `/foo`
697697+ null;
698698+699699+ resultingTree =
700700+ _differenceTree
701701+ positive._internalBase
702702+ positive._internalTree
703703+ negativeTreeWithPositiveBase;
704704+ in
705705+ # If the first file set is empty, we can never have any files in the result
706706+ if positive._internalIsEmptyWithoutBase then
707707+ _emptyWithoutBase
708708+ # If the second file set is empty, nothing gets removed, so the result is just the first file set
709709+ else if negative._internalIsEmptyWithoutBase then
710710+ positive
711711+ else
712712+ # We use the positive file set base for the result,
713713+ # because only files from the positive side may be included,
714714+ # which is what base path is for
715715+ _create positive._internalBase resultingTree;
716716+717717+ # Computes the set difference of two filesetTree's
718718+ # Type: Path -> filesetTree -> filesetTree
719719+ _differenceTree = path: lhs: rhs:
720720+ # If the lhs doesn't have any files, or the right hand side includes all files
721721+ if lhs == null || isString rhs then
722722+ # The result will always be empty
723723+ null
724724+ # If the right hand side has no files
725725+ else if rhs == null then
726726+ # The result is always the left hand side, because nothing gets removed
727727+ lhs
728728+ else
729729+ # Otherwise we always have two attribute sets to recurse into
730730+ mapAttrs (name: lhsValue:
731731+ _differenceTree (path + "/${name}") lhsValue (rhs.${name} or null)
732732+ ) (_directoryEntries path lhs);
733733+654734 _fileFilter = predicate: fileset:
655735 let
656736 recurse = path: tree:
+98
lib/fileset/tests.sh
···684684)
685685checkFileset 'intersection (unions [ ./a/b ./c/d ./c/e ]) (unions [ ./a ./c/d/f ./c/e ])'
686686687687+## Difference
688688+689689+# Subtracting something from itself results in nothing
690690+tree=(
691691+ [a]=0
692692+)
693693+checkFileset 'difference ./. ./.'
694694+695695+# The tree of the second argument should not be evaluated if not needed
696696+checkFileset 'difference _emptyWithoutBase (_create ./. (abort "This should not be used!"))'
697697+checkFileset 'difference (_create ./. null) (_create ./. (abort "This should not be used!"))'
698698+699699+# Subtracting nothing gives the same thing back
700700+tree=(
701701+ [a]=1
702702+)
703703+checkFileset 'difference ./. _emptyWithoutBase'
704704+checkFileset 'difference ./. (_create ./. null)'
705705+706706+# Subtracting doesn't influence the base path
707707+mkdir a b
708708+touch {a,b}/x
709709+expectEqual 'toSource { root = ./a; fileset = difference ./a ./b; }' 'toSource { root = ./a; fileset = ./a; }'
710710+rm -rf -- *
711711+712712+# Also not the other way around
713713+mkdir a
714714+expectFailure 'toSource { root = ./a; fileset = difference ./. ./a; }' 'lib.fileset.toSource: `fileset` could contain files in '"$work"', which is not under the `root` \('"$work"'/a\). Potential solutions:
715715+\s*- Set `root` to '"$work"' or any directory higher up. This changes the layout of the resulting store path.
716716+\s*- Set `fileset` to a file set that cannot contain files outside the `root` \('"$work"'/a\). This could change the files included in the result.'
717717+rm -rf -- *
718718+719719+# Difference actually works
720720+# We test all combinations of ./., ./a, ./a/x and ./b
721721+tree=(
722722+ [a/x]=0
723723+ [a/y]=0
724724+ [b]=0
725725+ [c]=0
726726+)
727727+checkFileset 'difference ./. ./.'
728728+checkFileset 'difference ./a ./.'
729729+checkFileset 'difference ./a/x ./.'
730730+checkFileset 'difference ./b ./.'
731731+checkFileset 'difference ./a ./a'
732732+checkFileset 'difference ./a/x ./a'
733733+checkFileset 'difference ./a/x ./a/x'
734734+checkFileset 'difference ./b ./b'
735735+tree=(
736736+ [a/x]=0
737737+ [a/y]=0
738738+ [b]=1
739739+ [c]=1
740740+)
741741+checkFileset 'difference ./. ./a'
742742+tree=(
743743+ [a/x]=1
744744+ [a/y]=1
745745+ [b]=0
746746+ [c]=0
747747+)
748748+checkFileset 'difference ./a ./b'
749749+tree=(
750750+ [a/x]=1
751751+ [a/y]=0
752752+ [b]=0
753753+ [c]=0
754754+)
755755+checkFileset 'difference ./a/x ./b'
756756+tree=(
757757+ [a/x]=0
758758+ [a/y]=1
759759+ [b]=0
760760+ [c]=0
761761+)
762762+checkFileset 'difference ./a ./a/x'
763763+tree=(
764764+ [a/x]=0
765765+ [a/y]=0
766766+ [b]=1
767767+ [c]=0
768768+)
769769+checkFileset 'difference ./b ./a'
770770+checkFileset 'difference ./b ./a/x'
771771+tree=(
772772+ [a/x]=0
773773+ [a/y]=1
774774+ [b]=1
775775+ [c]=1
776776+)
777777+checkFileset 'difference ./. ./a/x'
778778+tree=(
779779+ [a/x]=1
780780+ [a/y]=1
781781+ [b]=0
782782+ [c]=1
783783+)
784784+checkFileset 'difference ./. ./b'
687785688786## File filter
689787