A library to facilitate complex multi-host NixOS configurations.
prism README.md
9.0 kB
Markdown
at main

Development takes place on Tangled, however, I do maintain a GitHub mirror. Please make PRs to the Tangled repository.

prism #

prism is a library for multi-host NixOS and nix-darwin configurations. Rather than writing a configuration per host, you describe your machines with tags, and every module splits (or refracts) its configuration across those tags.

# flake.nix
tags = self: {
    graphical = {};
    laptop = {
        build = true;
        parents = [self.graphical];
    };
    desktop = {
        build = true;
        parents = [self.graphical];
    };
    server.build = true;
};
# modules/bluetooth.nix
{
    graphical.hardware.bluetooth.enable = true;
    desktop.hardware.bluetooth.powerOnBoot = true;
}

laptop and desktop get bluetooth, only desktop powers it on at boot, and server gets neither.

Mental Model #

Think of tags like CSS classes and modules like stylesheets: a machine is built from the tags it has, and each module section is a rule scoped to one tag. A tag inherits every section its ancestors define through parents, the same way an element inherits rules from every class it carries.

Sections don't override each other, they stack: graphical and desktop can each add their own bluetooth config to the same machine, just like two CSS classes both styling the same element. Settings like pkgs or mkSystem are different, they're single-valued, so something has to win. There the closer tag overrides the one further up the tree, but if a tag would inherit a setting from two ancestors that aren't related to each other, prism refuses to guess, unlike CSS's "last one wins." That's an error, and you fix it by setting the value directly on the tag doing the inheriting.

select is the other piece of the model: it lets a tag decide, per module, whether it's active, rather than being all-or-nothing across every module it touches.

Features #

Tags with multiple parents #

A tag selects its own sections along with those of every ancestor. Tags can have any number of parents, so machines are described by what they are rather than by which list they appear in.

tags = self: {
    graphical = {};
    workstation.parents = [self.graphical];
    gaming.parents = [self.graphical];
    laptop = {
        build = true;
        parents = [self.workstation];
    };
    desktop = {
        build = true;
        parents = [self.workstation self.gaming];
    };
    deck = {
        build = true;
        parents = [self.gaming];
    };
};

Conditional tags #

A tag with select decides whether it's active on a per-module basis. Here, chaotic applies to every system with the tag, except in modules which also configure gaming, where it only applies to gaming systems.

chaotic.select = {module, rootTags, ...}:
    !(module ? ${self.gaming.name}) || builtins.elem self.gaming.name rootTags;

Settings per tag #

pkgs, mkSystem, and specialArgs are set once in mkSystems and may be overridden by any tag. Systems inherit them from their most specific tag which sets them.

tags = self: {
    arm.pkgs = nixpkgs.legacyPackages.aarch64-linux;
    desktop = {
        build = true;
        parents = [self.arm];
    };
    mac = {
        build = true;
        pkgs = nixpkgs.legacyPackages.aarch64-darwin;
        mkSystem = nix-darwin.lib.darwinSystem;
    };
};

Modules per tag #

Tags can bring their own modules and extraModules, so external modules follow the tag which uses them instead of being listed for every host.

agenix.extraModules = [agenix.nixosModules.default];

Tutorials #

Installation #

Flakes #

  1. Add prism to your inputs:
{
    inputs = {
        nixpkgs.url = "github:nixos/nixpkgs?ref=nixpkgs-unstable";
        prism.url = "git+https://codeberg.org/poacher/prism.git";
    };
}
  1. Create your NixOS configurations with mkSystems:
{
    outputs = {nixpkgs, prism, ...}: {
        nixosConfigurations = prism.lib.mkSystems {
            mkSystem = nixpkgs.lib.nixosSystem;
            pkgs = nixpkgs.legacyPackages.x86_64-linux;
            modules = prism.lib.recursivelyImport [./modules];
            tags = self: {
                # ...
            };
        };
    };
}

Non-Flakes #

  1. Pin prism with your pinner of choice, I'll be using npins:
npins add git https://tangled.org/poacher.dev/prism -b main
  1. Import prism in your NixOS entry-point:
