我的一些 nix 學習經(jīng)驗:安裝和打包
最近,我首次嘗試了 Mac。直至現(xiàn)在,我注意到的最大缺點是其軟件包管理比 Linux 差很多。一段時間以來,我對于 homebrew 感到相當不滿,因為每次我安裝新的軟件包時,它大部分時間都花在了升級上。于是,我萌生了試試 nix 包管理器的想法!
公認的,nix 的使用存在一定困惑性(甚至它有自己單獨的編程語言!),因此,我一直在努力以最簡潔的方式掌握使用 nix,避開復雜的配置文件管理和新編程語言學習。以下是我至今為止學習到的內(nèi)容, 敬請期待如何進行:
如同以往,由于我對 nix 的了解還停留在入門階段,本篇文章可能存在一些表述不準確的地方。甚至我自己也對于我是否真的喜歡上 nix 感到模棱兩可 —— 它的使用真的讓人相當困惑!但是,它幫我成功編譯了一些以前總是難以編譯的軟件,并且通常來說,它比 homebrew 的安裝速度要快。
nix 為何引人關(guān)注?
通常,人們把 nix 定義為一種“聲明式的包管理”。盡管我對此并不太感興趣,但以下是我對 nix 的兩個主要欣賞之處:
- 它提供了二進制包(托管在 https://cache.nixos.org/ 上),你可以迅速下載并安裝
- 對于那些沒有二進制包的軟件,nix 使編譯它們變得更容易
我認為 nix 之所以擅長于編譯軟件,主要有以下兩個原因:
- 在你的系統(tǒng)中,可以安裝同一庫或程序的多個版本(例如,你可能有兩個不同版本的 libc)。舉個例子,我當前的計算機上就存在兩個版本的 node,一個位于
/nix/store/4ykq0lpvmskdlhrvz1j3kwslgc6c7pnv-nodejs-16.17.1
,另一個位于/nix/store/5y4bd2r99zhdbir95w5pf51bwfg37bwa-nodejs-18.9.1
。 - 除此之外,nix 在構(gòu)建包時是在隔離的環(huán)境下進行的,只使用你明確聲明的依賴項的特定版本。因此,你無需擔心這個包可能依賴于你的系統(tǒng)里的其它你并不了解的包,再也不用與
LD_LIBRARY_PATH
戰(zhàn)斗了!許多人投入了大量工作,來列出所有包的依賴項。
在本文后面,我將給出兩個例子,展示 nix 如何使我在編譯軟件時遇到了更小的困難。
我是如何開始使用 nix 的
下面是我開始使用 nix 的步驟:
- 安裝 nix。我忘記了我當時是如何做到這一點,但看起來有一個官方安裝程序 和一個來自 zero-to-nix.com 的 非官方安裝程序。在 MacOS 上使用標準的多用戶安裝卸載 nix 的 教程 有點復雜,所以選擇一個卸載教程更為簡單的安裝方法可能值得。
- 把
~/.nix-profile/bin
添加到我的PATH
- 用
nix-env -iA nixpkgs.NAME
命令安裝包 - 就是這樣。
基本上,是把 nix-env -iA
當作 brew install
或者 apt-get install
。
例如,如果我想安裝 fish
,我可以這樣做:
nix-env -iA nixpkgs.fish
這看起來就像是從 https://cache.nixos.org 下載一些二進制文件 - 非常簡單。
有些人使用 nix 來安裝他們的 Node 和 Python 和 Ruby 包,但我并沒有那樣做 —— 我仍然像我以前一樣使用 npm install
和 pip install
。
一些我沒有使用的 nix 功能
有一些 nix 功能/工具我并沒有使用,但我要提及一下。我最初認為你必須使用這些功能才能使用 nix,因為我讀過的大部分 nix 教程都討論了它們。但事實證明,你并不一定要使用它們。
- NixOS(一個 Linux 發(fā)行版)
- nix-shell
- nix flakes
- home-manager
- devenv.sh
我不去深入討論它們,因為我并沒真正使用過它們,而且網(wǎng)上已經(jīng)有很多詳解。
安裝軟件包
nix 包在哪里定義的?
我認為 nix 包主倉庫中的包是定義在 https://github.com/NixOS/nixpkgs/。
你可以在 https://search.nixos.org/packages 查找包。似乎有兩種官方推薦的查找包的方式:
nix-env -qaP NAME
,但這非常緩慢,并且我并沒有得到期望的結(jié)果nix --extra-experimental-features 'nix-command flakes' search nixpkgs NAME
,這倒是管用,但顯得有點兒冗長。并且,無論何種原因,它輸出的所有包都以legacyPackages
開頭
我找到了一種我更喜歡的從命令行搜索 nix 包的方式:
- 運行
nix-env -qa '*' > nix-packages.txt
獲取 Nix 倉庫中所有包的列表 - 編寫一個簡潔的
nix-search
腳本,僅在packages.txt
中進行 grep 操作(cat ~/bin/nix-packages.txt | awk '{print $1}' | rg "$1"
)
所有的東西都是通過符號鏈接來安裝的
nix 的一個主要設(shè)計是,沒有一個單一的 bin
文件夾來存放所有的包,而是使用了符號鏈接。有許多層的符號鏈接。比如,以下就是一些符號鏈接的例子:
- 我機器上的
~/.nix-profile
最終是一個到/nix/var/nix/profiles/per-user/bork/profile-111-link/
的鏈接 ~/.nix-profile/bin/fish
是到/nix/store/afkwn6k8p8g97jiqgx9nd26503s35mgi-fish-3.5.1/bin/fish
的鏈接
當我安裝某樣東西的時候,它會創(chuàng)建一個新的 profile-112-link
目錄并建立新的鏈接,并且更新我的 ~/.nix-profile
使其指向那個目錄。
我認為,這意味著如果我安裝了新版本的 fish
但我并不滿意,我可以很容易地退回先前的版本,只需運行 nix-env --rollback
,這樣就可以讓我回到之前的配置文件目錄了。
卸載包并不意味著刪除它們
如果我像這樣卸載 nix 包,實際上并不會釋放任何硬盤空間,而僅僅是移除了符號鏈接:
$ nix-env --uninstall oil
我尚不清楚如何徹底刪除包 - 我試著運行了如下的垃圾收集命令,這似乎刪除了一些項目:
$ nix-collect-garbage
...
85 store paths deleted, 74.90 MiB freed
然而,我系統(tǒng)上仍然存在 oil
包,在 /nix/store/8pjnk6jr54z77jiq5g2dbx8887dnxbda-oil-0.14.0
。
nix-collect-garbage
有一個更具攻擊性的版本,它也會刪除你配置文件的舊版本(這樣你就不能回滾了)。
$ nix-collect-garbage -d --delete-old
盡管如此,上述命令仍無法刪除 /nix/store/8pjnk6jr54z77jiq5g2dbx8887dnxbda-oil-0.14.0
,我不明白原因。
升級過程
你可以通過以下的方式升級 nix 包:
nix-channel --update
nix-env --upgrade
(這與 apt-get update && apt-get upgrade
類似。)
我還沒真正嘗試升級任何東西。我推測,如果升級過程中出現(xiàn)任何問題,我可以通過以下方式輕松地回滾(因為在 nix 中,所有事物都是不可變的?。?/p>
nix-env --rollback
有人向我推薦了 Ian Henry 的 這篇文章,該文章討論了 nix-env --upgrade
的一些令人困惑的問題 - 也許它并不總是如我們所料?因此,我會對升級保持警惕。
下一個目標:創(chuàng)建名為 paperjam 的自定義包
經(jīng)過幾個月使用現(xiàn)有的 nix 包后,我開始考慮制作自定義包,對象是一個名為 paperjam 的程序,它還沒有被打包封裝。
實際上,因為我系統(tǒng)上的 libiconv
版本不正確,我甚至在沒有 nix 的情況下也遇到了編譯 paperjam
的困難。我認為,盡管我還不懂如何制作 nix 包,但使用 nix 來編譯它可能會更為簡單。結(jié)果證明我的想法是對的!
然而,理清如何實現(xiàn)這個目標的過程相當復雜,因此我在這里寫下了一些我實現(xiàn)它的方式和步驟。
構(gòu)建示例包的步驟
在我著手制作 paperjam
自定義包之前,我想先試手構(gòu)建一個已存在的示例包,以便確保我已經(jīng)理解了構(gòu)建包的整個流程。這個任務(wù)曾令我頭痛不已,但在我在 Discord 提問之后,有人向我闡述了如何從 https://github.com/NixOS/nixpkgs/ 獲取一個可執(zhí)行的包并進行構(gòu)建。以下是操作步驟:
步驟 1: 從 GitHub 的 nixpkgs 下載任意一個包,以 dash
包為例:
wget https://raw.githubusercontent.com/NixOS/nixpkgs/47993510dcb7713a29591517cb6ce682cc40f0ca/pkgs/shells/dash/default.nix -O dash.nix
步驟 2: 用 with import <nixpkgs> {};
替換開頭的聲明({ lib , stdenv , buildPackages , autoreconfHook , pkg-config , fetchurl , fetchpatch , libedit , runCommand , dash }:
)。我不清楚為何需要這樣做,但事實證明這么做是有效的。
步驟 3: 運行 nix-build dash.nix
這將開始編譯該包。
步驟 4: 運行 nix-env -i -f dash.nix
這會將該包安裝到我的 ~/.nix-profile
目錄下。
就這么簡單!一旦我完成了這些步驟,我便感覺自己能夠逐步修改 dash
包,進一步創(chuàng)建屬于我自己的包了。
制作自定義包的過程
因為 paperjam
依賴于 libpaper
,而 libpaper
還沒有打包,所以我首先需要構(gòu)建 libpaper
包。
以下是 libpaper.nix
,我基本上是從 nixpkgs 倉庫中其他包的源碼中復制粘貼得到的。我猜測這里的原理是,nix 對如何編譯 C 包有一些默認規(guī)則,例如 “運行 make install
”,所以 make install
實際上是默認執(zhí)行的,并且我并不需要明確地去配置它。
with import <nixpkgs> {};
stdenv.mkDerivation rec {
pname = "libpaper";
version = "0.1";
src = fetchFromGitHub {
owner = "naota";
repo = "libpaper";
rev = "51ca11ec543f2828672d15e4e77b92619b497ccd";
hash = "sha256-S1pzVQ/ceNsx0vGmzdDWw2TjPVLiRgzR4edFblWsekY=";
};
buildInputs = [ ];
meta = with lib; {
homepage = "https://github.com/naota/libpaper";
description = "libpaper";
platforms = platforms.unix;
license = with licenses; [ bsd3 gpl2 ];
};
}
這個腳本基本上告訴 nix 如何從 GitHub 下載源代碼。
我通過運行 nix-build libpaper.nix
來構(gòu)建它。
接下來,我需要編譯 paperjam
。我制作的 nix 包 的鏈接在這里。除了告訴它從哪里下載源碼外,我需要做的主要事情有:
- 添加一些額外的構(gòu)建依賴項(像
asciidoc
) - 在安裝過程中設(shè)置一些環(huán)境變量(
installFlags = [ "PREFIX=$(out)" ];
),這樣它就會被安裝在正確的目錄,而不是/usr/local/bin
。
我首先從散列值為空開始,然后運行 nix-build
以獲取一個關(guān)于散列值不匹配的錯誤信息。然后我從錯誤信息中復制出正確的散列值。
我只是在 nixpkgs 倉庫中運行 rg PREFIX
來找出如何設(shè)置 installFlags
的 —— 我認為設(shè)置 PREFIX
應(yīng)該是很常見的操作,可能之前已經(jīng)有人做過了,事實證明我的想法是對的。所以我只是從其他包中復制粘貼了那部分代碼。
然后我執(zhí)行了:
nix-build paperjam.nix
nix-env -i -f paperjam.nix
然后所有的東西都開始工作了,我成功地安裝了 paperjam
!耶!
下一個目標:安裝一個五年前的 Hugo 版本
當前,我使用的是 2018 年的 Hugo 0.40 版本來構(gòu)建我的博客。由于我并不需要任何的新功能,因此我并沒有感到有升級的必要。對于在 Linux 上操作,這個過程非常簡單:Hugo 的發(fā)行版本是靜態(tài)二進制文件,這意味著我可以直接從 發(fā)布頁面 下載五年前的二進制文件并運行。真的很方便!
但在我的 Mac 電腦上,我遇到了一些復雜的情況。過去五年中,Mac 的硬件已經(jīng)發(fā)生了一些變化,因此我下載的 Mac 版 Hugo 二進制文件并不能運行。同時,我嘗試使用 go build
從源代碼編譯,但由于在過去的五年內(nèi) Go 的構(gòu)建規(guī)則也有所改變,因此沒有成功。
我曾試圖通過在 Linux docker 容器中運行 Hugo 來解決這個問題,但我并不太喜歡這個方法:盡管可以工作,但它運行得有些慢,而且我個人感覺這樣做有些多余。畢竟,編譯一個 Go 程序不應(yīng)該那么麻煩!
幸好,Nix 來救援!接下來,我將介紹我是如何使用 nix 來安裝舊版本的 Hugo。
使用 nix 安裝 Hugo 0.40 版本
我的目標是安裝 Hugo 0.40,并將其添加到我的 PATH 中,以 hugo-0.40
作為命名。以下是我實現(xiàn)此目標的步驟。盡管我采取了一種相對特殊的方式進行操作,但是效果不錯(可以參考 搜索和安裝舊版本的 Nix 包 來找到可能更常規(guī)的方法)。
步驟 1: 在 nixpkgs 倉庫中搜索找到 Hugo 0.40。
我在此鏈接中找到了相應(yīng)的 .nix
文件 https://github.com/NixOS/nixpkgs/blob/17b2ef2/pkgs/applications/misc/hugo/default.nix。
步驟 2: 下載該文件并進行構(gòu)建。
我下載了帶有 .nix
擴展名的文件(以及同一目錄下的另一個名為 deps.nix
的文件),將文件的首行替換為 with import <nixpkgs> {};
,然后使用 nix-build hugo.nix
進行構(gòu)建。
雖然這個過程幾乎無需進行修改就能成功運行,但我仍然做了兩處小調(diào)整:
- 把
with stdenv.lib
替換為with lib
。 - 為避免與我已安裝的其他版本的
hugo
沖突,我把包名改為了hugo040
。
步驟 3: 將 hugo
重命名為 hugo-0.40
。
我編寫了一個簡短的后安裝腳本,用以重命名 Hugo 二進制文件。
postInstall = ''
mv $out/bin/hugo $out/bin/hugo-0.40
'';
我是通過在 nixpkgs 倉庫中運行 rg 'mv '
命令,然后復制和修改一條看似相關(guān)的代碼片段來找到如何實施此步驟。
步驟 4: 安裝。
我通過運行 nix-env -i -f hugo.nix
命令,將 Hugo 安裝到了 ~/.nix-profile/bin
目錄中。
所有的步驟都順利運行了!我把最終的 .nix
文件存放到了我自己的 nixpkgs 倉庫 中,這樣我以后如果需要,就能再次使用它了。
可重復的構(gòu)建過程并非神秘,其實它們極其復雜
我覺得值得一提的是,這個 hugo.nix
文件并不是什么魔法——我之所以能在今天輕易地編譯 Hugo 0.40,完全歸功于許多人長期以來的付出,他們讓 Hugo 的這個版本得以以可重復的方式打包。
總結(jié)
安裝 paperjam
和這個五年前的 Hugo 版本過程驚人地順利,實際上比沒有 nix 來編譯它們更簡單。這是因為 nix 極大地方便了我使用正確的 libiconv
版本來編譯 paperjam
包,而且五年前就已經(jīng)有人辛苦地列出了 Hugo 的確切依賴關(guān)系。
我并無計劃詳細深入地使用 nix(真的,我很可能對它感到困擾,然后最后選擇回歸使用 homebrew!),但我們將拭目以待!我發(fā)現(xiàn),簡單入手然后按需逐步掌握更多功能,遠比一開始就全面接觸一堆復雜功能更容易掌握。
我可能不會在 Linux 上使用 nix —— 我一直都對 Debian 基礎(chǔ)發(fā)行版的 apt
和 Arch 基礎(chǔ)發(fā)行版的 pacman
感到滿意,它們策略明晰且少有混淆。而在 Mac 上,使用 nix 似乎會有所得。不過,誰知道呢!也許三個月后,我可能會對 nix 感到不滿然后再次選擇回歸使用 homebrew。