用 Nix 建立開發環境:四個快速範例
原文由 Michael Stapelberg 于 發布,訂閱此部落格
我想在其中一個專案中使用 GoCV(用來從較大的掃描檔中找出並擷取紙本文件),但又不想在系統上永久安裝 OpenCV。
這似乎是個很適合的範例,可以用來展示我常用的幾個 Nix 指令,涵蓋從快速、互動式、一次性的開發環境,到完全宣告式、隔離、可重現、可分享的開發環境。
特別要說明的是,你不需要使用 NixOS 就能執行這些指令!只要設定好 Nix 路徑或使用 Flakes,就能在 Debian、Arch 等任何 Linux 系統上安裝並使用 Nix(詳見設定一節)。
作為對照:Debian 的做法
在開始介紹 Nix 之前,我先示範如何在 Debian 上讓 GoCV 跑起來。
讓我們建立一個最小的 Go 程式,使用像 gocv.NewMat() 這樣的 GoCV 函式,來確認我們能否編譯這個程式:
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 容器並在裡面工作,但正因為它是獨立的環境,視任務而定,這麼做可能會很麻煩。以這個例子來說,我得指定 volume mount 才能讓 Docker 容器存取我的輸入檔案,還得先設定好環境變數,容器內的程式才能在宿主機上開啟圖形視窗……
接下來看看 Nix 如何幫我們解決這個問題!
設定:在 Debian 上使用 Nix(或在 Arch 上,等等)
NixOS 的使用者可以跳過這一節,因為 NixOS 系統本身就內建了可直接使用的 Nix。
在你自己的電腦上嘗試這些範例之前,需要先完成以下三個步驟:
- 安裝 Nix
- 啟用 Flakes
- 設定 Nix 路徑
步驟 1:安裝 Nix
Debian、Arch、Fedora 或其他 Linux 系統的使用者首先需要安裝 Nix。幸好,許多主流的 Linux 發行版都有提供 Nix:
- Debian 提供了 nix-setup-systemd
- Arch Linux 打包了 nix 並在 Nix Arch Wiki 頁面上提供了說明文件。實際上,我安裝了該套件並設定了幾個
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,否則執行的會是宿主系統上的版本(在開發環境之外),它找不到 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 language server,支援 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 references:
{
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 帶來的可重現性與掌控度!
隨機一篇部落格
留言
登入後參與討論