A lean Nix flake framework — auto-discovery, namespaces, and module composition.
purr.nixcafe.org
purr turns your project directory structure into a fully wired Nix flake — no boilerplate, no manual wiring. Drop files into conventional directories and purr discovers everything: modules, packages, shells, checks, apps, overlays, templates, NixOS/darwin systems, home-manager homes, and a shared lib.
Compared to other flake auto-discovery tools, purr stays minimal — a single dependency (nixpkgs-lib, ~2 MB) and a small, focused API surface.
# flake.nix — that's it. No manual wiring needed.
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
purr.url = "https://flakehub.com/f/nixcafe/purr/0.1.*.tar.gz";
};
outputs = inputs:
inputs.purr.lib.mkFlake {
inherit inputs;
src = ./.;
};
}Put your code in the right directories, and purr wires your entire flake:
.
├── flake.nix
├── modules/ → nixosModules, darwinModules, homeModules
├── packages/ → packages.<system>.* (also registered into pkgs.*)
├── legacyPackages/ → legacyPackages.<system>.*
├── shells/ → devShells.<system>.*
├── checks/ → checks.<system>.*
├── apps/ → apps.<system>.*
├── overlays/ → overlays.*
├── templates/ → templates.*
├── formatters/ → formatter.<system>
├── hydraJobs/ → hydraJobs.* (CI jobs, opt-in)
├── lib/ → lib.<namespace>.* (shared across all modules)
├── systems/ → nixosConfigurations, darwinConfigurations
└── homes/ → homeConfigurations
Scaffold a new purr project in seconds:
# Standalone (mkFlake)
nix flake init -t github:nixcafe/purr
# With flake-parts
nix flake init -t github:nixcafe/purr#flake-partspurr offers two integration modes — pick the one that fits your workflow.
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
purr.url = "github:nixcafe/purr";
};
outputs = inputs:
inputs.purr.lib.mkFlake {
inherit inputs;
src = ./.;
namespace = "myproject";
outputsBuilder = { pkgs, ... }: {
formatter = pkgs.nixfmt;
};
};
}{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
purr.url = "github:nixcafe/purr";
};
outputs = inputs:
inputs.flake-parts.lib.mkFlake { inherit inputs; } {
imports = [ inputs.purr.flakeModules.default ];
purr = {
enable = true;
src = ./.;
namespace = "myproject";
};
};
}Drop modules into modules/ — purr discovers them by subdirectory convention:
modules/
├── nixos/services/openssh/default.nix → nixosModules.services.openssh
├── darwin/system/defaults/default.nix → darwinModules.system.defaults
├── home/programs/git/default.nix → homeModules.programs.git
└── shared/users/default.nix → merged into nixos + darwin
Customize the mapping with moduleTypes:
moduleTypes = {
nixos = ["nixos" "shared" "container"];
};purr injects a namespace parameter into every module, scoping options under config.<namespace>.* — no more global option collisions:
{ config, lib, namespace, ... }:
let
cfg = config.${namespace}.my-module;
in
{
options.${namespace}.my-module.enable = lib.mkEnableOption "my module";
config = lib.mkIf cfg.enable { /* ... */ };
}Create a lib/ directory to share functions across all modules, packages, shells, and checks:
# lib/default.nix
{ lib, inputs, namespace }:
{
keys = import ./keys.nix { inherit lib inputs namespace; };
utils = import ./utils.nix { inherit lib inputs namespace; };
}Access anywhere with lib.<namespace>.keys, lib.<namespace>.utils, etc.
Modules under packages/ become per-system packages
(packages.<system>.<name>) and are auto-registered into pkgs, so any
module can reference them as pkgs.<name> — no imports or overlays to wire:
# packages/hello/default.nix
{ lib, ... }: { /* ... */ }# checks/lint/default.nix
{ pkgs, ... }: pkgs.hello # the discovered packageEach package is wrapped like a callPackage package (.override /
.overrideAttrs work) and added to pkgs through an overlay built on the
package-set fixpoint, so packages may depend on each other:
# packages/wrapper/default.nix — `hello` resolves from `pkgs`
{ hello, ... }: hello.override { /* ... */ }One-way dependencies resolve lazily; mutual cycles are an error, as with any
nixpkgs overlay. Disable registration with packagesToPkgs = false (packages
stay available as packages.<system>.<name>). Registration applies to both
mkFlake and the flake-parts module.
purr auto-discovers NixOS/darwin systems and home-manager homes using <arch>-<format>/<name>:
systems/
├── x86_64-linux/server/default.nix → nixosConfigurations.server
└── aarch64-darwin/macbook/default.nix → darwinConfigurations.macbook
homes/
└── x86_64-linux/alice@server/default.nix → homeConfigurations."alice@server"
Each host can carry host meta — a meta.nix next to its default.nix, or hosts.<name>.meta in the flake config. Declaring images in the meta makes the host an image-only recipe (e.g. installer ISOs): it is excluded from nixosConfigurations/darwinConfigurations unless you also set deployable = true. Its images are exposed as a top-level images.<host>.<format> output:
# systems/x86_64-linux/installer/meta.nix
{
images = [ "iso" ];
}The merged meta (auto-generated keys + your custom keys) is injected into the host's modules as purr.meta.
Homes named <user>@<host> auto-link to matching hosts. No extra wiring needed.
# systems/x86_64-linux/server/default.nix
{ config, pkgs, lib, purr, ... }:
{
networking.hostName = purr.meta.name; # "server"
# ...auto-injects home-manager config for alice@server automatically
}Forward home-manager config from any NixOS module via the namespace bridge — set purr.users.<name>.homeConfig on a host with linked homes:
{ config, pkgs, lib, ... }:
{
purr.users.alice.homeConfig = {
home.packages = [ pkgs.cowsay ];
programs.git.userName = "Alice";
};
}| purr | Other frameworks | |
|---|---|---|
| Dependencies | 1 (nixpkgs-lib, ~2 MB) |
3+ (flake-parts, flake-utils, ...) |
| API surface | Single mkFlake call or one flake-parts module |
Multiple nested imports |
| Option namespace | Built-in namespace injection |
Manual config.<name>. prefixing |
| Systems & homes | Auto-discovered, auto-linked | Manual wiring |
| Custom lib | lib.<namespace>.* propagated everywhere |
Manual import passthrough |
Tests live under tests/ and are auto-discovered — drop a
test-*.nix into tests/unit/ (1:1 with a lib/ module) or
tests/integration/ (real-project runs against tests/integration/project)
and it is picked up automatically.
nix flake check # CI gate: pre-commit + purr-tests
nix build .#checks.x86_64-linux.purr-tests && cat result # full per-test report
tests/run-tests.sh # run directly, live per-test output
nix develop --command purr-test # same, from the dev shellThe purr-tests check writes the full report into its result; if a test
fails the build fails and nix log shows the failing cases. Unit tests run
hermetically with nixpkgs-lib; integration tests mock nixosSystem /
homeManagerConfiguration to evaluate the generated configs through the real
lib.evalModules without pulling a full nixpkgs.
Full documentation at purr.nixcafe.org — flake-parts integration, full API reference, directory layout guide, and more.