Skip to content
Oeiuwq Faith Blog OpenSource Porfolio

Dauliac/nix-lib

null

Dauliac/nix-lib.json
{
"createdAt": "2026-01-29T02:25:23Z",
"defaultBranch": "main",
"description": null,
"fullName": "Dauliac/nix-lib",
"homepage": null,
"language": "Nix",
"name": "nix-lib",
"pushedAt": "2026-06-10T18:10:59Z",
"stargazersCount": 13,
"topics": [],
"updatedAt": "2026-06-10T18:11:18Z",
"url": "https://github.com/Dauliac/nix-lib"
}

A Nix library framework implementing the Lib Modules Pattern - where library functions are defined as module options with built-in types, tests, and documentation.

Writing Nix libraries typically means:

  • Functions scattered across files with no consistent structure
  • Tests living separately (or not existing at all)
  • Types and documentation as afterthoughts
  • No standard way to compose libraries

Define functions as config values that bundle everything together:

nix-lib.lib.double = {
type = lib.types.functionTo lib.types.int;
fn = x: x * 2;
description = "Double a number";
tests."doubles 5" = { args.x = 5; expected = 10; };
};

This gives you:

  • Type safety - explicit Nix types for your functions
  • Built-in testing - tests live with the code (nix-unit integration)
  • Documentation - descriptions in one place
  • Composition - use the NixOS module system to combine libraries
  • Nested propagation - libs from nested modules (home-manager in NixOS) are accessible in parent scope

nlib.mkFlake is the main entry point. It evaluates lib modules and optionally integrates with flake-parts:

{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
nlib.url = "github:Dauliac/nlib";
};
outputs = inputs:
inputs.nlib.mkFlake {
inherit inputs;
modules = [ ./libs/math.nix ];
flake-parts = inputs.flake-parts; # Optional: enables flake-parts integration
} {
systems = [ "x86_64-linux" "aarch64-linux" ];
perSystem = { lib, pkgs, ... }: {
# lib.math.* available in OPTIONS phase!
packages.default = pkgs.writeText "result"
"double 5 = ${toString (lib.math.double 5)}";
};
};
}
libs/math.nix
{ lib, config, ... }: {
lib.math.double = {
fn = x: x * 2;
description = "Double a number";
tests."doubles 5" = { args.x = 5; expected = 10; };
};
# Self-referencing via config
lib.math.quadruple = {
fn = x: config.lib.math.double.fn (config.lib.math.double.fn x);
description = "Quadruple using double";
};
}
{
outputs = inputs:
inputs.nlib.mkFlake {
inherit inputs;
modules = [ ./libs/math.nix ];
} {
packages.x86_64-linux.default = ...;
};
}
inputs.nlib.mkFlake {
inherit inputs;
modules = [
./libs/math.nix # Your lib modules
{ inherit soonix; } # External: soonix.lib -> lib.soonix.*
{ custom = otherLib; } # Renamed: otherLib.lib -> lib.custom.*
];
flake-parts = inputs.flake-parts;
} { ... }
{
inputs.nlib.url = "github:Dauliac/nlib";
outputs = { nlib, ... }:
nlib.inputs.flake-parts.lib.mkFlake { inherit inputs; } {
imports = [ nlib.flakeModules.default ];
# Define a pure flake-level lib
nix-lib.lib.double = {
type = lib.types.functionTo lib.types.int;
fn = x: x * 2;
description = "Double a number";
tests."doubles 5" = { args.x = 5; expected = 10; };
};
};
}

See examples/ and tests/scenarios/ for complete working examples.

┌──────────────────────────────────────────────────────────┐
│ nlib.mkFlake │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 1. Evaluate lib modules (BEFORE flake-parts) │ │
│ │ → produces lib.* │ │
│ └──────────────────────┬─────────────────────────────┘ │
│ │ inject into lib │
│ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 2. flake-parts.lib.mkFlake (if provided) │ │
│ │ specialArgs.lib = nixpkgs.lib // evaluatedLibs │ │
│ │ → lib.* available in OPTIONS phase! │ │
│ └──────────────────────┬─────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 3. NixOS/home-manager adapters │ │
│ │ → config.lib.* │ │
│ └────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
OptionTypeDescription
inputsattrsetFlake inputs (required)
moduleslistLib modules to evaluate
flake-partsinputOptional: flake-parts input for integration

