用 mise 統一管理所有語言的版本
我電腦上的程式語言,一直是各管各的。例如 Node 用 nvm,Python 用 pyenv,Rust 用 rustup,Go 和 Java 則直接用 Homebrew 裝。每一個都有自己的指令、自己的安裝目錄、自己要往 .zshrc 裡加的初始化程式碼。
隨著時間一長,.zshrc 裡就累積了這麼一堆東西:
zstyle ':omz:plugins:nvm' lazy yes
plugins=(git nvm history fzf-tab)
export PATH="/opt/homebrew/opt/openjdk@17/bin:$PATH"
export JAVA_HOME="/opt/homebrew/Cellar/openjdk@17/17.0.7/libexec/openjdk.jdk/Contents/Home"
alias gradle7='/opt/homebrew/Cellar/gradle@7/7.6.1/bin/gradle'
export PYENV_ROOT="$HOME/.pyenv"
command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
export PATH=$PATH:$HOME/go/bin
由於 nvm 是出了名的慢,所以我用了 oh-my-zsh 的 nvm 外掛,還開啟了 lazy 模式,等第一次用到 node 的時候再載入。pyenv 的兩行 eval 同樣每次開啟終端機都要跑一遍。
而最難受的是 Java。由於有一些專案需要 Java 17 和 Gradle 7,我就用 Homebrew 裝了 openjdk@17 和 gradle@7,然後 JAVA_HOME 只能寫死成 Homebrew Cellar 裡的路徑,路徑裡還帶著具體的版本號 17.0.7。所以只要 brew upgrade 把它升了一個小版本,這個路徑就失效了,又得回來手動改。
mise
直到我發現了 mise。它以前叫 rtx,今年年初才改名叫 mise,用 Rust 寫的,可以理解成一個更快的 asdf。一個工具就能管理 Node、Python、Go、Rust、Java 這些語言的版本,還能裝一些開發時用的命令列工具。
相較於 nvm、pyenv 這類工具,mise 有以下幾個優勢:
- 一個工具管理所有語言。指令都是一樣的,
mise use、mise install、mise ls,不用再記每個工具各自的用法。 - 快。它預設不用 shim,而是在每次顯示提示字元之前,根據目前目錄算出要用哪些版本,直接改
PATH。執行指令的時候沒有任何額外的開銷,這一步本身也非常快。 - 依目錄切換版本。在專案目錄裡放一個設定檔,進入目錄的時候版本就自動切換過去,離開的時候再切換回來。
- 相容舊的版本檔案。專案裡已經有的
.nvmrc、.python-version、.tool-versions,mise 都能直接讀取。
安裝
我用 Homebrew 安裝。
brew install mise
然後在 ~/.zshrc 裡啟用它,順便把補全也加上。
eval "$(mise activate zsh)"
# mise completion
source <(mise completion zsh)
mise activate 會在 zsh 裡註冊一個 hook,每次提示字元出現之前,它都會檢查一下目前目錄,然後把對應版本的路徑放進 PATH。
全域版本
全域要用的版本,寫在 ~/.config/mise/config.toml 裡。可以直接編輯這個檔案,也可以用 mise use -g,mise 會自動安裝並寫進設定。
mise use -g node@lts
mise use -g python@3.12
mise use -g go@latest
mise use -g rust@stable
mise use -g java@17
最後的設定檔大概是這樣的。
[tools]
node = "lts"
python = "3.12"
go = "latest"
rust = "stable"
java = "17"
gradle = "7"
bun = "latest"
pnpm = "9"
"npm:wrangler" = "3"
這裡的版本可以寫得很具體,比如 3.12.4,也可以只寫一個前綴,3.12 就表示 3.12 裡最新的那個,latest 則是最新版。例如 Rust 寫 stable 就會自動使用穩定版。
Java 也是由 mise 管理的,所以不用再手寫 JAVA_HOME 了,mise 會在切換 Java 版本的時候自動設定好。Gradle 也一樣,直接寫進設定裡,那個指向 Cellar 的 gradle7 別名也可以刪掉了。
除了語言本身,一些開發時用的工具也可以交給 mise。比如 pnpm 和 bun,以及 npm: 開頭的這種寫法,表示用 npm 安裝的命令列工具,Cloudflare 的 wrangler 我就是這麼裝的。這樣這些工具的版本也寫進了同一個檔案,不再需要 npm install -g。
專案裡的版本
專案需要特定版本的時候,在專案目錄下執行 mise use,不帶 -g。
cd some-project
mise use node@18
它會在專案目錄下產生一個 .mise.toml。
[tools]
node = "18"
之後只要進入這個目錄,node 就是 18,出了這個目錄就會恢復成全域的版本。而且這個檔案也可以提交進儲存庫,別人用 mise 的話,進來執行一下 mise install 就能裝好一樣的版本。
如果專案裡本來就有 .nvmrc 或者 .python-version,那就什麼都不用做,mise 會直接按照裡面的版本來。
環境變數
.mise.toml 裡還能設定環境變數,進入目錄的時候自動設定,離開的時候自動取消,有點像 direnv。
[env]
NODE_ENV = "development"
DATABASE_URL = "postgres://localhost/dev"
在編輯器裡用
mise activate 是靠 shell 的 hook 運作的,所以只在互動式的終端機裡生效。如果是從 Dock 開啟的編輯器,或者一些不經過 shell 的程式,它們就拿不到 mise 設定的 PATH。
這種情況可以用 shim 模式,在 ~/.zprofile 裡加上這一行。
eval "$(mise activate zsh --shims)"
它會把 ~/.local/share/mise/shims 加進 PATH,裡面是一個個和指令同名的小程式,呼叫的時候再由 mise 決定用哪個版本。不過目前我主要是在終端機裡開啟編輯器,所以暫時還沒開。
清理舊的工具
全部換成 mise 之後,原本那些東西就可以刪掉了。
brew uninstall nvm
rm -rf ~/.nvm
.zshrc 裡 nvm 的外掛、pyenv 的初始化、寫死的 JAVA_HOME 和 gradle7,也都刪掉了,只剩下 mise activate 那一行。
裝好之後可以用 mise ls 看一下目前裝了哪些版本,哪個是正在用的。
mise ls
想更新的話,mise upgrade 會把設定裡寫著 latest、lts 或者前綴版本的工具,升級到符合條件的最新版。遇到奇怪的問題,可以先跑一下 mise doctor,它會檢查 mise 有沒有啟用、設定檔有沒有問題。