···1-# creates a new aspects option.
2-# See flakeModule for usage.
3lib: cb: cfg:
4let
05 aspects = import ./aspects.nix lib cfg;
006 types = import ./types.nix lib;
007 option = lib.mkOption {
8 default = { };
9- description = ''
10- Attribute set of `<aspect>.<class>` modules.
11-12- Convenience transposition of `flake.modules.<class>.<aspect>`.
13- '';
14 type = types.aspectsType;
15 };
16in
017cb option aspects.transposed
···1+# Low-level aspect scope factory
2+# Creates aspect integration via callback pattern for maximum flexibility
3lib: cb: cfg:
4let
5+ # Import aspects transposer: validates and transposes aspect config
6 aspects = import ./aspects.nix lib cfg;
7+8+ # Import type system for aspect validation
9 types = import ./types.nix lib;
10+11+ # Create aspects input option
12 option = lib.mkOption {
13 default = { };
14+ description = "Aspect definitions organized as <aspect>.<class>";
000015 type = types.aspectsType;
16 };
17in
18+# Invoke callback with option and transposed results
19cb option aspects.transposed
+5-1
nix/resolve.nix
···0001lib:
2let
3-4 include =
5 class: aspect-chain: provider:
6 let
···8 in
9 resolve class aspect-chain provided;
10011 resolve = class: aspect-chain: provided: {
12 imports = lib.flatten [
13 (provided.${class} or { })
···1+# Core aspect resolution algorithm
2+# Resolves aspect definitions into nixpkgs modules with dependency resolution
3+4lib:
5let
6+ # Process a single provider: invoke with context and resolve
7 include =
8 class: aspect-chain: provider:
9 let
···11 in
12 resolve class aspect-chain provided;
1314+ # Main resolution: extract class config and recursively resolve includes
15 resolve = class: aspect-chain: provided: {
16 imports = lib.flatten [
17 (provided.${class} or { })
+105-220
nix/types.nix
···001lib:
2let
3- # Import the resolve function which handles aspect resolution and dependency injection
4 resolve = import ./resolve.nix lib;
56- # Top-level aspects container type
7- # This is the entry point for defining all aspects in a flake
8- # Structure: aspects.<aspectName> = { ... }
9- # Makes the entire aspects config available as 'aspects' in module args
10- # allowing cross-referencing between aspects
11- aspectsType = lib.types.submodule (
12- { config, ... }:
13- {
14- # Allow arbitrary aspect definitions as attributes
15- # Each aspect can be either:
16- # - An aspect submodule (aspectSubmoduleAttrs)
17- # - A provider function (providerType)
18- freeformType = lib.types.lazyAttrsOf (lib.types.either aspectSubmoduleAttrs providerType);
19- # Inject the aspects config into _module.args for cross-referencing
20- config._module.args.aspects = config;
21- }
22- );
23-24- # Type checker for provider functions with specific argument patterns
25- # Valid provider function signatures:
26- # 1. { class } => aspect-object
27- # 2. { aspect-chain } => aspect-object
28- # 3. { class, aspect-chain } => aspect-object
29- # 4. { class, ... } => aspect-object (with other ignored args)
30- # 5. { aspect-chain, ... } => aspect-object (with other ignored args)
31- #
32- # This ensures that provider functions receive the proper context when invoked
33- functionToAspect = lib.types.addCheck (lib.types.functionTo aspectSubmodule) (
34- f:
35- let
36- args = lib.functionArgs f;
37- arity = lib.length (lib.attrNames args);
38- hasClass = args ? class;
39- hasChain = args ? aspect-chain;
40- classOnly = hasClass && arity == 1;
41- chainOnly = hasChain && arity == 1;
42- both = hasClass && hasChain && arity == 2;
43- in
44- classOnly || chainOnly || both
45- );
46-47- # Provider functions can be:
48- # 1. A function taking { class, aspect-chain } and returning an aspect (functionToAspect)
49- # 2. A function taking parameters and returning another provider (curried)
50- # This allows for parametric aspects and lazy evaluation
51- functionProviderType = lib.types.either functionToAspect (lib.types.functionTo providerType);
52-53- # Provider type allows three forms:
54- # 1. A function provider (functionProviderType)
55- # 2. An aspect configuration (aspectSubmodule)
56- # This enables both immediate aspect definitions and deferred/parametric ones
57- providerType = lib.types.either functionProviderType aspectSubmodule;
58-59- # Additional validation for aspect submodules to ensure they're not mistyped functions
60- # An aspectSubmoduleAttrs is either:
61- # - Not a function at all (plain attribute set)
62- # - A function with submodule-style arguments (lib, config, options, aspect)
63- # This prevents accidentally treating provider functions as aspect configs
64- aspectSubmoduleAttrs = lib.types.addCheck aspectSubmodule (
65- m: (!builtins.isFunction m) || (isAspectSubmoduleFn m)
66- );
67-68- # Helper to identify if a function is a submodule-style function
69- # Submodule functions take args like { lib, config, options, aspect, ... }
70- # Returns true if the function accepts at least one of these special args
71- isAspectSubmoduleFn =
72- m:
73- lib.pipe m [
74- lib.functionArgs
75- lib.attrNames
76- (lib.intersectLists [
77- "lib"
78- "config"
79- "options"
80- "aspect"
81- ])
82- (x: lib.length x > 0)
83- ];
84-85- # Special type that accepts any value but always merges to null
86- # Used for internal computed values that shouldn't be serialized
87- # This prevents type errors when values don't have proper types
88 ignoredType = lib.types.mkOptionType {
89 name = "ignored type";
90 description = "ignored values";
···92 check = _: true;
93 };
9495- # Core aspect definition type
96- # Each aspect represents a reusable configuration module that can:
97- # - Define configuration for multiple "classes" (e.g., nixos, home-manager, darwin)
98- # - Include other aspects as dependencies
99- # - Provide sub-aspects for selective composition
100- # - Be parametrized via __functor
101- aspectSubmodule = lib.types.submodule (
102- {
103- name,
104- config,
105- ...
106- }:
107- {
108- # Allow arbitrary class configurations (e.g., nixos, home-manager, etc.)
109- # Each class maps to a deferred module that will be resolved later
110- freeformType = lib.types.attrsOf lib.types.deferredModule;
111-112- # Make the aspect config available as 'aspect' in module args
113- # This allows modules within the aspect to reference their own aspect
114- config._module.args.aspect = config;
115116- # Create "_" as a shorthand alias for "provides"
117- # Allows writing: aspect._.foo instead of aspect.provides.foo
118- # This improves ergonomics for the common case of defining sub-aspects
119- imports = [ (lib.mkAliasOptionModule [ "_" ] [ "provides" ]) ];
000000120121- # Human-readable aspect name, defaults to the attribute name
122- # Used in aspect-chain tracking and for display purposes
123- options.name = lib.mkOption {
124- description = "Aspect name";
125- default = name;
126- type = lib.types.str;
127- };
128129- # Optional description for documentation purposes
130- # Defaults to a generic description using the aspect name
131- options.description = lib.mkOption {
132- description = "Aspect description";
133- default = "Aspect ${name}";
134- type = lib.types.str;
135- };
136137- # Dependencies: list of other providers this aspect includes
138- # During resolution, included aspects are merged with this aspect
139- # Includes can be:
140- # - Direct aspect references: aspects.otherAspect
141- # - Parametrized providers: aspects.other.provides.foo "param"
142- # - Functorized aspects: aspects.otherAspect { param = value; }
143- # The resolution order matters for module merging semantics
144- options.includes = lib.mkOption {
145- description = "Providers to ask aspects from";
146- type = lib.types.listOf providerType;
147- default = [ ];
148- };
149150- # Sub-aspects that can be selectively included by other aspects
151- # This allows aspects to expose multiple named variants or components
152- # Creates a fixpoint where provides can reference the aspects in their scope
153- # The provides scope gets its own 'aspects' arg for internal cross-referencing
154- options.provides = lib.mkOption {
155- description = "Providers of aspect for other aspects";
156- default = { };
157- type = lib.types.submodule (
158- { config, ... }:
159- {
160- # Allow arbitrary sub-aspect definitions
161- freeformType = lib.types.attrsOf providerType;
162- # Make the provides scope available as 'aspects' for fixpoint references
163- # This enables provides.foo to reference provides.bar via aspects.bar
164- config._module.args.aspects = config;
165- }
166- );
167- };
168169- # Functor enables aspects to be callable like functions
170- # When defined, calling aspect { param = value; } invokes the functor
171- # The functor receives:
172- # 1. The aspect config itself
173- # 2. The parameters passed by the caller (which must include class and aspect-chain)
174- # This allows aspects to be parametrized and context-aware
175- #
176- # The default functor:
177- # - Takes the aspect config
178- # - Takes { class, aspect-chain } parameters
179- # - Returns the aspect unchanged (identity function with parameter access)
180- # - The weird `if true || (class aspect-chain) then` is to silence nixf-diagnose
181- # about unused variables while ensuring they're in scope
182- options.__functor = lib.mkOption {
183- internal = true;
184- visible = false;
185- description = "Functor to default provider";
186- type = lib.types.functionTo providerType;
187- default =
188- aspect:
189- { class, aspect-chain }:
190- # silence nixf-diagnose about unused variables
191- if true || (class aspect-chain) then aspect else aspect;
192- };
193194- # Convenience accessor: aspect.modules.<class> automatically resolves
195- # This is equivalent to calling aspect.resolve { class = "<class>"; }
196- # Returns a map of all classes with their resolved modules
197- #
198- # For example: aspect.modules.nixos == aspect.resolve { class = "nixos"; }
199- #
200- # This is computed lazily and uses ignoredType to avoid serialization issues
201- options.modules = lib.mkOption {
202- internal = true;
203- visible = false;
204- readOnly = true;
205- description = "resolved modules from this aspect";
206- type = ignoredType;
207- # For each class in the aspect, resolve it with empty aspect-chain
208- apply = _: lib.mapAttrs (class: _: config.resolve { inherit class; }) config;
209- };
00000000000000000000000000210211- # Main resolution function that converts an aspect into a nixpkgs module
212- # Takes { class, aspect-chain } and returns a resolved module
213- # - class: The target configuration class (e.g., "nixos", "home-manager")
214- # - aspect-chain: List of aspects traversed so far (for tracking dependencies)
215- #
216- # The resolution process:
217- # 1. Invokes the aspect config with class and aspect-chain parameters
218- # This triggers the __functor if defined, allowing parametrization
219- # 2. Calls resolve.nix to recursively resolve includes
220- # 3. Returns a module with imports from the aspect and its dependencies
221- #
222- # The aspect-chain parameter allows aspects to introspect their dependency tree
223- # This is useful for debugging and for aspects that need to know their context
224- options.resolve = lib.mkOption {
225- internal = true;
226- visible = false;
227- readOnly = true;
228- description = "function to resolve a module from this aspect";
229- type = ignoredType;
230- apply =
231 _:
232 {
233 class,
234 aspect-chain ? [ ],
235 }:
236- # Invoke config (the aspect) with class and aspect-chain parameters
237- # This works because config is wrapped with __functor via the submodule system
238- # Then pass the result to resolve for dependency resolution
239 resolve class aspect-chain (config {
240 inherit class aspect-chain;
241- });
0242 };
243 }
244 );
245246in
247{
248- inherit
249- aspectsType # Main entry point for flake.aspects
250- aspectSubmodule # Individual aspect definition type
251- providerType # Type for provider expressions
252- ;
000000000253}
···1+# Core type system for aspect-oriented configuration
2+3lib:
4let
05 resolve = import ./resolve.nix lib;
67+ # Type for computed values that only exist during evaluation
0000000000000000000000000000000000000000000000000000000000000000000000000000000008 ignoredType = lib.types.mkOptionType {
9 name = "ignored type";
10 description = "ignored values";
···12 check = _: true;
13 };
1415+ # Create internal read-only option with custom apply function
16+ mkInternal =
17+ desc: type: fn:
18+ lib.mkOption {
19+ internal = true;
20+ visible = false;
21+ readOnly = true;
22+ description = desc;
23+ inherit type;
24+ apply = fn;
25+ };
26+27+ # Check if function has submodule-style arguments
28+ isSubmoduleFn =
29+ m:
30+ lib.length (
31+ lib.intersectLists [ "lib" "config" "options" "aspect" ] (lib.attrNames (lib.functionArgs m))
32+ ) > 0;
003334+ # Check if function accepts { class } and/or { aspect-chain }
35+ isProviderFn =
36+ f:
37+ let
38+ args = lib.functionArgs f;
39+ n = lib.length (lib.attrNames args);
40+ in
41+ (args ? class && n == 1)
42+ || (args ? aspect-chain && n == 1)
43+ || (args ? class && args ? aspect-chain && n == 2);
4445+ # Direct provider function: ({ class, aspect-chain }) โ aspect
46+ directProviderFn = lib.types.addCheck (lib.types.functionTo aspectSubmodule) isProviderFn;
000004748+ # Curried provider function: (params) โ provider (enables parametrization)
49+ curriedProviderFn = lib.types.functionTo providerType;
000005051+ # Any provider function: direct or curried
52+ providerFn = lib.types.either directProviderFn curriedProviderFn;
00000000005354+ # Provider type: function or aspect that can provide configurations
55+ providerType = lib.types.either providerFn aspectSubmodule;
00000000000000005657+ # Core aspect submodule with all aspect properties
58+ aspectSubmodule = lib.types.submodule (
59+ { name, config, ... }:
60+ {
61+ freeformType = lib.types.lazyAttrsOf lib.types.deferredModule;
62+ config._module.args.aspect = config;
63+ imports = [ (lib.mkAliasOptionModule [ "_" ] [ "provides" ]) ];
000000000000000006465+ options = {
66+ name = lib.mkOption {
67+ description = "Aspect name";
68+ default = name;
69+ type = lib.types.str;
70+ };
71+72+ description = lib.mkOption {
73+ description = "Aspect description";
74+ default = "Aspect ${name}";
75+ type = lib.types.str;
76+ };
77+78+ includes = lib.mkOption {
79+ description = "Providers to ask aspects from";
80+ type = lib.types.listOf providerType;
81+ default = [ ];
82+ };
83+84+ provides = lib.mkOption {
85+ description = "Providers of aspect for other aspects";
86+ default = { };
87+ type = lib.types.submodule (
88+ { config, ... }:
89+ {
90+ freeformType = lib.types.lazyAttrsOf providerType;
91+ config._module.args.aspects = config;
92+ }
93+ );
94+ };
95+96+ __functor = lib.mkOption {
97+ internal = true;
98+ visible = false;
99+ description = "Functor to default provider";
100+ type = lib.types.functionTo providerType;
101+ default = aspect: { class, aspect-chain }: if true || (class aspect-chain) then aspect else aspect;
102+ };
103+104+ modules = mkInternal "resolved modules from this aspect" ignoredType (
105+ _: lib.mapAttrs (class: _: config.resolve { inherit class; }) config
106+ );
107108+ resolve = mkInternal "function to resolve a module from this aspect" ignoredType (
0000000000000000000109 _:
110 {
111 class,
112 aspect-chain ? [ ],
113 }:
000114 resolve class aspect-chain (config {
115 inherit class aspect-chain;
116+ })
117+ );
118 };
119 }
120 );
121122in
123{
124+ # Top-level aspects container with fixpoint semantics
125+ aspectsType = lib.types.submodule (
126+ { config, ... }:
127+ {
128+ freeformType = lib.types.lazyAttrsOf (
129+ lib.types.either (lib.types.addCheck aspectSubmodule (
130+ m: (!builtins.isFunction m) || isSubmoduleFn m
131+ )) providerType
132+ );
133+ config._module.args.aspects = config;
134+ }
135+ );
136+137+ inherit aspectSubmodule providerType;
138}