可复现开发环境:devenv.sh、devShells 与 direnv

开发环境的可复现比构建更棘手——语言版本、环境变量、pre-commit 钩子、IDE 集成都要一致。本文对比 devShells、devenv.sh 两种主流方案,详解多语言工具链、direnv 自动加载、pre-commit 集成与团队协作的最佳实践。

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

维度纯 devShellsdevenv.sh
依赖声明pkgs.mkShell { packages = [...] }devenv.nix + Nix modules
环境变量shellHook / envenv. + 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 多语言环境:语言特定打包

语言方式要点
Pythonpython3.withPackages用 withPackages 而非系统 pip 污染
Nodenodejs_20 + yarn/pnpm包靠 lockfile(yarn.lock)锁定
Gogo_1_22 + golangci-lint用 GOFLAGS=-mod=mod 控制
Rustrustc + cargo + rustfmt或 rustup 固定 toolchain
Javajdk17 + gradle用 jdk 指定版本避免默认漂移
Shell 工具shellcheck shfmtCI/本地一致
# 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: nodedevShell 未加载 / 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/ 的布局沉淀为可复用模板。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「DevOps」更多文章

  1. 备份与容灾自动化:RPO/RTO、Velero、PITR 与恢复演练
  2. 配置漂移与安全基线:IaC漂移检测、CIS合规、供应链安全与密钥轮换
  3. 内部开发者平台(IDP)工程化:Backstage、Golden Path 与自服务能力