Merge pull request #259065 from tweag/fileset.difference

`lib.fileset.difference`: init

authored by Silvan Mosberger and committed by GitHub fc28c5e5 1c7f17f3

+231
+53
lib/fileset/default.nix
··· 9 9 _fileFilter 10 10 _printFileset 11 11 _intersection 12 + _difference 12 13 ; 13 14 14 15 inherit (builtins) ··· 365 366 ]; 366 367 in 367 368 _intersection 369 + (elemAt filesets 0) 370 + (elemAt filesets 1); 371 + 372 + /* 373 + The file set containing all files from the first file set that are not in the second file set. 374 + See also [Difference (set theory)](https://en.wikipedia.org/wiki/Complement_(set_theory)#Relative_complement). 375 + 376 + The given file sets are evaluated as lazily as possible, 377 + with the first argument being evaluated first if needed. 378 + 379 + Type: 380 + union :: FileSet -> FileSet -> FileSet 381 + 382 + Example: 383 + # Create a file set containing all files from the current directory, 384 + # except ones under ./tests 385 + difference ./. ./tests 386 + 387 + let 388 + # A set of Nix-related files 389 + nixFiles = unions [ ./default.nix ./nix ./tests/default.nix ]; 390 + in 391 + # Create a file set containing all files under ./tests, except ones in `nixFiles`, 392 + # meaning only without ./tests/default.nix 393 + difference ./tests nixFiles 394 + */ 395 + difference = 396 + # The positive file set. 397 + # The result can only contain files that are also in this file set. 398 + # 399 + # This argument can also be a path, 400 + # which gets [implicitly coerced to a file set](#sec-fileset-path-coercion). 401 + positive: 402 + # The negative file set. 403 + # The result will never contain files that are also in this file set. 404 + # 405 + # This argument can also be a path, 406 + # which gets [implicitly coerced to a file set](#sec-fileset-path-coercion). 407 + negative: 408 + let 409 + filesets = _coerceMany "lib.fileset.difference" [ 410 + { 411 + context = "first argument (positive set)"; 412 + value = positive; 413 + } 414 + { 415 + context = "second argument (negative set)"; 416 + value = negative; 417 + } 418 + ]; 419 + in 420 + _difference 368 421 (elemAt filesets 0) 369 422 (elemAt filesets 1); 370 423
+80
lib/fileset/internal.nix
··· 651 651 # In all other cases it's the rhs 652 652 rhs; 653 653 654 + # Compute the set difference between two file sets. 655 + # The filesets must already be coerced and validated to be in the same filesystem root. 656 + # Type: Fileset -> Fileset -> Fileset 657 + _difference = positive: negative: 658 + let 659 + # The common base components prefix, e.g. 660 + # (/foo/bar, /foo/bar/baz) -> /foo/bar 661 + # (/foo/bar, /foo/baz) -> /foo 662 + commonBaseComponentsLength = 663 + # TODO: Have a `lib.lists.commonPrefixLength` function such that we don't need the list allocation from commonPrefix here 664 + length ( 665 + commonPrefix 666 + positive._internalBaseComponents 667 + negative._internalBaseComponents 668 + ); 669 + 670 + # We need filesetTree's with the same base to be able to compute the difference between them 671 + # This here is the filesetTree from the negative file set, but for a base path that matches the positive file set. 672 + # Examples: 673 + # For `difference /foo /foo/bar`, `negativeTreeWithPositiveBase = { bar = "directory"; }` 674 + # because under the base path of `/foo`, only `bar` from the negative file set is included 675 + # For `difference /foo/bar /foo`, `negativeTreeWithPositiveBase = "directory"` 676 + # because under the base path of `/foo/bar`, everything from the negative file set is included 677 + # For `difference /foo /bar`, `negativeTreeWithPositiveBase = null` 678 + # because under the base path of `/foo`, nothing from the negative file set is included 679 + negativeTreeWithPositiveBase = 680 + if commonBaseComponentsLength == length positive._internalBaseComponents then 681 + # The common prefix is the same as the positive base path, so the second path is equal or longer. 682 + # We need to _shorten_ the negative filesetTree to the same base path as the positive one 683 + # E.g. for `difference /foo /foo/bar` the common prefix is /foo, equal to the positive file set's base 684 + # So we need to shorten the base of the tree for the negative argument from /foo/bar to just /foo 685 + _shortenTreeBase positive._internalBaseComponents negative 686 + else if commonBaseComponentsLength == length negative._internalBaseComponents then 687 + # The common prefix is the same as the negative base path, so the first path is longer. 688 + # We need to lengthen the negative filesetTree to the same base path as the positive one. 689 + # E.g. for `difference /foo/bar /foo` the common prefix is /foo, equal to the negative file set's base 690 + # So we need to lengthen the base of the tree for the negative argument from /foo to /foo/bar 691 + _lengthenTreeBase positive._internalBaseComponents negative 692 + else 693 + # The common prefix is neither the first nor the second path. 694 + # This means there's no overlap between the two file sets, 695 + # and nothing from the negative argument should get removed from the positive one 696 + # E.g for `difference /foo /bar`, we remove nothing to get the same as `/foo` 697 + null; 698 + 699 + resultingTree = 700 + _differenceTree 701 + positive._internalBase 702 + positive._internalTree 703 + negativeTreeWithPositiveBase; 704 + in 705 + # If the first file set is empty, we can never have any files in the result 706 + if positive._internalIsEmptyWithoutBase then 707 + _emptyWithoutBase 708 + # If the second file set is empty, nothing gets removed, so the result is just the first file set 709 + else if negative._internalIsEmptyWithoutBase then 710 + positive 711 + else 712 + # We use the positive file set base for the result, 713 + # because only files from the positive side may be included, 714 + # which is what base path is for 715 + _create positive._internalBase resultingTree; 716 + 717 + # Computes the set difference of two filesetTree's 718 + # Type: Path -> filesetTree -> filesetTree 719 + _differenceTree = path: lhs: rhs: 720 + # If the lhs doesn't have any files, or the right hand side includes all files 721 + if lhs == null || isString rhs then 722 + # The result will always be empty 723 + null 724 + # If the right hand side has no files 725 + else if rhs == null then 726 + # The result is always the left hand side, because nothing gets removed 727 + lhs 728 + else 729 + # Otherwise we always have two attribute sets to recurse into 730 + mapAttrs (name: lhsValue: 731 + _differenceTree (path + "/${name}") lhsValue (rhs.${name} or null) 732 + ) (_directoryEntries path lhs); 733 + 654 734 _fileFilter = predicate: fileset: 655 735 let 656 736 recurse = path: tree:
+98
lib/fileset/tests.sh
··· 684 684 ) 685 685 checkFileset 'intersection (unions [ ./a/b ./c/d ./c/e ]) (unions [ ./a ./c/d/f ./c/e ])' 686 686 687 + ## Difference 688 + 689 + # Subtracting something from itself results in nothing 690 + tree=( 691 + [a]=0 692 + ) 693 + checkFileset 'difference ./. ./.' 694 + 695 + # The tree of the second argument should not be evaluated if not needed 696 + checkFileset 'difference _emptyWithoutBase (_create ./. (abort "This should not be used!"))' 697 + checkFileset 'difference (_create ./. null) (_create ./. (abort "This should not be used!"))' 698 + 699 + # Subtracting nothing gives the same thing back 700 + tree=( 701 + [a]=1 702 + ) 703 + checkFileset 'difference ./. _emptyWithoutBase' 704 + checkFileset 'difference ./. (_create ./. null)' 705 + 706 + # Subtracting doesn't influence the base path 707 + mkdir a b 708 + touch {a,b}/x 709 + expectEqual 'toSource { root = ./a; fileset = difference ./a ./b; }' 'toSource { root = ./a; fileset = ./a; }' 710 + rm -rf -- * 711 + 712 + # Also not the other way around 713 + mkdir a 714 + 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: 715 + \s*- Set `root` to '"$work"' or any directory higher up. This changes the layout of the resulting store path. 716 + \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.' 717 + rm -rf -- * 718 + 719 + # Difference actually works 720 + # We test all combinations of ./., ./a, ./a/x and ./b 721 + tree=( 722 + [a/x]=0 723 + [a/y]=0 724 + [b]=0 725 + [c]=0 726 + ) 727 + checkFileset 'difference ./. ./.' 728 + checkFileset 'difference ./a ./.' 729 + checkFileset 'difference ./a/x ./.' 730 + checkFileset 'difference ./b ./.' 731 + checkFileset 'difference ./a ./a' 732 + checkFileset 'difference ./a/x ./a' 733 + checkFileset 'difference ./a/x ./a/x' 734 + checkFileset 'difference ./b ./b' 735 + tree=( 736 + [a/x]=0 737 + [a/y]=0 738 + [b]=1 739 + [c]=1 740 + ) 741 + checkFileset 'difference ./. ./a' 742 + tree=( 743 + [a/x]=1 744 + [a/y]=1 745 + [b]=0 746 + [c]=0 747 + ) 748 + checkFileset 'difference ./a ./b' 749 + tree=( 750 + [a/x]=1 751 + [a/y]=0 752 + [b]=0 753 + [c]=0 754 + ) 755 + checkFileset 'difference ./a/x ./b' 756 + tree=( 757 + [a/x]=0 758 + [a/y]=1 759 + [b]=0 760 + [c]=0 761 + ) 762 + checkFileset 'difference ./a ./a/x' 763 + tree=( 764 + [a/x]=0 765 + [a/y]=0 766 + [b]=1 767 + [c]=0 768 + ) 769 + checkFileset 'difference ./b ./a' 770 + checkFileset 'difference ./b ./a/x' 771 + tree=( 772 + [a/x]=0 773 + [a/y]=1 774 + [b]=1 775 + [c]=1 776 + ) 777 + checkFileset 'difference ./. ./a/x' 778 + tree=( 779 + [a/x]=1 780 + [a/y]=1 781 + [b]=0 782 + [c]=1 783 + ) 784 + checkFileset 'difference ./. ./b' 687 785 688 786 ## File filter 689 787