Day 14:第二週複習 — 把開發環境完全移入 nix-shell
第二階段總複習,整理 Nix 語言核心觀念並透過實戰演練將開發環境完全移入 nix-shell。
Day 14:第二週複習 — 把開發環境完全移入 nix-shell
🗓 系列:NixOS 30 天學習之旅
📦 階段:第二階段 — 掌握 Nix 語言與開發環境 (Day 8 – Day 14)
🎯 階段核心目標:學會寫 Nix Expression,不再只是複製貼上
前言:第二週學了什麼?
回想一下 Day 7 結束時的你——對 configuration.nix 裡那些花括號和箭頭還一知半解,寫配置時大多是從網路上複製貼上,改到能動就好。
經過這一週的學習,你應該已經能夠:
- 讀懂並寫出 Nix Expression
- 理解
let ... in、with、inherit、import等核心語法 - 用
nix-shell建立隔離的開發環境 - 撰寫
shell.nix來定義專案的開發需求 - 理解 Nix channels 的版本管理機制
- 對 derivation 有初步的認識
今天是第二階段的最後一天,我們要做三件事:整理觀念、實戰演練、展望下一階段。
Nix 語言核心觀念總整理
在正式進入實戰之前,先把這一週學過的 Nix 語言觀念做一次系統性的整理。
1. 一切皆 Expression
Nix 語言最根本的特性:沒有 statement,只有 expression。每一段 Nix 程式碼都會回傳一個值。
# 這是一個 expression,回傳整數 42
40 + 2
# 這也是 expression,回傳一個 attribute set
{
name = "my-project";
version = "1.0";
}
# if-else 也是 expression(注意:沒有 if-then 沒有 else 是不合法的)
if true then "yes" else "no"
2. 基本資料型別
| 型別 | 範例 | 備註 |
|---|---|---|
| Integer | 42 | |
| Float | 3.14 | |
| String | "hello" | 支援 ${} 插值 |
| Multi-line String | ''多行文字'' | 用兩個單引號包裹 |
| Boolean | true / false | |
| Null | null | |
| Path | ./shell.nix | 和 string 不同,path 會被 Nix 追蹤 |
| List | [ 1 2 3 ] | 元素之間用空格分隔,不是逗號 |
| Attribute Set | { a = 1; b = 2; } | key-value 結構,每對結尾用 ; |
| Function | x: x + 1 | 單參數 lambda |
3. 關鍵語法一覽
let ... in:區域變數繫結
let
name = "NixOS";
version = "24.05";
in
"${name} ${version}"
# 結果:"NixOS 24.05"
with:把 attribute set 的 key 帶入 scope
let
colors = { red = "#ff0000"; blue = "#0000ff"; };
in
with colors; "Red is ${red}, Blue is ${blue}"
這就是為什麼 environment.systemPackages = with pkgs; [ vim git ]; 可以運作——with pkgs 把所有套件名稱帶進了 scope。
inherit:從外層 scope 繼承變數
let
name = "my-app";
version = "2.0";
in
{
inherit name version;
# 等同於 name = name; version = version;
}
import:載入其他 .nix 檔案
# 假設 ./config.nix 內容是 { port = 8080; }
let
config = import ./config.nix;
in
config.port # 結果:8080
Function:永遠只有一個參數
# 單參數
add1 = x: x + 1;
# 多參數用 currying
add = a: b: a + b;
add 3 5 # 結果:8
# 用 attribute set 做 pattern matching(最常見的模式)
mkGreeting = { name, greeting ? "Hello" }: "${greeting}, ${name}!";
mkGreeting { name = "James"; } # 結果:"Hello, James!"
4. 核心概念對照表
| 概念 | 一句話解釋 | 你在哪裡遇到它 |
|---|---|---|
| Pure | 同樣的 input 永遠得到同樣的 output | Nix 語言沒有 side effect |
| Lazy | 只有用到的值才會被計算 | 大型 nixpkgs 不會一次全部載入 |
| Immutable | 變數一旦繫結就不能改變 | let x = 1; x = 2; 是非法的 |
| Derivation | 描述「如何 build 一個東西」的 recipe | 每個套件的本質就是一個 derivation |
開發環境管理的完整流程
這一週最實用的收穫,莫過於學會用 nix-shell 管理開發環境。讓我們把整個流程串起來。
步驟一:理解 nix-shell 的角色
nix-shell 的核心功能是:建立一個暫時性的、隔離的 shell 環境。在這個環境中,你可以使用指定版本的工具和 library,而不會污染系統的全域環境。
# 最簡單的用法:臨時需要某個工具
nix-shell -p python3 nodejs
# 進入 shell 後
python3 --version # 可以用
node --version # 可以用
# 離開 nix-shell 後,這些工具就「消失」了
exit
python3 --version # command not found(如果系統沒裝的話)
步驟二:用 shell.nix 固化開發環境
臨時用 -p 雖然方便,但每次都要打一長串指令不太實際。更好的做法是在專案根目錄建立一個 shell.nix:
# shell.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = with pkgs; [
nodejs_20
yarn
git
curl
];
shellHook = ''
echo "🚀 開發環境已就緒!"
echo "Node.js: $(node --version)"
echo "Yarn: $(yarn --version)"
'';
}
之後只要在專案目錄執行 nix-shell,就會自動載入這個環境。
步驟三:理解 channel 的版本管理
<nixpkgs> 這個寫法引用的是你系統上的 Nix channel。Channel 決定了你能使用哪些套件、以及它們的版本。
# 查看目前的 channel
nix-channel --list
# 更新 channel(等同於 apt update 的概念)
nix-channel --update
# 指定使用特定版本的 channel
nix-channel --add https://nixos.org/channels/nixos-24.05 nixos
步驟四:用 pinned nixpkgs 確保 reproducibility
Channel 會隨時間更新,如果你希望團隊中每個人都使用完全相同版本的套件,可以 pin 住 nixpkgs 的版本:
# shell.nix(pinned 版本)
let
pkgs = import (fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/nixos-24.05.tar.gz";
sha256 = "0xxxxx..."; # 可先留空,Nix 會告訴你正確的 hash
}) {};
in
pkgs.mkShell {
buildInputs = with pkgs; [
nodejs_20
yarn
];
}
這樣一來,不管什麼時候、在哪台機器上執行 nix-shell,都會得到一模一樣的環境。
實戰:完全用 nix-shell 管理一個真實專案
紙上談兵不如實際操作。接下來我們用一個具體的場景:一個 Node.js + PostgreSQL 的後端專案,示範如何把整個開發環境完全移入 nix-shell。
專案需求
- Node.js 20
- pnpm(套件管理工具)
- PostgreSQL client(用來連線測試用資料庫)
- jq(處理 JSON 資料)
- 自動設定
DATABASE_URL環境變數
撰寫 shell.nix
# shell.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = with pkgs; [
# Runtime
nodejs_20
nodePackages.pnpm
# Database
postgresql_16
# 開發工具
jq
curl
git
];
# 環境變數
DATABASE_URL = "postgresql://localhost:5432/myapp_dev";
NODE_ENV = "development";
shellHook = ''
echo "╔══════════════════════════════════════╗"
echo "║ 🛠 My App 開發環境已載入 ║"
echo "╚══════════════════════════════════════╝"
echo ""
echo " Node.js : $(node --version)"
echo " pnpm : $(pnpm --version)"
echo " psql : $(psql --version | head -1)"
echo ""
echo " DATABASE_URL = $DATABASE_URL"
echo ""
# 確保 node_modules 是最新的
if [ -f package.json ] && [ ! -d node_modules ]; then
echo "📦 正在安裝 dependencies..."
pnpm install
fi
'';
}
使用方式
# 進入專案目錄
cd ~/projects/my-app
# 啟動開發環境
nix-shell
# 以下操作都在 nix-shell 環境中
pnpm dev # 啟動開發伺服器
pnpm test # 執行測試
psql $DATABASE_URL # 連線到資料庫
更進一步:搭配 direnv 自動切換環境
每次手動輸入 nix-shell 其實有點煩。搭配 direnv 可以做到進入目錄自動啟用、離開目錄自動還原。
首先在 shell.nix 同層建立一個 .envrc:
# .envrc
use nix
然後允許這個目錄:
direnv allow
從此以後,只要你 cd 進這個專案目錄,nix-shell 的環境就會自動載入;cd 離開時自動還原。無縫、透明、不需要任何額外動作。
💡 小提醒:記得把
.envrc加進.gitignore或是納入版本控管(取決於團隊約定),同時也別忘了在 NixOS 的configuration.nix裡安裝direnv和nix-direnv。
常見問題與解法
整理這一週學習過程中最容易遇到的幾個問題:
Q1:nix-shell 第一次啟動超慢?
原因:Nix 需要下載或 build 所有列在 buildInputs 裡的套件。第一次一定會比較久。
解法:
- 耐心等候,第二次以後會從 cache 載入,速度很快
- 確認你的 Nix binary cache 設定正確(預設會用
cache.nixos.org) - 可以用
nix-shell --pure來排除系統環境的干擾,但速度可能稍慢
Q2:shell.nix 裡的套件名稱怎麼找?
解法:
# 在命令列搜尋
nix search nixpkgs nodejs
# 或到網站搜尋(最推薦)
# https://search.nixos.org/packages
Q3:with pkgs; 和逐一列出的差別?
# 用 with(簡潔,但大型 expression 裡可能造成名稱衝突)
buildInputs = with pkgs; [ nodejs git ];
# 逐一列出(明確,不怕衝突)
buildInputs = [ pkgs.nodejs pkgs.git ];
一般小型的 shell.nix 用 with pkgs; 就好。等到你的 expression 變複雜、有多個 scope 交疊時,再考慮改成明確寫法。
Q4:nix-shell --pure 是什麼意思?
加上 --pure 後,nix-shell 會建立一個完全隔離的環境——系統上原本安裝的工具(例如你的 ~/.local/bin 裡的東西)都不會出現在 $PATH 中。
# 一般模式:nix-shell 的工具 + 系統原有工具
nix-shell
# 純淨模式:只有 nix-shell 指定的工具
nix-shell --pure
--pure 適合用來驗證你的 shell.nix 是否真的完整——如果在 pure 模式下你的專案能正常 build、正常跑測試,表示所有 dependency 都已正確宣告。
Q5:Derivation 到底是什麼?需要自己寫嗎?
Derivation 是 Nix 中描述「如何 build 一個東西」的核心抽象。當你寫 pkgs.nodejs_20 時,背後就是一個 derivation。它描述了:
- 需要哪些 source
- 需要哪些 build dependencies
- 用什麼指令去 build
- Output 放在
/nix/store的什麼位置
現階段你不太需要自己從頭寫 derivation。 mkShell、mkDerivation 這些 helper function 已經幫你處理了大部分細節。等到第三、四階段,我們會慢慢深入。
第二階段總結
讓我們回顧一下整個第二週的學習路徑:
| Day | 主題 | 你學到了什麼 |
|---|---|---|
| Day 8 | Nix 語言基礎 | 資料型別、expression、基本語法 |
| Day 9 | Function 與 Pattern Matching | 單參數 function、currying、attribute set 解構 |
| Day 10 | let、with、inherit、import | 四個關鍵字,解鎖更複雜的 expression |
| Day 11 | nix-shell 與開發環境 | 隔離環境、臨時工具、-p 參數 |
| Day 12 | shell.nix 實戰 | mkShell、buildInputs、shellHook |
| Day 13 | Channels 與 Derivation | 版本管理、pinning、derivation 概念 |
| Day 14 | 第二週複習 | 整合所有觀念,完整實戰演練 |
階段性成就
到了這裡,你已經從「看不懂 Nix 語法」進化到「能夠自己寫 shell.nix,為專案建立可重現的開發環境」。這是一個質的飛躍。
如果用一張圖來表示你目前的能力邊界:
✅ 能讀懂 configuration.nix 和 shell.nix
✅ 能自己撰寫 shell.nix,定義開發環境
✅ 理解 Nix 語言的核心語法與特性
✅ 知道如何搜尋套件、管理 channel
✅ 對 derivation 有概念性的理解
⬜ 還不會用 Flakes(下一階段)
⬜ 還不會用 Home Manager 管理 dotfiles
⬜ 還沒有自己寫過 derivation 來打包軟體
⬜ 還不太熟悉 overlay 和 override 機制
給自己的作業
在進入第三階段之前,建議你實際做一件事:
挑一個你正在開發的專案,為它寫一個完整的
shell.nix,然後嘗試在nix-shell --pure模式下把它跑起來。
這會幫你驗證幾件事情:
- 你是否真的理解了
mkShell的用法 - 你的專案是否有「隱性」的系統 dependency 沒被宣告
- 你對 Nix 語法的掌握程度是否足夠實用
第三階段預告:Flakes 與 Home Manager
從 Day 15 開始,我們進入第三階段——現代 Nix 工作流。
Flakes:Nix 的未來
你可能已經在社群中聽過「Flakes」這個詞。它是 Nix 生態系中一個相對新的特性,旨在解決傳統 Nix 的幾個痛點:
- Channel 不夠精確:Flakes 用
flake.lock取代 channel,lock 住每個 input 的確切版本 - 沒有標準化的專案結構:Flakes 定義了統一的
flake.nix介面 - Reproducibility 不夠嚴格:Flakes 預設是 pure evaluation,不允許存取未宣告的 input
# flake.nix 長什麼樣子(先看一眼就好)
{
description = "My project";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
};
outputs = { self, nixpkgs }: {
devShells.x86_64-linux.default =
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in pkgs.mkShell {
buildInputs = [ pkgs.nodejs_20 ];
};
};
}
看起來有點陌生?別擔心,我們會在 Day 15 – Day 17 循序漸進地拆解它。
Home Manager:用 Nix 管理你的 Dotfiles
你的 .bashrc、.gitconfig、.vimrc 這些 dotfiles,是否散落在各處、難以在不同機器之間同步?
Home Manager 讓你用 Nix 的 declarative 方式來管理使用者層級的設定:
# 一小段 Home Manager 配置的預覽
programs.git = {
enable = true;
userName = "James Hsueh";
userEmail = "james@example.com";
extraConfig = {
init.defaultBranch = "main";
pull.rebase = true;
};
};
programs.zsh = {
enable = true;
shellAliases = {
ll = "ls -la";
gs = "git status";
};
};
用 Nix 管理 dotfiles,意味著你可以把整個使用者環境放進 Git,隨時在任何機器上一鍵還原。
第三階段路線圖
| Day | 預計主題 |
|---|---|
| Day 15 | Flakes 概念與啟用 |
| Day 16 | 用 flake.nix 改寫開發環境 |
| Day 17 | Flakes 進階:inputs、outputs、overlay |
| Day 18 | Home Manager 入門 |
| Day 19 | 用 Home Manager 管理 shell 與 editor |
| Day 20 | Home Manager + Flakes 整合 |
| Day 21 | 第三週複習 |
小結
第二週的旅程到此告一段落。我們從完全不懂 Nix 語言,到現在能夠獨立撰寫 shell.nix、理解 Nix 的核心設計哲學、並且把開發環境完全宣告化管理。
NixOS 的學習曲線確實陡峭,但你已經翻過了第一個山丘。接下來的 Flakes 和 Home Manager 會讓你體會到 Nix 生態系的真正威力——不只是管理開發環境,而是管理你的整個數位生活。
我們 Day 15 見! 🚀
📚 延伸閱讀