13  多語言版本管理

不同專案可能需要不同版本的程式語言。版本管理工具讓你在它們之間無縫切換。

13.1 工具選擇

工具 特點
asdf 通用,支援多語言
mise asdf 的 Rust 重寫,更快
fnm 只管理 Node.js,極快
pyenv 只管理 Python
rbenv 只管理 Ruby

13.2 mise:推薦選擇

mise(前身 rtx)是 asdf 的現代替代品:

13.2.1 背景(問題發現)

開發時需要在不同專案間切換不同版本的程式語言。例如專案 A 需要 Node 18,專案 B 需要 Node 20。傳統做法是手動安裝多個版本並切換,容易出錯且麻煩。

13.2.2 方法

使用 mise 統一管理所有語言版本。mise 會: 1. 讀取專案設定檔(.mise.toml.tool-versions) 2. 自動切換到對應版本 3. 在 shell 初始化時載入必要的環境變數

13.2.3 結果(程式碼)

# 安裝
brew install mise

# 初始化
echo 'eval "$(mise activate zsh)"' >> ~/.zshrc

13.2.4 討論/延伸

  • mise 使用 Rust 編寫,比 asdf 快 2-3 倍
  • 相容 asdf 的 .tool-versions 格式,可無痛移轉
  • 支援 lazy loading,不會拖慢 shell 啟動速度
  • 可同時管理 Node、Python、Ruby、Go 等多種語言

13.2.5 基本使用

13.2.5.1 背景(問題發現)

需要在系統層級設定預設的程式語言版本,確保在任何目錄下都能使用特定版本。

13.2.5.2 方法

使用 mise install 下載特定版本,mise use 設定為全域預設版本。mise 會將版本資訊寫入 ~/.config/mise/config.toml

13.2.5.3 結果(程式碼)

# 安裝 Node.js
mise install node@20
mise use node@20

# 安裝 Python
mise install python@3.12
mise use python@3.12

# 查看已安裝版本
mise list

13.2.5.4 討論/延伸

  • mise use 預設是全域設定,加上 --local 可設定專案層級
  • 可使用 mise use -g node@20 明確指定全域
  • mise list 顯示所有已安裝版本及其使用狀態
  • 版本號可使用 @latest@lts 等別名

13.2.6 專案設定

13.2.6.1 背景(問題發現)

不同專案需要不同版本的語言環境。當切換專案時,希望能自動使用對應版本,而不需手動切換。

13.2.6.2 方法

在專案根目錄建立版本設定檔。當進入該目錄時,mise 會自動讀取設定並切換版本。有兩種格式可選: 1. .mise.toml:mise 原生格式,功能更豐富 2. .tool-versions:asdf 相容格式,適合團隊已使用 asdf

13.2.6.3 結果(程式碼)

在專案目錄建立 .mise.toml

[tools]
node = "20"
python = "3.12"

或使用 .tool-versions(與 asdf 相容):

node 20.10.0
python 3.12.0

13.2.6.4 討論/延伸

  • .mise.toml 支援更多功能,如環境變數、任務定義等
  • .tool-versions 適合與使用 asdf 的團隊協作
  • 可用 mise use node@20 --local 自動產生設定檔
  • 版本號建議使用主版本(如 20)而非完整版本(20.10.0),讓 mise 自動選擇最新的次版本
  • 專案設定優先於全域設定

13.3 fnm:Node.js 版本管理

如果只需要管理 Node.js,fnm 是最快的選擇:

13.3.1 背景(問題發現)

純前端開發者可能只需要管理 Node.js 版本,使用 mise 這類通用工具會有額外開銷。需要一個專注於 Node.js、啟動速度極快的工具。

13.3.2 方法

fnm(Fast Node Manager)專注於 Node.js 版本管理,使用 Rust 編寫,啟動時間僅需 1-2ms。它會: 1. 自動偵測 .node-version.nvmrc 檔案 2. 進入目錄時自動切換版本 3. 支援跨平台(macOS、Linux、Windows)

13.3.3 結果(程式碼)

# 安裝
brew install fnm

# 初始化
eval "$(fnm env)"

# 安裝 Node.js
fnm install 20
fnm use 20
fnm default 20

13.3.4 討論/延伸

  • fnm 比 nvm 快 40 倍以上
  • 自動偵測多種設定檔格式:.node-version.nvmrcpackage.jsonengines 欄位
  • 可用 fnm install --lts 安裝最新 LTS 版本
  • fnm default 設定全域預設版本
  • 建議在 .zshrc 加入 eval "$(fnm env --use-on-cd)" 啟用自動切換
  • 若使用 mise,則不需要額外安裝 fnm

13.4 Python 環境管理

13.4.1 uv:現代 Python 工具

uv 是 Rust 寫的超快 Python 套件管理器:

13.4.1.1 背景(問題發現)

Python 套件管理一直是痛點:pip 速度慢、依賴解析不可靠、虛擬環境管理繁瑣。大型專案安裝套件可能需要數分鐘,嚴重影響開發效率。

13.4.1.2 方法

uv 重新設計了 Python 套件管理流程: 1. 使用 Rust 實作,比 pip 快 10-100 倍 2. 先進的依賴解析演算法,避免衝突 3. 全域快取機制,避免重複下載 4. 統一管理虛擬環境與套件

13.4.1.3 結果(程式碼)

# 安裝
brew install uv

# 建立虛擬環境
uv venv

# 啟用虛擬環境
source .venv/bin/activate

# 安裝套件
uv pip install pandas numpy

