Day 30:畢業考 — 為 Nixpkgs 貢獻你的第一個 Package
NixOS 30 天旅程的畢業挑戰:完整走過為 Nixpkgs 貢獻 package 的流程,從 fork、撰寫 derivation 到提交 PR,回饋開源社群。
Day 30:畢業考 — 為 Nixpkgs 貢獻你的第一個 Package
🗓 系列:NixOS 30 天學習之旅
📦 階段:第四階段 — 工程師進階實務(Day 22 – Day 30)
🎯 階段核心目標:佈署、自動化、安全性與貢獻
前言:最後一天,畢業挑戰
走到 Day 30,你已經不是當初那個對 /nix/store 一頭霧水的新手了。
過去 29 天裡,你學會了 declarative configuration、Flakes、overlay、module system、Home Manager、CI/CD pipeline、remote deployment…這些知識加在一起,代表你已經具備了一位 NixOS power user 的基本素養。
但真正讓一個工程師「畢業」的,不是讀了多少文件,而是你有沒有能力回饋社群。
今天的畢業考只有一個任務:為 Nixpkgs 貢獻一個 package。
這不是什麼高不可攀的事。Nixpkgs 是全世界最大的套件倉庫之一,截至目前已經有超過 100,000 個 packages,而這些全都是由社群成員一個一個 PR 堆起來的。你寫的下一個 package,就可能成為其中之一。
如果你暫時找不到適合打包的軟體,本文也會提供另一條路線:徹底把你的工作流程用 Nix 自動化,作為這趟旅程的收尾。
為 Nixpkgs 貢獻的完整流程
在動手寫 code 之前,先搞懂整個貢獻流程。Nixpkgs 是一個託管在 GitHub 上的 monorepo,所有的 package definition 都在這個 repository 裡面。
Step 1:Fork 與 Clone
# Fork nixpkgs 到你的 GitHub 帳號後
git clone https://github.com/<your-username>/nixpkgs.git
cd nixpkgs
# 追蹤上游 repository
git remote add upstream https://github.com/NixOS/nixpkgs.git
git fetch upstream
Nixpkgs 的 repository 非常龐大(commit history 超過 60 萬筆),clone 的時候建議加上 --depth=1 做 shallow clone,後續再依需要 fetch 更多歷史:
git clone --depth=1 https://github.com/<your-username>/nixpkgs.git
Step 2:建立 Feature Branch
Nixpkgs 的慣例是基於 master branch 開新的 branch:
git checkout -b add-my-awesome-tool upstream/master
Branch 命名沒有嚴格規定,但建議用清楚的前綴,例如 add-<package-name> 或 update-<package-name>。
Step 3:確認你要打包的軟體
在動手之前,先確認這個軟體還沒被打包過:
# 在 nixpkgs repo 裡搜尋
grep -r "pname = \"my-tool\"" pkgs/
# 或者用 nix search(如果你已經有可用的 nixpkgs channel)
nix search nixpkgs my-tool
同時也到 Nixpkgs pull requests 搜尋,確認沒有人正在進行同樣的工作。
實戰:為一個開源軟體撰寫 Package
我們用一個假想的 CLI 工具 hello-nix 作為範例,示範完整的打包流程。這是一個用 Rust 寫的命令列工具,source code 託管在 GitHub 上。
決定 Package 放在哪裡
Nixpkgs 有自己的目錄結構慣例:
pkgs/by-name/<前兩個字母>/<package-name>/package.nix
這是 Nixpkgs 目前推薦的 by-name 結構。以 hello-nix 為例:
pkgs/by-name/he/hello-nix/package.nix
撰寫 package.nix
以下是一個典型的 Rust package 定義:
# pkgs/by-name/he/hello-nix/package.nix
{
lib,
fetchFromGitHub,
rustPlatform,
}:
rustPlatform.buildRustPackage rec {
pname = "hello-nix";
version = "1.2.3";
src = fetchFromGitHub {
owner = "example-org";
repo = "hello-nix";
rev = "v${version}";
hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
};
cargoHash = "sha256-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=";
meta = {
description = "A friendly CLI tool built with Rust";
homepage = "https://github.com/example-org/hello-nix";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ your-github-username ];
mainProgram = "hello-nix";
};
}
各欄位解說
| 欄位 | 說明 |
|---|---|
pname | Package 名稱,必須和目錄名稱一致 |
version | 上游的版本號 |
src | 從哪裡抓取 source code,fetchFromGitHub 是最常見的 fetcher |
hash | Source tarball 的 hash,用來確保 reproducibility |
cargoHash | Rust dependencies 的 hash(若是 Go 專案則使用 vendorHash) |
meta | Package 的 metadata,包含描述、授權、維護者等資訊 |
取得正確的 Hash
第一次寫 package 時,你不會知道正確的 hash。技巧是先填一個假的 hash,然後讓 Nix 告訴你正確值:
# 先用一個空的 hash
# hash = lib.fakeHash;
# 嘗試 build,Nix 會報錯並告訴你正確的 hash
nix-build -A hello-nix
錯誤訊息中會包含 got: sha256-XXXXX... 的字樣,把那串 hash 貼回 package.nix 即可。cargoHash 也是同樣的做法。
處理不同語言的 Build System
不同語言有不同的 builder,以下是常見的幾種:
Go 專案:
{ lib, fetchFromGitHub, buildGoModule }:
buildGoModule rec {
pname = "my-go-tool";
version = "0.5.0";
src = fetchFromGitHub {
owner = "example";
repo = "my-go-tool";
rev = "v${version}";
hash = "sha256-...";
};
vendorHash = "sha256-...";
meta = {
description = "A Go CLI tool";
homepage = "https://github.com/example/my-go-tool";
license = lib.licenses.asl20;
maintainers = with lib.maintainers; [ your-github-username ];
};
}
Python 專案:
{
lib,
fetchFromGitHub,
python3Packages,
}:
python3Packages.buildPythonApplication rec {
pname = "my-python-tool";
version = "2.0.0";
pyproject = true;
src = fetchFromGitHub {
owner = "example";
repo = "my-python-tool";
rev = "v${version}";
hash = "sha256-...";
};
build-system = [ python3Packages.setuptools ];
dependencies = [
python3Packages.requests
python3Packages.click
];
meta = {
description = "A Python CLI tool";
homepage = "https://github.com/example/my-python-tool";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ your-github-username ];
mainProgram = "my-python-tool";
};
}
本機測試
寫完 package 定義後,先在本機 build 並測試:
# 在 nixpkgs repo 根目錄執行
nix-build -A hello-nix
# 執行 build 出來的 binary
./result/bin/hello-nix --version
# 使用 nix-shell 進行互動測試
nix-shell -p '(import ./. {}).hello-nix'
確認以下幾件事:
- ✅ Build 能成功完成
- ✅ Binary 可以正常執行
- ✅
--help或--version輸出正確 - ✅ 核心功能運作正常
執行 Nixpkgs 的 Review Checks
Nixpkgs 有自己的 linting 工具,提交前務必跑一次:
# 檢查 package 的 meta 資訊是否完整
nix-env -f . -qaP -A hello-nix --meta --json | python3 -m json.tool
# 使用 nixpkgs-review 測試(推薦)
nix-shell -p nixpkgs-review --run "nixpkgs-review rev HEAD"
提交 PR 的規範與注意事項
Nixpkgs 的 PR 有一套嚴格的規範,遵循這些規範可以大幅提高你的 PR 被 merge 的速度。
Commit Message 格式
hello-nix: init at 1.2.3
A friendly CLI tool built with Rust.
https://github.com/example-org/hello-nix
格式重點:
- 第一行:
<package-name>: init at <version>(新增 package)或<package-name>: <version-old> -> <version-new>(更新 package) - 第二行:空行
- 第三行起:簡短描述與上游連結
PR Description 要包含什麼
Nixpkgs 的 PR template 會引導你填寫以下資訊:
- 動機:為什麼要加入這個 package?
- 測試方式:你如何測試過這個 package?
- Checklist:
- 是否在本機 build 成功?
- 是否遵循 Nixpkgs 的 Contributing Guide?
-
meta.maintainers是否有填入你自己?
將自己加入 Maintainers 列表
如果你是第一次貢獻,需要把自己加入 maintainers/maintainer-list.nix:
# maintainers/maintainer-list.nix 中找到合適的字母排序位置
your-github-username = {
email = "you@example.com";
github = "your-github-username";
githubId = 12345678; # 你的 GitHub user ID
name = "Your Name";
};
取得 GitHub user ID 的方式:
curl -s https://api.github.com/users/<your-github-username> | jq '.id'
常見的 Review 回饋
根據社群經驗,以下是 reviewer 最常指出的問題:
- Hash 不正確:確保所有 hash 都是實際 build 產生的,不要手動編造。
- 缺少
meta.mainProgram:如果 package 產出一個可執行檔,務必指定。 - License 不正確:仔細確認上游的 license,不要猜。
- 描述太簡短或太冗長:一句話簡潔描述即可。
- 不必要的 dependencies:只引入 build 和 runtime 真正需要的套件。
替代路線:把你的工作流程徹底自動化
如果你暫時找不到想打包的軟體,另一個值得做的畢業作業是:用 Nix 把你的整個工作環境自動化。
目標:一條指令,從裸機到完整開發環境
# flake.nix — 你的個人 dotfiles repo
{
description = "My complete development environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
# macOS 使用者
darwin = {
url = "github:LnL7/nix-darwin";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { nixpkgs, home-manager, darwin, ... }: {
# NixOS 主機配置
nixosConfigurations.workstation = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./hosts/workstation/configuration.nix
home-manager.nixosModules.home-manager
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.james = import ./home/james.nix;
}
];
};
# macOS 配置(透過 nix-darwin)
darwinConfigurations.macbook = darwin.lib.darwinSystem {
system = "aarch64-darwin";
modules = [
./hosts/macbook/configuration.nix
home-manager.darwinModules.home-manager
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.james = import ./home/james.nix;
}
];
};
};
}
共用的 Home Manager 配置
# home/james.nix
{ pkgs, ... }:
{
home.stateVersion = "24.05";
home.packages = with pkgs; [
# 開發工具
git
gh
neovim
ripgrep
fd
jq
yq-go
direnv
nix-direnv
# 容器與 Infrastructure
docker-compose
kubectl
terraform
ansible
# 語言工具鏈
rustup
go
nodejs_22
python3
];
programs.git = {
enable = true;
userName = "James Hsueh";
userEmail = "james@example.com";
extraConfig = {
init.defaultBranch = "main";
pull.rebase = true;
push.autoSetupRemote = true;
};
delta.enable = true;
};
programs.starship = {
enable = true;
settings = {
add_newline = false;
character.success_symbol = "[➜](bold green)";
};
};
programs.direnv = {
enable = true;
nix-direnv.enable = true;
};
programs.zsh = {
enable = true;
autosuggestion.enable = true;
syntaxHighlighting.enable = true;
shellAliases = {
ll = "ls -la";
gs = "git status";
gc = "git commit";
nrs = "sudo nixos-rebuild switch --flake .";
};
};
}
專案級的 devShell
# 某個專案的 flake.nix
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { nixpkgs, ... }:
let
forAllSystems = nixpkgs.lib.genAttrs [ "x86_64-linux" "aarch64-darwin" ];
in {
devShells = forAllSystems (system:
let pkgs = nixpkgs.legacyPackages.${system};
in {
default = pkgs.mkShell {
packages = with pkgs; [
nodejs_22
pnpm
postgresql_16
redis
];
shellHook = ''
echo "🚀 開發環境已就緒!"
echo "Node.js: $(node --version)"
echo "pnpm: $(pnpm --version)"
'';
env = {
DATABASE_URL = "postgresql://localhost:5432/myapp_dev";
REDIS_URL = "redis://localhost:6379";
};
};
}
);
};
}
搭配 direnv,進入專案目錄就會自動載入開發環境,離開就自動卸載,整個流程無縫銜接。
30 天學習回顧與成長軌跡
讓我們回顧一下這 30 天走過的路:
第一階段:基礎與生存守則(Day 1 – Day 7)
| Day | 主題 | 你學會了… |
|---|---|---|
| 1 | 觀念重塑 | Nix Store、Declarative vs Imperative、Immutability |
| 2 | 基礎配置 | configuration.nix 的編輯與 nixos-rebuild |
| 3 | 套件管理 | 安裝、移除、搜尋套件 |
| 4 | 使用者與服務 | 使用者管理、systemd services |
| 5 | Nix 語言入門 | 基本語法、函式、attribute sets |
| 6 | Derivation | 理解 Nix 的 build 基礎單元 |
| 7 | 階段複習 | 動手做一個完整的系統配置 |
第二階段:Flakes 與開發環境(Day 8 – Day 14)
| Day | 主題 | 你學會了… |
|---|---|---|
| 8 | Flakes 入門 | flake.nix、inputs/outputs |
| 9 | 開發環境 | devShell、nix develop |
| 10 | Overlay | 覆蓋與擴充 nixpkgs |
| 11 | Home Manager | 管理使用者級的 dotfiles |
| 12 | 多機器管理 | 模組化你的配置 |
| 13 | Nix Language 進階 | let...in、inherit、with |
| 14 | 階段專案 | 完整的 Flakes 開發環境 |
第三階段:系統管理與實務(Day 15 – Day 21)
| Day | 主題 | 你學會了… |
|---|---|---|
| 15 | 系統服務深入 | 自訂 systemd services |
| 16 | 網路配置 | Firewall、networking |
| 17 | Container | NixOS containers 與 OCI |
| 18 | 磁碟與檔案系統 | Declarative disk management |
| 19 | 安全性 | Hardening、secrets management |
| 20 | 除錯技巧 | 常見問題排解 |
| 21 | 階段專案 | 完整的 server 配置 |
第四階段:工程師進階實務(Day 22 – Day 30)
| Day | 主題 | 你學會了… |
|---|---|---|
| 22 | Remote Deployment | deploy-rs、colmena |
| 23 | CI/CD | GitHub Actions + Nix |
| 24 | Binary Cache | Cachix、自建 cache |
| 25 | NixOS Module System | 深入 module 機制 |
| 26 | Cross Compilation | 跨平台編譯 |
| 27 | Testing | NixOS VM test framework |
| 28 | Nix Darwin | macOS 上的 Nix 生態 |
| 29 | 大型專案實務 | Monorepo、workspace 管理 |
| 30 | 畢業考 | 為 Nixpkgs 貢獻 package 🎓 |
NixOS 進階學習資源
畢業不代表學習結束。以下是值得持續關注的資源:
官方資源
- Nix Reference Manual:Nix 語言與工具的完整 reference。
- Nixpkgs Manual:包含所有 builder、helper function 的說明。
- NixOS Manual:系統配置的完整文件。
- nix.dev:官方推薦的學習入口,有結構化的 tutorial。
社群資源
- NixOS Discourse:官方論壇,技術討論品質很高。
- NixOS Wiki:社群維護的知識庫,很多實務技巧。
- Awesome Nix:精選的 Nix 生態系資源列表。
- Zero to Nix:另一個適合入門的互動教學。
進階主題
- nix-community:社群維護的各種工具和 modules。
- Tvix:用 Rust 重新實作的 Nix evaluator,值得關注。
- Lix:Nix 的社群 fork,在效能和 UX 上做了不少改進。
- FlakeHub:Flakes 的 registry 與探索平台。
推薦追蹤的人物與部落格
追蹤這些人的 blog 或社群帳號,能讓你持續吸收 Nix 生態系的最新動態:
- Eelco Dolstra — Nix 的創造者
- Graham Christensen — Determinate Systems 創辦人
- Mitchell Hashimoto — 知名的 NixOS 使用者,有很多實務分享
建議的下一步
完成這 30 天之後,你可以朝以下方向繼續精進:
1. 維護你貢獻的 Package
一旦你的 PR 被 merge,你就成為了這個 package 的 maintainer。這代表:
- 上游發佈新版本時,你需要更新 nixpkgs 裡對應的 package。
- 如果有人開了 issue 或 PR 涉及你的 package,GitHub 會通知你。
- 這是一個持續貢獻的好起點。
# 更新 package 版本的 commit message 格式
# hello-nix: 1.2.3 -> 1.3.0
2. 建立你的 NixOS 配置 Repository
把你的完整系統配置放在 Git repository 裡,包含:
my-nix-config/
├── flake.nix # 入口
├── flake.lock # 鎖定版本
├── hosts/
│ ├── workstation/ # 工作機配置
│ ├── server/ # 伺服器配置
│ └── macbook/ # macOS 配置
├── home/ # Home Manager 配置
│ ├── common.nix # 共用設定
│ ├── dev-tools.nix # 開發工具
│ └── shell.nix # Shell 配置
├── modules/ # 自訂 modules
│ ├── services/
│ └── programs/
├── overlays/ # 自訂 overlays
└── pkgs/ # 自訂 packages
3. 探索更多 Nix 生態系工具
| 工具 | 用途 |
|---|---|
| devenv | 基於 Nix 的開發環境管理(更友善的 API) |
| nixos-anywhere | 遠端安裝 NixOS 到任意機器 |
| disko | Declarative disk partitioning |
| sops-nix | Secrets management for NixOS |
| impermanence | 讓 NixOS 在每次重開機時回到乾淨狀態 |
| stylix | 統一管理全系統的 theme 與配色 |
4. 加入台灣的 Nix 社群
台灣有一群熱情的 NixOS 使用者,可以在以下地方找到他們:
- Telegram 或 Matrix 上的台灣 NixOS 群組
- COSCUP、MOPCON 等技術研討會的 Nix 相關議程
- 在地 meetup 與線上讀書會
和社群互動是加速學習最有效的方式,不要害怕提問。
結語:恭喜畢業!🎓
30 天前,你可能只是聽說 NixOS 很酷、很潮,但完全不知道從何下手。
現在,你已經能夠:
- ✅ 用 declarative 的方式管理整台機器
- ✅ 用 Flakes 建立 reproducible 的開發環境
- ✅ 用 Home Manager 管理你的 dotfiles
- ✅ 用 NixOS module system 寫出可組合、可重用的配置
- ✅ 佈署遠端伺服器、設定 CI/CD pipeline
- ✅ 為 Nixpkgs 貢獻 package
這些不只是知識,更是一套思維方式的轉變。Declarative、Reproducible、Immutable — 這三個詞從此不再只是 buzzword,而是你每天工作的日常。
NixOS 的學習曲線確實陡峭,但你已經翻過了最難的那座山。接下來的路,是越走越順的下坡與延伸。
最後,容我引用 Nix 社群裡常說的一句話:
“It works on my machine."
"Then we’ll ship your machine.”
— 這就是 Nix 的精神。
感謝你走完這 30 天。去吧,用 Nix 把世界變得更 reproducible。🚀
— NixOS 30 天學習之旅・完結 —