Skip to content

Latest commit

 

History

69 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

srctree-nix

A Nix library for filesystem-based tree structures with functional algorithms.

Quick start

Add to your flake

Add srctree to your flake inputs:

inputs = {
  srctree.url = "github:z1-0/srctree-nix";
};

Example

Say you keep one Nix file per host in ./hosts, each exposing { enable, system, ... }. Load the tree, keep only the hosts that opted in, wrap each remaining host in nixosSystem, and expose them as nixosConfigurations:

{ srctree, nixpkgs }:

let
  lib = srctree.lib;

  # 1. Load the tree: ./hosts/foo.nix becomes a file Node
  hosts = lib.load ./hosts;

  # 2. Filter leaves: drop hosts with enable = false; dirs survive if any child does
  enabled = lib.alg.filterLeaves (node:
    node.content.enable or false
  ) hosts;

  # 3. Map leaves: turn each remaining host config into a NixOS system
  systems = lib.alg.mapLeaves (node:
    node // {
      content = nixpkgs.lib.nixosSystem {
        system = node.content.system;
        modules = [ node.content ];
      };
    }
  ) enabled;

  # 4. View the tree as an attribute set: systems.<hostname>.<attr>
  nixosConfigurations = lib.toAttrs systems;
in
  nixosConfigurations

Data structure

Every directory and .nix file is a Node with one of two shapes:

Directory node

{
  name = "dirname";
  type = "dir";
  path = /absolute/path/dir;
  children = [ ... ];          # child nodes
}

File node

Only .nix files are loaded (symlinks to them included). Everything else is skipped.

{
  name = "filename";
  type = "file";
  path = /absolute/path/f.nix;
  content = ...;               # evaluated result
}

Core API

lib.load

Load a directory of .nix files using import directly. Returns a root Node, or null if the directory does not exist; throws if the path exists but is not a directory.

lib.load :: Path -> Node | null

lib.toAttrs

toAttrs makes the tree read like a filesystem, haumea-style: each dir node also indexes its children, so the file at x/y/z.nix shows up as attrs.x.y.z, holding the Node itself. The tree itself doesn't change: name, type, path, and children stay, and every lib.alg function keeps working on the result. The one gotcha: the index is a snapshot of the tree at the moment toAttrs ran, so if you transform the tree afterwards, children updates but the index stays stale. Transform the tree first, then toAttrs last.

lib.toAttrs :: Node -> Attrs

Example:

lib.toAttrs (lib.load ./config-dir)
# {
#   name = "config-dir";
#   type = "dir";
#   path = /absolute/path/config-dir;
#   children = [ ... ];           # canonical, unchanged
#   foo = { ... };                # child dir, indexed by path segment
#   bar = <Node>;                 # child file node, indexed by path segment
# }

Warning

name, type, path, children, and content are reserved: a child with one of those names makes the colliding attribute a lazy error that fires only when touched, reminding you to rename the child.


Tree algorithms (lib.alg)

lib.alg.all / lib.alg.any

Test whether a predicate holds for every / at least one node.

lib.alg.all :: (Node -> Bool) -> Node -> Bool
lib.alg.any :: (Node -> Bool) -> Node -> Bool

Example (are all file contents positive?):

lib.alg.all (node: node.type != "file" || node.content > 0) tree

Example (is there any file with negative content?):

lib.alg.any (node: node.type == "file" && node.content < 0) tree

lib.alg.filter

Prune the tree by predicate. The predicate is tested against every node: a failed file is dropped; a failed dir is pruned with its whole subtree. Returns null if the root does not survive.

lib.alg.filter :: (Node -> Bool) -> Node -> (Node | null)

Example (keep every node except the dir named "sub"):

lib.alg.filter (node:
  node.type == "file" || node.name != "sub"
) tree

lib.alg.filterLeaves

Prune the tree by predicate, testing file nodes only. Dirs are kept automatically as long as at least one child survives.

lib.alg.filterLeaves :: (Node -> Bool) -> Node -> (Node | null)

Example (keep only files whose content is greater than 1):

lib.alg.filterLeaves (node:
  node.content > 1
) tree

lib.alg.find

Depth-first search for the first matching node (pre-order).

lib.alg.find :: (Node -> Bool) -> Node -> (Node | null)

Example (find the first file whose content is 42):

lib.alg.find (node: node.type == "file" && node.content == 42) tree

lib.alg.flatten

Flat list of all nodes (files and directories) via depth-first traversal (post-order).

lib.alg.flatten :: Node -> [Node]

Example (collect all paths):

map (node: node.path) (lib.alg.flatten tree)

lib.alg.fold

Depth-first fold (catamorphism). Good for aggregating metadata or counting.

lib.alg.fold :: (acc -> Node -> acc) -> acc -> Node -> acc

Example 1 (count all file nodes in the tree):

lib.alg.fold (acc: node:
  acc + (if node.type == "file" then 1 else 0)
) 0 tree

Example 2 (collect all node paths into a list):

lib.alg.fold (acc: node:
  acc ++ [ node.path ]
) [ ] tree

lib.alg.leaves

Flat list of file nodes only, depth first (post-order).

lib.alg.leaves :: Node -> [Node]

Example (list names of all Nix files):

map (fileNode: fileNode.name) (lib.alg.leaves tree)

lib.alg.map

Transform every node bottom-up (children first, then the parent). Tree structure stays intact.

lib.alg.map :: (Node -> Node) -> Node -> Node

Example (uppercase all file node content, assuming string content):

lib.alg.map (node:
  if node.type == "file"
  then node // { content = builtins.toUpper node.content; }
  else node
) tree

lib.alg.mapLeaves

Map a function over the file nodes only; dirs pass through untouched.

lib.alg.mapLeaves :: (Node -> Node) -> Node -> Node

Example (uppercase all file node content, assuming string content):

lib.alg.mapLeaves (node:
  node // { content = builtins.toUpper node.content; }
) tree

lib.alg.traverse

Low-level bottom-up traversal primitive used to build the combinators above. Receives one callback for file nodes and one for dir nodes (whose children are already transformed).

lib.alg.traverse :: (Node -> a) -> (Node -> a) -> Node -> a

Example (uppercase all file node content, dirs unchanged):

lib.alg.traverse
  (node: node // { content = builtins.toUpper node.content; })
  (node: node)
  tree

Related Projects

  • haumea: a filesystem-based module system for Nix.
  • metatree: builds on srctree, adding _meta metadata extracted from each .nix file's AST and stripped before evaluation.

About

A tree with algebraic operations for Nix

Topics

Resources

Stars

69 stars

Watchers

0 watching

Forks

Contributors

Languages