Lib modules are NixOS-style modules that define lib.*:

{ lib, config, ... }: {
lib.<namespace>.<name> = {
fn = ...; # Required: the function
description = "..."; # Optional: documentation
tests = { ... }; # Optional: test cases
type = ...; # Optional: type signature
visible = true; # Optional: public/private
};
}

For libs that follow the soonix pattern (input.lib = { pkgs }: { ... }), use nix-lib.imports in perSystem:

perSystem = { pkgs, config, ... }: {
nix-lib.imports = [
{ inherit soonix; } # soonix.lib { inherit pkgs; } -> config.lib.soonix.*
{ inherit anotherLib; } # -> config.lib.anotherLib.*
{ custom = someLib; } # -> config.lib.custom.*
];
# Now available:
devShells.default = pkgs.mkShell {
shellHook = config.lib.soonix.mkShellHook { ... };
};
};

For pure libs (no pkgs needed), use nix-lib.imports at flake level:

nix-lib.imports = [
{ inherit pureLib; } # pureLib.lib.* -> flake.lib.pureLib.*
];

Define libs at nix-lib.lib.<name> (supports nested namespaces like nix-lib.lib.utils.helper):

nix-lib.lib.myFunc = {
type = lib.types.functionTo lib.types.int; # Required: function signature
fn = x: x * 2; # Required: implementation
description = "What it does"; # Required: documentation
tests."test name" = { # Optional: test cases
args.x = 5;
expected = 10;
};
visible = true; # Optional: public (true) or private (false)
};
flowchart TB
subgraph Input["Define (nix-lib.lib.*)"]
D1["nix-lib.lib.myFunc = {<br/>type, fn, description, tests}"]
end
subgraph Process["nix-lib Processing"]
P1["Extract fn → config.lib.*"]
P2["Extract tests → flake.tests"]
P3["Store metadata → nix-lib._libsMeta"]
end
subgraph Output["Outputs"]
O1["config.lib.myFunc<br/>(use in module)"]
O2["flake.lib.namespace.myFunc<br/>(flake export)"]
O3["flake.tests.test_myFunc_*<br/>(nix-unit tests)"]
end
D1 --> P1
D1 --> P2
D1 --> P3
P1 --> O1
P1 --> O2
P2 --> O3
flowchart TB
subgraph Define["Define (nix-lib.lib.*)"]
D1[type + fn + description + tests]
end
subgraph Use["Use (config.lib.*)"]
U1[NixOS config.lib.foo]
U2[home-manager config.lib.bar]
U3[nixvim config.lib.baz]
end
subgraph Propagate["Nested Propagation"]
P1[NixOS config.lib.home.*]
P2[NixOS config.lib.home.vim.*]
end
subgraph Export["Flake Export (flake.lib.*)"]
E1[flake.lib.nixos.*]
E2[flake.lib.home.*]
E3[flake.lib.vim.*]
end
D1 --> U1
D1 --> U2
D1 --> U3
U2 --> P1
U3 --> P2
U1 --> E1
U2 --> E2
U3 --> E3

Libs defined in different module systems are available at different paths:

Defined inModule to importAccess within moduleFlake output
flake-parts nix-lib.lib.*flakeModules.defaultconfig.lib.flake.<name>flake.lib.flake.<name>
perSystem nix-lib.lib.*flakeModules.defaultconfig.lib.<name>legacyPackages.<system>.nix-lib.<name>
Defined inModule to importAccess within moduleFlake output
NixOS nix-lib.lib.*nixosModules.defaultconfig.lib.<name>flake.lib.nixos.<name>
home-manager nix-lib.lib.*homeModules.defaultconfig.lib.<name>flake.lib.home.<name>
nix-darwin nix-lib.lib.*darwinModules.defaultconfig.lib.<name>flake.lib.darwin.<name>
nixvim nix-lib.lib.*nixvimModules.defaultconfig.lib.<name>flake.lib.vim.<name>
system-manager nix-lib.lib.*systemManagerModules.defaultconfig.lib.<name>flake.lib.system.<name>

When a parent module imports a nested module system, the nested libs are automatically accessible in the parent scope under a namespace prefix.

