1. 开发环境的「最后一公里」难题
包可以复现、构建可以缓存,但开发者的 shell 环境仍然充满非确定性:node --version 是 18 还是 20?PYTHONPATH 指向哪?pre-commit 装的钩子版本对吗?换台机器、换个同事,环境就漂移。
Nix 解决这个问题的两条主线:
devShells(https://plumephp.com/nix-shell-development/ 的 Flakes 形态):纯 Nix 声明工具链devenv.sh:在 devShell 之上提供更高层的封装——进程管理、pre-commit、服务依赖、env 变量、docker-compose 集成
本文不重复 nix-shell 入门内容,聚焦「可复现开发环境」的工程化:多语言工具链怎么声明、怎么让 cd 进目录自动加载、怎么把 lint/test/format 统一成 pre-commit 钩子、以及团队如何共享同一套环境。
📌 相关专题:CI 侧复用同一 devShell 见 https://plumephp.com/nix-ci-cachix/;Flakes 布局见 https://plumephp.com/nix-flakes-best-practices/;DevOps 工具链见 https://plumephp.com/posts/devops/。
2. 方案选型:devShells vs devenv
| 维度 | 纯 devShells | devenv.sh |
|---|---|---|
| 依赖声明 | pkgs.mkShell { packages = [...] } | devenv.nix + Nix modules |
| 环境变量 | shellHook / env | env. + services. 声明式 |
| 进程管理 | 手动 & / trap | 内置 processes. 声明式 |
| pre-commit | 手动装 | 内置 pre-commit 模块 |
| 服务(DB 等) | 手动 | services.postgres 等 |
| 学习成本 | 低(纯 Nix) | 中(模块化 DSL) |
| 依赖注入 | Flake 原生化 | devenv 模块 + flakes |
| 适合场景 | 简单工具链、Nix 老手 | 完整 dev 环境、多服务、团队协作 |
结论:简单场景用 devShells,复杂场景用 devenv.sh。很多项目两者结合——devenv 内部就是生成一个 devShell,所以底层一致。
3. devShells:多语言工具链声明
3.1 一个覆盖多语言的 devShell
# flake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in {
devShells.default = pkgs.mkShell {
name = "fullstack-dev";
# 多语言工具链:Node、Python、Go、Rust 并存
packages = with pkgs; [
nodejs_20
yarn
python312
poetry
go_1_22
cargo
rustc
jq
yq
gh
direnv
];
# 统一的工具链版本检查脚本
shellHook = ''
echo "== dev env =="
node --version
python --version
go version
cargo --version
export NODE_ENV=development
'';
};
# 精简的 CI shell:不装 dev 专用工具
devShells.ci = pkgs.mkShell {
packages = with pkgs; [ nodejs_20 yarn jq ];
};
});
}
3.2 版本精确到 commit
「可复现」的关键是版本确定。与其跟随 unstable,不如用 nixpkgs 稳定分支或直接在 flake 里锁版本:
{
inputs = {
# 锁到具体 commit(通过 flake.lock 固化)
nixpkgs.url = "github:NixOS/nixpkgs?rev=abc123def456";
};
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in {
devShells.${system}.default = pkgs.mkShell {
packages = with pkgs; [
# 指定小版本而非跟随最新
(python312.withPackages (ps: [ ps.pip ps.pytest ]))
(nodejs_20)
];
};
};
}
3.3 多语言环境:语言特定打包
| 语言 | 方式 | 要点 |
|---|---|---|
| Python | python3.withPackages | 用 withPackages 而非系统 pip 污染 |
| Node | nodejs_20 + yarn/pnpm | 包靠 lockfile(yarn.lock)锁定 |
| Go | go_1_22 + golangci-lint | 用 GOFLAGS=-mod=mod 控制 |
| Rust | rustc + cargo + rustfmt | 或 rustup 固定 toolchain |
| Java | jdk17 + gradle | 用 jdk 指定版本避免默认漂移 |
| Shell 工具 | shellcheck shfmt | CI/本地一致 |
# Python 环境的最佳实践:withPackages 隔离
(python312.withPackages (ps: [
ps.pip
ps.pytest
ps.black
ps.ruff
]))
# Node 环境:pnpm + 固定 node
(pkgs.pnpm.override { nodejs = pkgs.nodejs_20; })
4. direnv:进入目录自动加载
4.1 安装与基础配置
# 安装 direnv(通过 nix profile 或系统包管理器)
nix profile install nixpkgs#direnv
# 在 shell rc 中启用(zsh 示例)
# echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc
4.2 .envrc 与 use flake
# .envrc —— 一行接入 flake 的 devShell
use flake
# 需要时指定系统
# use flake .#devShells.x86_64-linux.default
# 首次进入目录时授权
direnv allow
# 手动刷新
direnv reload
效果:cd 进项目目录 → direnv 自动构建/加载 devShell → 环境变量、PATH 全部就位;cd 出去自动卸载。
4.3 direnv + devenv 组合
# .envrc 使用 devenv 的加载器
use devenv
# devenv 也提供 direnv 集成脚本
nix profile install nixpkgs#devenv
devenv init # 生成 devenv.nix + .envrc
direnv allow
5. devenv.sh:更高层的开发环境框架
5.1 初始化
nix profile install nixpkgs#devenv
mkdir myproj && cd myproj
devenv init
# 生成文件:
# devenv.nix —— 环境声明
# devenv.lock —— 版本锁定(类似 flake.lock)
# .envrc —— direnv 接入
# .gitignore —— 忽略 .devenv* 与 result
5.2 devenv.nix 核心结构
# devenv.nix
{ pkgs, lib, config, inputs, ... }:
{
# 1. 工具链:语言与 CLI 工具
packages = with pkgs; [
nodejs_20
yarn
python312
jq
];
# 2. 环境变量(声明式,进入 shell 时注入)
env = {
NODE_ENV = "development";
DATABASE_URL = "postgres://localhost:5432/myapp";
};
# 3. 脚本入口:devenv run <name>
scripts.hello.exec = "echo 'hello from devenv'";
# 4. 进程管理:devenv up 一键拉起
processes.web.exec = "yarn dev";
processes.worker.exec = "yarn worker";
# 5. 服务:Postgres/Redis 等
services.postgres = {
enable = true;
package = pkgs.postgresql_15;
initialDatabases = [{ name = "myapp"; }];
};
services.redis = {
enable = true;
package = pkgs.redis;
};
# 6. pre-commit 钩子(见下节)
pre-commit.hooks = {
eslint.enable = true;
prettier.enable = true;
shellcheck.enable = true;
nixfmt.enable = true;
};
}
5.3 devenv 常用命令
devenv shell # 进入环境
devenv up # 按 processes.* 启动所有进程
devenv run hello # 运行 scripts.* 脚本
devenv test # 运行测试(CI 友好)
devenv update # 更新 devenv.lock
devenv gc # 清理旧环境
devenv info # 查看环境信息
6. pre-commit 集成:把质量门禁变成声明
6.1 devenv 内置 pre-commit
devenv 用 nix-pre-commit-hooks 生成标准 .pre-commit-config.yaml:
pre-commit.hooks = {
# 语言类
eslint.enable = true;
prettier.enable = true;
shellcheck.enable = true;
yamllint.enable = true;
markdownlint.enable = true;
# 通过 package 指定工具
"check-added-large-files".enable = true;
"check-merge-conflict".enable = true;
"end-of-file-fixer".enable = true;
# Nix 类
nixfmt.enable = true;
nixpkgs-fmt.enable = true;
statix.enable = true;
# 可自定义运行命令
my-custom = {
enable = true;
entry = "python scripts/check-something.py";
files = "\\.py$";
language = "system";
};
};
6.2 纯 devShell 方案:手动集成 pre-commit
{ pkgs, ... }:
pkgs.mkShell {
packages = [ pkgs.pre-commit pkgs.python312 ];
shellHook = ''
# 首次进入时安装 hooks
pre-commit install --install-hooks >/dev/null 2>&1 || true
'';
}
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.10.0.1
hooks:
- id: shellcheck
- repo: https://github.com/psf/black
rev: 24.8.0
hooks:
- id: black
最佳实践:pre-commit 的 rev 也要锁定。
pre-commit autoupdate定期升级并单独提交,避免钩子版本漂移破坏「可复现」。
7. 团队协作:共享环境的三道防线
7.1 防线一:所有工具都在 devShell 内
团队成员的机器上只装 Nix + direnv,其余一律进环境。.envrc + flake.lock 保证版本一致。
7.2 防线二:CI 使用同一 devShell
GitHub Actions 直接消费 devShell,本地与 CI 完全一致(见 https://plumephp.com/nix-ci-cachix/):
- name: Setup dev environment
run: |
nix develop .#ci --command bash -c "yarn install && yarn test"
或直接用 devenv:
- name: Install Nix
uses: cachix/install-nix-action@v30
- name: Run devenv tests
run: nix develop . --command devenv test
7.3 防线三:锁定 + 定期刷新
# 锁住 devenv 版本
devenv update
# 或对纯 devShell,依赖 flake.lock
nix flake update
| 协作问题 | 症状 | 解法 |
|---|---|---|
| 同事 node 版本不对 | 语法/行为不一致 | 统一 devShell 声明 |
| pre-commit 钩子版本漂移 | 本地过、CI 挂 | rev 锁定 + autoupdate 单独提交 |
| 新同事装环境 1 小时 | 手动依赖 | .envrc + direnv allow 秒级就位 |
| CI 与本地不一致 | 某工具只在 CI 装 | CI 用同一个 devShell / devenv |
8. 常见坑与调试
| 症状 | 原因 | 解法 |
|---|---|---|
direnv: error ... use flake not allowed | .envrc 未授权 | direnv allow |
shell 里 command not found: node | devShell 未加载 / direnv 卸载了 | 检查 nix develop 是否成功、.envrc 是否正确 |
| devenv 服务起不来 | 端口冲突 | devenv up 看日志,services.* 里改端口 |
| pre-commit 反复安装 | 每次进入 shell 都 install | 钩子已存在时跳过(加 ` |
nix develop 每次都重新构建 | 依赖变动 | 检查 flake.lock 是否提交、使用 Cachix |
| Python 包冲突 | withPackages 与 pip 混用 | 统一用 withPackages,禁用系统 pip |
调试命令:
# 检查当前环境里命令来自哪里
which node && readlink -f $(which node)
# 查看 devenv 生成的完整环境
devenv shell -- bash -c "env | sort"
# 强制重新求值(绕过缓存)
nix develop --rebuild
9. 总结
可复现开发环境的落地公式:
- devShells:纯 Nix 声明工具链,多语言并存,版本锁进
flake.lock - devenv.sh:在 devShell 之上加进程、服务、pre-commit、env,适合完整开发环境
- direnv:
cd即加载、cd即卸载,零成本接入 - pre-commit:质量门禁声明式,rev 锁定保证可复现
- CI 同源:CI 消费同一 devShell,本地与流水线零差异
团队基建顺序:先 devenv init + .envrc 让新成员秒进环境 → 再接 https://plumephp.com/nix-ci-cachix/ 缓存加速 → 最后用 https://plumephp.com/nix-flakes-best-practices/ 的布局沉淀为可复用模板。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。