用 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 有没有激活、配置文件有没有问题。