Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion lib/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
91 changes: 91 additions & 0 deletions lib/derivations.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
63 changes: 63 additions & 0 deletions lib/tests/misc.nix
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ let
bitOr
bitXor
boolToString
buildInputsClosureCond
callPackagesWith
callPackageWith
cartesianProduct
Expand Down Expand Up @@ -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";
Expand Down
Loading