Nix Module System
Scannednpx machina-cli add skill YPares/rigup.nix/nix-module-system --openclawNix Module System: Dark Corners
Practical knowledge about lib.evalModules that's hard to find in official docs.
Sources:
- nixpkgs/lib/modules.nix — implementation
- Module system docs — official chapter
- nix.dev deep dive — tutorial
- noogle.dev evalModules — function reference
Module Identity & Deduplication
When the same module is included multiple times (e.g., via imports from different places), evalModules deduplicates by identity:
Path-based modules: Deduplicated by path string
modules = [ ./foo.nix ./foo.nix ]; # Same path → evaluated once
Function/attrset modules: Deduplicated by key attribute
# Without key: each inclusion is separate (can cause "defined multiple times" errors)
modules = [ myModule myModule ]; # Evaluated twice!
# With key: deduplicated
myModule = {
key = "my-unique-module-id";
imports = [ actualModule ];
};
modules = [ myModule myModule ]; # Evaluated once
Use key when you wrap modules dynamically and need deduplication across import chains.
Module Arguments
_module.args vs specialArgs
Both inject arguments into module functions, but differ in timing:
evalModules {
specialArgs = { foo = "available during option declaration"; };
modules = [{
_module.args = { bar = "only available in config, not options"; };
}];
}
specialArgs | _module.args | |
|---|---|---|
Available in options = { ... } | ✓ | ✗ |
Available in config = { ... } | ✓ | ✓ |
Can reference config | ✗ | ✓ |
Rule of thumb: Use specialArgs for things needed to declare options (like lib), use _module.args for runtime values (like pkgs).
_module.check
Disable "unknown option" errors:
{ _module.check = false; }
Useful when modules set options that might not exist (e.g., optional integrations).
_module.freeformType
Allow arbitrary attributes in config without declaring options:
{
_module.freeformType = lib.types.attrsOf lib.types.anything;
# Now any attribute is allowed without explicit options
whatever.you.want = "works";
}
Priority & Merging
mkDefault / mkForce / mkOverride
Control which definition wins when multiple modules set the same option:
# Priority scale: lower number wins
lib.mkOverride 1000 "default priority" # Same as mkDefault
lib.mkOverride 100 "normal priority" # Default when no mk* used
lib.mkOverride 50 "force priority" # Same as mkForce
# Shorthands
lib.mkDefault x # mkOverride 1000 - easily overridden
lib.mkForce x # mkOverride 50 - overrides most things
mkMerge
Combine multiple config fragments:
config = lib.mkMerge [
{ services.foo.enable = true; }
(lib.mkIf condition { services.foo.port = 8080; })
];
mkIf (it's not just if)
lib.mkIf is not the same as Nix's if:
# Nix if: evaluated immediately, fails if option doesn't exist
config = if condition then { foo = 1; } else { };
# lib.mkIf: deferred, only evaluated if condition is true
config = lib.mkIf condition { foo = 1; };
mkIf prevents "infinite recursion" errors when the condition depends on other config values.
mkBefore / mkAfter / mkOrder
For list-type options, control ordering:
{
environment.systemPackages = lib.mkBefore [ earlyPkg ]; # Prepend
environment.systemPackages = lib.mkAfter [ latePkg ]; # Append
environment.systemPackages = lib.mkOrder 500 [ midPkg ]; # Explicit order
}
Disabling Modules
Remove a module from evaluation:
{
disabledModules = [
"services/web-servers/nginx.nix" # Path relative to modules root
someImportedModule # Direct reference
];
}
Useful for replacing NixOS modules with custom implementations.
Common Errors & Fixes
See references/troubleshooting.md for detailed error explanations.
Quick fixes:
- "The option ... is defined multiple times" → Add
keyattribute or uselib.mkForce/lib.mkMerge - "infinite recursion encountered" → Use
lib.mkIfinstead ofif, or check for circular dependencies - "The option ... does not exist" → Check spelling, or set
_module.check = falsefor optional deps
Source
git clone https://github.com/YPares/rigup.nix/blob/main/riglets/nix-module-system/SKILL.mdView on GitHub Overview
This guide teaches practical use of lib.evalModules in the Nix module system. It covers how modules are deduplicated by identity, how to pass arguments at declaration vs runtime, and how to merge and order config fragments to produce a stable final config.
How This Skill Works
evalModules computes a merged configuration from module definitions and deduplicates modules by identity: path-based modules are deduplicated by path, while function or attrset modules can be deduplicated using a key. It also demonstrates timing and merging with specialArgs, _module.args, and helpers like mkMerge, mkIf, mkDefault, mkOverride, and mkBefore/mkAfter/mkOrder.
When to Use It
- When the same module is included from multiple places and you need it evaluated only once (path dedupe).
- When wrapping modular fragments dynamically and you want cross-chain dedupe using a unique key.
- When declaring options vs configuring at runtime by choosing between specialArgs (during option declaration) and _module.args (during config).
- When composing final config with multiple fragments: use lib.mkMerge, lib.mkIf, and the mk* helpers (mkDefault, mkOverride, mkForce, mkMerge).
- When removing optional modules from evaluation with disabledModules or _module.check to prevent unknown option errors.
Quick Start
- Step 1: Identify repeated module inclusions and add a unique key or rely on path-based dedupe.
- Step 2: Decide between specialArgs (for option declaration) and _module.args (for config values) and implement accordingly.
- Step 3: Use lib.mkMerge with config fragments and lib.mkIf for conditional blocks; consider using _module.check when safe.
Best Practices
- Prefer path-based dedupe for static modules, and use a unique key on modules wrapped dynamically to enable cross-chain dedupe.
- Use specialArgs for values needed during option declaration (like lib), and _module.args for runtime config values (like pkgs).
- Leverage lib.mkIf for deferred config rather than a plain Nix if to avoid evaluation issues.
- Use lib.mkMerge to combine independent config fragments, and mkBefore/mkAfter/mkOrder to control list-type options.
- When you need to allow arbitrary attributes without declaring options, consider _module.freeformType, but use it sparingly.
Example Use Cases
- A module included via two imports is only evaluated once due to path-based dedupe.
- Wrap a module in a module attrset with a unique key to dedupe across import chains.
- Declare options with specialArgs and provide accessible values during option declaration; then pass runtime values with _module.args.
- Combine fragments with lib.mkMerge and conditionally enable features with lib.mkIf.
- Disable a module by listing it in disabledModules or setting _module.check = false for optional integrations.