Add postprocessing, add flakeModules.touchup

This commit is contained in:
Robert Hensing 2025-05-27 14:32:12 +02:00
parent 157c95de4d
commit 9d0762c18a
7 changed files with 334 additions and 2 deletions

View file

@ -30,6 +30,14 @@ let
}; };
}; };
# Replace derivations with a simple attrset so nix-unit can compare without
# hitting stack overflow on self-referential derivation attrsets.
# See NOTE: Derivation comparison
canon = v:
if v.type or null == "derivation" then { type = "derivation"; inherit (v) drvPath outPath name system; }
else if builtins.isAttrs v then builtins.mapAttrs (_: canon) v
else v;
empty = mkFlake empty = mkFlake
{ inputs.self = { }; } { inputs.self = { }; }
{ {
@ -465,6 +473,100 @@ in
}; };
}; };
touchup = {
"test: any filter keeps only matching attrs" = {
expr =
let
result = mkFlake { inputs.self = { }; } {
imports = [ flake-parts.flakeModules.touchup ];
systems = [ "x86_64-linux" "aarch64-darwin" ];
touchup.any = { attrName, ... }: { enable = attrName == "overlays"; };
perSystem = { ... }: {
packages.default = throw "packages.default should not be evaluated";
packages.hello = throw "packages.hello should not be evaluated";
};
};
in
result;
expected = {
overlays = { };
};
};
"test: any with mkDefault, attr override" = {
expr =
let
result = mkFlake { inputs.self = { }; } {
imports = [ flake-parts.flakeModules.touchup ];
systems = [ "x86_64-linux" "aarch64-darwin" ];
touchup.any = { ... }: { enable = lib.mkDefault false; };
touchup.attr.overlays = { enable = true; };
perSystem = { ... }: {
packages.default = throw "packages.default should not be evaluated";
packages.hello = throw "packages.hello should not be evaluated";
};
};
in
result;
expected = {
overlays = { };
};
};
"test: nested attr filtering per system" = {
expr =
let
result = mkFlake { inputs.self = { }; } {
imports = [ flake-parts.flakeModules.touchup ];
systems = [ "x86_64-linux" "aarch64-darwin" ];
touchup.attr.packages.attr.aarch64-darwin.attr.bar.enable = false;
perSystem = { system, ... }: {
packages.foo = pkg system "foo";
# This assertion proves the filtered value is never evaluated for darwin
packages.bar = assert system == "x86_64-linux"; pkg system "bar";
};
};
in
canon result.packages;
expected = canon {
aarch64-darwin = { foo = pkg "aarch64-darwin" "foo"; };
x86_64-linux = { foo = pkg "x86_64-linux" "foo"; bar = pkg "x86_64-linux" "bar"; };
};
};
"test: finish and attr composition" = {
expr =
mkFlake { inputs.self = { }; } {
imports = [ flake-parts.flakeModules.touchup ];
systems = [ "x86_64-linux" "aarch64-darwin" ];
touchup.any = { ... }: { enable = lib.mkDefault false; };
touchup.attr.overlays = { enable = true; finish = x: "hoi"; };
touchup.finish = x: x // { foo = "bar"; };
};
expected = {
overlays = "hoi";
foo = "bar";
};
};
# TODO: assert that the error context ("while touching up attribute 'broken'")
# appears in the trace. nix-unit's expectedError.msg only matches the thrown
# message, not addErrorContext frames.
"test: error context when enabled attr throws" = {
expr =
let
result = mkFlake { inputs.self = { }; } {
imports = [ flake-parts.flakeModules.touchup ];
systems = [ "x86_64-linux" ];
flake.broken = throw "the value is broken";
};
in
result.broken;
expectedError.type = "ThrownError";
expectedError.msg = "the value is broken";
};
};
formatter = { formatter = {
"test: conditional null throws helpful error" = { "test: conditional null throws helpful error" = {
expr = expr =

57
extras/touchup.nix Normal file
View file

@ -0,0 +1,57 @@
{ config, lib, ... }:
let
inherit (lib)
mkOption
types
;
in
{
options = {
touchup = mkOption {
description = ''
Controls which attributes appear in [`processedFlake`](flake-parts.md#opt-processedFlake) and how they are transformed.
The touchup configuration forms a tree that mirrors the flake output structure.
At each level, [`attr`](#opt-touchup.attr) targets specific attributes by name,
and [`any`](#opt-touchup.any) applies to all attributes at that level.
**Examples**:
Only output explicitly listed flake output attributes:
```nix
touchup = {
any = {
enable = lib.mkDefault false;
};
attr.packages.enable = true;
attr.checks.enable = true;
}
```
Hide a package from users, but not from your own modules:
```nix
touchup = {
attr.packages.any.attr.hello.enable = false;
};
```
Hide a package on a set of systems:
```nix
touchup = {
attr.packages.any = { attrName, ... }: { attr.hello.enable = ! lib.strings.hasSuffix "-darwin" attrName; }
};
```
'';
type = types.submoduleWith {
modules = [ ./touchup/attrs.nix ];
};
};
};
config = {
processedFlake = config.touchup.touchupApply config.flake;
};
}

20
extras/touchup/attr.nix Normal file
View file

@ -0,0 +1,20 @@
{ lib, ... }:
let
inherit (lib)
mkOption
types
;
in
{
imports = [ ./attrs.nix ];
options = {
enable = mkOption {
type = types.bool;
default = true;
description = "Whether to include the attribute in the output.";
};
};
config = {
_module.args.docsVisible = "shallow";
};
}

131
extras/touchup/attrs.nix Normal file
View file

@ -0,0 +1,131 @@
{ config, lib, options, docsVisible, ... }:
let
inherit (lib)
addErrorContext
concatMapAttrs
evalModules
mkOption
types
;
unNull = v:
if v == null then
{ }
else
v;
touchupAttrs =
v:
concatMapAttrs
(name: value:
let
eval = evalModules {
prefix = lib.lists.init options.attr.loc; # arbitrary pick
specialArgs = {
attrName = name;
};
modules = [
(config.attr.${name} or ./attr.nix)
(unNull config.any)
];
};
in
if addErrorContext "while figuring out whether to enable '${name}'" eval.config.enable then
# Apply the touchup configuration to the value.
{
"${name}" =
addErrorContext "while touching up attribute '${name}'" (
(addErrorContext "while evaluating the touchup configuration for '${name}'" eval.config.touchupApply)
(addErrorContext "while evaluating the original value of '${name}'" value)
);
}
else
{ }
)
v;
in
{
options = {
attr = mkOption {
type = types.lazyAttrsOf (types.deferredModuleWith {
staticModules = [ ./attr.nix ];
});
default = { };
visible = docsVisible;
description =
if docsVisible == "shallow" then ''
Touchup configuration for the next level of nesting.
Same structure as [`attr`](#opt-touchup.attr); see its description.
''
else ''
Per-attribute touchup configuration. Each value is a module that controls
whether and how the corresponding attribute appears in the output.
Each module contains the full touchup option set (`enable`, `attr`, `any`, `finish`),
so nested attributes can be configured to arbitrary depth.
This module is called with module argument `attrName`, which is the name of the attribute being touched up.
'';
};
any = mkOption {
type = types.nullOr (types.deferredModuleWith {
staticModules = [ ./attr.nix ];
});
default = null;
visible = docsVisible;
description =
if docsVisible == "shallow" then ''
Default configuration for all attributes at the next level.
Same structure as [`any`](#opt-touchup.any); see its description.
''
else ''
A module whose options are merged into every attribute's touchup configuration.
For example, `any.enable = false` disables all attributes by default.
Override specific ones via `attr.<name>`.
Only applies to immediate children does not recurse into nested attributes automatically.
This module is called with module argument `attrName`, which is the name of the attribute being touched up.
'';
};
type = mkOption {
type = types.raw;
default = types.raw;
defaultText = "raw";
description = ''
The type used for merging multiple definitions of ${options.finish}.
Override this if multiple modules need to compose their ${options.finish} functions.
'';
};
finish = mkOption {
type = types.functionTo config.type;
default = v: v;
defaultText = lib.literalMD "`v: v`, the identity function";
description = ''
A function applied after filtering and transforming (e.g. by ${options.attr} and ${options.any} at this level).
It receives the resulting attribute set and must return the value to use in its place.
'';
};
touchupApply = mkOption {
internal = true;
description = ''
A generated function that applies the touchups that are configured with the other options in this module.
'';
type = types.functionTo types.raw;
readOnly = true;
};
};
config = {
_module.args.docsVisible = lib.mkDefault true;
touchupApply = v:
config.finish
(if config.attr != { } || config.any != null
then
addErrorContext "while applying touchups from ${options.attr}: ${lib.options.showDefs options.attr.definitionsWithLocations}\n and from ${options.any}: ${lib.options.showDefs options.any.definitionsWithLocations}"
(touchupAttrs v)
else v);
};
}

View file

@ -51,6 +51,7 @@
modules = ./extras/modules.nix; modules = ./extras/modules.nix;
partitions = ./extras/partitions.nix; partitions = ./extras/partitions.nix;
bundlers = ./extras/bundlers.nix; bundlers = ./extras/bundlers.nix;
touchup = ./extras/touchup.nix;
}; };
in in
lib.mkFlake { inherit inputs; } { lib.mkFlake { inherit inputs; } {

View file

@ -159,7 +159,7 @@ let
let let
eval = flake-parts-lib.evalFlakeModule args module; eval = flake-parts-lib.evalFlakeModule args module;
in in
eval.config.flake; eval.config.processedFlake;
/** /**
Deprecated. Declare options directly, e.g. `options.foo.bar = mkOption { ... }`, Deprecated. Declare options directly, e.g. `options.foo.bar = mkOption { ... }`,

View file

@ -1,4 +1,4 @@
{ lib, ... }: { config, lib, options, ... }:
let let
inherit (lib) inherit (lib)
mkOption mkOption
@ -33,6 +33,27 @@ in
Raw flake output attributes. Any attribute can be set here, but some Raw flake output attributes. Any attribute can be set here, but some
attributes are represented by options, to provide appropriate attributes are represented by options, to provide appropriate
configuration merging. configuration merging.
Further processing may be applied to these attributes. See [`processedFlake`](flake-parts.md#opt-processedFlake) for more information.
'';
};
processedFlake = mkOption {
type = types.raw;
readOnly = true;
apply =
if options.processedFlake.isDefined
then x: x
else x: config.flake;
description = ''
The final flake output, as returned by `mkFlake`.
This separates two concerns:
- [`flake`](#opt-flake): the internal representation, where all attributes are available for use by other modules, regardless of whether they evaluate successfully for all configurations.
- `processedFlake`: the external output, which may have attributes removed or transformed to satisfy tools like `nix flake check`.
By default, `processedFlake` equals `flake` (no processing).
Import a module such as [`flakeModules.touchup`](#opt-touchup) to define it, or set it directly.
''; '';
}; };
}; };