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-03-06T09:55:46Z",
"stargazersCount": 14,
"topics": [],
"updatedAt": "2026-03-06T09:55:50Z",
"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";
};
Terminal window
cd tests
nix run .#test

Output:

=== 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:

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

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 system
  • 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