NixOS 與 Nix Flakes 新手入門

2023-06-07 12:00:27

獨立部落格閱讀: https://thiscute.world/posts/nixos-and-flake-basics/

長文警告⚠️

本文的目標 NixOS 版本為 22.11,Nix 版本為 2.13.3,在此環境下 Nix Flakes 仍然為實驗性功能。

零、為什麼選擇 Nix

好幾年前就聽說過 Nix 包管理器,它用 Nix 語言編寫設定來管理系統依賴,此外基於 Nix 包管理器設計的 Linux 發行版 NixOS,還能隨時回滾到任一歷史狀態。
雖然聽著很牛,但是不僅要多學一門語言,裝個包還得寫程式碼,當時覺得太麻煩就沒研究。
但是最近搞系統遷移遇到兩件麻煩事,使我決定嘗試下 Nix.

第一件事是在新組裝的 PC 主機上安裝 EndeavourOS(Arch Linux 的一個衍生髮行版),因為舊系統也是 EndeavourOS 系統,安裝完為了省事,我就直接把舊電腦的 Home 目錄 rsync 同步到了新 PC 上。
這一同步就出了問題,所有功能都工作正常,但是視訊播放老是卡住,firefox/chrome/mpv 都會卡住,網上找各種資料都沒解決,還是我靈光一閃想到是不是 Home 目錄同步的鍋,清空了 Home 目錄,問題立馬就解決了...後面又花好長時間從舊電腦一點點恢復 Home 目錄下的東西。

第二件事是,我最近想嚐鮮 wayland,把桌面從 i3wm 換成了 sway,但是因為用起來區別不明顯,再加上諸多不便(hidpi、sway 設定調優都要花時間精力),嫌麻煩就還是回退到了 i3wm。結果回退後,每次系統剛啟動時,有一段時間 firefox/thunar 等 GUI 程式會一直卡著,要大概 1 分鐘後才能正常啟動...

發生第二件事時我就懶得折騰了,想到歸根結底還是系統沒有版本控制跟回滾機制,導致出了問題不能還原,裝新系統時各種軟體包也全靠自己手工從舊機器匯出軟體包清單,再在新機器安裝恢復。就打算乾脆換成 NixOS.

我折騰的第一步是在我 Homelab 上開了臺 NixOS 虛擬機器器,在這臺虛擬機器器裡一步步偵錯,把我物理機的 EndeavourOS i3 設定遷移到 NixOS + Flakes,還原出了整個桌面環境。
在虛擬機器器裡搞定後問題就不大了,直接備份好我辦公電腦的 Home 目錄、軟體清單,然後將系統重灌為 NixOS,再 git clone 我偵錯好的 NixOS 設定,改一改硬碟掛載相關的引數,額外補充下 Nvidia 顯示卡相關的 NixOS 設定,最後一行命令部署設定。幾行命令就在我全新的 NixOS 系統上還原出了整個 i3 桌面環境跟我的常用軟體,那一刻真的很有成就感!

NixOS 的回滾能力給了我非常大的底氣——再也不怕把系統搞掛了,於是我前幾天我又進一步遷移到了 hyprland 桌面,確實比 i3 香多了,它的動畫效果我吹爆!(在以前 EndeavourOS 上我肯定是不太敢做這樣的切換的,原因前面已經解釋過了——萬一把系統搞出問題,會非常麻煩。)

補充:v2ex 上有 v 友反饋 btrfs 檔案系統的快照功能,也能提供類似的回滾能力,而且簡單很多。我研究了下發現確實如此,btrfs 甚至也可以像 NixOS 一樣設定 grub 從快照啟動。所以如果你只是想要系統回滾能力,那麼基於 btrfs 快照功能的 btrbk 也是一個不錯的選擇。當然如果你仍然對 Nix 感興趣,那學一學也絕對不虧,畢竟 Nix 的能力遠不止於此,系統快照只是它能力的一部分而已~

在學了大半個月的 NixOS 與 Nix Flakes 後,我終於將我的 PC 從 EndeavouOS 系統切換到了 NixOS,這篇文章就脫胎於我這段時間的折騰筆記,希望能對你有所幫助~

前因後果交代完畢,那麼下面開始正文!

一、Nix 簡介

Nix 包管理器,跟 DevOps 領域當前流行的 pulumi/terraform/kubernetes 類似,都是宣告式設定管理工具,使用者需要在某些組態檔中宣告好期望的系統狀態,而 Nix 負責達成目標。區別在於 Nix 的管理目標是軟體包,而 pulumi/terraform 的管理目標是雲上資源。

簡單解釋下什麼是「宣告式設定」,它是指使用者只需要宣告好自己想要的結果——比如說希望將 i3 桌面替換成 sway 桌面,Nix 就會幫使用者達成這個目標。使用者不需要關心底層細節(比如說 sway 需要安裝哪些軟體包,哪些 i3 相關的軟體包需要解除安裝掉,哪些系統設定或環境變數需要針對 sway 做調整、如果使用了 Nvidia 顯示卡 Sway 引數要做什麼調整才能正常執行等等),Nix 會自動幫使用者處理這些細節。

基於 Nix 構建的 Linux 發行版 NixOS,可以簡單用 OS as Code 來形容,它通過宣告式的 Nix 組態檔來描述整個作業系統的狀態。

NixOS 的設定只負責管理系統層面的狀態,使用者目錄不受它管轄。有另一個重要的社群專案 home-manager 專門用於管理使用者目錄,將 home-manager 與 NixOS、Git 結合使用,就可以得到一個完全可復現、可回滾的系統環境。

因為 Nix 宣告式、可復現的特性,Nix 不僅可用於管理桌面電腦的環境,也有很多人用它管理開發編譯環境、雲上虛擬機器器、容器映象構建,Nix 官方的 NixOps 與社群的 deploy-rs 都是基於 Nix 實現的運維工具。

Home 目錄下檔案眾多,行為也不一,因此不可能對其中的所有檔案進行版本控制,代價太高。一般僅使用 home-manager 管理一些重要的組態檔,而其他需要備份的檔案可以用 rsync/synthing 等手段做備份同步,或者用 btrbk 之類的工具對 home 目錄做快照。

Nix 的優點

  • 宣告式設定,Environment as Code,可以直接用 Git 管理設定,只要組態檔不丟,系統就可以隨時還原到任一歷史狀態(理想情況下)。
    • 這跟一些程式語言中 cargo.lock/go.mod 等檔案鎖定依賴庫版本以確保構建結果可復現的思路是一致的。
    • 與 Docker 相比,Dockerfile 實際是命令式的設定,而且也不存在版本鎖這樣的東西,所以 Docker 的可復現能力遠不如 Nix.
  • 高度便捷的系統自定義能力
  • 可回滾:可以隨時回滾到任一歷史環境,NixOS 甚至預設將所有舊版本都加入到了啟動項,確保系統滾掛了也能隨時回退。所以 Nix 也被認為是最穩定的包管理方式。
  • 沒有依賴衝突問題:因為 Nix 中每個軟體包都擁有唯一的 hash,其安裝路徑中也會包含這個 hash 值,因此可以多版本共存。
  • 社群很活躍,第三方專案也挺豐富,官方包倉庫 nixpkgs 貢獻者眾多,也有很多人分享自己的 Nix 設定,一遍瀏覽下來,整個生態給我一種發現新大陸的興奮感。

Nix 的缺點

  • 學習成本高:如果你希望系統完全可復現,並且避免各種不當使用導致的坑,那就需要學習瞭解 Nix 的整個設計,並以宣告式的方式管理系統,不能無腦 nix-env -i(這類似 apt-get install)。
  • 檔案混亂:首先 Nix Flakes 目前仍然是實驗性特性,介紹它本身的檔案目前比較匱乏。 其次 Nix 社群絕大多數檔案都只介紹了舊的 nix-env/nix-channel,想直接從 Nix Flakes 開始學習的話,需要參考大量舊檔案,從中提取出自己需要的內容。另外一些 Nix 當前的核心功能,官方檔案都語焉不詳(比如 imports 跟 Nixpkgs Module System),想搞明白基本只能看原始碼了...
  • 包數量比較少:撤回下這一條,官方宣稱 nixpkgs 是有 80000+ 個軟體包,使用下來確實絕大部分包都能在 nixpkgs 裡找到,體驗還是不錯滴。
  • 比較吃硬碟空間:為了保證系統可以隨時回退,nix 預設總是保留所有歷史環境,這非常吃硬碟空間。雖然可以定期使用 nix-collect-garbage 來手動清理舊的歷史環境,也還是建議設定個更大的硬碟...
  • 報錯資訊比較隱晦:一般的報錯提示還是比較清楚的,但是遇到好幾次依賴版本有問題或者傳參錯誤提示不出原因,--show-trace 直接輸出一堆的內部堆疊,都花了很長時間才定位到,通過升級依賴版本或者修正引數後問題解決。
    • 猜測導致這個問題的原因有兩個,一是 Nix 是動態語言,各種引數都是執行時才確定型別。二是我用到的 flake 包的錯誤處理邏輯寫得不太好,錯誤提示不清晰,一些隱晦的錯誤甚至通過錯誤堆疊也定位不到原因。

簡單總結下

總的來說,我覺得 NixOS 適合那些有一定 Linux 使用經驗與程式設計經驗,並且希望對自己的系統擁有更強掌控力的開發者。

另外一條資訊:在開發環境搭建方面 Nix 與相對流行的 Dev Containers 也有些競爭關係,它們的具體區別還有待我發掘~

二、安裝

Nix 有多種安裝方式,支援以包管理器的形式安裝到 MacOS/Linux/WSL 三種系統上,Nix 還額外提供了 NixOS ——一個使用 Nix 管理整個系統環境的 Linux 發行版。

我選擇了直接使用 NixOS 的 ISO 映象安裝 NixOS 系統,從而最大程度上通過 Nix 管理整個系統的環境。

安裝很簡單,這裡不多介紹,僅列一下我覺得比較有用的參考資料:

  1. Nix 的官方安裝方式: 使用 bash 指令碼編寫, 目前(2023-04-23)為止 nix-command & flakes 仍然是實驗性特性,需要手動開啟。
    1. 你需要參照 Enable flakes - NixOS Wiki 的說明啟用 nix-command & flakes
    2. 官方不提供任何解除安裝手段,要在 Linux/MacOS 上解除安裝 Nix,你需要手動刪除所有相關的檔案、使用者以及使用者組
  2. The Determinate Nix Installer: 第三方使用 Rust 編寫的 installer, 預設啟用 nix-command & flakes,並且提供瞭解除安裝命令。

三、Nix Flakes 與舊的 Nix

Nix 於 2020 年推出了 nix-command & flakes 兩個新特性,它們提供了全新的命令列工具、標準的 Nix 包結構定義、類似 cargo/npm 的 flake.lock 版本鎖檔案等等。這兩個特性極大地增強了 Nix 的能力,因此雖然至今(2023/5/5)它們仍然是實驗性特性,但是已經被 Nix 社群廣泛使用,是強烈推薦使用的功能。

目前 Nix 社群的絕大多數檔案仍然只介紹了傳統 Nix,不包含 Nix Flakes 相關的內容,但是從可復現、易於管理維護的角度講,舊的 Nix 包結構與命令列工具已經不推薦使用了,因此本檔案也不會介紹舊的 Nix 包結構與命令列工具的使用方法,也建議新手直接忽略掉這些舊的內容,從 nix-command & flakes 學起。

這裡列舉下在 nix-command & flakes 中已經不需要用到的舊的 Nix 命令列工具與相關概念,在查詢資料時,如果看到它們直接忽略掉就行:

  1. nix-channel: nix-channel 與 apt/yum/pacman 等其他 Linux 發行版的包管理工具類似,通過 stable/unstable/test 等 channel 來管理軟體包的版本。
    1. Nix Flakes 在 flake.nix 中通過 inputs 宣告依賴包的資料來源,通過 flake.lock 鎖定依賴版本,完全取代掉了 nix-channel 的功能。
  2. nix-env: 用於管理使用者環境的軟體包,是傳統 Nix 的核心命令列工具。它從 nix-channel 定義的資料來源中安裝軟體包,所以安裝的軟體包版本受 channel 影響。通過 nix-env 安裝的包不會被自動記錄到 Nix 的宣告式設定中,是完全脫離掌控的,無法在其他主機上覆現,因此不推薦使用。
    1. 在 Nix Flakes 中對應的命令為 nix profile
  3. nix-shell: nix-shell 用於建立一個臨時的 shell 環境
    1. 在 Nix Flakes 中它被 nix developnix shell 取代了。
  4. nix-build: 用於構建 Nix 包,它會將構建結果放到 /nix/store 路徑下,但是不會記錄到 Nix 的宣告式設定中。
    1. 在 Nix Flakes 中對應的命令為 nix build
  5. ...

四、NixOS 的 Flakes 包倉庫

跟 Arch Linux 類似,Nix 也有官方與社群的軟體包倉庫:

  1. nixpkgs 是一個包含了所有 Nix 包與 NixOS 模組/設定的 Git 倉庫,其 master 分支包含最新的 Nix 包與 NixOS 模組/設定。
  2. 比如 qq 就直接包含在 nixpkgs 中了
  3. NUR: 類似 Arch Linux 的 AUR,NUR 是 Nix 的一個第三方的 Nix 包倉庫,算是 nixpkgs 的一個增補包倉庫。
  4. 這些常用國產軟體,都可以通過 NUR 安裝:
  5. qqmusic
  6. wechat-uos
  7. dingtalk
  8. 更多程式,可以在這裡搜尋:Nix User Repositories
  9. Nix Flakes 也可直接從 Git 倉庫中安裝軟體包,這種方式可以用於安裝任何人提供的 Flakes 包

此外一些沒有 Nix 支援或者支援不佳的軟體,也可以考慮通過 Flatpak 或者 AppImage 的方式安裝使用,這兩個都是在所有 Linux 發行版上可用的軟體打包與安裝手段,詳情請自行搜尋,這裡就不介紹細節了。

五、Nix 語言基礎

https://nix.dev/tutorials/first-steps/nix-language

Nix 語言是 Nix 的基礎,要想玩得轉 NixOS 與 Nix Flakes,享受到它們帶來的諸多好處,就必須學會這門語言。

Nix 是一門比較簡單的函數式語言,在已有一定程式設計基礎的情況下,過一遍這些語法用時應該在 2 個小時以內,本文假設你具有一定程式設計基礎(也就是說寫得不會很細)。

這一節主要包含如下內容:

  1. 資料型別
  2. let...in... with inherit 等特殊語法
  3. 函數的宣告與呼叫語法
  4. 內建函數與庫函數
  5. inputs 的不純性(Impurities)
  6. 用於描述 Build Task 的 Derivation
  7. Overriding 與 Overlays
  8. ...

先把語法過一遍,有個大概的印象就行,後面需要用到時再根據右側目錄回來複習。

1. 基礎資料型別一覽

下面通過一個 attribute set (這類似 json 或者其他語言中的 map/dict)來簡要說明所有基礎資料型別:

{
  string = "hello";
  integer = 1;
  float = 3.141;
  bool = true;
  null = null;
  list = [ 1 "two" false ];
  attribute-set = {
    a = "hello";
    b = 2;
    c = 2.718;
    d = false;
  }; # comments are supported
}

以及一些基礎操作符(普通的算術運算、布林運算就跳過不介紹了):

# 列表拼接
[ 1 2 3 ] ++ [ 4 5 6 ] # [ 1 2 3 4 5 6 ]

# 將 // 後面的 attribut set 中的內容,全部更新到 // 前面的 attribute set 中
{ a = 1; b = 2; } // { b = 3; c = 4; } # 結果為 { a = 1; b = 3; c = 4; }

# 邏輯隱含,等同於 !b1 || b2.
bool -> bool

2. let ... in ...

Nix 的 let ... in ... 語法被稱作「let 表示式」或者「let 繫結」,它用於建立臨時使用的區域性變數:

let
  a = 1;
in
a + a  # 結果是 2

let 表示式中的變數只能在 in 之後的表示式中使用,理解成臨時變數就行。

3. attribute set 說明

花括號 {} 用於建立 attribute set,也就是 key-value 對的集合,類似於 JSON 中的物件。

attribute set 預設不支援遞迴參照,如下內容會報錯:

{
  a = 1;
  b = a + 1; # error: undefined variable 'a'
}

不過 Nix 提供了 rec 關鍵字(recursive attribute set),可用於建立遞迴參照的 attribute set:

rec {
  a = 1;
  b = a + 1; # ok
}

在遞迴參照的情況下,Nix 會按照宣告的順序進行求值,所以如果 ab 之後宣告,那麼 b 會報錯。

可以使用 . 操作符來存取 attribute set 的成員:

let
  a = {
    b = {
      c = 1;
    };
  };
in
a.b.c # 結果是 1

. 操作符也可直接用於賦值:

{ a.b.c = 1; }

此外 attribute set 還支援一個 has attribute 操作符,它可用於檢測 attribute set 中是否包含某個屬性,返回 bool 值:

let
  a = {
    b = {
      c = 1;
    };
  };
in
a?b  # 結果是 true,因為 a.b 這個屬性確實存在

has attribute 操作符在 nixpkgs 庫中常被用於檢測處理 args?system 等引數,以 (args?system)(! args?system) 的形式作為函數引數使用(歎號表示對 bool 值取反,是常見 bool 值運運算元)。

4. with 語句

with 語句的語法如下:

with <attribute-set> ; <expression>

with 語句會將 <attribute-set> 中的所有成員新增到當前作用域中,這樣在 <expression> 中就可以直接使用 <attribute-set> 中的成員了,簡化 attribute set 的存取語法,比如:

let
  a = {
    x = 1;
    y = 2;
    z = 3;
  };
in
with a; [ x y z ]  # 結果是 [ 1 2 3 ], 等價於 [ a.x a.y a.z ]

5. 繼承 inherit ...

inherit 語句用於從 attribute set 中繼承成員,同樣是一個簡化程式碼的語法糖,比如:

let
  x = 1;
  y = 2;
in
{
  inherit x y;
}  # 結果是 { x = 1; y = 2; }

inherit 還能直接從某個 attribute set 中繼承成員,語法為 inherit (<attribute-set>) <member-name>;,比如:

let
  a = {
    x = 1;
    y = 2;
    z = 3;
  };
in
{
  inherit (a) x y;
}  # 結果是 { x = 1; y = 2; }

6. ${ ... } 字串插值

${ ... } 用於字串插值,懂點程式設計的應該都很容易理解這個,比如:

let
  a = 1;
in
"the value of a is ${a}"  # 結果是 "the value of a is 1"

7. 檔案系統路徑

Nix 中不帶引號的字串會被解析為檔案系統路徑,路徑的語法與 Unix 系統相同。

8. 搜尋路徑

請不要使用這個功能,它會導致不可預期的行為。

Nix 會在看到 <nixpkgs> 這類三角括號語法時,會在 NIX_PATH 環境變數中指定的路徑中搜尋該路徑。

因為環境變數 NIX_PATH 是可變更的值,所以這個功能是不純的,會導致不可預期的行為。

在這裡做個介紹,只是為了讓你在看到別人使用類似的語法時不至於抓瞎。

9. 多行字串

多行字串的語法為 '',比如:

''
  this is a
  multi-line
  string
''

10. 函數

函數的宣告語法為:

<arg1>:
  <body>

舉幾個常見的例子:

# 單引數函數
a: a + a

# 巢狀函數
a: b: a + b

# 雙引數函數
{ a, b }: a + b

# 雙引數函數,帶預設值。問號後面的是引數的預設值
{ a ? 1, b ? 2 }: a + b

# 帶有命名 attribute set 作為引數的函數,並且使用 ... 收集其他可選引數
# 命名 args 與 ... 可選引數通常被一起作為函數的引數定義使用
args@{ a, b, ... }: a + b + args.c
# 如下內容等價於上面的內容,
{ a, b, ... }@args: a + b + args.c

# 但是要注意命名引數僅繫結了輸入的 attribute set,預設引數不在其中,舉例
let
  f = { a ? 1, b ? 2, ... }@args: args
in
  f {}  # 結果是 {},也就說明了 args 中包含預設值

# 函數的呼叫方式就是把引數放在後面,比如下面的 2 就是前面這個函數的引數
a: a + a 2  # 結果是 4

# 還可以給函數命名,不過必須使用 let 表示式
let
  f = a: a + a;
in
  f 2  # 結果是 4

內建函數

Nix 內建了一些函數,可通過 builtins.<function-name> 來呼叫,比如:

builtins.add 1 2  # 結果是 3

詳細的內建函數列表參見 Built-in Functions - Nix Reference Mannual

import 表示式

import 表示式以其他 Nix 檔案的路徑作為引數,返回該 Nix 檔案的執行結果。

import 的引數如果為資料夾路徑,那麼會返回該資料夾下的 default.nix 檔案的執行結果。

舉個例子,首先建立一個 file.nix 檔案:

$ echo "x: x + 1" > file.nix

然後使用 import 執行它:

import ./file.nix 1  # 結果是 2

pkgs.lib 函數包

除了 builtins 之外,Nix 的 nixpkgs 倉庫還提供了一個名為 lib 的 attribute set,它包含了一些常用的函數,它通常被以如下的形式被使用:

let
  pkgs = import <nixpkgs> {};
in
pkgs.lib.strings.toUpper "search paths considered harmful"  # 結果是 "SEARCH PATHS CONSIDERED HARMFUL"

可以通過 Nixpkgs Library Functions - Nixpkgs Manual 檢視 lib 函數包的詳細內容。

11. 不純(Impurities)

Nix 語言本身是純函數式的,是純的,「純」是指它就跟數學中的函數一樣,同樣的輸入永遠得到同樣的輸出。

Nix 有兩種構建輸入,一種是從檔案系統路徑等輸入源中讀取檔案,另一種是將其他函數作為輸入。

Nix 唯一的不純之處在這裡:從檔案系統路徑或者其他輸入源中讀取檔案作為構建任務的輸入,這些輸入源引數可能沒變化,但是檔案內容或資料來源的返回內容可能會變化,這就會導致輸入相同,Nix 函數的輸出卻可能不同——函數變得不純了。

Nix 中的搜尋路徑與 builtins.currentSystem 也是不純的,但是這兩個功能都不建議使用,所以這裡略過了。

12. Fetchers

構建輸入除了直接來自檔案系統路徑之外,還可以通過 Fetchers 來獲取,Fetcher 是一種特殊的函數,它的輸入是一個 attribute set,輸出是 Nix Store 中的一個系統路徑。

Nix 提供了四個內建的 Fetcher,分別是:

  • builtins.fetchurl:從 url 中下載檔案
  • builtins.fetchTarball:從 url 中下載 tarball 檔案
  • builtins.fetchGit:從 git 倉庫中下載檔案
  • builtins.fetchClosure:從 Nix Store 中獲取 Derivation

舉例:

builtins.fetchurl "https://github.com/NixOS/nix/archive/7c3ab5751568a0bc63430b33a5169c5e4784a0ff.tar.gz"
# result example => "/nix/store/7dhgs330clj36384akg86140fqkgh8zf-7c3ab5751568a0bc63430b33a5169c5e4784a0ff.tar.gz"

builtins.fetchTarball "https://github.com/NixOS/nix/archive/7c3ab5751568a0bc63430b33a5169c5e4784a0ff.tar.gz"
# result example(auto unzip the tarball) => "/nix/store/d59llm96vgis5fy231x6m7nrijs0ww36-source"

13. Derivations

一個構建動作的 Nix 語言描述被稱做一個 Derivation,它描述瞭如何構建一個軟體包,它的構建結果是一個 Store Object

Store Object 的存放路徑格式為 /nix/store/<hash>-<name>,其中 <hash> 是構建結果的 hash 值,<name> 是它的名字。路徑 hash 值確保了每個構建結果都是唯一的,因此可以多版本共存,而且不會出現依賴衝突的問題。

/nix/store 被稱為 Store,存放所有的 Store Objects,這個路徑被設定為唯讀,只有 Nix 本身才能修改這個路徑下的內容,以保證系統的可復現性。

在 Nix 語言的最底層,一個構建任務就是使用 builtins 中的不純函數 derivation 建立的,我們實際使用的 stdenv.mkDerivation 就是它的一個 wrapper,遮蔽了底層的細節,簡化了用法。

六、以宣告式的方式管理系統

https://nixos.wiki/wiki/Overview_of_the_NixOS_Linux_distribution

瞭解了 Nix 語言的基本用法之後,我們就可以開始使用 Nix 語言來設定 NixOS 系統了。NixOS 的系統設定路徑為 /etc/nixos/configuration.nix,它包含系統的所有宣告式設定,如時區、語言、鍵盤佈局、網路、使用者、檔案系統、啟動項等。

如果想要以可復現的方式修改系統的狀態(這也是最推薦的方式),就需要手工修改 /etc/nixos/configuration.nix 檔案,然後執行 sudo nixos-rebuild switch 命令來應用設定,此命令會根據組態檔生成一個新的系統環境,並將新的環境設為預設環境。
同時上一個系統環境會被保留,而且會被加入到 grub 的啟動項中,這確保了即使新的環境不能啟動,也能隨時回退到舊環境。

另一方面,/etc/nixos/configuration.nix 是傳統的 Nix 設定方式,它依賴 nix-channel 設定的資料來源,也沒有任何版本鎖定機制,實際無法確保系統的可復現性。
更推薦使用的是 Nix Flakes,它可以確保系統的可復現性,同時也可以很方便地管理系統的設定。

我們下面首先介紹下通過 NixOS 預設的設定方式來管理系統,然後再過渡到更先進的 Nix Flakes.

1. 使用 /etc/nixos/configuration.nix 設定系統

前面提過了這是傳統的 Nix 設定方式,也是當前 NixOS 預設使用的設定方式,它依賴 nix-channel 設定的資料來源,也沒有任何版本鎖定機制,實際無法確保系統的可復現性。

簡單起見我們先使用這種方式來設定系統,後面會介紹 Flake 的使用。

比如要啟用 ssh 並新增一個使用者 ryan,只需要在 /etc/nixos/configuration.nix 中新增如下設定:

# Edit this configuration file to define what should be installed on
# your system.  Help is available in the configuration.nix(5) man page
# and in the NixOS manual (accessible by running ‘nixos-help’).
{ config, pkgs, ... }:

{
  imports =
    [ # Include the results of the hardware scan.
      ./hardware-configuration.nix
    ];

  # 省略掉前面的設定......

  # 新增使用者 ryan
  users.users.ryan = {
    isNormalUser = true;
    description = "ryan";
    extraGroups = [ "networkmanager" "wheel" ];
    openssh.authorizedKeys.keys = [
        # replace with your own public key
        "ssh-ed25519 <some-public-key> ryan@ryan-pc"
    ];
    packages = with pkgs; [
      firefox
    #  thunderbird
    ];
  };

  # 啟用 OpenSSH 後臺服務
  services.openssh = {
    enable = true;
    permitRootLogin = "no";         # disable root login
    passwordAuthentication = false; # disable password login
    openFirewall = true;
    forwardX11 = true;              # enable X11 forwarding
  };

  # 省略其他設定......
}

這裡我啟用了 openssh 服務,為 ryan 使用者新增了 ssh 公鑰,並禁用了密碼登入。

現在執行 sudo nixos-rebuild switch 部署修改後的設定,之後就可以通過 ssh 金鑰遠端登入到我的這臺主機了。

這就是 NixOS 預設的宣告式系統設定,要對系統做任何可復現的變更,都只需要修改 /etc/nixos/configuration.nix 檔案,然後執行 sudo nixos-rebuild switch 部署變更即可。

/etc/nixos/configuration.nix 的所有設定項,可以在這幾個地方查到:

  • 直接 Google,比如 Chrome NixOS 就能找到 Chrome 相關的設定項,一般 NixOS Wiki 或 nixpkgs 倉庫原始碼的排名會比較靠前。
  • NixOS Options Search 中搜尋鍵碼
  • 系統級別的設定,可以考慮在 Configuration - NixOS Manual 找找相關檔案
  • 直接在 nixpkgs 倉庫中搜尋鍵碼,讀相關的原始碼。

2. 啟用 NixOS 的 Flakes 支援

與 NixOS 預設的設定方式相比,Nix Flakes 提供了更好的可復現性,同時它清晰的包結構定義原生支援了以其他 Git 倉庫為依賴,便於程式碼分享,因此更建議使用 Nix Flakes 來管理系統設定。

但是目前 Nix Flakes 作為一個實驗性的功能,仍未被預設啟用。所以我們需要手動啟用它,修改 /etc/nixos/configuration.nix 檔案,在函數塊中啟用 flakes 與 nix-command 功能:

# Edit this configuration file to define what should be installed on
# your system.  Help is available in the configuration.nix(5) man page
# and in the NixOS manual (accessible by running ‘nixos-help’).
{ config, pkgs, ... }:

{
  imports =
    [ # Include the results of the hardware scan.
      ./hardware-configuration.nix
    ];

  # 省略掉前面的設定......

  # 啟用 Nix Flakes 功能,以及配套的新 nix-command 命令列工具
  nix.settings.experimental-features = [ "nix-command" "flakes" ];

  environment.systemPackages = with pkgs; [
    git  # Nix Flakes 通過 git 命令從資料來源拉取依賴,所以必須先安裝好 git
    vim
    wget
  ];

  # 省略其他設定......
}

然後執行 sudo nixos-rebuild switch 應用修改後,即可使用 Nix Flakes 來管理系統設定。

額外還有個好處就是,現在你可以通過 nix repl 開啟一個 nix 互動式環境,有興趣的話,可以使用它複習測試一遍前面學過的所有 Nix 語法。

3. 將系統設定切換到 flake.nix

在啟用了 Nix Flakes 特性後,sudo nixos-rebuild switch 命令會優先讀取 /etc/nixos/flake.nix 檔案,如果找不到再嘗試使用 /etc/nixos/configuration.nix

可以首先使用官方提供的模板來學習 flake 的編寫,先查下有哪些模板:

nix flake show templates

其中有個 templates#full 模板展示了所有可能的用法,可以看看它的內容:

nix flake init -t templates#full
cat flake.nix

我們參照該模板建立檔案 /etc/nixos/flake.nix 並編寫好設定內容,後續系統的所有修改都將全部由 Nix Flakes 接管,範例內容如下:

{
  description = "Ryan's NixOS Flake";

  # 這是 flake.nix 的標準格式,inputs 是 flake 的依賴,outputs 是 flake 的輸出
  # inputs 中的每一項依賴都會在被拉取、構建後,作為引數傳遞給 outputs 函數
  inputs = {
    # flake inputs 有很多種參照方式,應用最廣泛的是 github:owner/name/reference,即 github 倉庫地址 + branch/commit-id/tag

    # NixOS 官方軟體源,這裡使用 nixos-unstable 分支
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    # home-manager,用於管理使用者設定
    home-manager = {
      url = "github:nix-community/home-manager/release-22.11";
      # `follows` 是 inputs 中的繼承語法
      # 這裡使 sops-nix 的 `inputs.nixpkgs` 與當前 flake 的 `inputs.nixpkgs` 保持一致,
      # 避免依賴的 nixpkgs 版本不一致導致問題
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  # outputs 即 flake 的所有輸出,其中的 nixosConfigurations 即 NixOS 系統設定
  # 一個 flake 可以有很多用途,也可以有很多種不同的輸出,nixosConfigurations 只是其中一種
  #
  # outputs 的引數都是 inputs 中定義的依賴項,可以通過它們的名稱來參照。
  # 不過 self 是個例外,這個特殊引數指向 outputs 自身(自參照),以及 flake 根目錄
  # 這裡的 @ 語法將函數的引數 attribute set 取了個別名,方便在內部使用
  outputs = { self, nixpkgs, ... }@inputs: {
    # 名為 nixosConfigurations 的 outputs 會在執行 `sudo nixos-rebuild switch` 時被使用
    # 預設情況下上述命令會使用與主機 hostname 同名的 nixosConfigurations
    # 但是也可以通過 `--flake /path/to/flake/direcotry#nixos-test` 來指定
    # 在 flakes 組態檔夾中執行 `sudo nixos-rebuild switch --flake .#nixos-test` 即可部署此設定
    #   其中 `.` 表示使用當前資料夾的 Flakes 設定,`#` 後面的內容則是 nixosConfigurations 的名稱
    nixosConfigurations = {
      # hostname 為 nixos-test 的主機會使用這個設定
      # 這裡使用了 nixpkgs.lib.nixosSystem 函數來構建設定,後面的 attributes set 是它的引數
      # 在 nixos 系統上使用如下命令即可部署此設定:`nixos-rebuild switch --flake .#nixos-test`
      "nixos-test" = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";

        # Nix 模組系統可將設定模組化,提升設定的可維護性
        #
        # modules 中每個引數,都是一個 Nix Module,nixpkgs manual 中有半份介紹它的檔案:
        #    <https://nixos.org/manual/nixpkgs/unstable/#module-system-introduction>
        # 說半份是因為它的檔案不全,只有一些簡單的介紹(Nix 檔案現狀...)
        # Nix Module 可以是一個 attribute set,也可以是一個返回 attribute set 的函數
        # 如果是函數,那麼它的引數就是當前的 NixOS Module 的引數.
        # 根據 Nix Wiki 對 Nix modules 的描述,Nix modules 函數的引數可以有這幾個:
        #
        #  lib:     nixpkgs 自帶的函數庫,提供了許多操作 Nix 表示式的實用函數
        #           詳見 https://nixos.org/manual/nixpkgs/stable/#id-1.4
        #  config:  當前 flake 的所有 config 引數的集何
        #  options: 當前 flake 中所有 NixOS Modules 中定義的所有引數的集合
        #  pkgs:    一個包含所有 nixpkgs 包的集合
        #           入門階段可以認為它的預設值為 `nixpkgs.legacyPackages."${system}"`
        #           可通過 `nixpkgs.pkgs` 這個 option 來自定義 pkgs 的值
        #  modulesPath: 預設 nixpkgs 的內建 Modules 資料夾路徑,常用於從 nixpkgs 中匯入一些額外的模組
        #               這個引數通常都用不到,我只在製作 iso 映象時用到過
        #
        # 預設只能傳上面這幾個引數,如果需要傳其他引數,必須使用 specialArgs,你可以取消註釋如下這行來啟用該引數
        # specialArgs = inputs  # 將 inputs 中的引數傳入所有子模組
        modules = [
          # 匯入之前我們使用的 configuration.nix,這樣舊的組態檔仍然能生效
          # 注: /etc/nixos/configuration.nix 本身也是一個 Nix Module,因此可以直接在這裡匯入
          ./configuration.nix
        ];
      };
    };
  };
}

這裡我們定義了一個名為 nixos-test 的系統,它的組態檔為 ./configuration.nix,這個檔案就是我們之前的組態檔,這樣我們仍然可以沿用舊的設定。

現在執行 sudo nixos-rebuild switch 應用設定,系統應該沒有任何變化,因為我們僅僅是切換到了 Nix Flakes,設定內容與之前還是一致的。

4. 通過 Flakes 來管理系統軟體

切換完畢後,我們就可以通過 Flakes 來管理系統了。管系統最常見的需求就是裝軟體,我們在前面已經見識過如何通過 environment.systemPackages 來安裝 pkgs 中的包,這些包都來自官方的 nixpkgs 倉庫。

現在我們學習下如何通過 Flakes 安裝其他來源的軟體包,這比直接安裝 nixpkgs 要靈活很多,最顯而易見的好處是你可以很方便地設定軟體的版本。
helix 編輯器為例,我們首先需要在 flake.nix 中新增 helix 這個 inputs 資料來源:

{
  description = "NixOS configuration of Ryan Yin";

  # ......

  inputs = {
    # ......

    # helix editor, use tag 23.05
    helix.url = "github:helix-editor/helix/23.05"
  };

  outputs = inputs@{ self, nixpkgs, ... }: {
    nixosConfigurations = {
      nixos-test = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";

        # 將所有 inputs 引數設為所有子模組的特殊引數,這樣就能在子模組中使用 helix 這個 inputs 了
        specialArgs = inputs;
        modules = [
          ./configuration.nix
        ];
      };
    };
  };
}

接下來在 configuration.nix 中就能參照這個 flake input 資料來源了:

# Edit this configuration file to define what should be installed on
# your system.  Help is available in the configuration.nix(5) man page
# and in the NixOS manual (accessible by running ‘nixos-help’).
# Nix 會通過名稱匹配,自動將 specialArgs 中的 helix 注入到此函數的第三個引數
{ config, pkgs, helix, ... }:

{
  # 省略掉前面的設定......

  environment.systemPackages = with pkgs; [
    git  # Nix Flakes 通過 git 命令從資料來源拉取依賴,所以必須先安裝好 git
    vim
    wget

    # 這裡從 helix 這個 inputs 資料來源安裝了 helix 程式
    helix."${pkgs.system}".packages.helix
  ];

  # 省略其他設定......
}

改好後再 sudo nixos-rebuild switch 部署,就能安裝好 helix 程式了,可直接在終端使用 helix 命令測試驗證。

5. 為 Flake 新增國內 cache 源

Nix 為了加快包構建速度,提供了 https://cache.nixos.org 提前快取構建結果提供給使用者,但是在國記憶體取這個 cache 地址非常地慢,如果沒有全域性代理的話,基本上是無法使用的。
另外 Flakes 的資料來源基本都是某個 Github 倉庫,在國內從 Github 下載 Flakes 資料來源也同樣非常非常慢。

在舊的 NixOS 設定方式中,可以通過 nix-channel 命令新增國內的 cache 映象源以提升下載速度,但是 Nix Flakes 會盡可能地避免使用任何系統級別的設定跟環境變數,以確保其構建結果不受環境的影響,因此在使用了 Flakes 後 nix-channel 命令就失效了。

為了自定義 cache 映象源,我們必須在 flake.nix 中新增相關設定,這就是 nixConfig 引數,範例如下:

{
  description = "NixOS configuration of Ryan Yin";

  # 為了確保夠純,Flake 不依賴系統自身的 /etc/nix/nix.conf,而是在 flake.nix 中通過 nixConfig 設定
  # 但是為了確保安全性,flake 預設僅允許直接設定少數 nixConfig 引數,其他引數都需要在執行 nix 命令時指定 `--accept-flake-config`,否則會被忽略
  #     <https://nixos.org/manual/nix/stable/command-ref/conf-file.html>
  # 注意:即使新增了國內 cache 映象,如果有些包國內映象下載不到,它仍然會走國外。
  # 我的解法是使用 openwrt 旁路由 + openclash 加速下載。
  # 臨時修改系統預設閘道器為我的旁路由 IP:
  #    sudo ip route add default via 192.168.5.201
  # 還原路由規則:
  #    sudo ip route del default via 192.168.5.201
  nixConfig = {
    experimental-features = [ "nix-command" "flakes" ];
    substituters = [
      # replace official cache with a mirror located in China
      "https://mirrors.bfsu.edu.cn/nix-channels/store"
      "https://cache.nixos.org/"
    ];

    # nix community's cache server
    extra-substituters = [
      "https://nix-community.cachix.org"
    ];
    extra-trusted-public-keys = [
      "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
    ];
  };

  inputs = {
    # 省略若干設定...
  };

  outputs = {
    # 省略若干設定...
  };
}

改完後使用 sudo nixos-rebuild switch 應用設定即可生效,後續所有的包都會優先從國內映象源查詢快取。

注:上述手段只能加速部分包的下載,許多 inputs 資料來源仍然會從 Github 拉取,另外如果找不到快取,會執行本地構建,這通常仍然需要從國外下載原始碼與構建依賴,因此仍然會很慢。為了完全解決速度問題,仍然建議使用旁路由等區域網全域性代理方案。

6. 安裝 home-manager

前面簡單提過,NixOS 自身的組態檔只能管理系統級別的設定,而使用者級別的設定則需要使用 home-manager 來管理。

根據官方檔案 Home Manager Manual,要將 home manager 作為 NixOS 模組安裝,首先需要建立 /etc/nixos/home.nix,設定方法如下:

{ config, pkgs, ... }:

{
  # 注意修改這裡的使用者名稱與使用者目錄
  home.username = "ryan";
  home.homeDirectory = "/home/ryan";

  # 直接將當前資料夾的組態檔,連結到 Home 目錄下的指定位置
  # home.file.".config/i3/wallpaper.jpg".source = ./wallpaper.jpg;

  # 遞迴將某個資料夾中的檔案,連結到 Home 目錄下的指定位置
  # home.file.".config/i3/scripts" = {
  #   source = ./scripts;
  #   recursive = true;   # 遞迴整個資料夾
  #   executable = true;  # 將其中所有檔案新增「執行」許可權
  # };

  # 直接以 text 的方式,在 nix 組態檔中寫死檔案內容
  # home.file.".xxx".text = ''
  #     xxx
  # '';

  # set cursor size and dpi for 4k monitor
  xresources.properties = {
    "Xcursor.size" = 16;
    "Xft.dpi" = 172;
  };

  # git 相關設定
  programs.git = {
    enable = true;
    userName = "Ryan Yin";
    userEmail = "[email protected]";
  };

  # Packages that should be installed to the user profile.
  home.packages = [
    pkgs.htop
    pkgs.btop
  ];

  # 啟用 starship,這是一個漂亮的 shell 提示符
  programs.starship = {
    enable = true;
    settings = {
      add_newline = false;
      aws.disabled = true;
      gcloud.disabled = true;
      line_break.disabled = true;
    };
  };

  # alacritty 終端設定
  programs.alacritty = {
    enable = true;
      env.TERM = "xterm-256color";
      font = {
        size = 12;
        draw_bold_text_with_bright_colors = true;
      };
      scrolling.multiplier = 5;
      selection.save_to_clipboard = true;
  };

  # This value determines the Home Manager release that your
  # configuration is compatible with. This helps avoid breakage
  # when a new Home Manager release introduces backwards
  # incompatible changes.
  #
  # You can update Home Manager without changing this value. See
  # the Home Manager release notes for a list of state version
  # changes in each release.
  home.stateVersion = "22.11";

  # Let Home Manager install and manage itself.
  programs.home-manager.enable = true;
}

新增好 /etc/nixos/home.nix 後,還需要在 /etc/nixos/flake.nix 中匯入該設定,它才能生效,可以使用如下命令,在當前資料夾中生成一個範例設定以供參考:

nix flake new example -t github:nix-community/home-manager#nixos

調整好引數後的 /etc/nixos/flake.nix 內容範例如下:

{
  description = "NixOS configuration";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    home-manager.url = "github:nix-community/home-manager";
    home-manager.inputs.nixpkgs.follows = "nixpkgs";
  };

  outputs = inputs@{ nixpkgs, home-manager, ... }: {
    nixosConfigurations = {
      # 這裡的 nixos-test 替換成你的主機名稱
      nixos-test = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ./configuration.nix

          # 將 home-manager 設定為 nixos 的一個 module
          # 這樣在 nixos-rebuild switch 時,home-manager 設定也會被自動部署
          home-manager.nixosModules.home-manager
          {
            home-manager.useGlobalPkgs = true;
            home-manager.useUserPackages = true;

            # 這裡的 ryan 也得替換成你的使用者名稱
            # 這裡的 import 函數在前面 Nix 語法中介紹過了,不再贅述
            home-manager.users.ryan = import ./home.nix;

            # 使用 home-manager.extraSpecialArgs 自定義傳遞給 ./home.nix 的引數
            # 取消註釋下面這一行,就可以在 home.nix 中使用 flake 的所有 inputs 引數了
            # home-manager.extraSpecialArgs = inputs;
          }
        ];
      };
    };
  };
}

然後執行 sudo nixos-rebuild switch 應用設定,即可完成 home-manager 的安裝。

安裝完成後,所有使用者級別的程式、設定,都可以通過 /etc/nixos/home.nix 管理,並且執行 sudo nixos-rebuild switch 時也會自動應用 home-manager 的設定。

home.nix 中 Home Manager 的設定項有這幾種查詢方式:

  • Home Manager - Appendix A. Configuration Options: 一份包含了所有設定項的列表,建議在其中關鍵字搜尋。
  • home-manager: 有些設定項在官方檔案中沒有列出,或者檔案描述不夠清晰,可以直接在這份 home-manager 的原始碼中搜尋閱讀對應的原始碼。

7. 模組化 NixOS 設定

到這裡整個系統的骨架基本就設定完成了,當前我們 /etc/nixos 中的系統設定結構應該如下:

$ tree
.
├── flake.lock
├── flake.nix
├── home.nix
└── configuration.nix

下面分別說明下這四個檔案的功能:

  • flake.lock: 自動生成的版本鎖檔案,它記錄了整個 flake 所有輸入的資料來源、hash 值、版本號,確保系統可復現。
  • flake.nix: 入口檔案,執行 sudo nixos-rebuild switch 時會識別並部署它。
  • configuration.nix: 在 flake.nix 中被作為系統模組匯入,目前所有系統級別的設定都寫在此檔案中。
  • home.nix: 在 flake.nix 中被 home-manager 作為 ryan 使用者的設定匯入,也就是說它包含了 ryan 這個使用者的所有 Home Manager 設定,負責管理其 Home 資料夾。

通過修改上面幾個組態檔就可以實現對系統與 Home 目錄狀態的修改。
但是隨著設定的增多,單純依靠 configuration.nixhome.nix 會導致組態檔臃腫,難以維護,因此更好的解決方案是通過 Nix 的模組機制,將組態檔拆分成多個模組,分門別類地編寫維護。

在前面的 Nix 語法一節有介紹過:「import 的引數如果為資料夾路徑,那麼會返回該資料夾下的 default.nix 檔案的執行結果」,實際 Nix 還提供了一個 imports 引數,它可以接受一個 .nix 檔案列表,並將該列表中的所有設定合併(Merge)到當前的 attribute set 中。注意這裡的用詞是「合併」,它表明 imports 如果遇到重複的設定項,不會簡單地按執行順序互相覆蓋,而是更合理地處理。比如說我在多個 modules 中都定義了 program.packages = [...],那麼 imports 會將所有 modules 中的 program.packages 這個 list 合併。不僅 list 能被正確合併,attribute set 也能被正確合併,具體行為各位看官可以自行探索。

我只在 nixpkgs-unstable 官方手冊 - evalModules parameters 中找到一句關於 imports 的描述:A list of modules. These are merged together to form the final configuration.,可以意會一下...(Nix 的檔案真的一言難盡...這麼核心的引數檔案就這麼一句...)

我們可以藉助 imports 引數,將 home.nixconfiguration.nix 拆分成多個 .nix 檔案。

比如我之前的 i3wm 系統設定 ryan4yin/nix-config/v0.0.2,結構如下:

├── flake.lock
├── flake.nix
├── home
│   ├── default.nix         # 在這裡通過 imports = [...] 匯入所有子模組
│   ├── fcitx5              # fcitx5 中文輸入法設定,我使用了自定義的小鶴音形輸入法
│   │   ├── default.nix
│   │   └── rime-data-flypy
│   ├── i3                  # i3wm 桌面設定
│   │   ├── config
│   │   ├── default.nix
│   │   ├── i3blocks.conf
│   │   ├── keybindings
│   │   └── scripts
│   ├── programs
│   │   ├── browsers.nix
│   │   ├── common.nix
│   │   ├── default.nix   # 在這裡通過 imports = [...] 匯入 programs 目錄下的所有 nix 檔案
│   │   ├── git.nix
│   │   ├── media.nix
│   │   ├── vscode.nix
│   │   └── xdg.nix
│   ├── rofi              #  rofi 應用啟動器設定,通過 i3wm 中設定的快捷鍵觸發
│   │   ├── configs
│   │   │   ├── arc_dark_colors.rasi
│   │   │   ├── arc_dark_transparent_colors.rasi
│   │   │   ├── power-profiles.rasi
│   │   │   ├── powermenu.rasi
│   │   │   ├── rofidmenu.rasi
│   │   │   └── rofikeyhint.rasi
│   │   └── default.nix
│   └── shell             # shell 終端相關設定
│       ├── common.nix
│       ├── default.nix
│       ├── nushell
│       │   ├── config.nu
│       │   ├── default.nix
│       │   └── env.nu
│       ├── starship.nix
│       └── terminals.nix
├── hosts
│   ├── msi-rtx4090      # PC 主機的設定
│   │   ├── default.nix                 # 這就是之前的 configuration.nix,不過大部分內容都拆出到 modules 了
│   │   └── hardware-configuration.nix  # 與系統硬體相關的設定,安裝 nixos 時自動生成的
│   └── nixos-test       # 測試用的虛擬機器器設定
│       ├── default.nix
│       └── hardware-configuration.nix
├── modules          # 從 configuration.nix 中拆分出的一些通用設定
│   ├── i3.nix
│   └── system.nix
└── wallpaper.jpg    # 桌面桌布,在 i3wm 設定中被參照

詳細結構與內容,請移步前面提供的 github 倉庫連結,這裡就不多介紹了。

8. 更新系統

在使用了 Nix Flakes 後,要更新系統也很簡單,先更新 flake.lock 檔案,然後部署即可。在組態檔夾中執行如下命令:

# 更新 flake.lock
nix flake update
# 部署系統
sudo nixos-rebuild switch

另外有時候安裝新的包,跑 sudo nixos-rebuild switch 時可能會遇到 sha256 不匹配的報錯,也可以嘗試通過 nix flake update 更新 flake.lock 來解決(原理暫時不太清楚)。

9. 回退個別軟體包的版本

在使用 Nix Flakes 後,目前大家用得比較多的都是 nixos-unstable 分支的 nixpkgs,有時候就會遇到一些 bug,比如我最近(2023/5/6)就遇到了 chrome/vscode 閃退的問題

這時候就需要退回到之前的版本,在 Nix Flakes 中,所有的包版本與 hash 值與其 input 資料來源的 git commit 是一一對應的關係,因此回退某個包的到歷史版本,就需要鎖定其 input 資料來源的 git commit.

為了實現上述需求,首先修改 /etc/nixos/flake.nix,範例內容如下(主要是利用 specialArgs 引數):

{
  description = "NixOS configuration of Ryan Yin"

  inputs = {
    # 預設使用 nixos-unstable 分支
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";

    # 最新 stable 分支的 nixpkgs,用於回退個別軟體包的版本,當前最新版本為 22.11
    nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-22.11";

    # 另外也可以使用 git commit hash 來鎖定版本,這是最徹底的鎖定方式
    nixpkgs-fd40cef8d.url = "github:nixos/nixpkgs/fd40cef8d797670e203a27a91e4b8e6decf0b90c";
  };

  outputs = inputs@{
    self,
    nixpkgs,
    nixpkgs-stable,
    nixpkgs-fd40cef8d,
    ...
  }: {
    nixosConfigurations = {
      nixos-test = nixpkgs.lib.nixosSystem rec {
        system = "x86_64-linux";

        # 核心引數是這個,將非預設的 nixpkgs 資料來源傳到其他 modules 中
        specialArgs = {
          # 注意每次 import 都會生成一個新的 nixpkgs 範例
          # 這裡我們直接在 flake.nix 中建立範例, 再傳遞到其他子 modules 中使用
          # 這樣能有效重用 nixpkgs 範例,避免 nixpkgs 範例氾濫。
          pkgs-stable = import nixpkgs-stable {
            system = system;  # 這裡遞迴參照了外部的 system 屬性
            # 為了拉取 chrome 等軟體包,需要允許安裝非自由軟體
            config.allowUnfree = true;
          };

          pkgs-fd40cef8d = import nixpkgs-fd40cef8d {
            system = system;
            config.allowUnfree = true;
          };
        };
        modules = [
          ./hosts/nixos-test

          # 省略其他模組設定...
        ];
      };
    };
  };
}

然後在你對應的 module 中使用該資料來源中的包,一個 Home Manager 的子模組範例:

{
  pkgs,
  config,
  # nix 會從 flake.nix 的 specialArgs 查詢並注入此引數
  pkgs-stable,
  # pkgs-fd40cef8d,  # 也可以使用固定 hash 的 nixpkgs 資料來源
  ...
}:

{
  # 這裡從 pkg-stable 中參照包
  home.packages = with pkgs-stable; [
    firefox-wayland

    # chrome wayland support was broken on nixos-unstable branch, so fallback to stable branch for now
    # https://github.com/swaywm/sway/issues/7562
    google-chrome
  ];

  programs.vscode = {
    enable = true;
    package = pkgs-stable.vscode;  # 這裡也一樣,從 pkgs-stable 中參照包
  };
}

設定完成後,通過 sudo nixos-rebuild switch 部署即可將 firefox/chrome/vscode 三個軟體包回退到 stable 分支的版本。

根據 @fbewivpjsbsby 補充的文章 1000 instances of nixpkgs,在子模組中用 import 來客製化 nixpkgs 不是一個好的習慣,因為每次 import 都會重新求值併產生一個新的 nixpkgs 範例,在設定越來越多時會導致構建時間變長、記憶體佔用變大。所以這裡改為了在 flake.nix 中建立所有 nixpkgs 範例。

10. 使用 Git 管理 NixOS 設定

NixOS 的組態檔是純文字,因此跟普通的 dotfiles 一樣可以使用 Git 管理。

此外 Nix Flakes 設定也不一定需要放在 /etc/nixos 目錄下,可以放在任意目錄下,只要在部署時指定正確的路徑即可。

我們在前面第 3 小節的程式碼註釋中有說明過,可以通過 sudo nixos-rebuild switch --flake .#xxx--flake 引數指定 Flakes 設定的資料夾路徑,並通過 # 後面的值來指定使用的 outputs 名稱。

比如我的使用方式是將 Nix Flakes 設定放在 ~/nixos-config 目錄下,然後在 /etc/nixos 目錄下建立一個軟連結:

sudo mv /etc/nixos /etc/nixos.bak  # 備份原來的設定
sudo ln -s ~/nixos-config/ /etc/nixos

然後就可以在 ~/nixos-config 目錄下使用 Git 管理設定了,設定使用普通的使用者級別許可權即可,不要求 owner 為 root.

另一種方法是直接刪除掉 /etc/nixos,並在每次部署時指定組態檔路徑:

sudo mv /etc/nixos /etc/nixos.bak  # 備份原來的設定
cd ~/nixos-config

# 通過 --flake .#nixos-test 引數,指定使用當前資料夾的 flake.nix,使用的 nixosConfiguraitons 名稱為 nixos-test
sudo nixos-rebuild switch --flake .#nixos-test

兩種方式都可以,看個人喜好。

11. 檢視與清理歷史資料

如前所述,NixOS 的每次部署都會生成一個新的版本,所有版本都會被新增到系統啟動項中,除了重啟電腦外,我們也可以通過如下命令查詢當前可用的所有歷史版本:

nix profile history --profile /nix/var/nix/profiles/system

以及清理歷史版本釋放儲存空間的命令:

# 清理 7 天之前的所有歷史版本
sudo nix profile wipe-history --profile /nix/var/nix/profiles/system  --older-than 7d
# 清理歷史版本並不會刪除資料,還需要手動 gc 下
sudo nix store gc --debug

以及檢視系統層面安裝的所有軟體包(這個貌似只能用 nix-env):

nix-env -qa

七、Nix Flakes 的使用

到這裡我們已經寫了不少 Nix Flakes 設定來管理 NixOS 系統了,這裡再簡單介紹下 Nix Flakes 更細節的內容,以及常用的 nix flake 命令。

1. Flake 的 inputs

flake.nix 中的 inputs 是一個 attribute set,用來指定當前 Flake 的依賴,inputs 有很多種型別,舉例如下:

{
  inputs = {
    # 以 GitHub 倉庫為資料來源,指定使用 master 分支,這是最常見的 input 格式
    nixpkgs.url = "github:Mic92/nixpkgs/master";
    # Git URL,可用於任何基於 https/ssh 協定的 Git 倉庫
    git-example.url = "git+https://git.somehost.tld/user/path?ref=branch&rev=fdc8ef970de2b4634e1b3dca296e1ed918459a9e";
    # 上面的例子會複製 .git 到本地, 如果資料量較大,建議使用 shallow=1 引數避免複製 .git
    git-directory-example.url = "git+file:/path/to/repo?shallow=1";
    # 本地資料夾 (如果使用絕對路徑,可省略掉字首 'path:')
    directory-example.url = "path:/path/to/repo";
    # 如果資料來源不是一個 flake,則需要設定 flake=false
    bar = {
      url = "github:foo/bar/branch";
      flake = false;
    };

    sops-nix = {
      url = "github:Mic92/sops-nix";
      # `follows` 是 inputs 中的繼承語法
      # 這裡使 sops-nix 的 `inputs.nixpkgs` 與當前 flake 的 inputs.nixpkgs 保持一致,
      # 避免依賴的 nixpkgs 版本不一致導致問題
      inputs.nixpkgs.follows = "nixpkgs";
    };

    # 將 flake 鎖定在某個 commit 上
    nix-doom-emacs = {
      url = "github:vlaci/nix-doom-emacs?rev=238b18d7b2c8239f676358634bfb32693d3706f3";
      flake = false;
    };

    # 使用 `dir` 引數指定某個子目錄
    nixpkgs.url = "github:foo/bar?dir=shu";
  }
}

2. Flake 的 outputs

flake.nix 中的 outputs 是一個 attribute set,是整個 Flake 的構建結果,每個 Flake 都可以有許多不同的 outputs。

一些特定名稱的 outputs 有特殊用途,會被某些 Nix 命令識別處理,比如:

  • Nix packages: 名稱為 apps.<system>.<name>, packages.<system>.<name>legacyPackages.<system>.<name> 的 outputs,都是 Nix 包,通常都是一個個應用程式。
    • 可以通過 nix build .#name 來構建某個 nix 包
  • Nix Helper Functions: 名稱為 lib 的 outputs 是 Flake 函數庫,可以被其他 Flake 作為 inputs 匯入使用。
  • Nix development environments: 名稱為 devShells 的 outputs 是 Nix 開發環境
    • 可以通過 nix develop 命令來使用該 Output 建立開發環境
  • NixOS configurations: 名稱為 nixosConfigurations.<hostname> 的 outputs,是 NixOS 的系統設定。
    • nixos-rebuild switch .#<hostname> 可以使用該 Output 來部署 NixOS 系統
  • Nix templates: 名稱為 templates 的 outputs 是 flake 模板
    • 可以通過執行命令 nix flake init --template <reference> 使用模板初始化一個 Flake 包
  • 其他使用者自定義的 outputs,可能被其他 Nix 相關的工具使用

NixOS Wiki 中給出的使用案例:

{ self, ... }@inputs:
{
  # Executed by `nix flake check`
  checks."<system>"."<name>" = derivation;
  # Executed by `nix build .#<name>`
  packages."<system>"."<name>" = derivation;
  # Executed by `nix build .`
  packages."<system>".default = derivation;
  # Executed by `nix run .#<name>`
  apps."<system>"."<name>" = {
    type = "app";
    program = "<store-path>";
  };
  # Executed by `nix run . -- <args?>`
  apps."<system>".default = { type = "app"; program = "..."; };

  # Formatter (alejandra, nixfmt or nixpkgs-fmt)
  formatter."<system>" = derivation;
  # Used for nixpkgs packages, also accessible via `nix build .#<name>`
  legacyPackages."<system>"."<name>" = derivation;
  # Overlay, consumed by other flakes
  overlays."<name>" = final: prev: { };
  # Default overlay
  overlays.default = {};
  # Nixos module, consumed by other flakes
  nixosModules."<name>" = { config }: { options = {}; config = {}; };
  # Default module
  nixosModules.default = {};
  # Used with `nixos-rebuild --flake .#<hostname>`
  # nixosConfigurations."<hostname>".config.system.build.toplevel must be a derivation
  nixosConfigurations."<hostname>" = {};
  # Used by `nix develop .#<name>`
  devShells."<system>"."<name>" = derivation;
  # Used by `nix develop`
  devShells."<system>".default = derivation;
  # Hydra build jobs
  hydraJobs."<attr>"."<system>" = derivation;
  # Used by `nix flake init -t <flake>#<name>`
  templates."<name>" = {
    path = "<store-path>";
    description = "template description goes here?";
  };
  # Used by `nix flake init -t <flake>`
  templates.default = { path = "<store-path>"; description = ""; };
}

3. Flake 命令列的使用

在啟用了 nix-command & flakes 功能後,我們就可以使用 Nix 提供的新一代 Nix 命令列工具 New Nix Commands 了,下面列舉下其中常用命令的用法:

# 解釋下這條指令涉及的引數:
#   `nixpkgs#ponysay` 意思是 `nixpkgs` 這個 flake 中的 `ponysay` 包。
#   `nixpkgs` 是一個 flakeregistry ida,
#    nix 會從 <https://github.com/NixOS/flake-registry/blob/master/flake-registry.json> 中
#    找到這個 id 對應的 github 倉庫地址
# 所以這個命令的意思是建立一個新環境,安裝並執行 `nixpkgs` 這個 flake 提供的 `ponysay` 包。
#   注:前面已經介紹過了,nix 包 是 flake outputs 中的一種。
echo "Hello Nix" | nix run "nixpkgs#ponysay"

# 這條命令和上面的命令作用是一樣的,只是使用了完整的 flake URI,而不是 flakeregistry id。
echo "Hello Nix" | nix run "github:NixOS/nixpkgs/nixos-unstable#ponysay"

# 這條命令的作用是使用 zero-to-nix 這個 flake 中名 `devShells.example` 的 outptus 來建立一個開發環境,
# 然後在這個環境中開啟一個 bash shell。
nix develop "github:DeterminateSystems/zero-to-nix#example"

# 除了使用遠端 flake uri 之外,你也可以使用當前目錄下的 flake 來建立一個開發環境。
mkdir my-flake && cd my-flake
## 通過模板初始化一個 flake
nix flake init --template "github:DeterminateSystems/zero-to-nix#javascript-dev"
## 使用當前目錄下的 flake 建立一個開發環境,並開啟一個 bash shell
nix develop
# 或者如果你的 flake 有多個 devShell 輸出,你可以指定使用名為 example 的那個
nix develop .#example

# 構建 `nixpkgs` flake 中的 `bat` 這個包
# 並在當前目錄下建立一個名為 `result` 的符號連結,連結到該構建結果資料夾。
mkdir build-nix-package && cd build-nix-package
nix build "nixpkgs#bat"
# 構建一個本地 flake 和 nix develop 是一樣的,不再贅述

此外 Zero to Nix - Determinate Systems 是一份全新的 Nix & Flake 新手入門檔案,寫得比較淺顯易懂,適合新手用來入門。

八、Nixpkgs 的高階用法

callPackage、Overriding 與 Overlays 是在使用 Nix 時偶爾會用到的技術,它們都是用來自定義 Nix 包的構建方法的。

我們知道許多程式都有大量構建引數需要設定,不同的使用者會希望使用不同的構建引數,這時候就需要 Overriding 與 Overlays 來實現。我舉幾個我遇到過的例子:

  1. fcitx5-rime.nix: fcitx5-rime 的 rimeDataPkgs 預設使用 rime-data 包,但是也可以通過 override 來自定義該引數的值,以載入自定義的 rime 設定(比如載入小鶴音形輸入法設定)。
  2. vscode/with-extensions.nix: vscode 的這個包也可以通過 override 來自定義 vscodeExtensions 引數的值來安裝自定義外掛。
    1. nix-vscode-extensions: 就是利用該引數實現的 vscode 外掛管理
  3. firefox/common.nix: firefox 同樣有許多可自定義的引數
  4. 等等

總之如果需要自定義上述這類 Nix 包的構建引數,或者實施某些比較底層的修改,我們就得用到 Overriding 跟 Overlays。

Overriding

Chapter 4. Overriding - nixpkgs Manual

簡單的說,所有 nixpkgs 中的 Nix 包都可以通過 <pkg>.override {} 來自定義某些構建引數,它返回一個使用了自定義引數的新 Derivation. 舉個例子:

pkgs.fcitx5-rime.override {rimeDataPkgs = [
    ./rime-data-flypy
];}

上面這個 Nix 表示式的執行結果就是一個新的 Derivation,它的 rimeDataPkgs 引數被覆蓋為 [./rime-data-flypy],而其他引數則沿用原來的值。

除了覆寫引數,還可以通過 overrideAttrs 來覆寫使用 stdenv.mkDerivation 構建的 Derivation 的屬性,比如:

helloWithDebug = pkgs.hello.overrideAttrs (finalAttrs: previousAttrs: {
  separateDebugInfo = true;
});

