Skip to content

Instantly share code, notes, and snippets.

@sini
Last active May 15, 2026 18:11
Show Gist options
  • Select an option

  • Save sini/8a9b24923c7e35c930274c1a847753ac to your computer and use it in GitHub Desktop.

Select an option

Save sini/8a9b24923c7e35c930274c1a847753ac to your computer and use it in GitHub Desktop.
nixos-module-graph: Visualize non-nixpkgs import tree of any NixOS configuration with flake input labeling
# nixos-module-graph.nix — Visualize the non-nixpkgs import tree of a NixOS configuration
#
# Reads evalModules.graph (available on every nixosSystem result since nixpkgs 24.05+),
# resolves nix store paths back to flake input names, filters out nixpkgs base modules,
# and renders the remaining user-defined + third-party module import tree as indented text.
#
# Usage:
# nix eval --impure --raw --expr '(import /tmp/nixos-module-graph.nix { flakePath = toString /home/sini/Documents/repos/den-configs/slashfiles; host = "evo-x2"; })'
#
# Or edit the defaults below and run:
# nix eval --impure --raw -f nixos-module-graph.nix
#
{
flakePath ? toString /home/sini/Documents/repos/sini/nix-config,
host ? "bitstream",
}:
let
flake = builtins.getFlake flakePath;
graph = flake.nixosConfigurations.${host}.graph;
inputs = flake.inputs;
# --- Store path -> flake input name resolution ---
inputPairs = builtins.concatLists (map (name:
let path = builtins.tryEval (toString (inputs.${name}.outPath or inputs.${name}));
in if path.success then [{ storePath = path.value; inputName = name; }] else []
) (builtins.attrNames inputs))
++ [{ storePath = toString flake.outPath; inputName = "self"; }];
resolve = file:
let
matches = builtins.filter (p:
builtins.substring 0 (builtins.stringLength p.storePath) file == p.storePath
) inputPairs;
sorted = builtins.sort (a: b:
builtins.stringLength a.storePath > builtins.stringLength b.storePath
) matches;
best = if sorted != [] then builtins.head sorted else null;
in
if best != null then {
input = best.inputName;
rel = builtins.substring (builtins.stringLength best.storePath + 1) 9999 file;
}
else if builtins.match ".*flake\\.nix#(.*)" file != null then {
input = "self";
rel = builtins.head (builtins.match ".*flake\\.nix#(.*)" file);
}
else { input = "?"; rel = file; };
# --- Filtering ---
# Filter out all nixpkgs-* inputs (the NixOS base module set).
# These are ~4000+ modules that add noise. Everything else is kept:
# your own modules (self), flake inputs (agenix, disko, etc.), unknowns.
isNixpkgs = input: builtins.match "nixpkgs.*" input != null;
# --- Tree rendering ---
mkIndent = depth: builtins.concatLists (builtins.genList (_: [" " " "]) depth);
indent = depth: builtins.concatStringsSep "" (mkIndent depth);
# Walk the full graph tree. Nixpkgs nodes are skipped but their
# non-nixpkgs children are promoted up to the parent's level.
renderNode = depth: node:
let
r = resolve node.file;
skip = isNixpkgs r.input;
label = if r.rel != "" then "${r.input}:${r.rel}" else r.input;
nixpkgsChildren = builtins.length (
builtins.filter (c: isNixpkgs (resolve c.file).input) node.imports
);
nonNixpkgsImports = builtins.filter (c:
! isNixpkgs (resolve c.file).input
) node.imports;
nixNote = if nixpkgsChildren > 0
then " (+${toString nixpkgsChildren} nixpkgs)"
else "";
childLines = builtins.concatLists (
map (renderNode (depth + 1)) nonNixpkgsImports
);
in
if skip then
# Skip this node, but recurse to find non-nixpkgs children
builtins.concatLists (map (renderNode depth) nonNixpkgsImports)
else
["${indent depth}${label}${nixNote}"] ++ childLines;
lines = builtins.concatLists (map (renderNode 0) graph);
# Remove exact duplicate lines (from deferred module wrapper duplication
# and os->linux forwarding that produces identical entries).
# Use a fold that tracks seen lines by hash to handle special characters.
dedup = list:
let
go = acc: item:
let h = builtins.hashString "sha256" item;
in if acc.seen ? ${h}
then acc
else { seen = acc.seen // { ${h} = true; }; out = acc.out ++ [item]; };
in (builtins.foldl' go { seen = {}; out = []; } list).out;
in builtins.concatStringsSep "\n" (dedup lines)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment