Commit 49e500d

HPCesia <me@hpcesia.com>
2026-10-15 17:47:46
den: pass host/user as module args to settings
Assisted-by: crush:kimi-k3 Assisted-by: crush:deepseek-v4.1-flash
1 parent 1305dd3
Changed files (1)
modules/mk-settings.nix
@@ -1,5 +1,6 @@
 # Settings surface shared by hosts and users. Aspects declare
-# `settings.{host,user}`; entities supply values and aspects read them back.
+# `settings.{host,user}` as an option set or as a function of the entity
+# instance; entities supply values and aspects read them back.
 {
   config,
   lib,
@@ -27,12 +28,19 @@
         if builtins.isAttrs s
         then s
         else null;
-      hasHost = builtins.isAttrs (settings.host or null);
-      hasUser = builtins.isAttrs (settings.user or null);
+      isDecl = v: builtins.isAttrs v || builtins.isFunction v;
+      hasHost = isDecl (settings.host or null);
+      hasUser = isDecl (settings.user or null);
       mine = lib.optional (hasHost || hasUser) {
         inherit prefix;
-        host = lib.optionalAttrs hasHost settings.host;
-        user = lib.optionalAttrs hasUser settings.user;
+        host =
+          if hasHost
+          then settings.host
+          else {};
+        user =
+          if hasUser
+          then settings.user
+          else {};
       };
       children = lib.concatMap (name: collect (prefix ++ [name]) node.${name}) (
         builtins.filter (name:
@@ -50,24 +58,31 @@
 
   collected = collect [] config.den.aspects;
 
-  # Build nested option declarations, grouping shared path prefixes.
-  mkOptions = nodes: let
+  # Build nested option declarations, grouping shared path prefixes. `ctx` is the
+  # entity instance, and forcing it here would recurse: the fixpoint is still
+  # being built (gen-schema ADR-0033), so only lazy reads such as defaults may touch it.
+  mkOptions = ctx: nodes: let
     grouped = lib.groupBy (n: builtins.head n.path) nodes;
   in
     lib.mapAttrs (
       name: group:
         if lib.all (n: builtins.length n.path == 1) group
-        then
+        then let
+          decls = (builtins.head group).decls;
+        in
           lib.mkOption {
             type = lib.types.submodule {
-              options = (builtins.head group).decls;
+              options =
+                if builtins.isFunction decls
+                then decls ctx
+                else decls;
             };
             default = {};
             description = "Settings for aspect `${name}'.";
           }
         else if !(lib.all (n: builtins.length n.path > 1) group)
         then throw "aspect settings: aspect `${name}' is both a settings leaf and a container"
-        else mkOptions (map (n: n // {path = lib.drop 1 n.path;}) group)
+        else mkOptions ctx (map (n: n // {path = lib.drop 1 n.path;}) group)
     )
     grouped;
 
@@ -81,11 +96,12 @@
     decls = n.user;
   }) (lib.filter (n: n.user != {}) collected);
 
-  userSettingsType = lib.types.submodule {
-    # The shared registry and hosts also carry raw user-system values
-    freeformType = lib.types.attrsOf lib.types.anything;
-    options.settings = mkOptions userNodes;
-  };
+  userSettingsType = ctx:
+    lib.types.submodule {
+      # The shared registry and hosts also carry raw user-system values
+      freeformType = lib.types.attrsOf lib.types.anything;
+      options.settings = mkOptions ctx userNodes;
+    };
 
   # Merge the shared `den.users.<username>` values as lowest-priority defs.
   registryAware = inner:
@@ -108,18 +124,32 @@ in {
   config = {
     den.reservedKeys = ["settings"];
 
-    den.schema.host.options.settings = lib.mkOption {
-      type = lib.types.submodule {
-        options = mkOptions hostNodes;
-      };
-      default = {};
-      description = "Host-provided settings for den aspects.";
-    };
+    # Through imports, not on the kind: the option type must be built inside the
+    # instance for function declarations to receive it.
+    den.schema.host.imports = [
+      ({host, ...}: {
+        options.settings = lib.mkOption {
+          type = lib.types.submodule {
+            options = mkOptions {inherit host;} hostNodes;
+          };
+          default = {};
+          description = "Host-provided settings for den aspects.";
+        };
+      })
+    ];
 
-    den.schema.user.options.system = lib.mkOption {
-      type = registryAware userSettingsType;
-      default = {};
-      description = "User-provided settings for den aspects.";
-    };
+    den.schema.user.imports = [
+      ({
+        host,
+        user,
+        ...
+      }: {
+        options.system = lib.mkOption {
+          type = registryAware (userSettingsType {inherit host user;});
+          default = {};
+          description = "User-provided settings for den aspects.";
+        };
+      })
+    ];
   };
 }