Skip to content

vic/flake-aspects

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

55 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Nix Flake CI Status License

<aspect>.<class> Transposition for Dendritic Nix

In aspect-oriented Dendritic setups, it is common to expose modules using the structure flake.modules.<class>.<aspect>.

However, for many users, a transposed attribute set, <aspect>.<class>, can be more intuitive. It often feels more natural to nest classes within aspects rather than the other way around.

This project provides a small, dependency-free transpose primitive that is powerful enough to implement cross-aspect dependencies for any Nix configuration class. It also includes a flake-parts module that transforms flake.aspects into flake.modules.

flake.aspects
{
  vim-btw = {
    nixos = ...;
    darwin = ...;
    homeManager = ...;
    nixvim = ...;
  };
  tiling-desktop = {
    nixos = ...;
    darwin = ...;
  };
  macos-develop = {
    darwin = ...;
    hjem = ...;
  };
}
image flake.modules
{
  nixos = {
    vim-btw = ...;
    tiling-desktop = ...;
  };
  darwin = {
    vim-btw = ...;
    tiling-desktop = ...;
    macos-develop = ...;
  };
  homeManager = {
    vim-btw = ...;
  };
  hjem = {
    macos-develop = ...;
  };
  nixvim = {
    vim-btw = ...;
  };
}

Unlike flake.modules.<class>.<aspect> which is flat, aspects can be nested forming a tree by using the provides (short alias: _) attribute. Each aspect can also specify a list of includes of other aspects, forming a graph of dependencies.

{
  flake.aspects = {
    gaming = {
      nixos  = {};
      darwin = {};

      provides.emulation = { aspect, ... }: {
        nixos = {};

        _.nes.nixos = {};
        _.gba.nixos = {};

        includes = with aspect._; [ nes gba ];
      };
    };
  };
}

Usage

The library can be used in two ways: as a flakes-independent dependency-free utility or as a flake-parts module.

As a Dependency-Free Library (./nix/default.nix)

The core of this project is the transpose function, which is powerful enough to implement cross-aspect dependencies for any Nix configuration class. It accepts an optional emit function that can be used to ignore items, modify them, or generate multiple items from a single input.

let transpose = import ./nix/default.nix { lib = pkgs.lib; }; in
transpose { a.b.c = 1; } # => { b.a.c = 1; }

This emit function is utilized by the aspects library to manage module dependencies between different aspects of the same class. Both transpose and aspects are independent of flakes.

Use aspects without flakes.

It is possible to use the aspects system as a library, without flakes. This can be used, for example, to avoid poluting flake-parts' flake.modules or by libraries that want to create own isolated aspects scope. For examples of this, see our own flake-parts integration, and how den creates its own den.aspects scope independent of flakes.aspects/flake.modules.

As a Dendritic Flake-Parts Module (flake.aspects option)

When used as a flake-parts module, the flake.aspects option is automatically transposed into flake.modules, making the modules available to consumers of your flake.

# The code in this example can (and should) be split into different Dendritic modules.
{ inputs, ... }: {
  imports = [ inputs.flake-aspects.flakeModule ];
  flake.aspects = {

    sliding-desktop = {
      description = "Next-generation tiling windowing";
      nixos  = { }; # Configure Niri on Linux
      darwin = { }; # Configure Paneru on macOS
    };

    awesome-cli = {
      description = "Enhances the environment with the best of CLI and 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

Aspects can declare dependencies on other aspects using the includes attribute. This allows you to compose configurations in a modular way.

Dependencies are defined at the aspect level, not within individual modules. When a module from an aspect is evaluated (e.g., flake.modules.nixos.development-server), the library resolves all dependencies for the nixos class and imports the corresponding modules if they exist.

In the example below, the development-server aspect includes the alice and bob aspects. This demonstrates how to create a consistent development environment across different operating systems and user configurations.

{
  flake.aspects = { aspects, ... }: {
    development-server = {
      # This aspect now includes modules from 'alice' and 'bob'.
      includes = with aspects; [ alice bob ];

      # Without flake-aspects, you would have to do this manually for each class.
      # nixos.imports  = [ inputs.self.modules.nixos.alice ];
      # darwin.imports = [ inputs.self.modules.darwin.bob ];
    };

    alice = {
      nixos = {};
      homeManager = {};
    };

    bob = {
      darwin = {};
      hjem = {};
    };
  };
}

Creating the final OS configurations is outside the scope of this library—for that, see vic/den. However, exposing them 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 = "aarch64-darwin";
    modules = [ inputs.self.modules.darwin.development-server ];
  };
}

Advanced Aspect Dependencies: Providers

Dependencies are managed through a powerful abstraction called providers. A provider is a value that returns an aspect object, which can then supply modules to the aspect that includes it.

A provider can be either a static aspect object or a function that dynamically returns one. This mechanism enables sophisticated dependency chains, conditional logic, and parameterization.

The Default Provider: provides.itself

Every aspect automatically has a default provider called itself, located at <aspect>.provides.itself. This provider simply returns the aspect itself.

The with aspects; [ bar baz ] syntax is a convenient shorthand that relies on this default:

# A 'foo' aspect that depends on 'bar' and 'baz' aspects.
flake.aspects = { aspects, ... }: {
  foo.includes = with aspects; [ bar baz ];
  # This is equivalent to:
  # foo.includes = [ aspects.bar.provides.itself aspects.baz.provides.itself ];
}

Custom Providers

You can define custom providers to implement more complex logic. A provider function receives the current class (e.g., "nixos") and the aspect-chain (the list of aspects that led to the call). This allows a provider to act as a conditional proxy or router for dependencies.

In this example, the kde-desktop aspect defines a custom karousel provider that only returns a module if certain conditions are met:

flake.aspects.kde-desktop.provides.karousel = { aspect-chain, class }:
  if someCondition aspect-chain && class == "nixos" then { nixos = { ... }; } else { };

The karousel provider can then be included in another aspect:

flake.aspects = { aspects, ... }: {
  home-server.includes = [ aspects.kde-desktop.provides.karousel ];
}

This pattern allows an included aspect to determine which configuration its caller should use, enabling a tree of dependencies where each node can be either static or parametric.

Parameterized Providers

Providers can be implemented as curried functions, allowing you to create parameterized modules. This is useful for creating reusable configurations that can be customized at the inclusion site.

For real-world examples, see how vic/den defines auto-imports and home-managed parametric aspects.

flake.aspects = { aspects, ... }: {
  system = {
    nixos.system.stateVersion = "25.11";
    provides.user = userName: {
      darwin.system.primaryUser = userName;
      nixos.users.${userName}.isNormalUser = true;
    };
  };

  home-server.includes = [
    aspects.system
    (aspects.system.provides.user "bob")
  ];
}

See the aspects."test provides" and aspects."test provides using fixpoints" sections in the checkmate tests for more examples of chained providers.

The _ Alias for provides

For convenience, _ is an alias for provides. This allows for more concise chaining of providers. For example, foo.provides.bar.provides.baz can be written as foo._.bar._.baz.

Testing

nix run ./checkmate#fmt --override-input target .
nix flake check ./checkmate --override-input target . -L

About

fake.modules transposition for aspect-oriented Dendritic Nix. with cross-aspect dependencies.

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

  •  

Packages

No packages published

Languages