From 9aff51ddc3d609c6788fa137b020adbb9b104179 Mon Sep 17 00:00:00 2001 From: Victor Borja Date: Fri, 13 Mar 2026 00:32:11 -0600 Subject: [PATCH] feat(batteries): Opt-in den._.bidirectional See docs at den.ctx.user --- .github/workflows/docs.yml | 4 +- .github/workflows/test.yml | 4 +- modules/aspects/provides/bidirectional.nix | 16 ++++ modules/context/host.nix | 28 +++++++ modules/context/os.nix | 28 ------- modules/context/user.nix | 82 +++++++++++++++++++ .../ci/modules/features/auto-parametric.nix | 3 + .../features/batteries/define-user.nix | 1 + .../modules/features/conditional-config.nix | 2 + .../features/context/host-propagation.nix | 1 + .../external-namespace-deep-aspect.nix | 2 + .../issue-201-forward-multiple-users.nix | 1 + .../ci/modules/features/host-options.nix | 6 +- 13 files changed, 142 insertions(+), 36 deletions(-) create mode 100644 modules/aspects/provides/bidirectional.nix create mode 100644 modules/context/host.nix delete mode 100644 modules/context/os.nix create mode 100644 modules/context/user.nix diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 07b748d2..9c0faf5b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,10 +2,10 @@ name: Docs on: push: branches: ["!main"] - paths: [ "docs/**" ] + paths: ["docs/**"] pull_request: types: [labeled, opened, synchronize, reopened, review_requested, ready_for_review] - paths: [ "docs/**" ] + paths: ["docs/**"] pull_request_review: types: [submitted] concurrency: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index faedd50a..5c15c4d5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,10 +1,10 @@ on: push: branches: [main] - paths: [ "**/*.nix" ] + paths: ["**/*.nix"] pull_request: types: [labeled, opened, synchronize, reopened, review_requested, ready_for_review] - paths: [ "**/*.nix" ] + paths: ["**/*.nix"] pull_request_review: types: [submitted] concurrency: diff --git a/modules/aspects/provides/bidirectional.nix b/modules/aspects/provides/bidirectional.nix new file mode 100644 index 00000000..2a2c9fa9 --- /dev/null +++ b/modules/aspects/provides/bidirectional.nix @@ -0,0 +1,16 @@ +{ den, ... }: +let + description = '' + Enable Den bidirectionality: User takes configuration from Host. + + **REALLY** IMPORTANT: Read the documentation for den.ctx.user + + Consider as alternative den.provides.mutual-provider. + ''; +in +{ + den.provides.bidirectional = { + inherit description; + includes = [ den.ctx.user.provides.bidirectional ]; + }; +} diff --git a/modules/context/host.nix b/modules/context/host.nix new file mode 100644 index 00000000..82081425 --- /dev/null +++ b/modules/context/host.nix @@ -0,0 +1,28 @@ +{ den, lib, ... }: +let + inherit (den.lib.parametric) fixedTo; + + ctx.host.description = '' + ## Context: den.ctx.host{host} + + Host context stage configures an OS + + A {host} context fan-outs into many {host,user} contexts. + + A `den.ctx.host{host}` transitions unconditionally into `den.ctx.default{host}` + + A `den.ctx.host{host}` obtains OS configuration nixos/darwin by using `fixedTo{host} host-aspect`. + fixedTo takes: + - host-aspect's owned attrs + - static includes like { nixos.foo = ... } or ({ class, aspect-chain }: { nixos.foo = ...; }) + - atLeast{host} parametric includes like ({ host }: { nixos.foo = ...; }) + ''; + + ctx.host.into.user = { host }: map (user: { inherit host user; }) (lib.attrValues host.users); + ctx.host.into.default = lib.singleton; + ctx.host.provides.host = { host }: fixedTo { inherit host; } den.aspects.${host.aspect}; + +in +{ + den.ctx = ctx; +} diff --git a/modules/context/os.nix b/modules/context/os.nix deleted file mode 100644 index 6353ac97..00000000 --- a/modules/context/os.nix +++ /dev/null @@ -1,28 +0,0 @@ -{ den, lib, ... }: -let - inherit (den.lib.parametric) fixedTo atLeast; - - ctx.host.description = "OS"; - ctx.host._.host = { host }: fixedTo { inherit host; } den.aspects.${host.aspect}; - ctx.host._.user = - { host, user }@ctx: - { - includes = [ (atLeast den.aspects.${host.aspect} ctx) ]; - }; - - ctx.host.into.default = lib.singleton; - ctx.host.into.user = { host }: map (user: { inherit host user; }) (lib.attrValues host.users); - - ctx.user.description = "OS user"; - ctx.user._.user = - { host, user }@ctx: - { - includes = [ (fixedTo ctx den.aspects.${user.aspect}) ]; - }; - - ctx.user.into.default = lib.singleton; - -in -{ - config.den.ctx = ctx; -} diff --git a/modules/context/user.nix b/modules/context/user.nix new file mode 100644 index 00000000..167e8a8b --- /dev/null +++ b/modules/context/user.nix @@ -0,0 +1,82 @@ +{ den, lib, ... }: +let + inherit (den.lib) take; + inherit (den.lib.parametric) fixedTo atLeast; + + ctx.user.description = '' + ## Context: den.ctx.user{host,user} + + User context stage is produced by Host for each user. + + This is a **continuation** of the pipeline started by `den.ctx.host`. + + IMPORTANT: The configuration obtained from `den.ctx.user` is provided to the Host OS level + + In Den, home-manager/hjem/maid are just forwarding classes that produce config at the OS + level: `home-manager.users.`, `hjem.users.`, `users.users.`, etc. + + A `den.ctx.user{host,user}` transitions unconditionally into `den.ctx.default{host,user}` + + A `den.ctx.user{host,user}` obtains OS configuration nixos/darwin by using `fixedTo{host,user} user-aspect`. + fixedTo takes: + - user-aspect's owned attrs + - static includes like { nixos.foo = ... } or ({ class, aspect-chain }: { nixos.foo = ...; }) + - atLeast{host,user} parametric includes like ({ host,user }: { nixos.foo = ...; }) + + ## Bidirectionality + + Battery `den.provides.bidirectional` can be included on each user that needs to take configuration from the Host. + + Enable per user: + den.aspects.tux.includes = [ den._.bidirectional ]; + + Enable for all users: + den.ctx.user.includes = [ den._.bidirectional ]; + + IMPORTANT: Enabling bidirectionality means that the following piepline is enabled: + + host-aspect{host} => user-aspect{host,user} => host-aspect{host,user} + + This means that any function at host-aspect.includes can be called: + - once when the host is obtaining its own configuration with context {host} + - once PER user that has bidirectionality enabled with context {host,user} + + Because of this, parametric aspects at host-aspect must be careful + + Instead of -in Nix both of these have the same functionArgs- + + ({host}: ...) + + or + + ({host, ...}: ...) + + Do this to prevent the function being invoked with `{host,user}` + + take.exactly ({host}: ...) + + Or this to avoid it being invoked with `{host}` + + take.atLeast ({host,user}: ...) + + Static aspects, -functions like `{class,aspect-chain}: ...`- at host-aspect.includes + have **no way** to distinguish when the calling context is `{host}` or `{host,user}` if + bidirectionality is enabled. + + Because of this, if you have such functions, they might produce duplicate values on list or + conflicting values on package types. A work around is to wrap them in a context-aware function: + + take.exactly ({host}: { includes = [ ({class, aspect-chain}: ...) ]; }) + + ''; + + ctx.user.into.default = lib.singleton; + ctx.user.provides.user = take.exactly from-user; + ctx.user.provides.bidirectional = take.exactly from-host; + + from-user = { host, user }: fixedTo { inherit host user; } den.aspects.${user.aspect}; + from-host = { host, user }: atLeast den.aspects.${host.aspect} { inherit host user; }; +in +{ + den.ctx = ctx; +} diff --git a/templates/ci/modules/features/auto-parametric.nix b/templates/ci/modules/features/auto-parametric.nix index e561557c..591f61a3 100644 --- a/templates/ci/modules/features/auto-parametric.nix +++ b/templates/ci/modules/features/auto-parametric.nix @@ -69,6 +69,7 @@ test-explicit-exactly-not-overridden-by-default = denTest ( { den, igloo, ... }: { + den.ctx.user.includes = [ den._.bidirectional ]; den.aspects.strict-helper = den.lib.parametric.exactly { includes = [ ( @@ -109,6 +110,7 @@ test-second-level-helper-owned-config-preserved = denTest ( { den, igloo, ... }: { + den.ctx.user.includes = [ den._.bidirectional ]; den.hosts.x86_64-linux.igloo.users.tux = { }; den.aspects.second-with-owned = { @@ -139,6 +141,7 @@ test-second-provides-helper-owned-config-preserved = denTest ( { den, igloo, ... }: { + den.ctx.user.includes = [ den._.bidirectional ]; den.hosts.x86_64-linux.igloo.users.tux = { }; den.aspects.second.provides.with-owned = { diff --git a/templates/ci/modules/features/batteries/define-user.nix b/templates/ci/modules/features/batteries/define-user.nix index 76425759..c6872aa2 100644 --- a/templates/ci/modules/features/batteries/define-user.nix +++ b/templates/ci/modules/features/batteries/define-user.nix @@ -26,6 +26,7 @@ { den.hosts.x86_64-linux.igloo.users.tux = { }; den.aspects.igloo.includes = [ den._.define-user ]; + den.ctx.user.includes = [ den._.bidirectional ]; expr = igloo.users.users.tux.isNormalUser; expected = true; } diff --git a/templates/ci/modules/features/conditional-config.nix b/templates/ci/modules/features/conditional-config.nix index b4b21ab3..5f83ff5a 100644 --- a/templates/ci/modules/features/conditional-config.nix +++ b/templates/ci/modules/features/conditional-config.nix @@ -41,6 +41,8 @@ users.tux.hasBar = true; }; + den.ctx.user.includes = [ den._.bidirectional ]; + den.aspects.igloo.includes = [ conditionalAspect ]; expr = igloo.something; diff --git a/templates/ci/modules/features/context/host-propagation.nix b/templates/ci/modules/features/context/host-propagation.nix index 938d4b21..1dc23043 100644 --- a/templates/ci/modules/features/context/host-propagation.nix +++ b/templates/ci/modules/features/context/host-propagation.nix @@ -20,6 +20,7 @@ { den.hosts.x86_64-linux.igloo.users.tux = { }; + den.ctx.user.includes = [ den._.bidirectional ]; den.aspects.igloo.funny.names = [ "host-owned" ]; den.aspects.igloo.includes = [ diff --git a/templates/ci/modules/features/deadbugs/external-namespace-deep-aspect.nix b/templates/ci/modules/features/deadbugs/external-namespace-deep-aspect.nix index 95b6f2ce..0ad049f5 100644 --- a/templates/ci/modules/features/deadbugs/external-namespace-deep-aspect.nix +++ b/templates/ci/modules/features/deadbugs/external-namespace-deep-aspect.nix @@ -73,6 +73,7 @@ test-functor-exactly-fires-only-in-user-context = denTest ( { + den, provider, igloo, ... @@ -86,6 +87,7 @@ ]; den.hosts.x86_64-linux.igloo.users.tux = { }; den.aspects.igloo.includes = [ provider.tools._.dev._.user-stamp ]; + den.ctx.user.includes = [ den._.bidirectional ]; expr = igloo.users.users.tux.description; expected = "user-of-igloo"; } diff --git a/templates/ci/modules/features/deadbugs/issue-201-forward-multiple-users.nix b/templates/ci/modules/features/deadbugs/issue-201-forward-multiple-users.nix index 88fc5294..37dd253b 100644 --- a/templates/ci/modules/features/deadbugs/issue-201-forward-multiple-users.nix +++ b/templates/ci/modules/features/deadbugs/issue-201-forward-multiple-users.nix @@ -13,6 +13,7 @@ }: { den.default.homeManager.home.stateVersion = "25.11"; + den.ctx.user.includes = [ den._.bidirectional ]; den.hosts.x86_64-linux.igloo.users = { tux = { }; diff --git a/templates/ci/modules/features/host-options.nix b/templates/ci/modules/features/host-options.nix index 1994707d..47ca8e4a 100644 --- a/templates/ci/modules/features/host-options.nix +++ b/templates/ci/modules/features/host-options.nix @@ -55,11 +55,9 @@ test-user-custom-username = denTest ( { den, igloo, ... }: { - den.hosts.x86_64-linux.igloo.users.tux = { - userName = "penguin"; - }; - den.default.homeManager.home.stateVersion = "25.11"; + den.hosts.x86_64-linux.igloo.users.tux.userName = "penguin"; den.aspects.igloo.includes = [ den._.define-user ]; + den.ctx.user.includes = [ den._.bidirectional ]; expr = igloo.users.users.penguin.isNormalUser; expected = true;