Development shells with Nix: four quick examples

Michael Stapelberg

使用 Nix 的開發殼層:四個快速範例

我想在其中一個專案中使用 GoCV(從較大的掃描檔案中找出並擷取紙本文件),但又不想在系統上永久安裝 OpenCV。

這似乎是個很好的範例情境,可以用來示範我常用的幾個 Nix 指令,涵蓋從快速、互動式的一次性 dev shells(開發殼層) 到完全宣告式、hermetic(隔離式)、可重現、可分享的 dev shells。

值得注意的是,你不需要使用 NixOS 才能執行這些指令!只要設定 Nix 路徑或使用 Flakes,就可以在任何 Linux 系統上安裝並使用 Nix,例如 Debian、Arch 等(請參閱設定)。

為了比較:Debian 的做法

在開始了解 Nix 之前,我先示範如何在 Debian 上讓 GoCV 運作。

讓我們建立一個使用 GoCV 函式(例如 gocv.NewMat())的最小 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]

在這個提示中回答「是」,會下載並安裝超過 500 個套件(需要幾分鐘時間)。

現在建置可以運作了:

debian % go build
debian % file minimal
minimal: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), […]

……但我們的系統上多了超過 500 個額外的套件,從此需要永遠更新,因此我希望將這個一次性的實驗與我平常的系統分開。

我們可以用 Docker 啟動一個 Debian 容器並在裡面工作,但視任務而定,正因為它是獨立的環境,可能會很麻煩。以這個範例來說,我需要指定 volume mount 才能讓 Docker 容器存取我的輸入檔案,還需要在 Docker 容器內的程式能在主機上開啟圖形視窗之前先設定環境變數……

讓我們來看看如何用 Nix 來幫忙解決這個問題!

設定:在 Debian 上使用 Nix(或在 Arch 上使用 Nix,或……)

NixOS 的使用者可以跳過這一節,因為 NixOS 系統已內建可直接使用的 Nix。

在你能在自己的電腦上嘗試這些範例之前,需要完成以下三個步驟:

  1. 安裝 Nix
  2. 啟用 Flakes
  3. 設定 Nix 路徑

步驟 1:安裝 Nix

Debian、Arch、Fedora 或其他 Linux 系統的使用者首先需要安裝 Nix。幸好,Nix 已可在許多熱門的 Linux 發行版上取得:

步驟 2:啟用 Flakes

Nix flakes 是「一種打包 Nix 產物的通用方式」

範例 3 和 4 使用 Nix flakes 來固定依賴套件版本,因此我們需要啟用 Nix flakes

步驟 3:設定 Nix 路徑

對於範例 1 和 2,我們想要使用 Nix 運算式 import <nixpkgs>

在 NixOS 上,這個運算式會跟隨系統版本,意思是如果你在 NixOS 25.05 安裝上使用 import <nixpkgs>,它將會參照nixpkgs 的 nixos-25.05 版本

在其他 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 殼層,讓指定的套件在其中可用。為了成功建置使用 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 時會執行主機上的版本(在 dev shell 之外),而它找不到 opencv4.pc

範例 2:nix-shell 設定檔:shell.nix

一旦我們找到適用於專案的套件組合(在我們的範例中,只有 pkg-configopencv),我們就可以建立一個 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

Emacs 在跳至 opencv 定義後顯示 opencv/4.x.nix

範例 3:隔離、已固定的 devShells:Nix Flakes

前面的範例使用來自你系統(或 Nix 路徑)的 nixpkgs,這意味著當你升級系統時不需要更改 .nix 檔案——取決於使用情境,我認為這種行為既可能是方便的,也可能是令人不安的。

對於那些無論周圍作業系統版本為何,都必須以完全相同方式建置 .nix 檔案的使用情境,我們可以使用Nix Flakes以 hermetic 的方式建置,並將依賴套件版本固定在 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 的殼層:

% 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

提示:除了殼層之外,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-shellnix develop 啟動開發殼層時才可用。

結論

上述四個範例如何比較?以下是總覽:

範例樣板程式碼已固定?與系統相依?
範例 1nix-shell -p …😊
範例 2shell.nix🙂
範例 3flake.nix😲
範例 4:與系統無關的 flake.nix🤨

對於個人的一次性實驗,我會使用 nix-shell

一旦實驗可行,我通常會想固定依賴套件版本,因此我會使用 flake.nix

如果這個軟體不僅需要版本控制,還需要發布(或由多人/多系統協作),我就會花功夫將它做成與系統無關的 flake.nix

我希望未來撰寫與系統無關的 flake 會變得更容易。

儘管仍有些粗糙之處,我很欣賞 Nix 帶給我的可重現性與掌控力!

原文由 Michael Stapelberg 發布

本文章由 muse-spark-1.2-contributor 進行翻譯