Chapter 04

Nix Shell 开发环境

用 Nix 创建完全隔离、跨机器一致的项目开发环境,告别 "in my machine" 问题

开发环境的两种方式

shell.nix(经典方式)
在项目根目录创建 shell.nix,通过 nix-shell 命令进入环境。无需启用 Flakes,兼容旧版 Nix,适合快速迁移现有项目。
flake.nix devShells(现代方式)
在 flake.nix 中定义 devShells,通过 nix develop 进入环境。与 Flakes 的依赖锁定集成,跨机器完全一致,推荐新项目使用。

shell.nix — 经典开发环境

# shell.nix — 放在项目根目录
{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShell {
  # 构建/运行时需要的包
  buildInputs = with pkgs; [
    nodejs_20
    yarn
    git
    curl
    jq
  ];

  # 进入 shell 时执行的命令(设置提示符、别名等)
  shellHook = ''
    echo "🔧 Dev environment ready"
    echo "Node: $(node --version)"
    echo "Yarn: $(yarn --version)"
    export PS1="\[\033[1;34m\][nix-dev]\[\033[0m\] \w $ "
  '';

  # 设置环境变量
  DATABASE_URL = "postgresql://localhost/myapp";
  NODE_ENV = "development";
}
# 进入开发环境
nix-shell          # 在项目目录下运行,自动查找 shell.nix
nix-shell shell.nix  # 显式指定文件

# 在开发环境中执行单个命令(不进入交互式 shell)
nix-shell --run "yarn install"
nix-shell --run "yarn build"

mkShell 详解

mkShell 是专为开发环境设计的辅助函数,它创建一个不产生可执行产物、只提供环境的 derivation:

{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShell {
  # buildInputs:运行时和构建时都需要的包(最常用)
  buildInputs = with pkgs; [
    gcc
    gnumake
    pkg-config
    openssl
  ];

  # nativeBuildInputs:只在构建机器上需要的工具(交叉编译时区分)
  nativeBuildInputs = with pkgs; [
    cmake
    ninja
  ];

  # 直接设置环境变量
  RUST_LOG = "debug";
  OPENSSL_DIR = "${pkgs.openssl.dev}";   # 使用包的路径

  # shellHook:进入 shell 时执行(bash 代码)
  shellHook = ''
    export CFLAGS="-O2 -Wall"
    alias ll="ls -la"
    source .env 2>/dev/null || true   # 可选:加载 .env 文件
  '';
}

nix develop — Flakes 开发环境

使用 Flakes 的现代方式,将开发环境定义在 flake.nixdevShells 中:

# flake.nix
{
  description = "My project dev environment";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
      in {
        # 默认开发环境(nix develop)
        devShells.default = pkgs.mkShell {
          buildInputs = with pkgs; [
            nodejs_20
            typescript
            nodePackages.ts-node
          ];
          shellHook = ''
            echo "TypeScript dev environment"
          '';
        };

        # 多个开发环境(nix develop .#ci)
        devShells.ci = pkgs.mkShell {
          buildInputs = with pkgs; [ nodejs_20 curl jq ];
        };
      }
    );
}
# 进入默认开发环境
nix develop

# 进入指定环境
nix develop .#ci

# 执行单条命令后退出
nix develop --command yarn build

# 使用 --ignore-environment 获得更纯净的环境(去除大部分系统变量)
nix develop --ignore-environment

direnv + nix-direnv:自动激活

手动执行 nix develop 很繁琐。direnv 可以在进入目录时自动激活环境,离开时自动停用:

# 安装 direnv 和 nix-direnv
nix profile install nixpkgs#direnv nixpkgs#nix-direnv

# 将 direnv hook 添加到 Shell 配置
# ~/.bashrc 或 ~/.zshrc:
eval "$(direnv hook bash)"    # bash
eval "$(direnv hook zsh)"     # zsh

# 在项目目录创建 .envrc 文件
echo "use flake" > .envrc      # 使用 flake.nix 中的 devShell
# 或者:echo "use nix" > .envrc  # 使用 shell.nix

# 首次允许(安全确认)
direnv allow

# 之后:进入目录自动激活,离开目录自动停用!
cd myproject/     # 自动进入 nix develop 环境
cd ..             # 环境自动停用

nix-direnv 缓存优化

# 在 ~/.config/direnv/direnvrc 中加载 nix-direnv
# 这样 nix-direnv 会缓存 shell 路径,重启后不需要重新构建
source $(nix-direnv-find)/share/nix-direnv/direnvrc

# 或者通过 Home Manager 配置(第8章会详述)

语言特定开发环境实战

Python 项目

# shell.nix — Python 项目
{ pkgs ? import <nixpkgs> {} }:

let
  # 选择 Python 版本并添加额外包
  python = pkgs.python311.withPackages (ps: with ps; [
    pip
    virtualenv
    setuptools
    wheel
    requests
    numpy
    pandas
  ]);
in
pkgs.mkShell {
  buildInputs = [ python pkgs.poetry ];

  shellHook = ''
    # 创建并激活虚拟环境(用于 pip install 的额外包)
    if [ ! -d .venv ]; then
      python -m venv .venv
    fi
    source .venv/bin/activate
    echo "Python $(python --version) ready"
  '';
}

Rust 项目

# flake.nix — Rust 项目(使用 fenix 提供 nightly toolchain)
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    fenix.url = "github:nix-community/fenix";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, fenix, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
        # 使用 stable Rust toolchain
        toolchain = fenix.packages.${system}.stable.toolchain;
      in {
        devShells.default = pkgs.mkShell {
          buildInputs = [
            toolchain
            pkgs.pkg-config
            pkgs.openssl
            pkgs.libiconv
          ];
          RUST_LOG = "debug";
          OPENSSL_DIR = "${pkgs.openssl.dev}";
          OPENSSL_LIB_DIR = "${pkgs.openssl.out}/lib";
        };
      }
    );
}

Go 项目

# shell.nix — Go 项目
{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShell {
  buildInputs = with pkgs; [
    go_1_22
    gopls         # Go LSP
    golangci-lint
    delve         # Go 调试器
    gotools       # goimports 等工具
  ];

  shellHook = ''
    export GOPATH="$HOME/go"
    export PATH="$GOPATH/bin:$PATH"
    echo "Go $(go version) ready"
  '';
}
实战:为 Python 项目创建完整隔离开发环境

完整流程:① 在项目根目录创建 flake.nix(见 Rust 示例结构),在 devShells.default 中用 pkgs.python311.withPackages 列出依赖;② 运行 nix flake lock 锁定 nixpkgs commit;③ 创建 .envrc 写入 use flake;④ 执行 direnv allow。之后任何人 clone 这个仓库,进入目录即可自动获得完全一致的开发环境。

本章小结

Nix 开发环境的核心是 mkShell:声明需要哪些工具(buildInputs)、设置哪些环境变量、进入时运行什么初始化脚本(shellHook)。配合 direnv,实现进目录自动激活的无缝体验。将 devShell 放入 Flakes 还能通过 flake.lock 锁定所有工具的精确版本,真正做到跨机器一致。