nix-darwin:macOS 的声明式系统配置

nix-darwin 实战:macOS 上的声明式系统配置——安装与 flake 集成、darwin 模块与系统配置、Homebrew 集成(casks/brews/taps)、defaults 系统偏好即代码、launchd 服务与定时任务、与 Home Manager 协同管理 dotfiles、用户与字体与 shell 配置、常见坑与升级流程、与 NixOS 的差异对比。

引言

macOS 没有 NixOS 那样的「配置即系统」,但 nix-darwin 补上了这块拼图:用一份声明式配置管理系统偏好(defaults)、launchd 服务、Homebrew 包、用户与字体——darwin-rebuild switch 一次,机器进入声明的状态。

它和 NixOS 的最大差异在于底层不是 Nix 生成的世界:macOS 的系统组件由 Apple 掌控,nix-darwin 只能「调用系统工具去设置」,因此有些操作不可逆、有些需要重启。理解这条边界,是用好 nix-darwin 的关键。

前置:NixOS 系统配置、Home Manager、NixOS 模块系统。


目录


1. nix-darwin 是什么

1.1 定位:macOS 版的「系统配置层」

nix-darwin 提供一套与 NixOS 模块系统同源的 darwin 模块,让你用 configuration.nix 风格的表达式声明 macOS 的系统状态:

管理对象nix-darwin 手段
系统偏好system.defaults.*
服务与定时任务launchd.*
Homebrew 包homebrew.*
用户与组users.*
系统级包environment.systemPackages
键盘/网络/字体system.keyboard / networking / fonts

1.2 与 NixOS 的根本差异

# NixOS:配置 → 生成整个系统(内核、init、服务全由 Nix 管)
# nix-darwin:配置 → 调用 macOS 原生工具去「设置」(defaults write / launchctl)

因此 nix-darwin 不是不可变系统:它管理的只是「设置项」,/System 与 /Applications 仍由 Apple 掌控,nix-darwin 只在 store 里放自己的包并用符号链接暴露。它适合「有大量偏好要跨机同步、用 flake 管开发环境、已在用 Home Manager 想再往上管系统层」的场景。

记忆:nix-darwin 是 macOS 的声明式系统配置层——用同源的 darwin 模块管理 defaults/launchd/Homebrew/用户;它不是不可变系统(macOS 由 Apple 掌控),只是「调用原生工具去设置」,理解这条边界是用好它的关键。


2. 安装与 flake 集成

2.1 前置:先装 Nix

# 推荐 Determinate Nix 或官方多用户安装,并启用 flakes
sh <(curl -L https://nixos.org/nix/install)
echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf

2.2 flake 里引入 nix-darwin

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    nix-darwin = {
      url = "github:LnL7/nix-darwin";
      inputs.nixpkgs.follows = "nixpkgs";   # 关键:避免两份 nixpkgs
    };
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs = { self, nixpkgs, nix-darwin, home-manager }: {
    darwinConfigurations."my-mac" = nix-darwin.lib.darwinSystem {
      modules = [ ./configuration.nix ];
    };
  };
}

2.3 首次激活

# 首次激活(bootstrap);之后可直接用 darwin-rebuild
nix run nix-darwin -- switch --flake .#my-mac

darwin-rebuild switch --flake .#my-mac    # 应用配置
darwin-rebuild build  --flake .#my-mac    # 只构建不切换
darwin-rebuild --list-generations         # 看历史世代
darwin-rebuild --rollback                 # 回滚到上一世代

记忆:nix-darwin 走 flake 集成——inputs 里 nix-darwin 的 nixpkgs 必须 follows;用 darwinSystem 生成 darwinConfigurations.<name>,darwin-rebuild switch --flake .#name 应用;它同样有世代与 rollback。


3. darwin 模块与系统配置

3.1 一份最小 configuration.nix

{ pkgs, ... }:
{
  # 声明主机名、平台、用户
  networking.hostName = "my-mac";
  nixpkgs.hostPlatform = "aarch64-darwin";   # Apple Silicon
  system.primaryUser = "alice";

  # 系统级包(对全机可见)
  environment.systemPackages = with pkgs; [ git ripgrep fd jq ];

  # Nix 守护进程设置
  nix.settings.experimental-features = [ "nix-command" "flakes" ];
  nix.settings.trusted-users = [ "alice" ];
}

3.2 系统级配置项一览

选项作用
system.stateVersion配置格式版本(首次设定后别改)
system.primaryUser主用户(影响 defaults 应用)
nixpkgs.hostPlatformaarch64-darwin 或 x86_64-darwin
environment.systemPackages系统级包
environment.variables全局环境变量
security.pam.servicesPAM 配置(谨慎)

3.3 键盘与网络

{
  system.keyboard = { enableKeyMapping = true; remapCapsLockToEscape = true; };
  networking.computerName = "my-mac";
  networking.localHostName = "my-mac";
}

