From 9d0762c18ab1069af003dedb83efbcb59cdb261b Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Tue, 27 May 2025 14:32:12 +0200 Subject: [PATCH] Add postprocessing, add flakeModules.touchup --- dev/tests/nix-unit.nix | 102 ++++++++++++++++++++++++++++++ extras/touchup.nix | 57 +++++++++++++++++ extras/touchup/attr.nix | 20 ++++++ extras/touchup/attrs.nix | 131 +++++++++++++++++++++++++++++++++++++++ flake.nix | 1 + lib.nix | 2 +- modules/flake.nix | 23 ++++++- 7 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 extras/touchup.nix create mode 100644 extras/touchup/attr.nix create mode 100644 extras/touchup/attrs.nix diff --git a/dev/tests/nix-unit.nix b/dev/tests/nix-unit.nix index 412b2a0..6228ecb 100644 --- a/dev/tests/nix-unit.nix +++ b/dev/tests/nix-unit.nix @@ -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 { 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 = { "test: conditional null throws helpful error" = { expr = diff --git a/extras/touchup.nix b/extras/touchup.nix new file mode 100644 index 0000000..308ad5b --- /dev/null +++ b/extras/touchup.nix @@ -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; + }; +} diff --git a/extras/touchup/attr.nix b/extras/touchup/attr.nix new file mode 100644 index 0000000..162419d --- /dev/null +++ b/extras/touchup/attr.nix @@ -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"; + }; +} diff --git a/extras/touchup/attrs.nix b/extras/touchup/attrs.nix new file mode 100644 index 0000000..06b38e6 --- /dev/null +++ b/extras/touchup/attrs.nix @@ -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.`. + + 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); + }; +} + diff --git a/flake.nix b/flake.nix index 8d1aef5..772fdac 100644 --- a/flake.nix +++ b/flake.nix @@ -51,6 +51,7 @@ modules = ./extras/modules.nix; partitions = ./extras/partitions.nix; bundlers = ./extras/bundlers.nix; + touchup = ./extras/touchup.nix; }; in lib.mkFlake { inherit inputs; } { diff --git a/lib.nix b/lib.nix index c8ea2ba..a470cbc 100644 --- a/lib.nix +++ b/lib.nix @@ -159,7 +159,7 @@ let let eval = flake-parts-lib.evalFlakeModule args module; in - eval.config.flake; + eval.config.processedFlake; /** Deprecated. Declare options directly, e.g. `options.foo.bar = mkOption { ... }`, diff --git a/modules/flake.nix b/modules/flake.nix index 7792529..de990fd 100644 --- a/modules/flake.nix +++ b/modules/flake.nix @@ -1,4 +1,4 @@ -{ lib, ... }: +{ config, lib, options, ... }: let inherit (lib) mkOption @@ -33,6 +33,27 @@ in Raw flake output attributes. Any attribute can be set here, but some attributes are represented by options, to provide appropriate 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. ''; }; };