<aspect>.<class> transposition for Dendritic Nix#
On aspect oriented Dendritic setups, it is common to expose modules using flake.modules.<class>.<aspect>.
However, for humans, it might be more intuitive to use a transposed attrset <aspect>.<class>. Because it feels more natural to nest classes inside aspects than the other way around.
This project provides a transpose primitive, small and powerful enough to implement cross-aspect dependencies for any nix configuration class, and a flake-parts module for turning flake.aspects into flake.modules.
flake.aspects
|
|
flake.modules
|
Usage#
As a deps-free library from ./default.nix:#
Our transpose library takes an optional emit function that
can be used to ignore some items, modify them or produce many other items on its place.
let transpose = import ./default.nix { lib = pkgs.lib; }; in
transpose { a.b.c = 1; } # => { b.a.c = 1; }
This emit function is used by our aspects library
(both libs are flakes-independent) to provide cross-aspects same-class module dependencies.
As a Dendritic flake-parts module that provides the flake.aspects option:#
flake.aspectstransposes intoflake.modules.
# code in this example can (and should) be split into different dendritic modules.
{ inputs, ... }: {
imports = [ inputs.flake-aspects.flakeModule ];
flake.aspects = {
sliding-desktop = {
description = "nextgen tiling windowing";
nixos = { }; # configure Niri on Linux
darwin = { }; # configure Paneru on MacOS
};
awesome-cli = {
description = "enhances environment with best of cli an tui";
nixos = { }; # os services
darwin = { }; # apps like ghostty, iterm2
homeManager = { }; # fish aliases, tuis, etc.
nixvim = { }; # plugins
};
work-network = {
description = "work vpn and ssh access.";
nixos = {}; # enable openssh
darwin = {}; # enable MacOS ssh server
terranix = {}; # provision vpn
hjem = {}; # home link .ssh keys and configs.
}
};
}
Declaring cross-aspect dependencies#
flake.aspects also allow aspects to declare dependencies between them.
Of course each module can have its own imports, however aspect requirements
are aspect-level instead of module-level. Dependencies will ultimately resolve to
modules and get imported only when they exist.
In the following example, our development-server aspect can be applied into
linux and macos hosts.
Note that alice prefers to use nixos+homeManager, while bob likes darwin+hjem.
The development-server aspect is a "usability concern", it configures the exact same
development environment on two different OS.
When applied to a NixOS machine, the alice.nixos module will likely
configure the alice user, but there is no nixos user for bob.
{
flake.aspects = {config, ...}: {
development-server = {
requires = with config; [ alice bob ];
# without flake-aspects, you'd normally do:
# nixos.imports = [ inputs.self.modules.nixos.alice ];
# darwin.imports = [ inputs.self.modules.darwin.bob ];
};
alice = {
nixos = {};
};
bob = {
darwin = {};
};
};
}
It is out of scope for this library to create OS configurations. As you might have guessed, exposing configurations would look like this:
{ inputs, ... }:
{
flake.nixosConfigurations.fooHost = inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ inputs.self.modules.nixos.development-server ];
};
flake.darwinConfigurations.fooHost = inputs.darwin.lib.darwinSystem {
system = "aarm64-darwin";
modules = [ inputs.self.modules.darwin.development-server ];
};
}
Advanced aspect dependencies.#
You have already seen that an aspect can have a requires list:
# A foo aspect that depends on aspects bar and baz.
flake.aspects = { config, ... }: {
foo.requires = [ config.bar config.baz ];
}
cross-aspect requirements work like this:
When a module flake.modules.nixos.foo is requested (eg, included in a nixosConfiguration),
a corresponding module will be computed from flake.aspects.foo.nixos.
flake.aspects.foo.requires is a list of functions (providers)
that will be called with {aspect = "foo"; class = "nixos"} to obtain another aspect
providing a module having the same class (nixos in our example).
providers are a way of asking: if I have a (foo, nixos) module what other
aspects can you provide that have nixos modules to be imported in foo.
This way, it is aspects being included who decide what configuration must be used by its caller aspect.
by default, all aspects have a <aspect>.provides.itself function that ignores its argument
and always returns the <aspect> itself.
This is why you can use the with config; [ bar baz ] syntax.
They are actually [ config.bar.provides.itself config.baz.provides.itself ].
but you can also define custom providers that can inspect the argument's aspect and class
and return some another aspect accordingly.
flake.aspects.alice.provides.os-user = { aspect, class, ... }: {
# perhaps regexp matching on aspect or class. eg, match all "hosts" aspects.
nixos = { };
}
the os-user provider can be now included in a requires list:
flake.aspects = {config, ...}: {
home-server.requires = [ config.alice.provides.os-user ];
}
Testing#
nix run ./checkmate#fmt --override-input target .
nix flake check ./checkmate --override-input target . -L