flowchart LR
subgraph NixOS
N[config.lib.*]
end
subgraph home-manager
H[nix-lib.lib.*]
end
subgraph nixvim
V[nix-lib.lib.*]
end
H -->|home.*| N
V -->|vim.*| H
V -->|home.vim.*| N
Parent moduleNested moduleLibs defined in nestedAccess in parent
NixOShome-managernix-lib.lib.fooconfig.lib.home.foo
NixOShome-manager → nixvimnix-lib.lib.barconfig.lib.home.vim.bar
nix-darwinhome-managernix-lib.lib.fooconfig.lib.home.foo
nix-darwinhome-manager → nixvimnix-lib.lib.barconfig.lib.home.vim.bar
home-managernixvimnix-lib.lib.barconfig.lib.vim.bar
Module systemNamespace prefix
home-managerhome
nixvimvim
nix-darwindarwin
system-managersystem

All libs are collected and exported at the flake level under flake.lib.<namespace>:

NamespaceSourceDescription
flake.lib.flake.*nix-lib.lib.* in flake-partsPure flake-level libs
flake.lib.nix-lib.*nix-lib internalsmkAdapter, backends utilities
flake.lib.nixos.*nixosConfigurations.*.config.lib.*NixOS configuration libs
flake.lib.home.*homeConfigurations.*.config.lib.*Standalone home-manager libs
flake.lib.darwin.*darwinConfigurations.*.config.lib.*nix-darwin libs
flake.lib.vim.*nixvimConfigurations.*.config.lib.*Standalone nixvim libs
flake.lib.system.*systemConfigs.*.config.lib.*system-manager libs
flake.lib.wrappers.*wrapperConfigurations.*.config.lib.*nix-wrapper-modules libs

Per-system libs are available at legacyPackages.<system>.lib.<namespace>.*.

Import the adapter for your module system. Libs are automatically available at config.lib.*:

ModuleImport path
flakeModules.defaultinputs.nix-lib.flakeModules.default
nixosModules.defaultnix-lib.nixosModules.default
homeModules.defaultnix-lib.homeModules.default
darwinModules.defaultnix-lib.darwinModules.default
nixvimModules.defaultnix-lib.nixvimModules.default
systemManagerModules.defaultnix-lib.systemManagerModules.default
wrapperModules.defaultnix-lib.wrapperModules.default
tests."test name" = {
args.x = 5; # Argument passed to fn
expected = 10; # Expected return value
};
tests."test name" = {
args.x = { a = 2; b = 3; }; # For fn = { a, b }: a + b
expected = 5;
};
tests."test name" = {
args.x = 5;
assertions = [
{ name = "is positive"; check = result: result > 0; }
{ name = "is even"; check = result: lib.mod result 2 == 0; }
{ name = "equals 10"; expected = 10; }
];
};

nix-lib supports wrapper-based module systems that create wrapped executables:

Both use lib.evalModules internally, making them compatible with nix-lib’s adapter system.

{
inputs = {
nix-lib.url = "github:Dauliac/nix-lib";
nix-wrapper-modules.url = "github:BirdeeHub/nix-wrapper-modules";
# Or: wrappers.url = "github:Lassulus/wrappers";
};
outputs = { nixpkgs, nix-lib, nix-wrapper-modules, ... }:
nix-lib.inputs.flake-parts.lib.mkFlake { inherit inputs; } {
imports = [ nix-lib.flakeModules.default ];
# Define wrapper configurations
flake.wrapperConfigurations.myApp = nixpkgs.lib.evalModules {
modules = [
# nix-lib adapter for wrappers
nix-lib.wrapperModules.default
# Your wrapper libs
{
nix-lib.enable = true;
nix-lib.lib.mkFlags = {
type = lib.types.functionTo lib.types.attrs;
fn = name: flags: { drv.flags.${name} = flags; };
description = "Generate wrapper flags";
};
}
];
};
};
}
# Use BirdeeHub's wrapper definitions
flake.wrapperConfigurations.alacritty =
inputs.nix-wrapper-modules.wrappers.alacritty.wrap {
inherit pkgs;
modules = [
nix-lib.wrapperModules.default
{
nix-lib.enable = true;
nix-lib.lib.terminalHelper = {
type = lib.types.functionTo lib.types.attrs;
fn = shell: { settings.terminal.shell.program = shell; };
description = "Set terminal shell";
};
}
];
# Use the helper
settings = config.lib.terminalHelper "${pkgs.zsh}/bin/zsh";
};
# Use Lassulus's wrapper modules
flake.wrapperConfigurations.mpv =
inputs.wrappers.wrapperModules.mpv.apply {
inherit pkgs;
modules = [
nix-lib.wrapperModules.default
{
nix-lib.enable = true;
nix-lib.lib.addScript = {
type = lib.types.functionTo lib.types.attrs;
fn = script: { scripts = [ script ]; };
description = "Add mpv script";
};
}
];
};

