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
{
inputs.nix-lib.url = "github:Dauliac/nix-lib";
outputs = { nix-lib, ... }:
nix-lib.inputs.flake-parts.lib.mkFlake { inherit inputs; } {
imports = [ nix-lib.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/ for complete working examples of each module system.
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 in | Module to import | Access within module | Flake output |
|---|---|---|---|
flake-parts nix-lib.lib.* |
flakeModules.default |
config.lib.flake.<name> |
flake.lib.flake.<name> |
perSystem nix-lib.lib.* |
flakeModules.default |
config.lib.<name> |
legacyPackages.<system>.nix-lib.<name> |
| Defined in | Module to import | Access within module | Flake output |
|---|---|---|---|
NixOS nix-lib.lib.* |
nixosModules.default |
config.lib.<name> |
flake.lib.nixos.<name> |
home-manager nix-lib.lib.* |
homeModules.default |
config.lib.<name> |
flake.lib.home.<name> |
nix-darwin nix-lib.lib.* |
darwinModules.default |
config.lib.<name> |
flake.lib.darwin.<name> |
nixvim nix-lib.lib.* |
nixvimModules.default |
config.lib.<name> |
flake.lib.vim.<name> |
system-manager nix-lib.lib.* |
systemManagerModules.default |
config.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 module | Nested module | Libs defined in nested | Access in parent |
|---|---|---|---|
| NixOS | home-manager | nix-lib.lib.foo |
config.lib.home.foo |
| NixOS | home-manager β nixvim | nix-lib.lib.bar |
config.lib.home.vim.bar |
| nix-darwin | home-manager | nix-lib.lib.foo |
config.lib.home.foo |
| nix-darwin | home-manager β nixvim | nix-lib.lib.bar |
config.lib.home.vim.bar |
| home-manager | nixvim | nix-lib.lib.bar |
config.lib.vim.bar |
| Module system | Namespace prefix |
|---|---|
| home-manager | home |
| nixvim | vim |
| nix-darwin | darwin |
| system-manager | system |
All libs are collected and exported at the flake level under flake.lib.<namespace>:
| Namespace | Source | Description |
|---|---|---|
flake.lib.flake.* |
nix-lib.lib.* in flake-parts |
Pure flake-level libs |
flake.lib.nix-lib.* |
nix-lib internals | mkAdapter, 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.*:
| Module | Import path |
|---|---|
flakeModules.default |
inputs.nix-lib.flakeModules.default |
nixosModules.default |
nix-lib.nixosModules.default |
homeModules.default |
nix-lib.homeModules.default |
darwinModules.default |
nix-lib.darwinModules.default |
nixvimModules.default |
nix-lib.nixvimModules.default |
systemManagerModules.default |
nix-lib.systemManagerModules.default |
wrapperModules.default |
nix-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:
- nix-wrapper-modules - Module system for wrapped packages with DAG-based flag ordering
- Lassulus/wrappers - Library for creating wrapped executables via module evaluation
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:
| Location | Path |
|---|---|
| Within wrapper module | config.lib.<name> |
| Flake output | flake.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,optionsargs) - No domain-specific options required - mkAdapter only sets
nix-lib.*andlib.*
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";
};| Type | Description | Collection Path |
|---|---|---|
flat |
Direct configuration set | flake.<configPath>.<name>.config.nix-lib._fns |
perSystem |
Per-system in legacyPackages | flake.legacyPackages.<system>.<configPath> |
nix-lib.collectorDefs.nixos.enable = false; # Disable NixOS collectionnix-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.
| Backend | Framework | Description |
|---|---|---|
nix-unit |
nix-unit | Default. Catches eval errors, uses Nix C++ API, in nixpkgs |
nixtest |
nixtest | Pure Nix, no nixpkgs dependency, lightweight |
nix-tests |
nix-tests | Rust CLI, parallel execution, helpers API |
runTests |
lib.debug.runTests |
Built-in nixpkgs testing function |
nixt |
nixt | TypeScript-based, describe/it blocks |
namaka |
namaka | Snapshot testing with review workflow |
nix-lib.testing = {
backend = "nix-unit"; # or "nixtest", "nix-tests", "runTests", "nixt", "namaka"
reporter = "junit";
outputPath = "test-results.xml";
};cd tests
nix run .#testOutput:
=== Running nix-unit tests ===
π 97/97 successful
=== All tests passed! ===
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:
| Layer | Location | Purpose |
|---|---|---|
| Unit tests | nix-lib.lib.*.tests |
Function behavior (defined with libs) |
| BDD tests | tests/bdd/*.nix |
Structure validation (namespaces, adapters) |
| perSystem tests | perSystem.nix-unit.tests |
System-specific lib checks |
All tests are merged into flake.tests and run together via nix-unit --flake .#tests.
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.
examples/- Working examples for each module systemtests/- Test flake with BDD testsCONTRIBUTING.md- Development and testing guide