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 の2行の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 のようなものです。1つのツールで Node、Python、Go、Rust、Java といった言語のバージョンを管理でき、開発時に使うコマンドラインツールもインストールできます。
nvm や pyenv といったツールと比べると、mise には次のような利点があります:
- 1つのツールですべての言語を管理。コマンドはすべて同じで、
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 にフックを登録し、プロンプトが表示される前に現在のディレクトリを確認して、対応するバージョンのパスを 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 はシェルのフックで動作するため、対話型のターミナルでしか有効になりません。Dock から開いたエディタや、シェルを経由しないプログラムは、mise が設定した PATH を取得できません。
この場合は shim モードを使います。~/.zprofileに次の1行を追加します。
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の1行だけです。
インストール後はmise lsで、現在どのバージョンがインストールされていて、どれが使われているかを確認できます。
mise ls
更新したいときは、mise upgradeは、設定にlatest、lts、あるいはプレフィックスで書かれたツールを、条件を満たす最新版にアップグレードします。奇妙な問題に遭遇したら、まずmise doctorを実行してみてください。mise が有効になっているか、設定ファイルに問題がないかを確認してくれます。