AgentSkillsCN

Nix Module System

Nix模块系统

SKILL.md

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

nix
modules = [ ./foo.nix ./foo.nix ];  # Same path → evaluated once

Function/attrset modules: Deduplicated by key attribute

nix
# 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:

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

nix
{ _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:

nix
{
  _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:

nix
# 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:

nix
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
# 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:

nix
{
  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:

nix
{
  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