使用 Nix 搭建开发环境:四个快速示例
原文由 Michael Stapelberg 于 发布,订阅该博客
我想在一个项目中使用 GoCV(在一张较大的扫描件中查找并提取纸质文档),但又不想在系统里永久安装 OpenCV。
这正好是个不错的例子,可以用来演示我常用的几个 Nix 命令,涵盖从临时的交互式一次性开发环境,到完全声明式、隔离、可复现、可分享的开发环境。
值得一提的是,运行这些命令并不需要使用 NixOS!只要设置好 Nix 路径或使用 Flakes,你就可以在 Debian、Arch 等任意 Linux 发行版上安装并使用 Nix(见环境准备)。
作为对比:Debian 的做法
在开始介绍 Nix 之前,我先演示一下如何在 Debian 上让 GoCV 跑起来。
我们先创建一个使用了 gocv.NewMat() 这类 GoCV 函数的最小 Go 程序,来验证程序能否编译:
package main
import "gocv.io/x/gocv"
func main() {
gocv.NewMat()
}如果在 Debian 系统上尝试构建,会得到如下结果:
debian % mkdir -p /tmp/minimal
debian % cd /tmp/minimal
debian % cat > minimal.go <<'EOT'
package main
import "gocv.io/x/gocv"
func main() { gocv.NewMat(); }
EOT
debian % go mod init minimal
go: creating new go.mod: module minimal
go: to add module requirements and sums:
go mod tidy
debian % go mod tidy
go: finding module for package gocv.io/x/gocv
go: downloading gocv.io/x/gocv v0.41.0
go: found gocv.io/x/gocv in gocv.io/x/gocv v0.41.0
debian % go build
# gocv.io/x/gocv
# [pkg-config --cflags -- opencv4]
Package opencv4 was not found in the pkg-config search path.
Perhaps you should add the directory containing `opencv4.pc'
to the PKG_CONFIG_PATH environment variable
Package 'opencv4', required by 'virtual:world', not found在 Debian 上,我们可以这样安装 OpenCV:
debian % sudo apt install libopencv-dev
[…]
Summary:
Upgrading: 7, Installing: 512, Removing: 0, Not Upgrading: 27
Download size: 367 MB
Space needed: 1590 MB / 281 GB available
Continue? [Y/n]在这个提示下输入 “yes”,就会下载并安装 500 多个软件包(需要几分钟)。
现在就可以构建了:
debian % go build
debian % file minimal
minimal: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), […]……但这样一来,系统里就多了 500 多个额外的软件包,之后得一直为它们做更新,所以我想把这种一次性的实验和日常使用的系统隔离开来。
我们当然可以用 Docker 启动一个 Debian 容器并在其中工作,但正因为它是独立的环境,具体做起来有时会很麻烦。就拿这个例子来说,我需要指定挂载卷才能让 Docker 容器访问输入文件,还得先设置好环境变量,容器里的程序才能在宿主机上打开图形窗口……
下面来看看如何用 Nix 来解决这个问题!
准备工作:在 Debian(或 Arch 等)上使用 Nix
NixOS 用户可以跳过这一节,因为 NixOS 自带开箱即用的 Nix。
在自己的电脑上尝试这些示例之前,需要先完成以下三步:
- 安装 Nix
- 启用 Flakes
- 设置 Nix 路径
第 1 步:安装 Nix
Debian、Arch、Fedora 或其他 Linux 系统的用户首先需要安装 Nix。好在许多主流 Linux 发行版都提供了 Nix:
- Debian 提供了 nix-setup-systemd
- Arch Linux 打包了 nix 并在 Arch Wiki 的 Nix 页面上提供了文档。实际操作中,我安装了该软件包并配置了几个
nixbld用户。 - 更一般地,针对不少发行版都有可用的 Nix 构建(rpm、deb、pacman):https://github.com/nix-community/nix-installers
第 2 步:启用 Flakes
Nix flakes 是“一种通用的 Nix 产物打包方式”。
示例 3 和 4 使用 Nix flakes 来锁定依赖,因此我们需要启用 Nix flakes。
第 3 步:设置 Nix 路径
在示例 1 和 2 中,我们要使用 Nix 表达式 import <nixpkgs>。
在 NixOS 上,该表达式会跟随系统版本,也就是说,如果你在 NixOS 25.05 上使用 import <nixpkgs>,它会引用nixos-25.05 版本的 nixpkgs。
在其他 Linux 系统上,你会看到类似这样的报错:
debian-server % nix-shell -p pkg-config opencv
error: file 'nixpkgs' was not found in the Nix search path (add it using $NIX_PATH or -I)
at «string»:1:25:
1| {...}@args: with import <nixpkgs> args; (pkgs.runCommandCC or pkgs.runCommand) "shell" { buildInputs = [ (pkg-config) (opencv) ]; } ""
| ^
(use '--show-trace' to show detailed location information)我们需要通过设置Nix 搜索路径来告诉 Nix 该使用哪个版本的 nixpkgs:
debian-server % export NIX_PATH=nixpkgs=channel:nixos-25.05
debian-server % nix-shell -p pkg-config opencv
[nix-shell:/tmp/opencv]#好了!现在准备就绪,来看看第一个示例吧!
示例 1:临时交互式环境:nix-shell
Nix 提供了一种介于在系统上安装 OpenCV(如上例中的 apt install)和在独立的 Docker 容器中安装 OpenCV 之间的折中方案:Nix 可以让 OpenCV 可用,而无需永久安装。
我们可以运行 nix-shell(1) 来启动一个 bash shell,在其中指定的软件包可用。要成功构建使用了 GoCV 的 Go 代码,我们需要让 OpenCV 可用:
% nix-shell -p pkg-config opencv
these 194 paths will be fetched (175.80 MiB download, 764.10 MiB unpacked):
/nix/store/ig2nk0hsha9xaailhaj69yv677nv95q4-abseil-cpp-20210324.2
/nix/store/yw5xqn8lqinrifm9ij80nrmf0i6fdcbx-alsa-lib-1.2.13
[…]
[nix-shell:/tmp/opencv]$ pkg-config --cflags opencv4
-I/nix/store/mh5b1dx2ifv4jkp9a8lgssxwhzxssb96-opencv-4.11.0/include/opencv4顺便一提:是的,我们确实需要在这个 nix-shell 命令中显式指定 pkg-config,否则运行 pkg-config 时会调用宿主机上的版本(在开发环境之外),它找不到 opencv4.pc。
示例 2:nix-shell 配置文件:shell.nix
一旦我们找到了适用于项目的软件包组合(在这个例子里就是 pkg-config 和 opencv),就可以创建一个 shell.nix(放在任意目录,通常是项目根目录),nix-shell(不带 -p 参数)会自动读取它:
{
pkgs ? import <nixpkgs> { },
}:
pkgs.mkShell {
packages = with pkgs; [
# Explicitly list pkg-config so that mkShell will arrange
# for the PKG_CONFIG_PATH to find the .pc files.
pkg-config
opencv
];
}……然后,我们只需运行 nix-shell:
% nix-shell
[nix-shell:/tmp/opencv]$ pkg-config --cflags opencv4
-I/nix/store/mh5b1dx2ifv4jkp9a8lgssxwhzxssb96-opencv-4.11.0/include/opencv4如果你对软件包列表周围的样板代码感到好奇,这里有几点文档说明:
- 第 1 至 3 行声明了一个函数,带有一个参数集——这是
nix-shell能够调用你的shell.nix文件所要求的结构。 pkgs.mkShell是配合nix-shell使用的便捷辅助函数。with pkgs;这部分让我们可以直接写opencv,而不用写成pkgs.opencv。
顺带一提:借助 nixd 语言服务器,支持LSP 的编辑器可以显示软件包解析到的版本、指出拼写错误,或提供“跳转到定义”等功能。
例如,在这张截图中,我正在 Emacs 里编辑 shell.nix,好奇 opencv 软件包的 Nix 源码是什么样子。将光标“point”放在 opencv 上并按下 M-.(xref-find-definitions),我就跳转到了本地 Nix store 中的 opencv/4.x.nix:

示例 3:隔离且锁定的 devShell:Nix Flakes
前面的示例使用的是来自系统(或 Nix 路径)的 nixpkgs,这意味着升级系统时无需修改 .nix 文件——在我看来,这种行为是方便还是可怕,取决于具体的使用场景。
对于那些要求无论宿主操作系统是什么版本,.nix 文件都必须以完全相同方式构建的场景,我们可以使用Nix Flakes以隔离的方式构建,并通过 flake.lock 文件锁定依赖版本。
flake.nix 包含了与上面相同的 mkShell 表达式,但在其外层声明了结构:mkShell 表达式位于 outputs.devShells.x86_64-linux.default 属性中,而 inputs 属性则包含了可供此次构建使用的Flake 引用:
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
outputs =
{ self, nixpkgs }:
{
devShells.x86_64-linux.default =
let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
in
pkgs.mkShell {
packages = with pkgs; [
# Explicitly list pkg-config so that mkShell will arrange
# for the PKG_CONFIG_PATH to find the .pc files.
pkg-config
opencv
];
};
};
}顺带一提:尽管名字如此,最佳实践是使用 nixpkgs.legacyPackages,它在概念上提供了单一的 import nixpkgs 结果(出于效率考虑)。
现在,我可以使用 nix develop 来获得一个带有 OpenCV 的 shell:
% nix develop
michael@midna$ pkg-config --cflags opencv4
-I/nix/store/mh5b1dx2ifv4jkp9a8lgssxwhzxssb96-opencv-4.11.0/include/opencv4首次运行 nix develop 会生成一个 flake.lock 文件,因此之后再次运行 nix develop 会得到完全相同的环境。要更新到更新的版本,请使用 nix flake update。
提示:除了启动 shell,nix develop --command=emacs 也是一个实用的变体。
示例 4:让 Flake 与系统架构无关
遗憾的是,上面的 flake.nix 硬编码了 x86_64-linux,因此在例如 aarch64-linux(ARM)机器或 x86_64-darwin(Mac)上将无法使用。
默认需要显式指定 system 是人们对 Nix Flakes 长久以来的诟病之一。
有不少变通方法。例如,我们可以使用 numtide/flake-utils,并重构 flake.nix 以使用其 eachDefaultSystem 便捷函数:
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
self,
nixpkgs,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
formatter = pkgs.nixfmt-tree;
devShells.default = pkgs.mkShell {
packages = with pkgs; [
# Explicitly list pkg-config so that mkShell will arrange
# for the PKG_CONFIG_PATH to find the .pc files.
pkg-config
opencv
];
};
}
);
}或者,我们也可以使用它的精神续作 numtide/blueprint。
LucPerkins 的 dev-templates 实际上内联了这一技巧的某个版本。
对于一个不属于 Nix 本身、但与 Nix 紧密相关的解决方案:devenv 是一个基于 Nix 构建的独立工具(不再使用 CppNix 实现,而是实际上使用 tvix),但它有自己的一套 .nix 文件。
提示:让软件包常驻
如果你发现 nix develop 或类似命令在 flake.lock 未发生变化的情况下仍会拉取软件包,可以将该 Flake 安装到你的 profile 中,以将其声明为 Nix 的 gcroot:
% nix profile install .#devShells.x86_64-linux.default但等等,这难道不是让我们又回到了Debian 那种方式的状态吗?并不是!即使你将 flake 安装到 profile 中从而让 OpenCV 一直可用,仍然存在一层隔离:在系统层面,OpenCV 并不可用,只有当你通过 nix-shell 或 nix develop 启动开发环境时才可用。
结论
上面的四个示例该如何比较?以下是概览:
| 示例 | 样板代码 | 是否锁定? | 是否依赖系统? |
|---|---|---|---|
示例 1: nix-shell -p … | 😊 | 否 | 否 |
示例 2: shell.nix | 🙂 | 否 | 否 |
示例 3: flake.nix | 😲 | 是 | 是 |
示例 4: 与系统无关的 flake.nix | 🤨 | 是 | 否 |
对于个人的一次性实验,我会使用 nix-shell。
一旦实验跑通,我通常会想锁定依赖,于是会使用 flake.nix。
如果这个软件不仅要做版本管理,还要发布(或由多人/多系统协作),我就会花功夫把它做成与系统无关的 flake.nix。
我希望未来编写与系统无关的 flake 会变得更容易。
尽管仍有一些粗糙之处,我还是很欣赏 Nix 带来的可复现性和掌控感!
随机一篇博客
评论
登录后参与讨论