13.4.1.4 討論/延伸

  • uv 與 pip 完全相容,可直接替換 pip 指令
  • 使用全域快取,多個專案共用相同套件時不會重複下載
  • 支援 pyproject.toml 格式的現代專案結構
  • 可用 uv pip compile 產生 lock 檔案,確保依賴版本一致
  • 大型專案(如 pandas)安裝時間從 2 分鐘降到 10 秒
  • 由 Astral 團隊開發(Ruff 的作者),持續活躍維護

13.4.2 自動啟用虛擬環境

13.4.2.1 背景(問題發現)

每次進入 Python 專案目錄都需要手動執行 source .venv/bin/activate 啟用虛擬環境,容易忘記且重複勞動。

13.4.2.2 方法

利用 zsh 的 chpwd 函數,在切換目錄時自動偵測 .venv 目錄並啟用虛擬環境。

13.4.2.3 結果(程式碼)

# 在 .zshrc 中
if [ -f ".venv/bin/activate" ]; then
  source .venv/bin/activate
fi

13.4.2.4 討論/延伸

  • 這段程式碼應放在 chpwd() 函數內(見下方「進入目錄時自動處理」範例)
  • 僅當目錄包含 .venv/bin/activate 時才啟用,不會影響其他目錄
  • 離開專案目錄時需要手動 deactivate,或使用更進階的自動管理方案
  • 可搭配 direnv 使用,實現更完整的環境變數管理
  • 注意:同時開啟多個專案時,可能會互相干擾虛擬環境

13.4.3 pipx:全域工具

pipx 安裝全域 Python 工具:

13.4.3.1 背景(問題發現)

某些 Python 工具需要全域使用(如 ruffblackpytest),但直接用 pip 安裝會污染系統 Python 環境,且不同工具的依賴可能衝突。

13.4.3.2 方法

使用 uv tool 為每個工具建立獨立的虛擬環境,並將執行檔連結到 PATH。這樣每個工具都有自己的依賴,互不干擾。

13.4.3.3 結果(程式碼)

# 使用 uv 作為 pipx 替代
alias pipx='uv tool'

# 安裝全域工具
uv tool install ruff
uv tool install black

13.4.3.4 討論/延伸

  • uv 的 tool 子命令功能等同於 pipx,但速度更快
  • 工具會安裝到 ~/.local/share/uv/tools/
  • 執行檔連結到 ~/.local/bin/,確保該目錄在 PATH 中
  • 可用 uv tool list 查看已安裝工具
  • 常見全域工具:ruff(linter)、black(formatter)、pytestipython
  • 與專案的虛擬環境完全隔離,不會衝突

13.5 實用設定

13.5.1 自動初始化 Python 專案

13.5.1.1 背景(問題發現)

開始新 Python 專案時,總是需要重複執行兩個指令:建立虛擬環境、啟用虛擬環境。希望簡化為單一指令。

13.5.1.2 方法

建立 shell alias,將兩個指令組合成一個。使用 && 確保第一個指令成功後才執行第二個。

13.5.1.3 結果(程式碼)

alias uvinit="uv venv && source .venv/bin/activate"

13.5.1.4 討論/延伸

  • 使用方式:在專案目錄執行 uvinit 即可

  • 可擴充為函數,加入更多初始化步驟:

    uvinit() {
      uv venv && \
      source .venv/bin/activate && \
      uv pip install ruff black pytest
    }
  • 也可考慮用 uv init 建立完整專案結構(包含 pyproject.toml)

  • 若需要指定 Python 版本:uv venv --python 3.12

13.5.2 進入目錄時自動處理

13.5.2.1 背景(問題發現)

開發時有兩個常見痛點: 1. 忘記啟用虛擬環境就開始工作,導致套件安裝到系統 Python 2. Dropbox 同步 .venvnode_modules 造成大量上傳、浪費空間

13.5.2.2 方法

利用 zsh 的 chpwd hook 函數,在每次切換目錄時自動執行: 1. 偵測並啟用 Python 虛擬環境 2. 設定檔案屬性,告訴 Dropbox 忽略特定目錄

xattr -w 'com.apple.fileprovider.ignore#P' 1 是 macOS 的特殊指令,標記目錄不要同步到雲端。

13.5.2.3 結果(程式碼)

function chpwd() {
  # 自動啟用 Python 虛擬環境
  if [ -f ".venv/bin/activate" ]; then
    source .venv/bin/activate
  fi

  # 忽略 Dropbox 同步
  for dir in .venv node_modules; do
    if [[ -d $dir ]]; then
      xattr -w 'com.apple.fileprovider.ignore#P' 1 "$dir"
    fi
  done
}

13.5.2.4 討論/延伸

  • chpwd 是 zsh 內建的 hook,每次 cd 時自動執行
  • 若使用 bash,需改用 PROMPT_COMMAND
  • 這段程式碼應加入 ~/.zshrc 或 dotfiles 管理
  • 虛擬環境啟用是單向的(進入時啟用),若需要離開時自動停用,需要更複雜的邏輯
  • xattr 指令對其他雲端服務(Google Drive、OneDrive)也有類似效果
  • 可擴充到其他需要忽略的目錄:.gitbuild/dist/
  • 效能考量:大量目錄切換時會有輕微延遲(約 10-50ms)

13.6 實作練習

  1. 安裝 mise 並設定不同版本的 Node.js
  2. 用 uv 建立 Python 虛擬環境
  3. 在專案中建立 .mise.toml
Tip效能提示

uv 比 pip 快 10 到 100 倍。在大型專案中差異非常明顯。