Libs defined in wrapper configurations are collected at:

LocationPath
Within wrapper moduleconfig.lib.<name>
Flake outputflake.lib.wrappers.<name>

mkAdapter is generic and works with any NixOS-style module system:

# Create adapter for your custom module system
flake.myModules.default = inputs.nix-lib.outputs.lib.nix-lib.mkAdapter {
name = "my-module-system";
namespace = "my";
};
# Use in your module system
{ lib, config, ... }: {
imports = [ myModules.default ];
nix-lib.enable = true;
nix-lib.lib.myHelper = {
type = lib.types.functionTo lib.types.attrs;
fn = x: { result = x; };
description = "Custom helper";
};
# Available at: config.lib.myHelper
}
  • Module system must support NixOS-style modules (config, lib, options args)
  • No domain-specific options required - mkAdapter only sets nix-lib.* and lib.*

Collectors aggregate libs from flake outputs into flake.lib.<namespace>. Define custom collectors via nix-lib.collectorDefs:

# In your flake-parts module
nix-lib.collectorDefs.wrappers = {
pathType = "flat"; # "flat" or "perSystem"
configPath = [ "wrapperConfigurations" ]; # Path in flake outputs
namespace = "wrappers"; # Output at flake.lib.wrappers.*
description = "nix-wrapper-modules libs";
};
TypeDescriptionCollection Path
flatDirect configuration setflake.<configPath>.<name>.config.nix-lib._fns
perSystemPer-system in legacyPackagesflake.legacyPackages.<system>.<configPath>
nix-lib.collectorDefs.nixos.enable = false; # Disable NixOS collection
nix-lib.collectorDefs.nixos.namespace = "os"; # flake.lib.os.* instead of flake.lib.nixos.*

nix-lib supports multiple testing frameworks through a pluggable backend system. Tests defined in nix-lib.lib.*.tests are automatically converted to the selected backend format.

BackendFrameworkDescription
nix-unitnix-unitDefault. Catches eval errors, uses Nix C++ API, in nixpkgs
nixtestnixtestPure Nix, no nixpkgs dependency, lightweight
nix-testsnix-testsRust CLI, parallel execution, helpers API
runTestslib.debug.runTestsBuilt-in nixpkgs testing function
nixtnixtTypeScript-based, describe/it blocks
namakanamakaSnapshot testing with review workflow
nix-lib.testing = {
backend = "nix-unit"; # or "nixtest", "nix-tests", "runTests", "nixt", "namaka"
reporter = "junit";
outputPath = "test-results.xml";
};

Run unit tests for a single scenario:

Terminal window
cd tests/scenarios/nix-unit
nix run .#test

Run all E2E test scenarios:

Terminal window
nix run .#test-e2e
flowchart TB
subgraph Define["Define Libraries"]
L1["nix-lib.lib.double = {<br/>fn, type, tests...}"]
L2["nix-lib.lib.add = {<br/>fn, type, tests...}"]
end
subgraph BDD["BDD Tests (tests/bdd/)"]
B1["collectors.nix"]
B2["adapters.nix"]
B3["libDef.nix"]
end
subgraph PerSystem["perSystem.nix-unit.tests"]
PS["System-specific tests"]
end
subgraph Generate["Auto-Generated"]
G1["test_double_doubles_5"]
G2["test_add_adds_positives"]
end
subgraph Merge["flake.tests"]
M["All tests merged"]
end
subgraph Run["nix run .#test"]
R["nix-unit --flake .#tests<br/>🎉 97/97 successful"]
end
L1 --> G1
L2 --> G2
G1 --> M
G2 --> M
B1 --> M
B2 --> M
B3 --> M
PS --> M
M --> R

Tests are organized in three layers:

LayerLocationPurpose
Unit testsnix-lib.lib.*.testsFunction behavior (defined with libs)
BDD teststests/bdd/*.nixStructure validation (namespaces, adapters)
perSystem testsperSystem.nix-unit.testsSystem-specific lib checks
E2E scenariostests/scenarios/*/End-to-end integration per backend

