From 038f2a8df5ab59d1e88d3a27fe8406bc4f654ee8 Mon Sep 17 00:00:00 2001 From: Victor Borja Date: Mon, 6 Apr 2026 16:28:56 -0600 Subject: [PATCH 1/2] aspect-meta --- nix/lib/aspects/resolve.nix | 29 +++-- nix/lib/aspects/types.nix | 40 +++++- templates/ci/modules/features/aspect-meta.nix | 119 ++++++++++++++++++ 3 files changed, 175 insertions(+), 13 deletions(-) create mode 100644 templates/ci/modules/features/aspect-meta.nix diff --git a/nix/lib/aspects/resolve.nix b/nix/lib/aspects/resolve.nix index b44fa991..f4040410 100644 --- a/nix/lib/aspects/resolve.nix +++ b/nix/lib/aspects/resolve.nix @@ -3,21 +3,34 @@ let inherit (den.lib) canTake take; + apply = + provided: args: + let + res = if canTake.upTo args provided then take.upTo provided args else provided; + mod = + den.lib.aspects.types.aspectType.merge + [ ] + [ + { + file = ""; + value = res; + } + ]; + in + if lib.isFunction res then mod else res; + resolve = class: prev-chain: provided: let - aspect = if canTake.upTo args provided then take.upTo provided args else provided; + aspect = apply provided { inherit class aspect-chain; }; aspect-chain = prev-chain ++ [ provided ] ++ (lib.optional (provided != aspect) aspect); - args = { - inherit aspect class aspect-chain; - }; - - imports = - (lib.optional (aspect ? ${class}) aspect.${class}) - ++ (map (resolve class aspect-chain) (aspect.includes or [ ])); + classModule = lib.optional (aspect ? ${class}) ( + lib.setDefaultModuleLocation "${class}@${aspect.name}" aspect.${class} + ); + imports = classModule ++ (map (resolve class aspect-chain) (aspect.includes or [ ])); in { inherit imports; diff --git a/nix/lib/aspects/types.nix b/nix/lib/aspects/types.nix index cdf4dba6..aa383105 100644 --- a/nix/lib/aspects/types.nix +++ b/nix/lib/aspects/types.nix @@ -9,7 +9,6 @@ let }; isProviderFn = canTake.upTo { - aspect = true; aspect-chain = true; class = true; }; @@ -48,9 +47,31 @@ let let sub = aspectSubmodule cnf; in - sub - // { - merge = loc: defs: sub.merge loc defs; + sub // { merge = mergeWithAspectMeta sub; }; + + mergeWithAspectMeta = + sub: loc: defs: + sub.merge loc ( + defs + ++ [ + { + file = (lib.head defs).file; + value = aspectMeta loc defs; + } + ] + ); + + aspectMeta = + loc: defs: + { config, ... }: + let + names = map (x: if builtins.isString x then x else "") loc; + nameFromLoc = lib.concatStringsSep "." names; + in + { + name = nameFromLoc; + meta.file = (lib.last defs).file; + meta.loc = loc; }; aspectSubmodule = @@ -59,7 +80,6 @@ let { name, config, ... }: { freeformType = lib.types.lazyAttrsOf lib.types.deferredModule; - config._module.args.aspect = config; imports = [ (lib.mkAliasOptionModule [ "_" ] [ "provides" ]) ]; options = { @@ -77,6 +97,16 @@ let type = lib.types.str; }; + meta = lib.mkOption { + description = "Aspect attached meta data"; + type = lib.types.submodule { + freeformType = lib.types.lazyAttrsOf lib.types.unspecified; + self = config; + }; + defaultText = lib.literalExpression "{ }"; + default = { }; + }; + includes = lib.mkOption { description = "Providers to ask aspects from"; type = lib.types.listOf (providerType cnf); diff --git a/templates/ci/modules/features/aspect-meta.nix b/templates/ci/modules/features/aspect-meta.nix new file mode 100644 index 00000000..7d1a7ab8 --- /dev/null +++ b/templates/ci/modules/features/aspect-meta.nix @@ -0,0 +1,119 @@ +{ denTest, ... }: +{ + flake.tests.aspect-meta = { + + test-meta-can-be-referenced = denTest ( + { den, funnyNames, ... }: + { + den.aspects.foo = { + funny.names = [ den.aspects.foo.meta.nick ]; + meta.nick = "McLoving"; + }; + + expr = funnyNames den.aspects.foo; + expected = [ "McLoving" ]; + } + ); + + test-meta-can-be-self-referenced = denTest ( + { den, funnyNames, ... }: + { + den.aspects.foo = + { config, ... }: + { + funny.names = [ config.meta.nick ]; + meta.nick = "McLoving"; + }; + + expr = funnyNames den.aspects.foo; + expected = [ "McLoving" ]; + } + ); + + test-name-can-be-referenced = denTest ( + { den, funnyNames, ... }: + { + den.aspects.foo = { + funny.names = [ den.aspects.foo.name ]; + }; + + expr = funnyNames den.aspects.foo; + expected = [ "den.aspects.foo" ]; + } + ); + + test-name-can-be-self-referenced = denTest ( + { den, funnyNames, ... }: + { + den.aspects.foo = + { config, ... }: + { + funny.names = [ config.name ]; + }; + + expr = funnyNames den.aspects.foo; + expected = [ "den.aspects.foo" ]; + } + ); + + test-meta-keys-at-host-aspect = denTest ( + { + den, + igloo, + lib, + ... + }: + { + den.hosts.x86_64-linux.igloo = { }; + den.aspects.igloo.includes = [ den.aspects.foo ]; + + den.aspects.foo = + { host }: + { + nixos.environment.sessionVariables = { + KEYS = lib.attrNames den.aspects.foo.meta.self.meta; + }; + }; + + expr = { + inherit (igloo.environment.sessionVariables) + KEYS + ; + }; + expected.KEYS = "file:loc:self"; + } + ); + + test-meta-keys-at-host-fixpoint = denTest ( + { + den, + igloo, + lib, + ... + }: + { + den.hosts.x86_64-linux.igloo = { }; + den.aspects.igloo.includes = [ den.aspects.foo ]; + + den.aspects.foo = + { host }: + { class, aspect-chain }: + { config, lib, ... }: + { + nixos.environment.sessionVariables = { + KEYS = lib.attrNames config.meta; + }; + meta.foo = 12; + }; + + expr = { + inherit (igloo.environment.sessionVariables) + KEYS + ; + }; + expected.KEYS = "file:foo:loc:self"; + } + ); + + }; +} From da445b40c26da6c650ab492a681e591edb22a2c1 Mon Sep 17 00:00:00 2001 From: Victor Borja Date: Mon, 6 Apr 2026 23:37:44 -0600 Subject: [PATCH 2/2] add docs --- .../content/docs/guides/configure-aspects.mdx | 48 +++++++++++++++++++ templates/ci/modules/features/aspect-meta.nix | 1 - 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/docs/src/content/docs/guides/configure-aspects.mdx b/docs/src/content/docs/guides/configure-aspects.mdx index 611b1584..59fe8010 100644 --- a/docs/src/content/docs/guides/configure-aspects.mdx +++ b/docs/src/content/docs/guides/configure-aspects.mdx @@ -185,3 +185,51 @@ Owned configs from `den.default` are deduplicated across pipeline stages. Parametric functions in `den.default.includes` are evaluated at every context stage. Use `den.lib.take.exactly` if a function should only run in specific contexts. + + +## Aspect Meta-Data + +Aspects have a `meta` submodule that can be used to attach meta-data +at the aspect-level. + +This is useful for introspection, for example, using some key to filter out aspects +or communicate information about aspects to other tooling like graph generators. + +Since `.meta` is a freeformType you can add any custom attribute or import a module on it. + +The following are default `meta.*` attributes defined by Den: + +```nix +aspect.name # "den.aspects.igloo" +meta.loc # the location [ "den" "aspects" "igloo" ] from where name is derived. +meta.file # the first file where this aspect was defined. +meta.self # a reference to the aspect module `config`. +``` + +You can acess meta values by referencing an aspect: + +```nix +den.aspects.igloo.meta.name +``` + +Or if an aspect needs to access its own meta data as part of its parametric functor, +it can do something like: + + +```nix +den.aspects.foo = + { config, lib, ... }: # module args to access `config.meta` + { + meta.foo = "bar"; + __functor = self: ctx: + if config.meta.foo == "bar" + then { } # do nothing, skip. + { + includes = filter (i: someCondition i) self.includes; + }; + } + +``` + + + diff --git a/templates/ci/modules/features/aspect-meta.nix b/templates/ci/modules/features/aspect-meta.nix index 7d1a7ab8..b3b0aa06 100644 --- a/templates/ci/modules/features/aspect-meta.nix +++ b/templates/ci/modules/features/aspect-meta.nix @@ -97,7 +97,6 @@ den.aspects.foo = { host }: - { class, aspect-chain }: { config, lib, ... }: { nixos.environment.sessionVariables = {