diff --git a/lib/default.nix b/lib/default.nix index e10332ca58dd4..2d474a8da973b 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -401,7 +401,12 @@ let renameCrossIndexTo mapCrossIndex ; - inherit (self.derivations) lazyDerivation optionalDrvAttr warnOnInstantiate; + inherit (self.derivations) + buildInputsClosureCond + lazyDerivation + optionalDrvAttr + warnOnInstantiate + ; inherit (self.generators) mkLuaInline; inherit (self.meta) addMetaAttrs diff --git a/lib/derivations.nix b/lib/derivations.nix index 574b01695d867..3268a43447e93 100644 --- a/lib/derivations.nix +++ b/lib/derivations.nix @@ -213,6 +213,97 @@ in */ optionalDrvAttr = cond: value: if cond then value else null; + /** + Compute a filtered closure of build inputs. + + Specifically, `buildInputsClosureCond cond startSet` computes the closure formed + by recursive application of `p: filter cond p.buildInputs ++ filter cond p.propagatedBuildInputs` + to `startSet`. + + The function recursively traverses the dependency tree starting from `startSet`, + applying the condition function to filter both `buildInputs` and `propagatedBuildInputs` + at each level. + + # Why buildInputs and propagatedBuildInputs + + These two attributes represent (typically) runtime dependencies - packages that become part of + or are needed by the final output. This gives the closure semantic coherence: all + items share a similar role in the build and runtime environment. + + Other dependency attributes like `nativeBuildInputs` are excluded because they serve + a different purpose (build-time tools that don't become part of the output). Mixing + these would produce a closure with inconsistent semantics. + + The inclusion of `propagatedBuildInputs` is essential because these dependencies are + transitively required - if A propagates B, then anything depending on A also needs B. + Omitting propagated inputs would produce an incomplete closure. + + # Why startSet is not filtered + + The initial `startSet` is NOT filtered by the condition - all items in `startSet` + are included in the result regardless of whether they satisfy the condition. This + allows explicitly requesting specific root packages while filtering their dependencies. + + # Inputs + + `cond` + + : A predicate function that takes a derivation and returns a boolean, + used to filter which dependencies to include in the closure. + + `startSet` + + : A list of derivations to start the closure computation from. + These will all be included in the result without filtering. + + # Type + + ``` + buildInputsClosureCond :: (Derivation -> Bool) -> [Derivation] -> [Derivation] + ``` + + # Examples + :::{.example} + ## `lib.derivations.buildInputsClosureCond` usage example + + ```nix + # Get all internal dependencies (those in a specific set) + let + internalDrvs = { "${pkg1.drvPath}" = null; "${pkg2.drvPath}" = null; }; + isInternal = dep: internalDrvs ? ${dep.drvPath or ""}; + in + buildInputsClosureCond isInternal [ myPackage ] + # => [ myPackage pkg1 pkg2 ] (only internal dependencies) + ``` + + ::: + */ + buildInputsClosureCond = + cond: startSet: + let + closure = builtins.genericClosure { + startSet = map (d: { + key = d.drvPath; + value = d; + }) startSet; + operator = + d: + let + r = + map + (d': { + key = d'.drvPath; + value = d'; + }) + ( + lib.filter cond d.value.buildInputs or [ ] ++ lib.filter cond d.value.propagatedBuildInputs or [ ] + ); + in + r; + }; + in + map (item: item.value) closure; + /** Wrap a derivation such that instantiating it produces a warning. diff --git a/lib/tests/misc.nix b/lib/tests/misc.nix index e24ffa29d4f7d..75a9d275200c0 100644 --- a/lib/tests/misc.nix +++ b/lib/tests/misc.nix @@ -31,6 +31,7 @@ let bitOr bitXor boolToString + buildInputsClosureCond callPackagesWith callPackageWith cartesianProduct @@ -4369,6 +4370,68 @@ runTests { expected = derivation; }; + testBuildInputsClosureCond = + let + # Create mock derivations with drvPath and buildInputs + mkDrv = name: buildInputs: propagatedBuildInputs: { + inherit name buildInputs propagatedBuildInputs; + drvPath = "/nix/store/test-${name}.drv"; + }; + + # Internal packages (marked by having "internal" in name) + internal1 = mkDrv "internal1" [ ] [ ]; + internal2 = mkDrv "internal2" [ internal1 ] [ ]; + internal3 = mkDrv "internal3" [ ] [ internal1 ]; + + # External packages + external1 = mkDrv "external1" [ ] [ ]; + external2 = mkDrv "external2" [ external1 ] [ ]; + + # Mixed dependencies + mixed = mkDrv "mixed" [ internal2 external2 ] [ internal3 ]; + + # Predicate to filter internal packages + isInternal = pkg: lib.hasInfix "internal" pkg.name; + + # Compute closure of mixed package, filtering for internal dependencies only + result = buildInputsClosureCond isInternal [ mixed ]; + + # Extract names for easier comparison + resultNames = map (d: d.name) result; + in + { + expr = lib.sort builtins.lessThan resultNames; + # Should include: mixed (from startSet, not filtered), internal2, internal3, internal1 + # Should NOT include: external1, external2 + expected = [ + "internal1" + "internal2" + "internal3" + "mixed" + ]; + }; + + testBuildInputsClosureCondStartSetNotFiltered = + let + mkDrv = name: { + inherit name; + drvPath = "/nix/store/test-${name}.drv"; + buildInputs = [ ]; + propagatedBuildInputs = [ ]; + }; + + externalPkg = mkDrv "external"; + isInternal = pkg: lib.hasInfix "internal" pkg.name; + + # Even though externalPkg doesn't match the condition, + # it should still be in the result because it's in startSet + result = buildInputsClosureCond isInternal [ externalPkg ]; + in + { + expr = map (d: d.name) result; + expected = [ "external" ]; + }; + testTypeDescriptionInt = { expr = (with types; int).description; expected = "signed integer";