记忆:darwin 模块与 NixOS 同源但选项不同——核心项是 hostPlatform(aarch64/x86_64-darwin)、primaryUser、stateVersion(首次设定后不改)、systemPackages;键盘与网络有独立选项。


4. Homebrew 集成

4.1 为什么还需要 Homebrew

有些 macOS 软件(尤其是 GUI 应用与 casks)没有 nixpkgs 包或维护滞后。nix-darwin 的做法是声明式地管理 Homebrew:把要装的包写进配置,让 nix-darwin 生成 Brewfile 并执行。

4.2 声明式 Homebrew

{
  homebrew = {
    enable = true;
    onActivation = {
      autoUpdate = true;
      upgrade = true;
      cleanup = "zap";    # 移除不在配置里的包(谨慎)
    };
    taps = [ "homebrew/cask-fonts" ];
    brews = [ "gnu-sed" "coreutils" ];     # CLI 工具
    casks = [ "google-chrome" "rectangle" "1password" ];   # GUI 应用
    masApps = { "Xcode" = 497799835; };    # App Store 应用
  };
}

4.3 cleanup 的三档语义

值行为
"none"不清理,配置外的包保留
"uninstall"卸载不在配置里的 formula/cask
"zap"连同配置文件一起删除(最彻底、最危险)

4.4 与 nixpkgs 的分工

原则是优先用 nixpkgs(可复现、可回滚、参与 Nix 闭包),只在 nixpkgs 缺失或滞后时用 Homebrew(GUI/cask 为主);不要把同一个工具既装 nixpkgs 又装 brew,会 PATH 冲突。

记忆:nix-darwin 的 Homebrew 集成是声明式的——taps/brews/casks/masApps 写进配置、onActivation 控制更新与清理(cleanup 三档 none/uninstall/zap);原则是 nixpkgs 优先、Homebrew 只补 GUI 与滞后包,且不要重复安装。


5. defaults:系统偏好即代码

5.1 defaults 是什么

macOS 的偏好设置底层是 defaults write <domain> <key> <value>。nix-darwin 把常用 domain 封装成选项,让偏好可声明、可版本化。

5.2 常用声明

{
  system.defaults = {
    # Dock:自动隐藏、放大、位置、图标大小
    dock = { autohide = true; magnification = true; orientation = "left"; tilesize = 48; };
    # Finder:显示隐藏文件、显示扩展名
    finder = { AppleShowAllFiles = true; AppleShowAllExtensions = true; FXPreferredViewStyle = "Nlsv"; };
    # 全局:按键重复速度、深色外观
    NSGlobalDomain = { KeyRepeat = 2; InitialKeyRepeat = 15; AppleInterfaceStyle = "Dark"; };
    # 屏幕截图位置
    screencapture.location = "~/Pictures/screenshots";
  };
}

5.3 未封装的 domain 与注意事项

未封装的 domain 走 CustomUserPreferences,例如 system.defaults.CustomUserPreferences."com.apple.Safari" = { IncludeDevelopMenu = true; };。

# ☐ defaults 只在「用户登录后」应用;system.primaryUser 必须正确
# ☐ 部分偏好需要重启对应 App(Dock/Finder)或注销才生效
# ☐ 不是所有键都被 nix-darwin 封装——未封装的用 CustomUserPreferences

记忆:defaults 把 macOS 偏好变成可声明的代码——dock/finder/NSGlobalDomain/screencapture 等有封装选项、未封装的走 CustomUserPreferences;注意它只在用户登录后应用且部分需重启 App 才生效。


6. launchd 服务管理

6.1 launchd 与 systemd 的对应

systemd(Linux)launchd(macOS)
serviceagent / daemon
timerStartCalendarInterval
systemctllaunchctl
system unitdaemon(root)
user unitagent(用户)

6.2 声明一个定时任务

{
  launchd.user.agents.backup = {
    serviceConfig = {
      ProgramArguments = [ "/run/current-system/sw/bin/rsync" "-a" "/Users/alice/doc/" "/Volumes/Backup/" ];
      # 每天 2:30 执行
      StartCalendarInterval = [ { Hour = 2; Minute = 30; } ];
      RunAtLoad = false;
    };
  };
}

6.3 系统级守护进程

{
  launchd.daemons.myagent = {
    serviceConfig = {
      ProgramArguments = [ "/run/current-system/sw/bin/myagent" ];
      KeepAlive = true;         # 崩溃自动重启
      RunAtLoad = true;
      StandardOutPath = "/var/log/myagent.log";
    };
  };
}

6.4 调试

调试三件套:launchctl list | grep myagent 看是否加载、launchctl print gui/$(id -u)/myagent 看详情与状态、log show --predicate 'process == "myagent"' --last 1h 看日志。

记忆:launchd 是 macOS 的服务系统——agent(用户级)对应 systemd user unit、daemon(系统级)对应 system unit,用 StartCalendarInterval 做定时;调试走 launchctl list/print 与 log show。


7. 与 Home Manager 协同

7.1 职责划分

层工具管什么
系统nix-darwindefaults、launchd、Homebrew、系统包
用户Home Managerdotfiles、shell、用户级包与程序配置

