编程 NixOS 深度拆解:当声明式配置成为操作系统的第一性原理——从 flake 到原子化升级的工程全貌

2026-07-16 14:47:00 +0800 CST views 11

NixOS 深度拆解:当声明式配置成为操作系统的第一性原理——从 flake 到原子化升级的工程全貌

2026年,NixOS 正在从极客小众走向主流工程实践。Determinate Nix Installer 下载量突破 700 万次,NixOS 中文社区活跃度同比增长 340%。这不是偶然——当容器化、不可变基础设施和 GitOps 理念深入人心,NixOS 的声明式配置哲学恰好踩中了云原生时代工程师的集体焦虑:如何让开发环境可复现、如何让系统状态可回滚、如何让团队协作不再因为「在我机器上能跑」而崩溃?本文从工程师视角,深度拆解 Nix/NixOS 的架构设计、Nix 语言的核心机制、flakes 的来龙去脉,以及在 2026 年如何用 NixOS 重构你的开发工作流。


一、背景:从配置地狱到声明式乌托邦

1.1 每个工程师都经历过的噩梦

想象这样一个场景:你在本地开发环境折腾了三天,终于把所有依赖版本调通——Python 3.11.4、Node.js 18.17.0、PostgreSQL 14.5、Redis 7.0.4,连 CUDA 版本都是 12.2。代码在本地跑得飞起。然后你把项目交给同事,同事的机器报了一屏幕的依赖冲突。你开始写 README:「首先安装 Python 3.11... 等等,不对,是 3.11.4,不是 3.11.3...」

这不是一个工程问题,这是一个文化问题。传统 Linux 系统的配置是命令式的:一步一步执行安装命令,系统状态是这些命令累积执行的结果。但命令的执行顺序、环境差异、版本漂移都会导致最终状态的不一致。我们试图用 Docker、Ansible、Homebrew 的各种方案来缓解,但本质上都是在打补丁。

NixOS 的核心主张是:把操作系统当作代码来写。 你的 configuration.nix 文件就是系统的完整规格说明,用 Nix 语言描述你想要什么样的系统,然后 Nix 直接从规格推导出系统的最终状态。无论你在这台机器上构建多少次,结果都是一致的。

1.2 NixOS 是什么

NixOS 是基于 Nix 包管理器的 Linux 发行版,由 Eelco Dolstra 于 2003 年发起,至今已发展超过 20 年。与传统发行版的根本区别:

