|
29 | 29 | filterByAttrs |
30 | 30 | findFirst |
31 | 31 | flatMap |
32 | | - inputHasPatches |
33 | 32 | inputIsOverride |
34 | 33 | mkInputName |
35 | 34 | stringStartsWith |
|
56 | 55 | }) config.repositories |
57 | 56 | ); |
58 | 57 |
|
| 58 | + # Apply patches to a flake source and return a realised, context-free store |
| 59 | + # path usable as a locked `path:` flake input. Realising (readDir/IFD) makes |
| 60 | + # the path exist when genflake renders the input; discarding context lets the |
| 61 | + # `nix eval --raw` genflake output accept it (it forbids store-path context). |
| 62 | + # Shared by whole-repo patches (fetchModulesRepository) and input patches |
| 63 | + # (_getModuleInputs) so the three pure-eval gotchas are handled in one place. |
| 64 | + _mkPatchedSource = |
| 65 | + { |
| 66 | + name, |
| 67 | + src, |
| 68 | + patches, |
| 69 | + }: |
| 70 | + let |
| 71 | + inherit (builtins) readDir seq unsafeDiscardStringContext; |
| 72 | + patched = pkgs.applyPatches { inherit name src patches; }; |
| 73 | + in |
| 74 | + seq (readDir patched) (unsafeDiscardStringContext (toString patched)); |
| 75 | + |
| 76 | + # Resolve config-root-relative patch strings (from config.toml) to store |
| 77 | + # paths the applyPatches builder can read in its sandbox. Stage-aware: |
| 78 | + # - genflake: impure `--file` eval with `inputs` empty → copy from the host |
| 79 | + # config root via builtins.path. |
| 80 | + # - build: pure flake eval where host paths are unreadable and |
| 81 | + # ICEDOS_CONFIG_ROOT is masked → read from the config flake input |
| 82 | + # (genflake keeps these files in `filteredConfigRoot`). |
| 83 | + # Used for whole-repo patches and consumer-declared input patches; author |
| 84 | + # input patches are Nix path literals and bypass this (already store paths). |
| 85 | + # Closes over the top-level `inputs`, so callers that shadow `inputs` (e.g. |
| 86 | + # _getModuleInputs, where `inputs` is a module's input set) still resolve |
| 87 | + # icedos-config correctly. |
| 88 | + _resolveConfigPatches = |
| 89 | + patches: |
| 90 | + map ( |
| 91 | + p: |
| 92 | + if (ICEDOS_STAGE == "genflake") then |
| 93 | + builtins.path { path = /. + "${ICEDOS_CONFIG_ROOT}/${p}"; } |
| 94 | + else |
| 95 | + inputs.icedos-config + "/${p}" |
| 96 | + ) patches; |
| 97 | + |
| 98 | + # Consumer-declared input patches from config.toml, as a flat |
| 99 | + # "<repo url>|<module>|<input>" -> [patch strings] lookup. Lets a user patch a |
| 100 | + # module's flake input without forking the module — the consumer-facing analog |
| 101 | + # of a module author's `inputs.<x>.patches`. Separator `|` never appears in a |
| 102 | + # flake url / module name / input name. |
| 103 | + _consumerInputPatches = builtins.listToAttrs ( |
| 104 | + flatten ( |
| 105 | + map ( |
| 106 | + repo: |
| 107 | + map (ip: { |
| 108 | + name = "${repo.url}|${ip.module}|${ip.input}"; |
| 109 | + value = ip.patches; |
| 110 | + }) (repo.inputPatches or [ ]) |
| 111 | + ) config.repositories |
| 112 | + ) |
| 113 | + ); |
| 114 | + |
59 | 115 | # Fetch a modules repository, resolving the URL and loading its icedos modules |
60 | 116 | # Handles overrides, flake resolution, and module file loading |
61 | 117 | fetchModulesRepository = |
62 | 118 | { |
63 | 119 | url, |
64 | 120 | overrides, |
| 121 | + patches ? [ ], |
65 | 122 | ... |
66 | 123 | }: |
67 | 124 | let |
@@ -109,19 +166,52 @@ let |
109 | 166 | # Build complete flake URL with revision |
110 | 167 | flakeUrl = "${baseUrl}${flakeRev}"; |
111 | 168 |
|
112 | | - # Load the flake (either fresh or from inputs) |
113 | | - flake = if (ICEDOS_STAGE == "genflake") then (getFlake flakeUrl) else inputs.${repoName}; |
| 169 | + # Resolve the repo flake: fresh at genflake, from the locked input at |
| 170 | + # build. For a patched repo the build-stage input already resolves to the |
| 171 | + # patched tree (see `fetchUrl` below), so `baseFlake` is the patched flake |
| 172 | + # there — fine, since the patch machinery is only forced at genflake. |
| 173 | + baseFlake = if (ICEDOS_STAGE == "genflake") then (getFlake flakeUrl) else inputs.${repoName}; |
| 174 | + |
| 175 | + # Optional whole-repo patches — the repo analog of `_getModuleInputs`' |
| 176 | + # input patching. The patched tree is emitted as the repo's own `path:` |
| 177 | + # flake input (see `fetchUrl`): nix locks that input (narHash in |
| 178 | + # flake.lock), so the build stage consumes it as a normal locked input |
| 179 | + # rather than via `getFlake`, which pure eval rejects for an unlocked |
| 180 | + # path. The diff stays on the locked rev since it is applied to the |
| 181 | + # upstream `baseFlake` resolved at genflake. |
| 182 | + hasPatches = patches != [ ]; |
| 183 | + |
| 184 | + # Realised, context-free patched tree (see `_mkPatchedSource`); patch |
| 185 | + # files are config-root-relative strings resolved via the shared helper. |
| 186 | + # Emitted below as the repo's own locked `path:` input. |
| 187 | + patchedPath = _mkPatchedSource { |
| 188 | + name = "${repoName}-patched"; |
| 189 | + src = baseFlake.outPath; |
| 190 | + patches = _resolveConfigPatches patches; |
| 191 | + }; |
| 192 | + |
| 193 | + # icedos modules come from: upstream when unpatched; the freshly patched |
| 194 | + # tree at genflake (impure getFlake ok); the locked patched input at build. |
| 195 | + moduleFlake = |
| 196 | + if !hasPatches then |
| 197 | + baseFlake |
| 198 | + else if (ICEDOS_STAGE == "genflake") then |
| 199 | + getFlake "path:${patchedPath}" |
| 200 | + else |
| 201 | + inputs.${repoName}; |
114 | 202 |
|
115 | | - # Extract icedos modules from the flake |
116 | | - modules = flake.icedosModules { icedosLib = finalIcedosLib; }; |
| 203 | + # Extract icedos modules from the (possibly patched) flake |
| 204 | + modules = moduleFlake.icedosModules { icedosLib = finalIcedosLib; }; |
117 | 205 | in |
118 | 206 | { |
119 | 207 | url = nameParsed.baseUrl; |
120 | | - fetchUrl = baseUrl; |
121 | | - inherit (flake) narHash; |
| 208 | + # Patched repos are emitted as a locked `path:` input pointing at the |
| 209 | + # realised patched tree; unpatched repos keep their upstream url. |
| 210 | + fetchUrl = if hasPatches then "path:${patchedPath}" else baseUrl; |
| 211 | + inherit (moduleFlake) narHash; |
122 | 212 | files = flatten modules; |
123 | 213 | } |
124 | | - // (optionalAttrs (hasAttr "rev" flake) { inherit (flake) rev; }); |
| 214 | + // (optionalAttrs (!hasPatches && hasAttr "rev" baseFlake) { inherit (baseFlake) rev; }); |
125 | 215 |
|
126 | 216 | # Convert external modules into flake input declarations |
127 | 217 | # Filters out modules marked to skip as inputs |
|
165 | 255 | modules: |
166 | 256 | let |
167 | 257 | inherit (builtins) attrNames filter getFlake; |
168 | | - inherit (pkgs) applyPatches; |
169 | 258 | modulesWithInputs = filter (hasAttr "inputs") modules; |
170 | 259 | in |
171 | 260 | flatten ( |
|
180 | 269 | i: |
181 | 270 | let |
182 | 271 | isOverride = inputIsOverride { input = inputs.${i}; }; |
183 | | - hasPatches = inputHasPatches { input = inputs.${i}; }; |
| 272 | + |
| 273 | + # Author patches (Nix path literals in the module, already store |
| 274 | + # paths) + consumer patches (config.toml strings declared via |
| 275 | + # `[[icedos.repositories.inputPatches]]`, resolved here). Both feed |
| 276 | + # one patched input; author patches apply first. |
| 277 | + _authorPatches = inputs.${i}.patches or [ ]; |
| 278 | + _consumerPatches = _resolveConfigPatches ( |
| 279 | + _consumerInputPatches."${_repoInfo.url}|${meta.name}|${i}" or [ ] |
| 280 | + ); |
| 281 | + patches = _authorPatches ++ _consumerPatches; |
| 282 | + hasPatches = patches != [ ]; |
184 | 283 |
|
185 | 284 | moduleIdentifier = mkInputName { |
186 | 285 | parts = [ |
@@ -218,10 +317,10 @@ let |
218 | 317 |
|
219 | 318 | _patchSrcUrl = "${_patchSrcParsed.baseUrl}${_patchSrcRev}"; |
220 | 319 |
|
221 | | - patchedInputSource = applyPatches { |
| 320 | + patchedInputSource = _mkPatchedSource { |
222 | 321 | name = "${moduleIdentifier}-${i}-patched"; |
223 | | - patches = inputs.${i}.patches; |
224 | 322 | src = getFlake _patchSrcUrl |> toString; |
| 323 | + inherit patches; |
225 | 324 | }; |
226 | 325 |
|
227 | 326 | patchedInput = rec { |
|
371 | 470 | else |
372 | 471 | existingOverrides; |
373 | 472 |
|
| 473 | + # Build a set of repo-url -> patch-list mappings from config repositories. |
| 474 | + # Mirrors `_buildOverridesMap` so a repository's `patches` apply to EVERY |
| 475 | + # fetch of that url — including transitive (self-)dependency fetches, which |
| 476 | + # otherwise re-fetch the repo unpatched and leak an unpatched input (the repo |
| 477 | + # maps to a single flake input, so its patch set must be consistent). |
| 478 | + _buildPatchesMap = |
| 479 | + { |
| 480 | + newDeps, |
| 481 | + loadOverrides, |
| 482 | + existingPatches, |
| 483 | + }: |
| 484 | + let |
| 485 | + inherit (builtins) filter listToAttrs; |
| 486 | + filteredDeps = filter (dep: (dep.patches or [ ]) != [ ]) newDeps; |
| 487 | + in |
| 488 | + if loadOverrides then |
| 489 | + listToAttrs ( |
| 490 | + map (dep: { |
| 491 | + name = dep.url; |
| 492 | + value = dep.patches; |
| 493 | + }) filteredDeps |
| 494 | + ) |
| 495 | + else |
| 496 | + existingPatches; |
| 497 | + |
374 | 498 | # Load module files from a repository and ensure a default module exists |
375 | 499 | # Returns list of modules with _repoInfo attached to each |
376 | 500 | _loadModulesFromRepo = |
|
475 | 599 | newDeps, |
476 | 600 | existingDeps ? [ ], |
477 | 601 | existingOverrides ? [ ], |
| 602 | + existingPatches ? { }, |
478 | 603 | loadOverrides ? false, |
479 | 604 | }: |
480 | 605 | let |
|
491 | 616 | inherit newDeps loadOverrides existingOverrides; |
492 | 617 | }; |
493 | 618 |
|
| 619 | + # Build patch map (repo url -> patch list) the same way, so a repo's |
| 620 | + # patches follow it across the whole dependency tree, not just its |
| 621 | + # top-level config entry. |
| 622 | + patchesMap = _buildPatchesMap { |
| 623 | + inherit newDeps loadOverrides existingPatches; |
| 624 | + }; |
| 625 | + |
494 | 626 | # Process each dependency and accumulate modules + missing-reference diagnostics |
495 | 627 | result = |
496 | 628 | foldl' |
|
501 | 633 | missingModules = filter (mod: !_isModuleLoaded existingDeps newDep.url mod) (newDep.modules or [ ]); |
502 | 634 |
|
503 | 635 | # Fetch repository if new modules are needed or default isn't loaded |
504 | | - newRepo = optional ( |
505 | | - ((length missingModules) > 0) || !_isModuleLoaded existingDeps newDep.url "default" |
506 | | - ) (fetchModulesRepository (newDep // { inherit overrides; })); |
| 636 | + newRepo = |
| 637 | + optional (((length missingModules) > 0) || !_isModuleLoaded existingDeps newDep.url "default") |
| 638 | + ( |
| 639 | + fetchModulesRepository ( |
| 640 | + newDep |
| 641 | + // { |
| 642 | + inherit overrides; |
| 643 | + patches = patchesMap.${newDep.url} or [ ]; |
| 644 | + } |
| 645 | + ) |
| 646 | + ); |
507 | 647 |
|
508 | 648 | # All modules present in the fetched repository (includes synthesized "default") |
509 | 649 | repoModules = flatMap _loadModulesFromRepo newRepo; |
|
558 | 698 | newDeps = innerDeps; |
559 | 699 | existingDeps = allKnownKeys; |
560 | 700 | existingOverrides = overrides; |
| 701 | + existingPatches = patchesMap; |
561 | 702 | } |
562 | 703 | else |
563 | 704 | { |
|
0 commit comments