7.2 作为 darwin 模块引入

{
  outputs = { self, nixpkgs, nix-darwin, home-manager }: {
    darwinConfigurations."my-mac" = nix-darwin.lib.darwinSystem {
      modules = [ ./configuration.nix home-manager.darwinModules.home-manager {
        home-manager.useGlobalPkgs = true;      # 复用系统 pkgs
        home-manager.useUserPackages = true;
        home-manager.users.alice = import ./home.nix;
      } ];
    };
  };
}

7.3 useGlobalPkgs 的意义

useGlobalPkgs = true 让 Home Manager 复用 nix-darwin 的 pkgs——避免重复求值 nixpkgs,也让 overlay 与 config 设置一致生效。不设时 Home Manager 会自己 import nixpkgs,可能造成两份包集。

记忆:nix-darwin 管系统层、Home Manager 管用户层,用 home-manager.darwinModules.home-manager 引入;务必开 useGlobalPkgs = true 复用系统 pkgs,否则会出现两份 nixpkgs 求值。


8. 用户、字体与 shell

8.1 声明用户

{
  # 注意:nix-darwin 不负责创建 macOS 账户,账户要先存在
  users.users.alice = { home = "/Users/alice"; shell = "/run/current-system/sw/bin/zsh"; };
  system.primaryUser = "alice";
}

8.2 安装字体

{
  fonts.packages = with pkgs; [ nerd-fonts.jetbrains-mono noto-fonts-cjk-sans ];
}

字体安装到系统字体目录,对所有应用可见。

8.3 默认 shell

把 zsh 设为默认:environment.shells = [ pkgs.zsh ](写入 /etc/shells)+ programs.zsh.enable = true + users.users.alice.shell = pkgs.zsh。

记忆:nix-darwin 能声明用户属性、字体与 shell——但 macOS 账户本身要先存在(它不改账户创建);字体走 fonts.packages 装到系统目录,shell 走 environment.shells + users.users.<u>.shell。


9. 常见坑与升级

9.1 高频坑

现象原因解决
defaults 不生效用户未登录 / primaryUser 错设对 primaryUser 并重新 switch
darwin-rebuild 找不到PATH 里没有用 nix run nix-darwin -- switch
两份 nixpkgs忘记 follows给 nix-darwin/home-manager 加 follows
升级后配置报错选项改名/移除看 release notes 逐项迁移
Apple Silicon 上 x86 包hostPlatform 写错改成 aarch64-darwin

9.2 升级流程

# 1) 更新 inputs;2) 先 build 验证;3) 通过再 switch;4) 出问题 rollback
nix flake update
darwin-rebuild build  --flake .#my-mac
darwin-rebuild switch --flake .#my-mac
darwin-rebuild --rollback

9.3 不可逆操作提醒

# ☐ defaults 写过的键,回滚 nix-darwin 世代不会自动还原(系统偏好已落盘)
# ☐ Homebrew cleanup = "zap" 会删除配置外的应用,第一次先设 "none"
# ☐ 不要用 nix-darwin 管理 macOS 系统组件(它做不到,也不该做)

记忆:nix-darwin 三大坑——defaults 不生效(查 primaryUser)、两份 nixpkgs(加 follows)、升级选项改名(看 release notes);升级走「update → build → switch → 可 rollback」,但要记住 defaults 与 brew zap 造成的系统侧改动不会随世代回滚。


10. 速查表与一句话记忆

需求配置 / 命令一句话
应用配置darwin-rebuild switch --flake .#name主命令
只构建darwin-rebuild build先验证再切换
回滚darwin-rebuild --rollback世代级
系统偏好system.defaults.*可声明
未封装偏好CustomUserPreferences兜底
服务launchd.user.agents / daemonsagent vs daemon
Homebrewhomebrew.brews/caskscleanup 慎用 zap
复用 pkgsuseGlobalPkgs = true防两份 nixpkgs

一句话记忆:nix-darwin 把 macOS 系统层纳入声明式配置——用同源的 darwin 模块管理 system.defaults(偏好即代码,未封装走 CustomUserPreferences)、launchd(agent/daemon 对应 systemd 的 user/system unit)、homebrew(声明 brews/casks/masApps,cleanup 慎用 zap)、用户字体 shell 与系统包;flake 集成时 nix-darwin 与 home-manager 的 nixpkgs 都必须 follows,并开 useGlobalPkgs = true 避免两份包集;它管的是「设置项」而非不可变系统——Apple 掌控的组件改不了,且 defaults 与 brew 造成的系统侧改动不会随世代回滚。核心纪律是「nixpkgs 优先、Homebrew 补缺、系统层归 darwin、用户层归 Home Manager」。


延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「nix」更多文章

  1. nixpkgs 贡献与维护:从 by-name 到 backport
  2. Nix 中的 CUDA 与机器学习环境:cudaPackages 与 PyTorch
  3. Nix 远程构建与分布式构建:builders 协议、ssh-ng 与跨架构