1# Functions for copying sources to the Nix store.
2{ lib }:
3
4let
5 inherit (builtins)
6 hasContext
7 match
8 readDir
9 split
10 storeDir
11 tryEval
12 ;
13 inherit (lib)
14 filter
15 getAttr
16 isString
17 pathExists
18 readFile
19 ;
20in
21rec {
22
23 # Returns the type of a path: regular (for file), symlink, or directory
24 pathType = p: getAttr (baseNameOf p) (readDir (dirOf p));
25
26 # Returns true if the path exists and is a directory, false otherwise
27 pathIsDirectory = p: if pathExists p then (pathType p) == "directory" else false;
28
29 # Returns true if the path exists and is a regular file, false otherwise
30 pathIsRegularFile = p: if pathExists p then (pathType p) == "regular" else false;
31
32 # Bring in a path as a source, filtering out all Subversion and CVS
33 # directories, as well as backup files (*~).
34 cleanSourceFilter = name: type: let baseName = baseNameOf (toString name); in ! (
35 # Filter out version control software files/directories
36 (baseName == ".git" || type == "directory" && (baseName == ".svn" || baseName == "CVS" || baseName == ".hg")) ||
37 # Filter out editor backup / swap files.
38 lib.hasSuffix "~" baseName ||
39 match "^\\.sw[a-z]$" baseName != null ||
40 match "^\\..*\\.sw[a-z]$" baseName != null ||
41
42 # Filter out generates files.
43 lib.hasSuffix ".o" baseName ||
44 lib.hasSuffix ".so" baseName ||
45 # Filter out nix-build result symlinks
46 (type == "symlink" && lib.hasPrefix "result" baseName)
47 );
48
49 # Filters a source tree removing version control files and directories using cleanSourceWith
50 #
51 # Example:
52 # cleanSource ./.
53 cleanSource = src: cleanSourceWith { filter = cleanSourceFilter; inherit src; };
54
55 # Like `builtins.filterSource`, except it will compose with itself,
56 # allowing you to chain multiple calls together without any
57 # intermediate copies being put in the nix store.
58 #
59 # lib.cleanSourceWith {
60 # filter = f;
61 # src = lib.cleanSourceWith {
62 # filter = g;
63 # src = ./.;
64 # };
65 # }
66 # # Succeeds!
67 #
68 # builtins.filterSource f (builtins.filterSource g ./.)
69 # # Fails!
70 #
71 # Parameters:
72 #
73 # src: A path or cleanSourceWith result to filter and/or rename.
74 #
75 # filter: A function (path -> type -> bool)
76 # Optional with default value: constant true (include everything)
77 # The function will be combined with the && operator such
78 # that src.filter is called lazily.
79 # For implementing a filter, see
80 # https://nixos.org/nix/manual/#builtin-filterSource
81 #
82 # name: Optional name to use as part of the store path.
83 # This defaults to `src.name` or otherwise `"source"`.
84 #
85 cleanSourceWith = { filter ? _path: _type: true, src, name ? null }:
86 let
87 isFiltered = src ? _isLibCleanSourceWith;
88 origSrc = if isFiltered then src.origSrc else src;
89 filter' = if isFiltered then name: type: filter name type && src.filter name type else filter;
90 name' = if name != null then name else if isFiltered then src.name else "source";
91 in {
92 inherit origSrc;
93 filter = filter';
94 outPath = builtins.path { filter = filter'; path = origSrc; name = name'; };
95 _isLibCleanSourceWith = true;
96 name = name';
97 };
98
99 # Filter sources by a list of regular expressions.
100 #
101 # E.g. `src = sourceByRegex ./my-subproject [".*\.py$" "^database.sql$"]`
102 sourceByRegex = src: regexes:
103 let
104 isFiltered = src ? _isLibCleanSourceWith;
105 origSrc = if isFiltered then src.origSrc else src;
106 in lib.cleanSourceWith {
107 filter = (path: type:
108 let relPath = lib.removePrefix (toString origSrc + "/") (toString path);
109 in lib.any (re: match re relPath != null) regexes);
110 inherit src;
111 };
112
113 # Get all files ending with the specified suffices from the given
114 # directory or its descendants. E.g. `sourceFilesBySuffices ./dir
115 # [".xml" ".c"]'.
116 sourceFilesBySuffices = path: exts:
117 let filter = name: type:
118 let base = baseNameOf (toString name);
119 in type == "directory" || lib.any (ext: lib.hasSuffix ext base) exts;
120 in cleanSourceWith { inherit filter; src = path; };
121
122 pathIsGitRepo = path: (tryEval (commitIdFromGitRepo path)).success;
123
124 # Get the commit id of a git repo
125 # Example: commitIdFromGitRepo <nixpkgs/.git>
126 commitIdFromGitRepo =
127 let readCommitFromFile = file: path:
128 let fileName = toString path + "/" + file;
129 packedRefsName = toString path + "/packed-refs";
130 absolutePath = base: path:
131 if lib.hasPrefix "/" path
132 then path
133 else toString (/. + "${base}/${path}");
134 in if pathIsRegularFile path
135 # Resolve git worktrees. See gitrepository-layout(5)
136 then
137 let m = match "^gitdir: (.*)$" (lib.fileContents path);
138 in if m == null
139 then throw ("File contains no gitdir reference: " + path)
140 else
141 let gitDir = absolutePath (dirOf path) (lib.head m);
142 commonDir'' = if pathIsRegularFile "${gitDir}/commondir"
143 then lib.fileContents "${gitDir}/commondir"
144 else gitDir;
145 commonDir' = lib.removeSuffix "/" commonDir'';
146 commonDir = absolutePath gitDir commonDir';
147 refFile = lib.removePrefix "${commonDir}/" "${gitDir}/${file}";
148 in readCommitFromFile refFile commonDir
149
150 else if pathIsRegularFile fileName
151 # Sometimes git stores the commitId directly in the file but
152 # sometimes it stores something like: «ref: refs/heads/branch-name»
153 then
154 let fileContent = lib.fileContents fileName;
155 matchRef = match "^ref: (.*)$" fileContent;
156 in if matchRef == null
157 then fileContent
158 else readCommitFromFile (lib.head matchRef) path
159
160 else if pathIsRegularFile packedRefsName
161 # Sometimes, the file isn't there at all and has been packed away in the
162 # packed-refs file, so we have to grep through it:
163 then
164 let fileContent = readFile packedRefsName;
165 matchRef = match "([a-z0-9]+) ${file}";
166 isRef = s: isString s && (matchRef s) != null;
167 # there is a bug in libstdc++ leading to stackoverflow for long strings:
168 # https://github.com/NixOS/nix/issues/2147#issuecomment-659868795
169 refs = filter isRef (split "\n" fileContent);
170 in if refs == []
171 then throw ("Could not find " + file + " in " + packedRefsName)
172 else lib.head (matchRef (lib.head refs))
173
174 else throw ("Not a .git directory: " + path);
175 in readCommitFromFile "HEAD";
176
177 pathHasContext = builtins.hasContext or (lib.hasPrefix storeDir);
178
179 canCleanSource = src: src ? _isLibCleanSourceWith || !(pathHasContext (toString src));
180}