aspects is a flake-parts extension for arbitrarily nested module namespaces.
Every namespace can provide NixOS, Home Manager, nix-darwin, and flake-parts
modules while also aggregating matching modules from its children. Importing a
parent endpoint therefore imports all matching descendants unless aggregation
is restricted. The design is inspired by Rust's module system.
I use Aspects in my public Nix configuration.
{
inputs = {
flake-parts.url = "github:hercules-ci/flake-parts";
aspects = {
url = "gitlab:rubogubo/aspects";
inputs.flake-parts.follows = "flake-parts";
};
};
outputs = inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [ inputs.aspects.flakeModules.default ];
};
}An ordinary flake-parts module can define an aspect at any depth:
{
aspects.steam = {
nixos = { programs.steam.enable = true; };
home = { home.sessionVariables.STEAM_FRAME_FORCE_CLOSE = "1"; };
};
aspects.rubogubo.desktop.steam.nixos =
{ aspects, ... }:
{
imports = [ aspects.steam.nixos ];
programs.steam.remotePlay.openFirewall = true;
};
}Consume the specialized module with:
imports = [ inputs.self.aspects.rubogubo.desktop.steam.nixos ];Every namespace aggregates matching modules from its immediate children. Thus
inputs.self.aspects.rubogubo.desktop.nixos also includes the Steam module.
Use _include, _exclude, or _aggregate = false to control aggregation.
Modules reuse other modules through ordinary Nix imports; aspects provides
no separate inheritance mechanism. Every resolved endpoint injects the complete
tree as the aspects module argument, so aspect modules can import one another
without receiving self through specialArgs. The initial module that enters
the aspect tree still uses the flake output, or can receive
aspects = self.aspects through its evaluator's specialArgs.
modules/
├── steam/
│ ├── nixos.nix
│ └── home.nix
└── rubogubo/
└── desktop/
├── mod.nix
└── steam/
├── nixos.nix
└── home.nix
Load it from a flake-parts module:
{ inputs, ... }:
{
aspects = inputs.aspects.lib.loadTree ./modules;
}Or import a complete tree, including any aggregated flakeParts.nix modules:
imports = [
inputs.aspects.flakeModules.default
(inputs.aspects.lib.mkTree ./modules)
];The resolved tree is exported as self.aspects. Import an endpoint into a
machine configuration with inputs.self.aspects.<path>.<type>. Aspect modules
themselves receive the complete tree through the aspects module argument.
Unlike a generic recursive importer such as import-tree, Aspects does not only
combine every file into one module. It exposes typed, independently addressable
endpoints such as aspects.desktop.nixos, aspects.desktop.home, and
aspects.desktop.steam.nixos. Use a simpler import-tree utility if you only need
to import every discovered file together.
A directory's mod.nix is merged with its discovered children and typed module
files. It can be a plain attribute set or a function receiving children and
path:
{ children, path }:
{
_include = [ "steam" ];
nixos = {
networking.networkmanager.enable = true;
};
}children contains children discovered from both directories and dotted files.
path is the logical namespace path, including expansion of dotted directory
names.
_includeis an allowlist of immediate children to aggregate._excluderemoves immediate children from aggregation._aggregate = falsedisables child aggregation at that namespace.- Directories and files prefixed with
_are ignored by discovery.
Dots in discovered file and directory names are namespace separators. These layouts are equivalent:
common/nixos.nix
common.nixos.nix
Likewise, rubogubo.desktop/ is loaded as aspects.rubogubo.desktop.
Shorthands can be combined:
rubogubo.desktop/steam.nixos.nix
This defines aspects.rubogubo.desktop.steam.nixos. Empty components,
underscore-prefixed components, and module-type names used as namespace
components are rejected so path interpretation stays unambiguous.
If something is unclear, open an issue or message me on Matrix at
@rubogubo:matrix.org. If the project becomes popular, I will create a
dedicated room.