at 23.05-pre 9.9 kB view raw
1# Functions for copying sources to the Nix store. 2{ lib }: 3 4# Tested in lib/tests/sources.sh 5let 6 inherit (builtins) 7 match 8 readDir 9 split 10 storeDir 11 tryEval 12 ; 13 inherit (lib) 14 boolToString 15 filter 16 getAttr 17 isString 18 pathExists 19 readFile 20 ; 21 22 /* 23 Returns the type of a path: regular (for file), symlink, or directory. 24 */ 25 pathType = path: getAttr (baseNameOf path) (readDir (dirOf path)); 26 27 /* 28 Returns true if the path exists and is a directory, false otherwise. 29 */ 30 pathIsDirectory = path: if pathExists path then (pathType path) == "directory" else false; 31 32 /* 33 Returns true if the path exists and is a regular file, false otherwise. 34 */ 35 pathIsRegularFile = path: if pathExists path then (pathType path) == "regular" else false; 36 37 /* 38 A basic filter for `cleanSourceWith` that removes 39 directories of version control system, backup files (*~) 40 and some generated files. 41 */ 42 cleanSourceFilter = name: type: let baseName = baseNameOf (toString name); in ! ( 43 # Filter out version control software files/directories 44 (baseName == ".git" || type == "directory" && (baseName == ".svn" || baseName == "CVS" || baseName == ".hg")) || 45 # Filter out editor backup / swap files. 46 lib.hasSuffix "~" baseName || 47 match "^\\.sw[a-z]$" baseName != null || 48 match "^\\..*\\.sw[a-z]$" baseName != null || 49 50 # Filter out generates files. 51 lib.hasSuffix ".o" baseName || 52 lib.hasSuffix ".so" baseName || 53 # Filter out nix-build result symlinks 54 (type == "symlink" && lib.hasPrefix "result" baseName) || 55 # Filter out sockets and other types of files we can't have in the store. 56 (type == "unknown") 57 ); 58 59 /* 60 Filters a source tree removing version control files and directories using cleanSourceFilter. 61 62 Example: 63 cleanSource ./. 64 */ 65 cleanSource = src: cleanSourceWith { filter = cleanSourceFilter; inherit src; }; 66 67 /* 68 Like `builtins.filterSource`, except it will compose with itself, 69 allowing you to chain multiple calls together without any 70 intermediate copies being put in the nix store. 71 72 Example: 73 lib.cleanSourceWith { 74 filter = f; 75 src = lib.cleanSourceWith { 76 filter = g; 77 src = ./.; 78 }; 79 } 80 # Succeeds! 81 82 builtins.filterSource f (builtins.filterSource g ./.) 83 # Fails! 84 85 */ 86 cleanSourceWith = 87 { 88 # A path or cleanSourceWith result to filter and/or rename. 89 src, 90 # Optional with default value: constant true (include everything) 91 # The function will be combined with the && operator such 92 # that src.filter is called lazily. 93 # For implementing a filter, see 94 # https://nixos.org/nix/manual/#builtin-filterSource 95 # Type: A function (path -> type -> bool) 96 filter ? _path: _type: true, 97 # Optional name to use as part of the store path. 98 # This defaults to `src.name` or otherwise `"source"`. 99 name ? null 100 }: 101 let 102 orig = toSourceAttributes src; 103 in fromSourceAttributes { 104 inherit (orig) origSrc; 105 filter = path: type: filter path type && orig.filter path type; 106 name = if name != null then name else orig.name; 107 }; 108 109 /* 110 Add logging to a source, for troubleshooting the filtering behavior. 111 Type: 112 sources.trace :: sourceLike -> Source 113 */ 114 trace = 115 # Source to debug. The returned source will behave like this source, but also log its filter invocations. 116 src: 117 let 118 attrs = toSourceAttributes src; 119 in 120 fromSourceAttributes ( 121 attrs // { 122 filter = path: type: 123 let 124 r = attrs.filter path type; 125 in 126 builtins.trace "${attrs.name}.filter ${path} = ${boolToString r}" r; 127 } 128 ) // { 129 satisfiesSubpathInvariant = src ? satisfiesSubpathInvariant && src.satisfiesSubpathInvariant; 130 }; 131 132 /* 133 Filter sources by a list of regular expressions. 134 135 Example: src = sourceByRegex ./my-subproject [".*\.py$" "^database.sql$"] 136 */ 137 sourceByRegex = src: regexes: 138 let 139 isFiltered = src ? _isLibCleanSourceWith; 140 origSrc = if isFiltered then src.origSrc else src; 141 in lib.cleanSourceWith { 142 filter = (path: type: 143 let relPath = lib.removePrefix (toString origSrc + "/") (toString path); 144 in lib.any (re: match re relPath != null) regexes); 145 inherit src; 146 }; 147 148 /* 149 Get all files ending with the specified suffices from the given 150 source directory or its descendants, omitting files that do not match 151 any suffix. The result of the example below will include files like 152 `./dir/module.c` and `./dir/subdir/doc.xml` if present. 153 154 Type: sourceLike -> [String] -> Source 155 156 Example: 157 sourceFilesBySuffices ./. [ ".xml" ".c" ] 158 */ 159 sourceFilesBySuffices = 160 # Path or source containing the files to be returned 161 src: 162 # A list of file suffix strings 163 exts: 164 let filter = name: type: 165 let base = baseNameOf (toString name); 166 in type == "directory" || lib.any (ext: lib.hasSuffix ext base) exts; 167 in cleanSourceWith { inherit filter src; }; 168 169 pathIsGitRepo = path: (_commitIdFromGitRepoOrError path)?value; 170 171 /* 172 Get the commit id of a git repo. 173 174 Example: commitIdFromGitRepo <nixpkgs/.git> 175 */ 176 commitIdFromGitRepo = path: 177 let commitIdOrError = _commitIdFromGitRepoOrError path; 178 in commitIdOrError.value or (throw commitIdOrError.error); 179 180 # Get the commit id of a git repo. 181 182 # Returns `{ value = commitHash }` or `{ error = "... message ..." }`. 183 184 # Example: commitIdFromGitRepo <nixpkgs/.git> 185 # not exported, used for commitIdFromGitRepo 186 _commitIdFromGitRepoOrError = 187 let readCommitFromFile = file: path: 188 let fileName = path + "/${file}"; 189 packedRefsName = path + "/packed-refs"; 190 absolutePath = base: path: 191 if lib.hasPrefix "/" path 192 then path 193 else toString (/. + "${base}/${path}"); 194 in if pathIsRegularFile path 195 # Resolve git worktrees. See gitrepository-layout(5) 196 then 197 let m = match "^gitdir: (.*)$" (lib.fileContents path); 198 in if m == null 199 then { error = "File contains no gitdir reference: " + path; } 200 else 201 let gitDir = absolutePath (dirOf path) (lib.head m); 202 commonDir'' = if pathIsRegularFile "${gitDir}/commondir" 203 then lib.fileContents "${gitDir}/commondir" 204 else gitDir; 205 commonDir' = lib.removeSuffix "/" commonDir''; 206 commonDir = absolutePath gitDir commonDir'; 207 refFile = lib.removePrefix "${commonDir}/" "${gitDir}/${file}"; 208 in readCommitFromFile refFile commonDir 209 210 else if pathIsRegularFile fileName 211 # Sometimes git stores the commitId directly in the file but 212 # sometimes it stores something like: «ref: refs/heads/branch-name» 213 then 214 let fileContent = lib.fileContents fileName; 215 matchRef = match "^ref: (.*)$" fileContent; 216 in if matchRef == null 217 then { value = fileContent; } 218 else readCommitFromFile (lib.head matchRef) path 219 220 else if pathIsRegularFile packedRefsName 221 # Sometimes, the file isn't there at all and has been packed away in the 222 # packed-refs file, so we have to grep through it: 223 then 224 let fileContent = readFile packedRefsName; 225 matchRef = match "([a-z0-9]+) ${file}"; 226 isRef = s: isString s && (matchRef s) != null; 227 # there is a bug in libstdc++ leading to stackoverflow for long strings: 228 # https://github.com/NixOS/nix/issues/2147#issuecomment-659868795 229 refs = filter isRef (split "\n" fileContent); 230 in if refs == [] 231 then { error = "Could not find " + file + " in " + packedRefsName; } 232 else { value = lib.head (matchRef (lib.head refs)); } 233 234 else { error = "Not a .git directory: " + toString path; }; 235 in readCommitFromFile "HEAD"; 236 237 pathHasContext = builtins.hasContext or (lib.hasPrefix storeDir); 238 239 canCleanSource = src: src ? _isLibCleanSourceWith || !(pathHasContext (toString src)); 240 241 # -------------------------------------------------------------------------- # 242 # Internal functions 243 # 244 245 # toSourceAttributes : sourceLike -> SourceAttrs 246 # 247 # Convert any source-like object into a simple, singular representation. 248 # We don't expose this representation in order to avoid having a fifth path- 249 # like class of objects in the wild. 250 # (Existing ones being: paths, strings, sources and x//{outPath}) 251 # So instead of exposing internals, we build a library of combinator functions. 252 toSourceAttributes = src: 253 let 254 isFiltered = src ? _isLibCleanSourceWith; 255 in 256 { 257 # The original path 258 origSrc = if isFiltered then src.origSrc else src; 259 filter = if isFiltered then src.filter else _: _: true; 260 name = if isFiltered then src.name else "source"; 261 }; 262 263 # fromSourceAttributes : SourceAttrs -> Source 264 # 265 # Inverse of toSourceAttributes for Source objects. 266 fromSourceAttributes = { origSrc, filter, name }: 267 { 268 _isLibCleanSourceWith = true; 269 inherit origSrc filter name; 270 outPath = builtins.path { inherit filter name; path = origSrc; }; 271 }; 272 273in { 274 inherit 275 pathType 276 pathIsDirectory 277 pathIsRegularFile 278 279 pathIsGitRepo 280 commitIdFromGitRepo 281 282 cleanSource 283 cleanSourceWith 284 cleanSourceFilter 285 pathHasContext 286 canCleanSource 287 288 sourceByRegex 289 sourceFilesBySuffices 290 291 trace 292 ; 293}