macOS 跟 Linux 可以用同一個 dotfile repo。不需要 fork 成兩個 repo——兩個 repo 的同步成本會隨時間膨脹,改了 git config 或 neovim 設定要在兩邊各 commit 一次,忘了同步就漂移。

本文的做法基於 GNU Stow——將 dotfile repo 的檔案透過 symlink 對應到家目錄的工具(完整說明見管理策略與選型)。一個 repo 跨平台的做法是三層分離,每層處理不同粒度的差異:

第一層:stow 選擇性安裝

Stow 的 package 機制天然支持跨平台。repo 裡同時放 macOS 和 Linux 的配置,安裝時只 stow 該 OS 需要的 package:

1# macOS
2stow zsh git zellij btop broot
3
4# Linux desktop
5stow zsh git zellij btop broot hyprland waybar wofi mako

hyprland、waybar 這些目錄放在 repo 裡但 macOS 不 stow,不會有副作用。bootstrap script 裡用 uname 自動決定要 stow 哪些 package:

1PACKAGES=(zsh git zellij btop broot)
2
3if [[ "$(uname -s)" == "Linux" ]]; then
4    for pkg in hyprland waybar wofi mako hyprlock; do
5        [[ -d "$pkg" ]] && PACKAGES+=("$pkg")
6    done
7fi

第二層:配置檔內的 OS 分流

同一份配置檔裡,用 uname 區分 macOS 和 Linux 的差異。適合處理 PATH、工具初始化路徑這類「同一個用途、不同 OS 路徑」的狀況:

 1# path.zsh
 2typeset -U PATH
 3export PATH="$HOME/.local/bin:$PATH"    # 共用
 4
 5if [[ "$(uname)" == "Darwin" ]]; then
 6    # Homebrew Ruby、FVM、Android SDK(macOS 路徑)
 7    [[ -d "/opt/homebrew/opt/ruby/bin" ]] && export PATH="/opt/homebrew/opt/ruby/bin:$PATH"
 8    export PATH="$HOME/fvm/default/bin:$PATH"
 9    export PATH="$PATH:$HOME/Library/Android/sdk/emulator"
10fi
11
12if [[ "$(uname)" == "Linux" ]]; then
13    # Android SDK(Linux 路徑)
14    [[ -d "$HOME/Android/Sdk/emulator" ]] && export PATH="$PATH:$HOME/Android/Sdk/emulator"
15fi
1# tools.zsh — autojump 的 source 路徑在兩個 OS 不同
2if [[ "$(uname)" == "Darwin" ]]; then
3    [ -f /opt/homebrew/etc/profile.d/autojump.sh ] && . /opt/homebrew/etc/profile.d/autojump.sh
4else
5    [ -f /usr/share/autojump/autojump.sh ] && . /usr/share/autojump/autojump.sh
6fi

每個路徑加 [[ -d ]][ -f ] 存在性檢查——即使在正確的 OS 上,如果該工具沒裝也不會報錯。

第三層:local.zsh 放機器專屬設定

不是「macOS 的設定」而是「這台特定機器的設定」——工作專案 alias、公司 VPN proxy、只有這台機器需要的 PATH——放在 ~/.config/zsh/local.zsh,不進 Git。

1# .zshrc 裡的載入方式
2[[ -f "$HOME/.config/zsh/local.zsh" ]] && source "$HOME/.config/zsh/local.zsh"
1# ~/.config/zsh/local.zsh(不在 repo 裡,每台機器自己建)
2alias unimall-dev='cd ~/project/unimall_shop && ./scripts/run_commands.sh dev'
3alias unipos-dev='cd ~/project/unipos && ./scripts/run_commands.sh dev'

Repo 裡放一份 local.zsh.example 作為範本,.gitignore 裡排除 local.zsh 本身。

套件清單也分 OS

1~/dotfiles/
2├── Brewfile              # macOS: brew bundle dump 產生
3├── packages-arch.txt     # Arch Linux: pacman -Qqe > packages-arch.txt
4└── ...

Bootstrap script 依 OS 讀對應的清單安裝套件。

Stow 跨平台的邊界

這個三層模型適合「配置檔本身大部分通用、差異只在 PATH 和少數工具路徑」的狀況——也就是多數開發者的實際情境。判斷是否需要遷移到 chezmoi 的訊號:

  • 同一份配置檔裡超過一半的行數是 OS 分流 → chezmoi template 更乾淨
  • 需要在配置檔裡注入 secret(API key、token) → chezmoi 的 secret 管理是必要功能
  • 管理的機器超過三種 OS/角色組合 → template 的條件判斷比 shell if-else 更可維護

多數情況下 stow + uname + local.zsh 就足夠。