|
29 | 29 | filterByAttrs |
30 | 30 | findFirst |
31 | 31 | flatMap |
32 | | - inputHasPatches |
33 | 32 | inputIsOverride |
34 | 33 | mkInputName |
35 | 34 | stringStartsWith |
|
46 | 45 | }) config.repositories |
47 | 46 | ); |
48 | 47 |
|
| 48 | + # Apply patches to a flake source and return a realised, context-free store |
| 49 | + # path usable as a locked `path:` flake input. Realising (readDir/IFD) makes |
| 50 | + # the path exist when genflake renders the input; discarding context lets the |
| 51 | + # `nix eval --raw` genflake output accept it (it forbids store-path context). |
| 52 | + # Shared by whole-repo patches (fetchModulesRepository) and input patches |
| 53 | + # (_getModuleInputs) so the three pure-eval gotchas are handled in one place. |
| 54 | + _mkPatchedSource = |
| 55 | + { |
| 56 | + name, |
| 57 | + src, |
| 58 | + patches, |
| 59 | + }: |
| 60 | + let |
| 61 | + inherit (builtins) readDir seq unsafeDiscardStringContext; |
| 62 | + patched = pkgs.applyPatches { inherit name src patches; }; |
| 63 | + in |
| 64 | + seq (readDir patched) (unsafeDiscardStringContext (toString patched)); |
| 65 | + |
| 66 | + # Resolve config-root-relative patch strings (from config.toml) to store |
| 67 | + # paths the applyPatches builder can read in its sandbox. Stage-aware: |
| 68 | + # - genflake: impure `--file` eval with `inputs` empty → copy from the host |
| 69 | + # config root via builtins.path. |
| 70 | + # - build: pure flake eval where host paths are unreadable and |
| 71 | + # ICEDOS_CONFIG_ROOT is masked → read from the config flake input |
| 72 | + # (genflake keeps these files in `filteredConfigRoot`). |
| 73 | + # Used for whole-repo patches and consumer-declared input patches; author |
| 74 | + # input patches are Nix path literals and bypass this (already store paths). |
| 75 | + # Closes over the top-level `inputs`, so callers that shadow `inputs` (e.g. |
| 76 | + # _getModuleInputs, where `inputs` is a module's input set) still resolve |
| 77 | + # icedos-config correctly. |
| 78 | + _resolveConfigPatches = |
| 79 | + patches: |
| 80 | + map ( |
| 81 | + p: |
| 82 | + if (ICEDOS_STAGE == "genflake") then |
| 83 | + builtins.path { path = /. + "${ICEDOS_CONFIG_ROOT}/${p}"; } |
| 84 | + else |
| 85 | + inputs.icedos-config + "/${p}" |
| 86 | + ) patches; |
| 87 | + |
| 88 | + # Consumer-declared input patches from config.toml, as a flat |
| 89 | + # "<repo url>|<module>|<input>" -> [patch strings] lookup. Lets a user patch a |
| 90 | + # module's flake input without forking the module — the consumer-facing analog |
| 91 | + # of a module author's `inputs.<x>.patches`. Separator `|` never appears in a |
| 92 | + # flake url / module name / input name. |
| 93 | + _consumerInputPatches = builtins.listToAttrs ( |
| 94 | + flatten ( |
| 95 | + map ( |
| 96 | + repo: |
| 97 | + map (ip: { |
| 98 | + name = "${repo.url}|${ip.module}|${ip.input}"; |
| 99 | + value = ip.patches; |
| 100 | + }) (repo.inputPatches or [ ]) |
| 101 | + ) config.repositories |
| 102 | + ) |
| 103 | + ); |
| 104 | + |
49 | 105 | # Fetch a modules repository, resolving the URL and loading its icedos modules |
50 | 106 | # Handles overrides, flake resolution, and module file loading |
51 | 107 | fetchModulesRepository = |
52 | 108 | { |
53 | 109 | url, |
54 | 110 | overrides, |
| 111 | + patches ? [ ], |
55 | 112 | ... |
56 | 113 | }: |
57 | 114 | let |
|
99 | 156 | # Build complete flake URL with revision |
100 | 157 | flakeUrl = "${baseUrl}${flakeRev}"; |
101 | 158 |
|
102 | | - # Load the flake (either fresh or from inputs) |
103 | | - flake = if (ICEDOS_STAGE == "genflake") then (getFlake flakeUrl) else inputs.${repoName}; |
| 159 | + # Resolve the repo flake: fresh at genflake, from the locked input at |
| 160 | + # build. For a patched repo the build-stage input already resolves to the |
| 161 | + # patched tree (see `fetchUrl` below), so `baseFlake` is the patched flake |
| 162 | + # there — fine, since the patch machinery is only forced at genflake. |
| 163 | + baseFlake = if (ICEDOS_STAGE == "genflake") then (getFlake flakeUrl) else inputs.${repoName}; |
| 164 | + |
| 165 | + # Optional whole-repo patches — the repo analog of `_getModuleInputs`' |
| 166 | + # input patching. The patched tree is emitted as the repo's own `path:` |
| 167 | + # flake input (see `fetchUrl`): nix locks that input (narHash in |
| 168 | + # flake.lock), so the build stage consumes it as a normal locked input |
| 169 | + # rather than via `getFlake`, which pure eval rejects for an unlocked |
| 170 | + # path. The diff stays on the locked rev since it is applied to the |
| 171 | + # upstream `baseFlake` resolved at genflake. |
| 172 | + hasPatches = patches != [ ]; |
| 173 | + |
| 174 | + # Realised, context-free patched tree (see `_mkPatchedSource`); patch |
| 175 | + # files are config-root-relative strings resolved via the shared helper. |
| 176 | + # Emitted below as the repo's own locked `path:` input. |
| 177 | + patchedPath = _mkPatchedSource { |
| 178 | + name = "${repoName}-patched"; |
| 179 | + src = baseFlake.outPath; |
| 180 | + patches = _resolveConfigPatches patches; |
| 181 | + }; |
| 182 | + |
| 183 | + # icedos modules come from: upstream when unpatched; the freshly patched |
| 184 | + # tree at genflake (impure getFlake ok); the locked patched input at build. |
| 185 | + moduleFlake = |
| 186 | + if !hasPatches then |
| 187 | + baseFlake |
| 188 | + else if (ICEDOS_STAGE == "genflake") then |
| 189 | + getFlake "path:${patchedPath}" |
| 190 | + else |
| 191 | + inputs.${repoName}; |
104 | 192 |
|
105 | | - # Extract icedos modules from the flake |
106 | | - modules = flake.icedosModules { icedosLib = finalIcedosLib; }; |
| 193 | + # Extract icedos modules from the (possibly patched) flake |
| 194 | + modules = moduleFlake.icedosModules { icedosLib = finalIcedosLib; }; |
107 | 195 | in |
108 | 196 | { |
109 | 197 | url = nameParsed.baseUrl; |
110 | | - fetchUrl = baseUrl; |
111 | | - inherit (flake) narHash; |
| 198 | + # Patched repos are emitted as a locked `path:` input pointing at the |
| 199 | + # realised patched tree; unpatched repos keep their upstream url. |
| 200 | + fetchUrl = if hasPatches then "path:${patchedPath}" else baseUrl; |
| 201 | + inherit (moduleFlake) narHash; |
112 | 202 | files = flatten modules; |
113 | 203 | } |
114 | | - // (optionalAttrs (hasAttr "rev" flake) { inherit (flake) rev; }); |
| 204 | + // (optionalAttrs (!hasPatches && hasAttr "rev" baseFlake) { inherit (baseFlake) rev; }); |
115 | 205 |
|
116 | 206 | # Convert external modules into flake input declarations |
117 | 207 | # Filters out modules marked to skip as inputs |
|
155 | 245 | modules: |
156 | 246 | let |
157 | 247 | inherit (builtins) attrNames filter getFlake; |
158 | | - inherit (pkgs) applyPatches; |
159 | 248 | modulesWithInputs = filter (hasAttr "inputs") modules; |
160 | 249 | in |
161 | 250 | flatten ( |
|
170 | 259 | i: |
171 | 260 | let |
172 | 261 | isOverride = inputIsOverride { input = inputs.${i}; }; |
173 | | - hasPatches = inputHasPatches { input = inputs.${i}; }; |
| 262 | + |
| 263 | + # Author patches (Nix path literals in the module, already store |
| 264 | + # paths) + consumer patches (config.toml strings declared via |
| 265 | + # `[[icedos.repositories.inputPatches]]`, resolved here). Both feed |
| 266 | + # one patched input; author patches apply first. |
| 267 | + _authorPatches = inputs.${i}.patches or [ ]; |
| 268 | + _consumerPatches = _resolveConfigPatches ( |
| 269 | + _consumerInputPatches."${_repoInfo.url}|${meta.name}|${i}" or [ ] |
| 270 | + ); |
| 271 | + patches = _authorPatches ++ _consumerPatches; |
| 272 | + hasPatches = patches != [ ]; |
174 | 273 |
|
175 | 274 | moduleIdentifier = mkInputName { |
176 | 275 | parts = [ |
@@ -208,10 +307,10 @@ let |
208 | 307 |
|
209 | 308 | _patchSrcUrl = "${_patchSrcParsed.baseUrl}${_patchSrcRev}"; |
210 | 309 |
|
211 | | - patchedInputSource = applyPatches { |
| 310 | + patchedInputSource = _mkPatchedSource { |
212 | 311 | name = "${moduleIdentifier}-${i}-patched"; |
213 | | - patches = inputs.${i}.patches; |
214 | 312 | src = getFlake _patchSrcUrl |> toString; |
| 313 | + inherit patches; |
215 | 314 | }; |
216 | 315 |
|
217 | 316 | patchedInput = rec { |
|
361 | 460 | else |
362 | 461 | existingOverrides; |
363 | 462 |
|
| 463 | + # Build a set of repo-url -> patch-list mappings from config repositories. |
| 464 | + # Mirrors `_buildOverridesMap` so a repository's `patches` apply to EVERY |
| 465 | + # fetch of that url — including transitive (self-)dependency fetches, which |
| 466 | + # otherwise re-fetch the repo unpatched and leak an unpatched input (the repo |
| 467 | + # maps to a single flake input, so its patch set must be consistent). |
| 468 | + _buildPatchesMap = |
| 469 | + { |
| 470 | + newDeps, |
| 471 | + loadOverrides, |
| 472 | + existingPatches, |
| 473 | + }: |
| 474 | + let |
| 475 | + inherit (builtins) filter listToAttrs; |
| 476 | + filteredDeps = filter (dep: (dep.patches or [ ]) != [ ]) newDeps; |
| 477 | + in |
| 478 | + if loadOverrides then |
| 479 | + listToAttrs ( |
| 480 | + map (dep: { |
| 481 | + name = dep.url; |
| 482 | + value = dep.patches; |
| 483 | + }) filteredDeps |
| 484 | + ) |
| 485 | + else |
| 486 | + existingPatches; |
| 487 | + |
364 | 488 | # Load module files from a repository and ensure a default module exists |
365 | 489 | # Returns list of modules with _repoInfo attached to each |
366 | 490 | _loadModulesFromRepo = |
|
461 | 585 | newDeps, |
462 | 586 | existingDeps ? [ ], |
463 | 587 | existingOverrides ? [ ], |
| 588 | + existingPatches ? { }, |
464 | 589 | loadOverrides ? false, |
465 | 590 | }: |
466 | 591 | let |
|
477 | 602 | inherit newDeps loadOverrides existingOverrides; |
478 | 603 | }; |
479 | 604 |
|
| 605 | + # Build patch map (repo url -> patch list) the same way, so a repo's |
| 606 | + # patches follow it across the whole dependency tree, not just its |
| 607 | + # top-level config entry. |
| 608 | + patchesMap = _buildPatchesMap { |
| 609 | + inherit newDeps loadOverrides existingPatches; |
| 610 | + }; |
| 611 | + |
480 | 612 | # Process each dependency and accumulate modules + missing-reference diagnostics |
481 | 613 | result = |
482 | 614 | foldl' |
|
487 | 619 | missingModules = filter (mod: !_isModuleLoaded existingDeps newDep.url mod) (newDep.modules or [ ]); |
488 | 620 |
|
489 | 621 | # Fetch repository if new modules are needed or default isn't loaded |
490 | | - newRepo = optional ( |
491 | | - ((length missingModules) > 0) || !_isModuleLoaded existingDeps newDep.url "default" |
492 | | - ) (fetchModulesRepository (newDep // { inherit overrides; })); |
| 622 | + newRepo = |
| 623 | + optional (((length missingModules) > 0) || !_isModuleLoaded existingDeps newDep.url "default") |
| 624 | + ( |
| 625 | + fetchModulesRepository ( |
| 626 | + newDep |
| 627 | + // { |
| 628 | + inherit overrides; |
| 629 | + patches = patchesMap.${newDep.url} or [ ]; |
| 630 | + } |
| 631 | + ) |
| 632 | + ); |
493 | 633 |
|
494 | 634 | # All modules present in the fetched repository (includes synthesized "default") |
495 | 635 | repoModules = flatMap _loadModulesFromRepo newRepo; |
|
542 | 682 | newDeps = innerDeps; |
543 | 683 | existingDeps = allKnownKeys; |
544 | 684 | existingOverrides = overrides; |
| 685 | + existingPatches = patchesMap; |
545 | 686 | } |
546 | 687 | else |
547 | 688 | { |
|
0 commit comments