let 
    inherit (sources) nixpkgs;
    sources = import ./npins;
    prism = import sources.prism;
in {
    # ...
}
  1. Create your NixOS configurations using mkSystems:
let
    # ...
in {
    nixosConfigurations = prism.mkSystems {
        mkSystem = import "${nixpkgs}/nixos/lib/eval-config.nix";
        pkgs = import nixpkgs {};
        modules = prism.recursivelyImport [./modules];
        tags = self: {
            # ...
        };
    };
}

Getting Started #

  1. Describe your machines. Every tag with build = true becomes a system, and the all preset allows them to share configs:
tags = self: {
    inherit (prism.lib.presets) all;
    graphical.parents = [self.all];
    laptop = {
        build = true;
        parents = [self.all self.graphical];
    };
    server = {
        build = true;
        parents = [self.all];
    };
};
  1. Write a module. Each section is standard NixOS configuration, keyed by the tag it applies to:
# modules/desktop-environment.nix
{
    all.services.openssh.enable = true;
    laptop.services.power-profiles-daemon.enable = true;
    graphical = {pkgs, ...}: {
        services.desktopManager.plasma6.enable = true;
        environment.systemPackages = [pkgs.firefox];
    };
}

Reference #

mkSystems #

mkSystems creates a system for every tag with build = true. It accepts the following arguments:

  1. tags a function from the finished tag set (self) to tag definitions.
  2. modules ? [] prism module files.
  3. extraModules ? [] non-prism modules imported into every system.
  4. mkSystem the function used to create each system. On NixOS this should be nixpkgs.lib.nixosSystem, on Darwin this should be nix-darwin.lib.darwinSystem.
  5. pkgs ? null the nixpkgs instance, set as nixpkgs.pkgs.
  6. specialArgs ? {} arguments passed to every module.

Tags #

Each tag is an attribute set which may contain:

  1. parents ? [] tags whose sections are also selected, referenced through self (e.g. self.graphical).
  2. build whether this tag is a system. Defaults to true if the tag is a child tag (no other tag lists it as a parent) and doesn't have a select, otherwise false.
  3. select a function deciding, per module, whether this tag is active.
  4. mkSystem, pkgs, specialArgs overrides of the mkSystems defaults.
  5. modules, extraModules additions to the mkSystems lists.

select #

select receives the following and returns a bool:

  1. root the tag being built.
  2. rootTags the names of root and all of its ancestors.
  3. tag the tag being selected.
  4. module the prism module being selected from.
  5. path the path of that module.
  6. tags every tag.

Modules #

Modules are attribute sets of tag sections. Each section is standard configuration passed to mkSystem which only applies to systems where its tag is selected.

enable ? true is reserved on every module and determines whether or not a module is evaluated.

{
    # Doesn't get evaluated, won't install hyprland
    enable = false;
    all.programs.hyprland.enable = true;
}

lib.presets #

  1. all a tag to be used as a parent of every system.
  2. these a tag which is active when the module has a section for any of the built tag's tags, other than all, these, and others.
  3. others a tag which is active when these isn't.

lib.closureOf #

closureOf returns the names of a tag and all of its parents. It can be used to pass a system's tags to its modules:

laptop = {
    build = true;
    specialArgs.tags = prism.lib.closureOf self.laptop;
};

lib.childTagsOf #

childTagsOf returns the names of every tag in a tag set that isn't used as another tag's parent. mkSystems uses this to default build for child tags:

tags = self: {
    # ...
    specialArgs.childTags = prism.lib.childTagsOf self;
};

lib.recursivelyImport #

recursivelyImport returns every .nix file within a list of paths. Files starting with _ are ignored.

readOnlyPkgs #

On NixOS, importing nixpkgs.nixosModules.readOnlyPkgs through extraModules is recommended as it prevents modules from reconfiguring pkgs.

Living Examples #

prism is used in the following configs:

  1. mine

If you would like your config added here then please open an issue or PR.

History #

This project was formerly known as booyah. It has been renamed to prism after I got peer-pressured.