大约六个月前,在室友的推荐下,我把 PC 切换到了 NixOS。当时的原因很实际:我已经把 Ubuntu 桌面环境折腾到不好修了,重装反而更省事。

NixOS 是一个基于 Nix 包管理器的 Linux 发行版,它支持声明式配置和可重现构建。

Nix 是一种纯函数式编程语言,用于管理包和配置。它驱动 Nix 包管理器和 NixOS,允许用户以声明方式定义系统设置和开发环境。

NixOS 让包和配置管理少了很多临时手工状态,所以我把它作为 PC 上的主系统。后来熟悉 Nix 之后,我也在 MacBook 上安装了 Nix。目标很简单:让 NixOS 桌面和 macOS 笔记本尽量使用同一套开发环境。

  • 使用 Nix 在两个系统上设置相同的全局开发环境,例如特定的 Python 解释器或 Java 编译器版本。
  • 在两个系统上定义共享的 shell 环境(如 zsh),从而轻松重用 shell 脚本、变量和别名。

这套配置可以在这里看到。

我想要实现的目标

我需要一个解决方案,能够:

  • 确保跨平台包版本完全相同。
  • 将整个开发栈以代码形式声明,便于复制。
  • 支持高级并行计算设置,包括 OpenCL、OpenMP 和 MPI。
  • 允许针对特定平台的配置调整。

我的设置架构

为了实现这种整合,我以模块化方式构建了 Nix 配置。以下是目录结构的概述:

nix/
├── home-manager/
│ ├── shared/
│ │ └── programming.nix # 跨平台包
│ ├── linux/
│ │ └── home.nix # Linux 特定配置
│ └── darwin/
│ └── home.nix # macOS 特定配置
├── nixos/
│ └── configuration.nix # NixOS 系统配置
├── nix-darwin/
│ └── configuration.nix # macOS 系统配置
└── flake.nix # 主配置入口点

这种结构将共享元素集中起来,同时允许操作系统特定的调整。

共享编程环境

我的设置核心是 programming.nix 文件,它定义了跨平台一致的开发工具。这确保了从编译器到库的一切版本都相同。

# 跨平台编程环境包
{ config, pkgs, ... }:
{
home.packages = with pkgs; [
# 开发工具
git
wget
curl
# 编程语言和运行时
nodejs
typescript
(python3.withPackages (ps: with ps; [
numpy
pandas
scipy
matplotlib
scikit-learn
jupyterlab
]))
# Java 开发
jdk21
maven
gradle
# C/C++ 开发
gcc
cmake
gdb
lldb
pkg-config
# 并行编程栈
mpi
llvmPackages.openmp
opencl-headers
opencl-clhpp
ocl-icd
clinfo
# 文档和文本处理
pandoc
typst
# 开发环境
docker
];
home.sessionVariables = {
JAVA_HOME = "${pkgs.jdk21}";
CC = "${pkgs.gcc}/bin/gcc";
CXX = "${pkgs.gcc}/bin/g++";
};
}

这份配置引入的是我最常用的工具:数据工作的 Python 包、后端开发需要的 Java 工具,以及 OpenCL/OpenMP/MPI 这类并行计算组件。

在 macOS 上整合 Homebrew 与 Nix

虽然 Nix 处理了我大部分命令行工具,但我依赖 Homebrew 来管理 GUI 应用程序和某些 Nix 中不可用的 macOS 特定包。为了保持一切声明式,我使用 nix-darwin 中的 homebrew 模块将 Homebrew 直接整合到 Nix 配置中。

# macOS 的 Homebrew 配置
{ config, pkgs, ... }:
{
# Homebrew 声明式配置
homebrew = {
enable = true;
# 应用安装偏好
onActivation = {
autoUpdate = true; # 更新 Homebrew 本身
upgrade = true; # 将所有包升级到最新版本
cleanup = "zap"; # 卸载未在配置中列出的包
# 额外的更新选项(可选)
extraFlags = [
"--verbose" # 在更新期间显示详细输出
];
};
# 全局 Homebrew 设置
global = {
brewfile = true; # 使用 Brewfile 进行管理
lockfiles = false; # 不创建锁文件
};
# 来自 brew 列表的 GUI 应用程序
casks = [
# 云存储和同步
"baidunetdisk"
"google-drive"
"nutstore"
# 学习和教育
"eudic"
"pdf-expert"
# 浏览器和网络客户端
"firefox"
"google-chrome"
"bilibili"
# 开发工具
"github"
"jetbrains-toolbox"
"visual-studio-code"
"cursor"
"kate"
# macUI
#"sketchybar" see home manager
"font-hack-nerd-font"
# 设计和生产力
"canva"
"figma"
"obsidian"
"typora"
# 通信
"qq"
"wechat"
"whatsapp"
"microsoft-teams"
"telegram-desktop"
# AI 和生产力
"chatgpt"
"cherry-studio"
# 网络和系统工具
"clash-verge-rev"
#"stats"
# 办公和生产力
"zoom"
"slack"
"microsoft-auto-update"
# 游戏和娱乐
"steam"
# 学术和研究
"zotero"
# 统计计算和分析
"r"
"rstudio"
# 文档准备
"mactex" # 完整的 MacTeX 发行版,包括 TeXLive
];
# Formulae(命令行工具) - 目前保持为空,因为大多数由 nix 处理
brews = [
# AI 和 ML 工具现在由 nixpkgs 处理
];
# Mac App Store 应用(如果有)
masApps = {
# 示例: "Xcode" = 497799835;
};
};
}

这种方法让我的 macOS 设置保持干净且可重现。Homebrew 处理图形应用,而 Nix 管理其余部分,一切都以代码形式定义。

益处与挑战

这套配置让桌面和笔记本之间的切换少了很多脆弱状态。一致的环境减少了设置时间,也让配置问题更容易定位。

它也有成本:有些包需要平台特定的调整,初始设置对 Nix 新手来说会比较陡。对我自己的工作流来说,把配置版本化是值得的。

如果重新开始,我还是会从 Nix flakes 入手,因为它更容易把模块边界保留下来。