Get the FREE Ultimate OpenClaw Setup Guide →

Nix Module System

Scanned
npx machina-cli add skill YPares/rigup.nix/nix-module-system --openclaw
Files (1)
SKILL.md
4.4 KB

Nix Module System: Dark Corners

Practical knowledge about lib.evalModules that's hard to find in official docs.

Sources:

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 key attribute or use lib.mkForce/lib.mkMerge
  • "infinite recursion encountered" → Use lib.mkIf instead of if, or check for circular dependencies
  • "The option ... does not exist" → Check spelling, or set _module.check = false for 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

  1. Step 1: Identify repeated module inclusions and add a unique key or rely on path-based dedupe.
  2. Step 2: Decide between specialArgs (for option declaration) and _module.args (for config values) and implement accordingly.
  3. 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.

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers