NixOS 與 Nix Flakes 新手入門

獨立博客閱讀: 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 中也列出了我參考過的其他配置倉庫
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章