上面這個例子中,helloWithDebug 就是一個新的 Derivation,它的 separateDebugInfo 引數被覆蓋為 true,而其他引數則沿用原來的值。

Overlays

Chapter 3. Overlays - nixpkgs Manual

前面介紹的 override 函數都會生成新的 Derivation,不影響 pkgs 中原有的 Derivation,只適合作為區域性引數使用。
但如果你需要覆寫的 Derivation 還被其他 Nix 包所依賴,那其他 Nix 包使用的仍然會是原有的 Derivation.

為了解決這個問題,Nix 提供了 overlays 能力。簡單的說,Overlays 可以全域性修改 pkgs 中的 Derivation。

在舊的 Nix 環境中,Nix 預設會自動應用 ~/.config/nixpkgs/overlays.nix ~/.config/nixpkgs/overlays/*.nix 這類路徑下的所有 overlays 設定。

但是在 Flakes 中,為了確保系統的可復現性,它不能依賴任何 Git 倉庫之外的設定,所以這種舊的方法就不能用了。

在使用 Nix Flakes 編寫 NixOS 設定時,Home Manager 與 NixOS 都提供了 nixpkgs.overlays 這個 option 來引入 overlays, 相關檔案:

舉個例子,如下內容就是一個載入 Overlays 的 Module,它既可以用做 Home Manager Module,也可以用做 NixOS Module,因為這倆定義完全是一致的:

不過我使用發現,Home Manager 畢竟是個外部元件,而且現在全都用的 unstable 分支,這導致 Home Manager Module 有時候會有點小毛病,因此更建議以 NixOS Module 的形式引入 overlays

{ config, pkgs, lib, ... }:

{
  nixpkgs.overlays = [
    # overlayer1 - 引數名用 self 與 super,表達繼承關係
    (self: super: {
     google-chrome = super.google-chrome.override {
       commandLineArgs =
         "--proxy-server='https=127.0.0.1:3128;http=127.0.0.1:3128'";
     };
    })

    # overlayer2 - 還可以使用 extend 來繼承其他 overlay
    # 這裡改用 final 與 prev,表達新舊關係
    (final: prev: {
      steam = prev.steam.override {
        extraPkgs = pkgs:
          with pkgs; [
            keyutils
            libkrb5
            libpng
            libpulseaudio
            libvorbis
            stdenv.cc.cc.lib
            xorg.libXcursor
            xorg.libXi
            xorg.libXinerama
            xorg.libXScrnSaver
          ];
        extraProfile = "export GDK_SCALE=2";
      };
    })

    # overlay3 - 也可以將 overlay 定義在其他檔案中
    (import ./overlays/overlay3.nix)
  ];
}

這裡只是個範例設定,參照此格式編寫你自己的 overlays 設定,將該設定作為 NixOS Module 或者 Home Manager Module 引入,然後部署就可以看到效果了。

模組化 overlays 設定

上面的例子說明了如何編寫 overlays,但是所有 overlays 都一股腦兒寫在一起,就有點難以維護了,寫得多了自然就希望模組化管理這些 overlays.

這裡介紹下我找到的一個 overlays 模組化管理的最佳實踐。

首先在 Git 倉庫中建立 overlays 資料夾用於存放所有 overlays 設定,然後建立 overlays/default.nix,其內容如下:

args:
  # import 當前資料夾下所有的 nix 檔案,並以 args 為引數執行它們
  # 返回值是一個所有執行結果的列表,也就是 overlays 的列表
  builtins.map
  (f: (import (./. + "/${f}") args))  # map 的第一個引數,是一個 import 並執行 nix 檔案的函數
  (builtins.filter          # map 的第二個引數,它返回一個當前資料夾下除 default.nix 外所有 nix 檔案的列表
    (f: f != "default.nix")
    (builtins.attrNames (builtins.readDir ./.)))

後續所有 overlays 設定都新增到 overlays 資料夾中,一個範例設定 overlays/fcitx5/default.nix 內容如下:

# 為了不使用預設的 rime-data,改用我自定義的小鶴音形資料,這裡需要 override
# 參考 https://github.com/NixOS/nixpkgs/blob/e4246ae1e7f78b7087dce9c9da10d28d3725025f/pkgs/tools/inputmethods/fcitx5/fcitx5-rime.nix
{pkgs, config, lib, ...}:

(self: super: {
  # 小鶴音形設定,設定來自 flypy.com 官方網路硬碟的鼠須管設定壓縮包「小鶴音形「鼠須管」for macOS.zip」
  rime-data = ./rime-data-flypy;
  fcitx5-rime = super.fcitx5-rime.override { rimeDataPkgs = [ ./rime-data-flypy ]; };
})

我通過上面這個 overlays 修改了 fcitx5-rime 輸入法的預設資料,載入了我自定義的小鶴音形輸入法。

最後,還需要通過 nixpkgs.overlays 這個 option 載入 overlays/default.nix 返回的所有 overlays 設定,在任一 NixOS Module 中新增如下引數即可:

{ config, pkgs, lib, ... } @ args:

{
  # ......

  # 新增此引數
  nixpkgs.overlays = import /path/to/overlays/dir;

  # ......
}

比如說直接寫 flake.nix 裡:

{
  description = "NixOS configuration of Ryan Yin";

  # ......

  inputs = {
    # ......
  };

  outputs = inputs@{ self, nixpkgs, ... }: {
    nixosConfigurations = {
      nixos-test = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        specialArgs = inputs;
        modules = [
          ./hosts/nixos-test

          # 新增如下內嵌 module 定義
          #   這裡將 modules 的所有引數 args 都傳遞到了 overlays 中
          (args: { nixpkgs.overlays = import ./overlays args; })

          # ......
        ];
      };
    };
  };
}

按照上述方法進行設定,就可以很方便地模組化管理所有 overlays 設定了,以我的設定為例,overlays 資料夾的結構大致如下:

.
├── flake.lock
├── flake.nix
├── home
├── hosts
├── modules
├── ......
├── overlays
│   ├── default.nix         # 它返回一個所有 overlays 的列表
│   └── fcitx5              # fcitx5 overlay
│       ├── default.nix
│       ├── README.md
│       └── rime-data-flypy  # 自定義的 rime-data,需要遵循它的資料夾格式
│           └── share
│               └── rime-data
│                   ├── ......  # rime-data 檔案
└── README.md

你可以在我的設定倉庫 ryan4yin/nix-config/v0.0.4 檢視更詳細的內容,獲取些靈感。

進階玩法

逐漸熟悉 Nix 這一套工具鏈後,可以進一步讀一讀 Nix 的三本手冊,挖掘更多的玩法:

  • Nix Reference Manual: Nix 包管理器使用手冊,主要包含 Nix 包管理器的設計、命令列使用說明。
  • nixpkgs Manual: 主要介紹 Nixpkgs 的引數、Nix 包的使用、修改、打包方法。
  • NixOS Manual: NixOS 系統使用手冊,主要包含 Wayland/X11, GPU 等系統級別的設定說明。
  • nix-pills: Nix Pills 對如何使用 Nix 構建軟體包進行了深入的闡述,寫得比官方檔案清晰易懂,而且也足夠深入,值得一讀。

在對 Nix Flakes 熟悉到一定程度後,你可以嘗試一些 Flakes 的進階玩法,如下是一些比較流行的社群專案,可以試用:

  • flake-parts: 通過 Module 模組系統簡化設定的編寫與維護。
  • flake-utils-plus:同樣是用於簡化 Flake 設定的第三方包,不過貌似更強大些
  • digga: 一個大而全的 Flake 模板,揉合了各種實用 Nix 工具包的功能,不過結構比較複雜,需要一定經驗才能玩得轉。
  • ......

以及其他許多實用的社群專案可探索,我比較關注的有這幾個:

  • devenv: 開發環境管理
  • agenix: secrets 管理
  • nixos-generator: 映象生成工具,從 nixos 設定生成 iso/qcow2 等格式的映象
  • lanzaboote: 啟用 secure boot
  • impermanence: 用於設定無狀態系統。可用它持久化你指定的檔案或資料夾,同時再將 /home 目錄掛載為 tmpfs 或者每次啟動時用工具擦除一遍。這樣所有不受 impermanence 管理的資料都將成為臨時資料,如果它們導致了任何問題,重啟下系統這些資料就全部還原到初始狀態了!
  • colmena: NixOS 主機部署工具

總結

這是本系列文章的第一篇,介紹了使用 Nix Flakes 設定 NixOS 系統的基礎知識,跟著這篇文章把系統設定好,就算是入門了。

我會在後續文章中介紹 NixOS & Nix Flakes 的進階知識:開發環境管理、secrets 管理、軟體打包、遠端主機管理等等,盡請期待。

參考

如下是我參考過的比較有用的 Nix 相關資料:

  • Zero to Nix - Determinate Systems: 淺顯易懂的 Nix Flakes 新手入門檔案,值得一讀。
  • NixOS 系列: 這是 LanTian 大佬的 NixOS 系列文章,寫得非常清晰明瞭,新手必讀。
  • Nix Flakes Series: 官方的 Nix Flakes 系列文章,介紹得比較詳細,作為新手入門比較 OK
  • Nix Flakes - Wiki: Nix Flakes 的官方 Wiki,此文介紹得比較粗略。
  • ryan4yin/nix-config: 我的 NixOS 設定倉庫,README 中也列出了我參考過的其他設定倉庫