All tests are merged into flake.tests and run together via nix-unit --flake .#tests. E2E scenarios are run via nix run .#test-e2e.

Tests are defined alongside lib definitions:

nix-lib.lib.add = {
type = lib.types.functionTo lib.types.int;
fn = { a, b }: a + b;
description = "Add two numbers";
tests = {
"adds positives" = { args.x = { a = 2; b = 3; }; expected = 5; };
"adds negatives" = { args.x = { a = -1; b = -2; }; expected = -3; };
};
};

For BDD-style structure tests, create modules in tests/bdd/:

tests/bdd/myTests.nix
{ lib, config, ... }:
{
# System-agnostic tests
flake.tests = {
"test_myFeature_works" = {
expr = lib.hasAttr "myAttr" config.flake.lib;
expected = true;
};
};
# System-specific tests
perSystem = { config, ... }: {
nix-unit.tests = {
"test_perSystem_lib_exists" = {
expr = config.legacyPackages.lib != { };
expected = true;
};
};
};
}

Note: nix-unit requires test names to start with test.

nix-lib includes a built-in documentation generator that produces a markdown API reference from your lib metadata. It renders types, function arguments, descriptions, implementation bodies, and test cases.

Terminal window
nix build .#nix-lib-docs
cat result/docs.md

Configure via nix-lib.docs.* in your perSystem:

OptionTypeDefaultDescription
srcpath | nullnullSource root for fn body extraction (set to self to enable)
showIndexbooltrueInclude a function index at the top
showTitlebooltrueInclude the title and lib count header
enableOutputbooltrueExport docs as packages.nix-lib-docs
perSystem = { config, ... }: {
nix-lib.docs = {
src = self; # Enable tree-sitter fn body extraction
showIndex = true;
showTitle = true;
};
};

For each lib function, the generated docs.md includes:

  • Function arguments — set-pattern ({ a, b, c ? default }) or curried (x → y → z), rendered in fenced code blocks
  • Type — human-readable type signature
  • Description — from the lib definition
  • Source file — link to the source
  • Implementation body — collapsible section with the function body (requires src to be set, uses tree-sitter-nix)
  • Test cases — collapsible table with input/expected columns
  • Namespace headings — libs are grouped hierarchically by source (see below)

Libs in the generated docs are automatically namespaced by where they are defined. The doc generator prefixes each lib name with its source module system, then organizes them into a hierarchical heading structure.

Defined inDoc namespace prefixExample in docs
Flake-level nix-lib.lib.*flake.flake.math.double
NixOS nix-lib.lib.*nixos.nixos.mkService
home-manager nix-lib.lib.*home.home.mkShell
nix-darwin nix-lib.lib.*darwin.darwin.mkApp
nixvim nix-lib.lib.*vim.vim.mkPlugin
system-manager nix-lib.lib.*system.system.mkUnit
nix-wrapper-modules nix-lib.lib.*wrappers.wrappers.mkFlags
perSystem nix-lib.lib.*(none)myHelper

Nested namespaces create hierarchical headings in the output. For example, libs defined as nix-lib.lib.math.double and nix-lib.lib.math.add at the flake level produce:

### flake
#### math
##### `double`
##### `add`

When nix-lib.docs.src is set, the generator uses tree-sitter-nix to parse your source files and extract function implementation bodies. Without it, docs are generated in pure Nix (faster, but no implementation bodies).

nix-lib provides an E2E test runner that executes all test scenarios:

Terminal window
nix run .#test-e2e

This runs each scenario in tests/scenarios/ and reports pass/fail:

ScenarioDescription
nix-unitTests using nix-unit backend
nix-testsTests using nix-tests backend with devour-flake
standaloneStandalone test setup
mkFlake-flake-partsmkFlake with flake-parts integration
mkFlake-standalonemkFlake without flake-parts
linter-failVerifies linter correctly rejects invalid code
  • examples/ - Working examples for each module system
  • examples/docs.nix - Documentation generator configuration and options
  • examples/e2e-tests.nix - E2E test runner usage and scenario structure
  • tests/scenarios/mkFlake-standalone/ - mkFlake standalone example
  • tests/scenarios/mkFlake-flake-parts/ - mkFlake with flake-parts example
  • tests/bdd/ - BDD tests for structure validation
  • CONTRIBUTING.md - Development and testing guide