Loading...

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 ... inwithinheritimport 等核心語法
  • 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. 基本資料型別

型別範例備註
Integer42
Float3.14
String"hello"支援 ${} 插值
Multi-line String''多行文字''用兩個單引號包裹
Booleantrue / false
Nullnull
Path./shell.nix和 string 不同,path 會被 Nix 追蹤
List[ 1 2 3 ]元素之間用空格分隔,不是逗號
Attribute Set{ a = 1; b = 2; }key-value 結構,每對結尾用 ;
Functionx: 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 永遠得到同樣的 outputNix 語言沒有 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 裡安裝 direnvnix-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.nixwith 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。 mkShellmkDerivation 這些 helper function 已經幫你處理了大部分細節。等到第三、四階段,我們會慢慢深入。


第二階段總結

讓我們回顧一下整個第二週的學習路徑:

Day主題你學到了什麼
Day 8Nix 語言基礎資料型別、expression、基本語法
Day 9Function 與 Pattern Matching單參數 function、currying、attribute set 解構
Day 10letwithinheritimport四個關鍵字,解鎖更複雜的 expression
Day 11nix-shell 與開發環境隔離環境、臨時工具、-p 參數
Day 12shell.nix 實戰mkShellbuildInputsshellHook
Day 13Channels 與 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 模式下把它跑起來。

這會幫你驗證幾件事情:

  1. 你是否真的理解了 mkShell 的用法
  2. 你的專案是否有「隱性」的系統 dependency 沒被宣告
  3. 你對 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 15Flakes 概念與啟用
Day 16flake.nix 改寫開發環境
Day 17Flakes 進階:inputs、outputs、overlay
Day 18Home Manager 入門
Day 19用 Home Manager 管理 shell 與 editor
Day 20Home Manager + Flakes 整合
Day 21第三週複習

小結

第二週的旅程到此告一段落。我們從完全不懂 Nix 語言,到現在能夠獨立撰寫 shell.nix、理解 Nix 的核心設計哲學、並且把開發環境完全宣告化管理。

NixOS 的學習曲線確實陡峭,但你已經翻過了第一個山丘。接下來的 Flakes 和 Home Manager 會讓你體會到 Nix 生態系的真正威力——不只是管理開發環境,而是管理你的整個數位生活

我們 Day 15 見! 🚀


📚 延伸閱讀