维度传统发行版(Ubuntu/CentOS)NixOS
包管理安装到固定路径(/usr/bin安装到 Nix Store(哈希路径)
配置方式命令式(apt install / yum install)声明式(描述期望状态)
多版本共存需要手动版本切换(pyenv/rvm)原生支持,同时存在
升级回滚升级覆盖,回滚困难原子化切换,随时回滚
可复现性依赖外部环境配置即规格,规格即系统

1.3 2026 年的生态现状

截至 2026 年 7 月,Nix 生态有几个值得关注的里程碑:

  • Determinate Nix Installer 下载量突破 700 万次,解决了 Nix 官方安装器长期以来的体验问题(非单用户模式下需要手动配置 service)
  • flakes 成为事实标准:Nix 2.18 正式稳定了 flakes 特性,几乎所有新项目都默认使用 flake.nix 管理依赖
  • ** devenv.sh 集成**:由 cachix 团队维护的 devenv.sh 已成为 NixOS/nix-darwin 上最流行的开发环境管理工具
  • home-manager 成熟:用户配置管理从实验性功能升级为稳定生态,支持跨机器同步 dotfiles

二、核心概念:Nix 语言的第一性原理

2.1 函数式语言的核心设计

Nix 语言是一门纯函数式、延迟求值(lazy evaluation)的领域特定语言(DSL),编译为 C 代码执行。它的设计目标只有一个:描述「构建过程」,并保证构建结果可复现

让我们从最基础的表达式开始理解 Nix 语言的设计哲学:

# Nix 语言的几个核心概念

# 1. 一切皆表达式,没有语句
# 传统 Shell: if [ $x -eq 1 ]; then echo "yes"; fi
# Nix:
let
  x = 1;
in
  if x == 1 then "yes" else "no"

# 2. 属性集(Attribute Set)- 类似 JSON 的数据结构
myConfig = {
  name = "my-project";
  version = "1.0.0";
  dependencies = [ "python" "postgresql" ];
  settings = {
    port = 8080;
    debug = true;
  };
};

# 访问属性
myConfig.name        # => "my-project"
myConfig.settings.port  # => 8080

# 3. 路径类型 - Nix 能理解路径的哈希
# Nix 会自动计算路径内容的哈希,用于内容寻址
# /nix/store/abc123-hello -> 内容 abc123 是文件内容的哈希

为什么 Nix 语言要这样设计? 关键在于引用透明性(Referential Transparency):一个 Nix 表达式,给定相同的输入,总是产生相同的输出。这让我们能够精确地追踪「这个包依赖哪些文件」和「这个系统由哪些包组成」。

2.2 Nix Store:内容寻址的包存储

Nix Store 是整个系统最核心的设计,也是理解 Nix 可复现性的关键。

在传统 Linux 系统中:

/usr/bin/python -> /usr/bin/python3.11

包管理器覆盖式安装,每次升级都会替换文件。如果新版本有 bug,你只能祈祷有备份。

在 Nix 中:

/nix/store/abc1234-python3.11.4/lib/python3.11/site-packages/pkg/
/nix/store/def5678-python3.11.5/lib/python3.11/site-packages/pkg/

每个包都被安装到一个由内容哈希命名的目录中。哈希是基于包的内容计算出来的——这意味着:

  • 相同内容的包,路径永远相同
  • 不同版本的包,路径必然不同
  • 旧版本不会被覆盖,只是不再被引用

这个设计叫做 内容寻址存储(Content-Addressable Store)。类比一下:就像 Git 的 commit hash——相同内容的提交,hash 永远一样;不同内容必然不同 hash。

2.3 Derivation:构建过程的形式化描述

Derivation(推导)是 Nix 中「构建」的概念——它用 Nix 语言描述了一个构建任务的完整规格:

# 这是一个 derivation 的简化示例
# 实际使用时通过 mkDerivation 构造

derivation {
  system = "x86_64-linux";
  builder = "/bin/bash";
  args = [ "-c" "echo 'Building...' && gcc hello.c -o hello" ];
  
  # gcc 这个包,作为输入,也会被哈希进输出路径
  # 所以如果 gcc 版本变了,hello 的路径也会变
  gcc = (import <nixpkgs> {}).gcc;
  
  # 原始输入文件
  src = ./hello.c;
}

执行这个 derivation 的结果是什么?Nix 会:

  1. 计算所有输入的哈希(包括 gcc 的路径、hello.c 的内容)
  2. 根据输入哈希计算出输出路径:/nix/store/<hash>-hello
  3. 在隔离的 sandbox 环境中执行构建
  4. 将构建产物放入该路径

这意味着:如果你在两台机器上用相同的 nixpkgs revision 和相同的源码构建,得到的结果路径完全相同。 这就是可复现性的数学基础。

2.4 延迟求值与 DAG 依赖

Nix 语言的 lazy evaluation 特性让构建过程变成一个有向无环图(DAG)。当你写:

let
  pkgs = import <nixpkgs> {};
  python = pkgs.python311;
  django = pkgs.callPackage ./django-overlay.nix { inherit python; };
  myapp = pkgs.callPackage ./myapp.nix { inherit django; };
in
  myapp

Nix 不会立即计算所有包——它只在你真正需要时才求值。但同时,它会构建完整的依赖图,并自动检测循环依赖。这带来的实际好处是:Nix 可以高效地分析依赖关系,告诉你「如果我升级这个包,哪些包会受影响」


三、声明式系统配置:从 configuration.nix 到完整规格

3.1 配置文件结构

NixOS 的核心是 /etc/nixos/configuration.nix。这是一个完整的 Nix 表达式,返回一个 NixOS 模块系统(Module System)配置:

# /etc/nixos/configuration.nix

{ config, pkgs, ... }:

{
  imports = [
    # 导入其他模块(如硬件配置)
    ./hardware-configuration.nix
    # 导入 home-manager 模块
    <home-manager/nixos>
  ];

  # =====================
  # 网络引导启动 (Bootloader)
  # =====================
  boot.loader.grub = {
    enable = true;
    device = "/dev/sda";
    configurationLimit = 10;  # 保留最近 10 个启动项
  };

  # =====================
  # 文件系统配置
  # =====================
  fileSystems."/" = {
    device = "/dev/sda1";
    fsType = "ext4";
  };

  # =====================
  # 用户管理
  # =====================
  users.users.qnnet = {
    isNormalUser = true;
    description = "Developer";
    extraGroups = [ "wheel" "docker" "networkmanager" ];
    # 这里可以直接写 shell = pkgs.fish;
    # 而不是通过命令安装 fish
  };

  # =====================
  # 系统包(所有用户共享)
  # =====================
  environment.systemPackages = with pkgs; [
    # git 和传统包管理器安装没什么区别
    git
    curl
    wget
    
    # 但这里可以指定精确版本!
    # 不再受制于发行版的版本库
    python311
    nodejs_18
    postgresql_15
    
    # Docker 和 Docker Compose
    docker-compose
    
    # 甚至可以安装 GUI 应用
    firefox
    vscode
  ];

  # =====================
  # 系统服务(systemd)
  # =====================
  services.postgresql = {
    enable = true;
    package = pkgs.postgresql_15;
    # 直接在配置里写 postgresql.conf 参数
    settings = {
      max_connections = 200;
      shared_buffers = "256MB";
      effective_cache_size = "1GB";
    };
    # 也可以写 SQL 初始化脚本
    initialScript = pkgs.writeText "init.sql" ''
      CREATE DATABASE myapp;
      CREATE USER myapp WITH PASSWORD 'secret';
    '';
  };

  # =====================
  # Docker 服务
  # =====================
  virtualisation.docker = {
    enable = true;
    autoPrune = {
      enable = true;
      dates = "weekly";
      flags = [ "--all" ];
    };
  };

  # =====================
  # 时区和语言
  # =====================
  time.timeZone = "Asia/Shanghai";
  i18n.defaultLocale = "zh_CN.UTF-8";
  i18n.supportedLocales = [ "zh_CN.UTF-8" "en_US.UTF-8" ];

  # =====================
  # 内核模块(自动加载哪些内核模块)
  # =====================
  boot.kernelModules = [ "zfs" "nvidia" ];
  boot.extraModulePackages = with pkgs; [ nvidia_x11 ];

  # =====================
  # 网络配置
  # =====================
  networking.hostName = "nixos-workstation";
  networking.firewall.enable = true;
  networking.firewall.allowedTCPPorts = [ 22 80 443 ];

  # =====================
  # 安全性
  # =====================
  security.sudo.wheelNeedsPassword = false;
  programs.zsh.enable = true;

  # 应用到系统
  system.stateVersion = "24.05";
}

这段配置翻译成传统命令式操作,大约相当于执行了:

# 这只是一个粗略的等价命令列表
# 实际操作远比这复杂
apt update && apt install -y git curl wget docker.io postgresql-15
systemctl enable postgresql
systemctl start postgresql
# ... 几十到上百条命令

关键区别:命令式操作需要你知道「做什么」,声明式配置只需要你描述「要什么」。系统状态的推导由 NixOS 完成。

3.2 NixOS Module System:配置的组合数学

NixOS 真正强大的地方在于模块系统(Module System)。NixOS 的配置不是一个大文件,而是一组可以自由组合的模块。每个模块都可以定义一些选项(Option),其他模块可以引用这些选项:

# my-nginx-module.nix
# 这是一个自定义模块,定义了一个 nginx 网站配置选项
{ config, lib, pkgs, ... }:

{
  options = {
    my-nginx-sites = lib.mkOption {
      type = lib.types.attrsOf (lib.types.submodule {
        options = {
          serverName = lib.mkOption { type = lib.types.str; };
          root = lib.mkOption { type = lib.types.str; };
          enableSSL = lib.mkOption { type = lib.types.bool; default = false; };
        };
      });
      default = {};
    };
  };

  config = {
    services.nginx = {
      virtualHosts = lib.mapAttrs (name: site: {
        inherit (site) serverName root;
        enableACME = site.enableSSL;
        forceSSL = site.enableSSL;
      }) config.my-nginx-sites;
    };
  };
}

然后在 configuration.nix 中使用:

imports = [ ./my-nginx-module.nix ];

my-nginx-sites.my-blog = {
  serverName = "blog.example.com";
  root = "/var/www/blog";
  enableSSL = true;
};

这种设计的优雅之处在于:你可以在网上找到任意 NixOS 模块,直接复制到项目中,就能以声明式的方式引入新的系统能力。就像前端工程师引入一个 npm 包一样简单。


四、Flakes:从实验性特性到生态系统的事实标准

4.1 Flakes 解决了什么问题

Nix 传统的 <nixpkgs> 引用方式存在一个根本性问题:它无法精确锁定依赖版本

# 传统方式 - 问题:master 分支随时变
nix-env -f '<nixpkgs>' -iA python311

# flake 方式 - 精确锁定
# flake.nix 中声明:
{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
  # 每次 nixos-rebuild 都会使用完全相同的 nixpkgs revision
}

Flakes 是 Nix 2.0 引入的实验性特性,经过多年迭代在 Nix 2.18 正式稳定。它的核心是引入了一个标准化的项目描述文件 flake.nix 和一个全局的 flake 注册表。

4.2 flake.nix 的结构

# flake.nix
{
  description = "My Development Environment";

  # ========== 输入声明 ==========
  inputs = {
    # Nixpkgs - 锁定版本
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
    
    # home-manager - 用户配置管理
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";  # 和主 nixpkgs 同步
    };
    
    # devenv - 开发环境
    devenv = {
      url = "github:cachix/devenv";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    
    # 你的自定义包
    my-overlay = {
      url = "path:./overlays/my-pkg";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  # ========== 输出定义 ==========
  outputs = { self, nixpkgs, home-manager, devenv, my-overlay, ... }@inputs:
    let
      # 为每个系统平台构建
      forAllSystems = nixpkgs.lib.genAttrs [
        "x86_64-linux" 
        "aarch64-linux" 
        "x86_64-darwin" 
        "aarch64-darwin"
      ];
    in
    {
      # NixOS 配置输出
      nixosConfigurations = {
        my-laptop = nixpkgs.lib.nixosSystem {
          system = "x86_64-linux";
          specialArgs = { inherit inputs; };  # 传递 flake inputs 给 modules
          modules = [
            ./hosts/laptop/configuration.nix
            home-manager.nixosModules.home-manager
            {
              home-manager.useGlobalPkgs = true;
              home-manager.useUserPackages = true;
              home-manager.users.qnnet = import ./home/qnnet.nix;
              home-manager.activationPackage = (self: hm: hm.path);
            }
          ];
        };
        
        my-server = nixpkgs.lib.nixosSystem {
          system = "aarch64-linux";
          specialArgs = { inherit inputs; };
          modules = [ ./hosts/server/configuration.nix ];
        };
      };

      # 开发环境(devenv)
      devShells = forAllSystems (system:
        let pkgs = nixpkgs.legacyPackages.${system};
        in {
          default = devenv.lib.mkDevShell {
            inherit inputs pkgs;
            modules = [ ./devenv.nix ];
          };
        }
      );

      # overlay - 将自定义包注入 nixpkgs
      overlay = final: prev: {
        my-package = my-overlay.packages.${system}.default;
      };

      # 打包输出(可供 nix build 使用)
      packages = forAllSystems (system:
        let pkgs = nixpkgs.legacyPackages.${system};
        in {
          default = pkgs.my-package;
          my-app = pkgs.callPackage ./my-app {};
        }
      );
    };
}

这个文件结构中蕴含了 flake 设计的几个核心思想:

  1. 输入锁定(Input Locking):flake.lock 文件记录了所有输入的精确哈希——这意味着每次 clone 项目并构建,结果都完全一致
  2. 跨平台输出:同一个 flake.nix 可以为 Linux 和 macOS(通过 nix-darwin)生成配置
  3. 组合性:overlay 系统让你可以在不 fork nixpkgs 的情况下添加自己的包

4.3 flake.lock:精确依赖锁定的实现

执行 nix flake update 后生成的 flake.lock 文件:

{
  "version": 7,
  "inputs": {
    "nixpkgs": {
      "type": "github",
      "owner": "NixOS",
      "repo": "nixpkgs",
      "ref": "nixos-24.05",
      "rev": "7c9cc5a6e5d38082001718a46bb0e8f2c7a5f2b4",
      "lastModified": 1719878400
    },
    "home-manager": {
      "type": "github",
      "owner": "nix-community",
      "repo": "home-manager",
      "ref": "release-24.05",
      "rev": "a1b2c3d4e5f6789012345678abcdef01",
      "lastModified": 1719500000
    }
  }
}

这个文件就是 Flakes 可复现性的关键。它记录了每个输入的精确 Git commit SHA。和 Go 的 go.sum、Node 的 package-lock.json、Python 的 Poetry.lock 是同一个思想,只是 Nix 把锁定的粒度扩展到了整个系统和所有依赖。

4.4 devenv.sh:开发环境的原子化

对于日常开发,最常用的不是 nixos-rebuild,而是 devenv.sh——一个基于 Nix 的开发环境工具。它让你为每个项目定义一个 devenv.nix

# devenv.nix
{ pkgs, ... }:

{
  # Python 环境
  languages.python = {
    enable = true;
    version = "3.11";
    packages = pypi-deps.generatePackageList {
      pythonVersion = "3.11";
      requirements = builtins.readFile ./requirements.txt;
    };
  };

  # Node.js 环境
  languages.nodejs = {
    enable = true;
    version = "20";
    packageLock = ./package-lock.json;
  };

  # 数据库服务
  services.postgres = {
    enable = true;
    port = 5432;
    initialScript = ./init.sql;
    settings = {
      max_connections = 100;
      shared_buffers = "128MB";
    };
  };

  # Redis
  services.redis.enable = true;

  # 环境变量
  env.GIN_MODE = "release";
  env.DATABASE_URL = "postgres://localhost:5432/myapp";

  # 启动脚本
  enterShell = ''
    echo "Welcome to the dev environment!"
    # 可以在这里跑任意初始化命令
    alembic upgrade head
  '';
}

开发者只需要在项目根目录运行 devenv up,就能启动一个完整的、可复现的开发环境:Python 包、Node 包、PostgreSQL、Redis 全部自动配置好,而且和 CI 环境完全一致。


五、原子化升级与回滚:系统状态的版本控制

5.1 系统配置的版本化

NixOS 最有实用价值的功能之一:每次系统配置变更都会生成一个新的启动项(generation),可以随时回滚

# 查看所有系统代数(generations)
nix-env --list-generations

# 输出类似:
#   97  2026-07-10 14:30:12
#   98  2026-07-12 09:15:33  ← 当前
#   99  2026-07-14 11:00:01
#  100  2026-07-16 08:45:22  ← 刚刚构建

# 回滚到上一个代数
sudo /run/current-system/sw/bin/switch-to-configuration boot

# 或者直接回滚
sudo nixos-rebuild switch --rollback

# 升级系统
sudo nixos-rebuild switch --upgrade

# 构建但不激活(先预览)
sudo nixos-rebuild build --no-out-link

这个机制背后的原理:NixOS 的 /run/current-system 是一个符号链接,指向当前活跃的系统配置。当执行 nixos-rebuild switch 时:

  1. Nix 根据 configuration.nix 构建新系统(产生新的 store path)
  2. 将符号链接指向新的 store path
  3. 旧系统配置仍然存在于 store 中,只是没有符号链接指向它

所以即使激活了新系统,旧系统依然可以通过选择旧的启动项恢复。

5.2 软件包级别的回滚

不仅是系统级别,单个软件包也可以回滚

# 列出某个包的多个版本
nix-env --list-generations --profile /nix/var/nix/profiles/system

# 查看某个包的构建历史
nix log python311  # 查看 Python 的构建过程

# 回滚到旧版本(通过 generational rollback)
nix-env --rollback

# 或者显式安装某个旧版本
nix-env --install python311.10  # 安装到 python311.10

5.3 NixOS 中的 GitOps 实践

结合 Git,NixOS 可以实现完整的 GitOps:

# 典型的工作流
cd ~/nixos-config
git pull                           # 拉取最新配置
nixos-rebuild switch --flake .     # 从 flake 构建并激活
# 如果出问题
git log --oneline -5              # 看看改了什么
git diff HEAD~1                    # 对比差异
git checkout HEAD~1                # 回到上一个版本
nixos-rebuild switch --rollback   # 回滚系统

对于团队协作,可以把 NixOS 配置放在私有 Git 仓库中:

# 从私有 Git 仓库拉取配置
inputs.my-config.url = "git+ssh://git@github.com/myorg/nixos-config";
inputs.my-config.inputs.nixpkgs.follows = "nixpkgs";

六、home-manager:用户配置的可复现化管理

6.1 dotfiles 的传统困境

经典的 dotfiles 管理难题:.bashrc.vimrc.gitconfig.ssh/config 等散落在 $HOME 目录的各个角落。你要同步到新机器,要么用 symlink 方案(GNU Stow),要么用专门的 dotfiles 管理器。

home-manager 把这个问题的解决方案提升到了声明式的级别:

# home/qnnet.nix
{ config, pkgs, ... }:

{
  # home-manager 有一个等效于 configuration.nix 的 home 文件
  home.username = "qnnet";
  home.homeDirectory = "/home/qnnet";

  # =====================
  # dotfiles 管理
  # =====================
  home.file = {
    # 通过 home.file 直接生成文件内容
    ".config/alacritty/alacritty.yml".text = ''
      font:
        normal:
          family: "JetBrains Mono"
          size: 12
      colors:
        primary:
          background: '#1e1e2e'
          foreground: '#cdd6f4'
    '';

    ".gitconfig".text = ''
      [user]
        name = QNNet
        email = qnnet@example.com
      [alias]
        lg = log --oneline --graph --decorate
        st = status -sb
    '';
  };

  # =====================
  # Shell 配置
  # =====================
  programs.zsh = {
    enable = true;
    enableCompletion = true;
    plugins = with pkgs; [
      { name = "zsh-autosuggestions"; src = fetchFromGitHub {...}; }
      { name = "zsh-syntax-highlighting"; src = fetchFromGitHub {...}; }
    ];
    initExtra = ''
      # 自定义 Zsh 配置
      export EDITOR=vim
      alias ll='ls -lah'
    '';
  };

  programs.fish = {
    enable = true;
    interactiveShellInit = ''
      set -g EDITOR vim
      function ll; ls -lah $argv; end
    '';
  };

  # =====================
  # Neovim 配置(用 Nix 管理插件和配置)
  # =====================
  programs.neovim = {
    enable = true;
    plugins = with pkgs.vimPlugins; [
      nvim-lspconfig
      nvim-treesitter
      telescope-nvim
      nvim-cmp
    ];
    configure = {
      customRC = ''
        set number
        set relativenumber
        colorscheme catppuccin
      '';
    };
  };

  # =====================
  # GitHub CLI
  # =====================
  programs.gh = {
    enable = true;
    settings = {
      git_protocol = "ssh";
      prompt = "enabled";
    };
  };

  # =====================
  # 环境变量
  # =====================
  home.sessionVariables = {
    EDITOR = "vim";
    VISUAL = "vim";
    LANG = "zh_CN.UTF-8";
    XDG_CONFIG_HOME = "$HOME/.config";
  };

  # 让 home-manager 生成激活脚本
  home.stateVersion = "24.05";
}

这个配置的好处:它不依赖任何外部同步工具。任何人 clone 了这个配置仓库,运行 home-manager switch,就能在任意 NixOS 机器上重建完全相同的用户环境。

6.2 多机器 dotfiles 同步

home-manager 天然支持多机器配置差异化:

# home/qnnet.nix
{ inputs, pkgs, lib, ... }:

{
  # 基础配置(所有机器共享)
  home.username = "qnnet";

  # 通过 specialArgs 传递机器标识
  # 在 flake.nix 中调用时传入:
  # home-manager.users.qnnet = import ./home/qnnet.nix;

  programs.starship = {
    enable = true;
    settings = {
      format = "$username$directory$git_branch$python$cmd_duration$line_break$status";
    };
  };

  programs.wezterm = {
    enable = true;
    font = lib.mkIf (pkgs.stdenv.hostPlatform.system == "x86_64-linux") {
      # Linux 上用 JetBrains Mono
      font = "JetBrains Mono:style=Regular";
    };
    font = lib.mkIf (pkgs.stdenv.hostPlatform.system == "aarch64-darwin") {
      # macOS 上用 SF Mono
      font = "SF Mono";
    };
  };
}

七、实战:从零搭建 NixOS 开发环境

7.1 安装:Determinate Nix Installer

传统 Nix 安装在 macOS/Linux 多用户模式下需要配置 daemon,Determinate Nix Installer 把这个过程简化到一条命令:

# macOS / Linux 通用安装
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install

# 或者指定安装 channel
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix/pr/3.21.2 | sh

# 安装后自动启用 flakes(不再需要手动配置 experimental-features)
nix --version  # 应该是 2.18+

7.2 从 flake 初始化项目

# 创建一个 NixOS 配置仓库
mkdir ~/nixos-config && cd ~/nixos-config
git init

# 从模板创建(推荐)
nix flake init

# 目录结构
tree
# .
# ├── flake.lock
# ├── flake.nix
# ├── hosts/
# │   ├── laptop/
# │   │   └── configuration.nix
# │   └── server/
# │       └── configuration.nix
# ├── home/
# │   └── qnnet.nix
# └── modules/
#     └── nginx-site.nix

7.3 第一个 NixOS 配置

# hosts/laptop/configuration.nix
{ inputs, config, pkgs, ... }:

{
  imports = [
    # 导入 nixpkgs 的硬件检测配置
    <nixpkgs/nixos/modules/profiles/qemu-guest.nix>
    # 如果是物理机:运行 nixos-generate-config 自动生成
    ./hardware-configuration.nix
  ];

  # 桌面环境(可以选择 GNOME/KDE/Xfce/Sway 等)
  services.xserver = {
    enable = true;
    displayManager.gdm.enable = true;
    desktopManager.gnome.enable = true;
  };

  # 电源管理和显卡
  powerManagement = {
    enable = true;
    cpuFreqGovernor = "powersave";
  };

  hardware.nvidia = {
    enable = true;
    modesetting.enable = true;
    powerManagement.enable = false;  # NVIDIA 独显
  };

  # 系统升级策略
  system.autoUpgrade = {
    enable = true;
    dates = "04:00";
    flake = "path:.";  # 每天自动用 flake 更新
    flags = [ "--accept-flake-config" ];
  };

  # 完整配置...
}

7.4 切换并激活

# 预览构建(不激活)
sudo nixos-rebuild build --flake .#laptop

# 激活新配置(当前会话切换)
sudo nixos-rebuild switch --flake .#laptop

# 更新 flake 依赖到最新
nix flake update

# 查看构建差异
nix-env --query --available --profile /nix/var/nix/profiles/system --compare-versions

八、性能优化:Nix 的构建与存储

8.1 二进制缓存:让构建几乎不需要

Nix 最大的痛点是编译时间——从源码构建所有包需要数小时。但 Nix 的解决方案优雅且高效:二进制缓存(Binary Cache)

# 启用 cachix(最大的 Nix 二进制缓存服务)
nix-env -iA nixpkgs.cachix

# 使用公共缓存
cachix use nix-community

# 或者在 flake.nix 中自动配置
# 在 outputs 中添加:
# nixConfig.substituters = ["https://nix-community.cachix.org"];
# nixConfig.trusted-public-keys = ["nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="];

# 查看缓存命中率
nix-store --query --stats /run/current-system

cachix 的工作原理:如果 Nix 要构建一个 derivation,但它在远程缓存中已经存在(通过 derivation 的输入哈希标识),就直接下载预编译的产物,而不是从源码构建。Nix 社区维护了多个公共缓存,覆盖了绝大多数常用包。

8.2 并行构建

# 在 configuration.nix 中配置并行构建
nix.settings = {
  max-jobs = "auto";        # 自动使用所有 CPU 核心
  cores = 0;                # 0 = 无限制
  substituters = [
    "https://nix-community.cachix.org"
  ];
  trusted-public-keys = [
    "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
  ];
};

8.3 垃圾回收

# 查看 store 大小
nix-store --query --size /run/current-system

# 查看可回收的空间
nix-store --gc --print-dead

# 清理旧代数引用的包(保守清理)
sudo nix-collect-garbage --delete-old

# 激进清理(清理所有不可达包)
sudo nix-collect-garbage -d

# 保留最近的 3 个代数
sudo nix-collect-garbage --delete-older-than 3d

九、NixOS 在 2026 年的工程适用场景

9.1 适用场景

最适合 NixOS/Nix 的场景:

  1. 多设备开发环境一致性:你有 macOS 笔记本 + Linux 服务器 + WSL 工作站,三个环境的开发工具版本统一管理
  2. CI/CD 流水线:用 Nix 构建完全一致的开发/测试/生产镜像,无需 Docker layer 层叠
  3. 数据科学团队:Python/R/Julia 环境锁定,避免「numpy 版本导致结果不同」的科学计算噩梦
  4. DevOps/SRE:声明式基础设施配置,配置变更可审计、可回滚
  5. 个人 dotfiles 管理:新机器上跑一条命令,完全重建工作环境

9.2 局限性

NixOS 不是银弹,以下场景要谨慎:

  1. NVIDIA 闭源驱动:NixOS 对 NVIDIA 驱动的支持需要额外的模块配置,Wayland + NVIDIA 组合尤其折腾
  2. 企业软件依赖:某些商业软件(钉钉、某些 VPN 客户端)只提供 deb/rpm 包,不兼容 NixOS
  3. 学习曲线:Nix 语言的调试比传统 Shell 脚本困难得多,错误信息有时不够友好
  4. 社区文档质量:Nix 社区文档虽然丰富,但组织方式不够友好,新手容易迷路

9.3 2026 年的生态工具链

工具用途成熟度
Determinate Nix Installer一键安装 Nix⭐⭐⭐⭐⭐
devenv.sh开发环境管理⭐⭐⭐⭐⭐
home-manager用户配置管理⭐⭐⭐⭐
nix-darwinmacOS 上的 NixOS⭐⭐⭐
cachix二进制缓存⭐⭐⭐⭐⭐
lorri开发目录守护进程⭐⭐⭐
direnv环境目录切换⭐⭐⭐⭐
nan.nvimNeovim 的 Nix LSP⭐⭐⭐

十、总结:声明式配置的时代意义

10.1 从 NixOS 看基础设施的未来

NixOS 代表的不是一种操作系统,而是一种系统管理的思想实验。它的成功证明了几个重要的工程命题:

第一,「配置即代码」不仅是 SRE 的口号,它可以在语言层面得到原生支持。Nix 语言的一切皆表达式、引用透明、延迟求值,让系统规格变成了一段可分析、可测试、可版本化的程序。

第二,不可变性(Immutability)是可靠性的朋友。NixOS 的包不可覆盖、系统代数可回滚,这个设计思想正在被越来越多的工具采纳:Docker 的层不可变镜像、React 的不可变状态、Git 的不可变提交历史。

第三,生态系统的组合性比单体架构更强大。flake 的输入-输出模型,让整个 Nix 生态变成了一个可插拔的系统。你不需要 fork nixpkgs,只需要写一个 overlay,就能引入任意新的包和配置。

10.2 给工程师的建议

如果你今天想尝试 NixOS/Nix,建议从以下路径开始:

  1. 第一周:在一台虚拟机或备用设备上安装 NixOS(不要拿主力机冒险)
  2. 第二周:用 flakes 管理你的 dotfiles(home-manager),体验声明式 dotfiles
  3. 第三周:用 devenv.sh 为一个现有项目构建开发环境
  4. 第四周:把 NixOS 配置 Git 化,加入团队协作流程

** NixOS 不是一个你「学会」然后「用着」的技能,它是一个你「习惯」然后「离不来」的操作系统哲学。**


参考资源:

  • NixOS 官方文档:https://nixos.org/manual/nix/stable/
  • NixOS Wiki:https://nixos.wiki/
  • Determinate Systems Blog:https://determinate.systems/posts/
  • devenv.sh 文档:https://devenv.sh/
  • home-manager 手册:https://nix-community.github.io/home-manager/
  • NixOS 中文社区:https://nixos-cn.org/

推荐文章

Elasticsearch 监控和警报
2024-11-19 10:02:29 +0800 CST
mysql int bigint 自增索引范围
2024-11-18 07:29:12 +0800 CST
如何在Vue3中定义一个组件?
2024-11-17 04:15:09 +0800 CST
程序员茄子在线接单