at master 14 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 (lib.strings) 7 match 8 split 9 storeDir 10 ; 11 inherit (lib) 12 boolToString 13 filter 14 isString 15 readFile 16 ; 17 inherit (lib.filesystem) 18 pathIsRegularFile 19 ; 20 21 /** 22 A basic filter for `cleanSourceWith` that removes 23 directories of version control system, backup files (*~) 24 and some generated files. 25 26 # Inputs 27 28 `name` 29 30 : 1\. Function argument 31 32 `type` 33 34 : 2\. Function argument 35 */ 36 cleanSourceFilter = 37 name: type: 38 let 39 baseName = baseNameOf (toString name); 40 in 41 !( 42 # Filter out version control software files/directories 43 ( 44 baseName == ".git" 45 || 46 type == "directory" 47 && (baseName == ".svn" || baseName == "CVS" || baseName == ".hg" || baseName == ".jj") 48 ) 49 || 50 # Filter out editor backup / swap files. 51 lib.hasSuffix "~" baseName 52 || match "^\\.sw[a-z]$" baseName != null 53 || match "^\\..*\\.sw[a-z]$" baseName != null 54 || 55 56 # Filter out generates files. 57 lib.hasSuffix ".o" baseName 58 || lib.hasSuffix ".so" baseName 59 || 60 # Filter out nix-build result symlinks 61 (type == "symlink" && lib.hasPrefix "result" baseName) 62 || 63 # Filter out sockets and other types of files we can't have in the store. 64 (type == "unknown") 65 ); 66 67 /** 68 Filters a source tree removing version control files and directories using cleanSourceFilter. 69 70 # Inputs 71 72 `src` 73 74 : 1\. Function argument 75 76 # Examples 77 :::{.example} 78 ## `cleanSource` usage example 79 80 ```nix 81 cleanSource ./. 82 ``` 83 84 ::: 85 */ 86 cleanSource = 87 src: 88 cleanSourceWith { 89 filter = cleanSourceFilter; 90 inherit src; 91 }; 92 93 /** 94 Like `builtins.filterSource`, except it will compose with itself, 95 allowing you to chain multiple calls together without any 96 intermediate copies being put in the nix store. 97 98 # Examples 99 :::{.example} 100 ## `cleanSourceWith` usage example 101 102 ```nix 103 lib.cleanSourceWith { 104 filter = f; 105 src = lib.cleanSourceWith { 106 filter = g; 107 src = ./.; 108 }; 109 } 110 # Succeeds! 111 112 builtins.filterSource f (builtins.filterSource g ./.) 113 # Fails! 114 ``` 115 116 ::: 117 */ 118 cleanSourceWith = 119 { 120 # A path or cleanSourceWith result to filter and/or rename. 121 src, 122 # Optional with default value: constant true (include everything) 123 # The function will be combined with the && operator such 124 # that src.filter is called lazily. 125 # For implementing a filter, see 126 # https://nixos.org/nix/manual/#builtin-filterSource 127 # Type: A function (path -> type -> bool) 128 filter ? _path: _type: true, 129 # Optional name to use as part of the store path. 130 # This defaults to `src.name` or otherwise `"source"`. 131 name ? null, 132 }: 133 let 134 orig = toSourceAttributes src; 135 in 136 fromSourceAttributes { 137 inherit (orig) origSrc; 138 filter = path: type: filter path type && orig.filter path type; 139 name = if name != null then name else orig.name; 140 }; 141 142 /** 143 Add logging to a source, for troubleshooting the filtering behavior. 144 145 # Inputs 146 147 `src` 148 149 : Source to debug. The returned source will behave like this source, but also log its filter invocations. 150 151 # Type 152 153 ``` 154 sources.trace :: sourceLike -> Source 155 ``` 156 */ 157 trace = 158 # Source to debug. The returned source will behave like this source, but also log its filter invocations. 159 src: 160 let 161 attrs = toSourceAttributes src; 162 in 163 fromSourceAttributes ( 164 attrs 165 // { 166 filter = 167 path: type: 168 let 169 r = attrs.filter path type; 170 in 171 builtins.trace "${attrs.name}.filter ${path} = ${boolToString r}" r; 172 } 173 ) 174 // { 175 satisfiesSubpathInvariant = src ? satisfiesSubpathInvariant && src.satisfiesSubpathInvariant; 176 }; 177 178 /** 179 Filter sources by a list of regular expressions. 180 181 # Inputs 182 183 `src` 184 185 : 1\. Function argument 186 187 `regexes` 188 189 : 2\. Function argument 190 191 # Examples 192 :::{.example} 193 ## `sourceByRegex` usage example 194 195 ```nix 196 src = sourceByRegex ./my-subproject [".*\.py$" "^database.sql$"] 197 ``` 198 199 ::: 200 */ 201 sourceByRegex = 202 src: regexes: 203 let 204 isFiltered = src ? _isLibCleanSourceWith; 205 origSrc = if isFiltered then src.origSrc else src; 206 in 207 lib.cleanSourceWith { 208 filter = ( 209 path: type: 210 let 211 relPath = lib.removePrefix (toString origSrc + "/") (toString path); 212 in 213 lib.any (re: match re relPath != null) regexes 214 ); 215 inherit src; 216 }; 217 218 /** 219 Get all files ending with the specified suffices from the given 220 source directory or its descendants, omitting files that do not match 221 any suffix. The result of the example below will include files like 222 `./dir/module.c` and `./dir/subdir/doc.xml` if present. 223 224 # Inputs 225 226 `src` 227 228 : Path or source containing the files to be returned 229 230 `exts` 231 232 : A list of file suffix strings 233 234 # Type 235 236 ``` 237 sourceLike -> [String] -> Source 238 ``` 239 240 # Examples 241 :::{.example} 242 ## `sourceFilesBySuffices` usage example 243 244 ```nix 245 sourceFilesBySuffices ./. [ ".xml" ".c" ] 246 ``` 247 248 ::: 249 */ 250 sourceFilesBySuffices = 251 # Path or source containing the files to be returned 252 src: 253 # A list of file suffix strings 254 exts: 255 let 256 filter = 257 name: type: 258 let 259 base = baseNameOf (toString name); 260 in 261 type == "directory" || lib.any (ext: lib.hasSuffix ext base) exts; 262 in 263 cleanSourceWith { inherit filter src; }; 264 265 pathIsGitRepo = path: (_commitIdFromGitRepoOrError path) ? value; 266 267 /** 268 Get the commit id of a git repo. 269 270 # Inputs 271 272 `path` 273 274 : 1\. Function argument 275 276 # Examples 277 :::{.example} 278 ## `commitIdFromGitRepo` usage example 279 280 ```nix 281 commitIdFromGitRepo <nixpkgs/.git> 282 ``` 283 284 ::: 285 */ 286 commitIdFromGitRepo = 287 path: 288 let 289 commitIdOrError = _commitIdFromGitRepoOrError path; 290 in 291 commitIdOrError.value or (throw commitIdOrError.error); 292 293 # Get the commit id of a git repo. 294 295 # Returns `{ value = commitHash }` or `{ error = "... message ..." }`. 296 297 # Example: commitIdFromGitRepo <nixpkgs/.git> 298 # not exported, used for commitIdFromGitRepo 299 _commitIdFromGitRepoOrError = 300 let 301 readCommitFromFile = 302 file: path: 303 let 304 fileName = path + "/${file}"; 305 packedRefsName = path + "/packed-refs"; 306 absolutePath = 307 base: path: if lib.hasPrefix "/" path then path else toString (/. + "${base}/${path}"); 308 in 309 if 310 pathIsRegularFile path 311 # Resolve git worktrees. See gitrepository-layout(5) 312 then 313 let 314 m = match "^gitdir: (.*)$" (lib.fileContents path); 315 in 316 if m == null then 317 { error = "File contains no gitdir reference: " + path; } 318 else 319 let 320 gitDir = absolutePath (dirOf path) (lib.head m); 321 commonDir'' = 322 if pathIsRegularFile "${gitDir}/commondir" then lib.fileContents "${gitDir}/commondir" else gitDir; 323 commonDir' = lib.removeSuffix "/" commonDir''; 324 commonDir = absolutePath gitDir commonDir'; 325 refFile = lib.removePrefix "${commonDir}/" "${gitDir}/${file}"; 326 in 327 readCommitFromFile refFile commonDir 328 329 else if 330 pathIsRegularFile fileName 331 # Sometimes git stores the commitId directly in the file but 332 # sometimes it stores something like: «ref: refs/heads/branch-name» 333 then 334 let 335 fileContent = lib.fileContents fileName; 336 matchRef = match "^ref: (.*)$" fileContent; 337 in 338 if matchRef == null then { value = fileContent; } else readCommitFromFile (lib.head matchRef) path 339 340 else if 341 pathIsRegularFile packedRefsName 342 # Sometimes, the file isn't there at all and has been packed away in the 343 # packed-refs file, so we have to grep through it: 344 then 345 let 346 fileContent = readFile packedRefsName; 347 matchRef = match "([a-z0-9]+) ${file}"; 348 isRef = s: isString s && (matchRef s) != null; 349 # there is a bug in libstdc++ leading to stackoverflow for long strings: 350 # https://github.com/NixOS/nix/issues/2147#issuecomment-659868795 351 refs = filter isRef (split "\n" fileContent); 352 in 353 if refs == [ ] then 354 { error = "Could not find " + file + " in " + packedRefsName; } 355 else 356 { value = lib.head (matchRef (lib.head refs)); } 357 358 else 359 { error = "Not a .git directory: " + toString path; }; 360 in 361 readCommitFromFile "HEAD"; 362 363 pathHasContext = builtins.hasContext or (lib.hasPrefix storeDir); 364 365 canCleanSource = src: src ? _isLibCleanSourceWith || !(pathHasContext (toString src)); 366 367 # -------------------------------------------------------------------------- # 368 # Internal functions 369 # 370 371 # toSourceAttributes : sourceLike -> SourceAttrs 372 # 373 # Convert any source-like object into a simple, singular representation. 374 # We don't expose this representation in order to avoid having a fifth path- 375 # like class of objects in the wild. 376 # (Existing ones being: paths, strings, sources and x//{outPath}) 377 # So instead of exposing internals, we build a library of combinator functions. 378 toSourceAttributes = 379 src: 380 let 381 isFiltered = src ? _isLibCleanSourceWith; 382 in 383 { 384 # The original path 385 origSrc = if isFiltered then src.origSrc else src; 386 filter = if isFiltered then src.filter else _: _: true; 387 name = if isFiltered then src.name else "source"; 388 }; 389 390 # fromSourceAttributes : SourceAttrs -> Source 391 # 392 # Inverse of toSourceAttributes for Source objects. 393 fromSourceAttributes = 394 { 395 origSrc, 396 filter, 397 name, 398 }: 399 { 400 _isLibCleanSourceWith = true; 401 inherit origSrc filter name; 402 outPath = builtins.path { 403 inherit filter name; 404 path = origSrc; 405 }; 406 }; 407 408 # urlToName : (URL | Path | String) -> String 409 # 410 # Transform a URL (or path, or string) into a clean package name. 411 urlToName = 412 url: 413 let 414 inherit (lib.strings) stringLength; 415 base = baseNameOf (lib.removeSuffix "/" (lib.last (lib.splitString ":" (toString url)))); 416 # chop away one git or archive-related extension 417 removeExt = 418 name: 419 let 420 matchExt = match "(.*)\\.(git|tar|zip|gz|tgz|bz|tbz|bz2|tbz2|lzma|txz|xz|zstd)$" name; 421 in 422 if matchExt != null then lib.head matchExt else name; 423 # apply function f to string x while the result shrinks 424 shrink = 425 f: x: 426 let 427 v = f x; 428 in 429 if stringLength v < stringLength x then shrink f v else x; 430 in 431 shrink removeExt base; 432 433 # shortRev : (String | Integer) -> String 434 # 435 # Given a package revision (like "refs/tags/v12.0"), produce a short revision ("12.0"). 436 shortRev = 437 rev: 438 let 439 baseRev = baseNameOf (toString rev); 440 matchHash = match "[a-f0-9]+" baseRev; 441 matchVer = match "([A-Za-z]+[-_. ]?)*(v)?([0-9.]+.*)" baseRev; 442 in 443 if matchHash != null then 444 builtins.substring 0 7 baseRev 445 else if matchVer != null then 446 lib.last matchVer 447 else 448 baseRev; 449 450 # revOrTag : String -> String -> String 451 # 452 # Turn git `rev` and `tag` pair into a revision usable in `repoRevToName*`. 453 revOrTag = 454 rev: tag: 455 if tag != null then 456 tag 457 else if rev != null then 458 rev 459 else 460 "HEAD"; 461 462 # repoRevToNameFull : (URL | Path | String) -> (String | Integer | null) -> (String | null) -> String 463 # 464 # See `repoRevToName` below. 465 repoRevToNameFull = 466 repo_: rev_: suffix_: 467 let 468 repo = urlToName repo_; 469 rev = if rev_ != null then "-${shortRev rev_}" else ""; 470 suffix = if suffix_ != null then "-${suffix_}" else ""; 471 in 472 "${repo}${rev}${suffix}-source"; 473 474 # repoRevToName : String -> (URL | Path | String) -> (String | Integer | null) -> String -> String 475 # 476 # Produce derivation.name attribute for a given repository URL/path/name and (optionally) its revision/version tag. 477 # 478 # This is used by fetch(zip|git|FromGitHub|hg|svn|etc) to generate discoverable 479 # /nix/store paths. 480 # 481 # This uses a different implementation depending on the `pretty` argument: 482 # "source" -> name everything as "source" 483 # "versioned" -> name everything as "${repo}-${rev}-source" 484 # "full" -> name everything as "${repo}-${rev}-${fetcher}-source" 485 repoRevToName = 486 kind: 487 # match on `kind` first to minimize the thunk 488 if kind == "source" then 489 ( 490 repo: rev: suffix: 491 "source" 492 ) 493 else if kind == "versioned" then 494 ( 495 repo: rev: suffix: 496 repoRevToNameFull repo rev null 497 ) 498 else if kind == "full" then 499 repoRevToNameFull 500 else 501 throw "repoRevToName: invalid kind"; 502 503in 504{ 505 506 pathType = 507 lib.warnIf (lib.oldestSupportedReleaseIsAtLeast 2305) 508 "lib.sources.pathType has been moved to lib.filesystem.pathType." 509 lib.filesystem.pathType; 510 511 pathIsDirectory = 512 lib.warnIf (lib.oldestSupportedReleaseIsAtLeast 2305) 513 "lib.sources.pathIsDirectory has been moved to lib.filesystem.pathIsDirectory." 514 lib.filesystem.pathIsDirectory; 515 516 pathIsRegularFile = 517 lib.warnIf (lib.oldestSupportedReleaseIsAtLeast 2305) 518 "lib.sources.pathIsRegularFile has been moved to lib.filesystem.pathIsRegularFile." 519 lib.filesystem.pathIsRegularFile; 520 521 inherit 522 pathIsGitRepo 523 commitIdFromGitRepo 524 525 cleanSource 526 cleanSourceWith 527 cleanSourceFilter 528 pathHasContext 529 canCleanSource 530 531 urlToName 532 shortRev 533 revOrTag 534 repoRevToName 535 536 sourceByRegex 537 sourceFilesBySuffices 538 539 trace 540 ; 541}