nix-platform-specific-options
23
总安装量
23
周安装量
#16000
全站排名
安装命令
npx skills add https://github.com/edmundmiller/dotfiles --skill nix-platform-specific-options
Agent 安装分布
opencode
23
gemini-cli
23
codex
23
cursor
23
antigravity
22
junie
22
Skill 文档
Nix Module Platform-Specific Options
Overview
When writing Nix modules that need to hide platform-specific options (NixOS vs Darwin), using mkIf alone causes infinite recursion. This skill documents the correct pattern.
The Problem
mkIf is evaluated lazily but the option path is still visible during module evaluation. This causes errors like:
error: The option `users.defaultUserShell' does not exist.
Or infinite recursion when config is referenced in option defaults or optionalAttrs conditions.
The Pattern
Use optionalAttrs for platform checks, mkIf for config-dependent checks.
| Check Type | Tool | Evaluated |
|---|---|---|
Platform (isDarwin, !isDarwin) |
optionalAttrs |
Parse time |
Config values (cfg.enable, cfg.flavor) |
mkIf |
Lazy |
Examples
â Wrong: mkIf for platform check
config = mkIf (!isDarwin) {
users.defaultUserShell = pkgs.zsh; # Darwin sees this path!
};
â Correct: optionalAttrs for platform check
config = optionalAttrs (!isDarwin) {
users.defaultUserShell = pkgs.zsh; # Hidden from Darwin
};
â Wrong: Config value in optionalAttrs condition
# cfg.flavor evaluated at parse time â infinite recursion
(optionalAttrs (isDarwin && cfg.flavor == "personal") {
services.onepassword-secrets.enable = true;
})
â Correct: Nest mkIf inside optionalAttrs
# Platform check at parse time, config check lazy
(optionalAttrs isDarwin (mkIf (cfg.flavor == "personal") {
services.onepassword-secrets.enable = true;
}))
â Wrong: config reference in option default
options.modules.foo = {
user = mkOpt types.str config.user.name; # Infinite recursion!
};
â Correct: Static default, use config in config section
options.modules.foo = {
user = mkOpt types.str null;
};
config = mkIf cfg.enable (let
user = if cfg.user != null then cfg.user else config.user.name;
in {
# Use 'user' variable here
});
Combined Pattern
For modules with both platform-specific options AND config-dependent behavior:
config = mkIf cfg.enable (mkMerge [
# Common config (all platforms)
{ /* ... */ }
# Darwin-only options
(optionalAttrs isDarwin {
programs.zsh.interactiveShellInit = "...";
})
# NixOS-only options
(optionalAttrs (!isDarwin) {
users.defaultUserShell = pkgs.zsh;
})
# Darwin + config-dependent (nested)
(optionalAttrs isDarwin (mkIf (cfg.flavor == "personal") {
services.onepassword-secrets.enable = true;
}))
]);
Quick Reference
| Scenario | Pattern |
|---|---|
| NixOS-only option | optionalAttrs (!isDarwin) { ... } |
| Darwin-only option | optionalAttrs isDarwin { ... } |
| Platform + enable check | optionalAttrs isDarwin (mkIf cfg.enable { ... }) |
| Platform + config value | optionalAttrs isDarwin (mkIf (cfg.foo == "bar") { ... }) |
| Option default from config | Use null default, resolve in config section |
Debugging
When you see infinite recursion errors mentioning _module.freeformType or anon-43:
- Search for
config.references in option defaults - Search for
cfg.references inoptionalAttrsconditions - Search for
mkIf (!isDarwin)ormkIf isDarwinguarding platform-specific options
# Find problematic patterns
grep -rn "mkOpt.*config\." modules/
grep -rn "optionalAttrs.*cfg\." modules/
grep -rn "mkIf.*isDarwin" modules/