mise でランタイムを一元管理する — asdf から乗り換えてわかった良し悪し
ある日、プロジェクトを切り替えた瞬間に python --version が変わる。node も go も、.tool-versions に書いた通りに差し替わる。切り替えの体感は 0.1 秒もかからない。asdf を 2 年ほど使ってきたが、mise に移してからこの速さが手放せなくなった。
半年ほど本番の開発機で運用してみて、見えてきた良し悪しがある。この記事は、その記録。
なぜ asdf から乗り換えたのか
正直、最初は乗り換える気はなかった。asdf で困っていなかったし、プラグイン資産もある。
きっかけは、Raspberry Pi 5 上で Python 3.12 のビルドがいつも遅いことへの小さなイライラだった。shim ベースの asdf は、コマンドを叩くたびに Bash スクリプトで現在のバージョンを探しにいく。ローカルの macOS では誤差の範囲でも、Pi の上だと python -c 'print(1)' に 200ms ほど余計にかかる。1 日に何百回叩くコマンドで、この差は効く。
mise は Rust 製で、shim ではなく PATH に直接エントリを差し込む方式(正確には shim モードとの両対応)。同じマシンで計測したら python --version の応答が 240ms → 30ms に縮んだ。数字を見たその日に移行を決めた。
インストールと最初の一歩
公式スクリプトでサクッと入る。Homebrew でも apt リポジトリ経由でも可。
curl https://mise.run | sh
echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc
exec bash
activate を仕込むと、cd した瞬間に .tool-versions を読みにいって PATH を書き換えてくれる。ここまでは asdf と同じ。
初回だけ、使いたいランタイムを入れる:
mise use -g python@3.12 node@20 go@1.22
mise ls
-g はグローバル既定。プロジェクト固定は -g を外して該当ディレクトリで叩くだけ。
asdf の .tool-versions はそのまま読める
乗り換え時にハマりそうなのが、既存の .tool-versions の互換性。mise は asdf 形式をそのまま読んでくれるので、ファイルを触らず並走できる。自分は半月ほど「asdf と mise 両方インストールした状態」で様子を見てから、asdf を抜いた。並走中に競合は起きなかった。
プロジェクト単位の自動切り替え
これが mise の本懐。.mise.toml をプロジェクト直下に置くと、以下のような宣言ができる:
[tools]
python = "3.12"
node = "20"
[env]
DATABASE_URL = "postgresql://localhost/myapp_dev"
PYTHONDONTWRITEBYTECODE = "1"
[tasks.test]
run = "pytest -x"
[tasks.deploy]
run = "./scripts/deploy.sh"
depends = ["test"]
注目すべきは [env] ブロック。以前書いたdirenv でプロジェクト毎の環境変数を扱う記事と同じことが、mise 単体で完結する。direnv と mise を二枚重ねで動かしていた頃は、たまに .envrc の再読み込み順で PATH が壊れることがあったが、1 本化してからはゼロ。
ちなみに自分は [env] の値を直書きせず、_.file = ".env.local" で別ファイルに逃がしている。秘密情報を .mise.toml に書くとうっかり git に乗るので。
タスクランナーとしての mise
ここが想定外に便利だった。[tasks.*] で書いたコマンドは、mise run test や mise run deploy で呼べる。depends で依存を書けるから、雑な Makefile の置き換えに十分使える。
以前紹介したPython プロジェクトの Makefile 自動化では make test や make lint を並べていたが、mise のタスクは「このプロジェクトのランタイム下で実行される」という保証が付く。Makefile だとうっかりシステムの Python が呼ばれる事故があったが、mise run の中では必ず固定バージョンが使われる。
試しに Makefile を .mise.toml に統合したら、1 プロジェクト分で 40 行ほど削れた。
シェル補完とエイリアス
タスクが増えるとタイプが面倒なので、mr という alias を切っている:
alias mr='mise run'
complete -F _mise_completion mr
mr <tab> でタスク一覧が出る。Makefile 時代と同じ指の動きで、中身だけ差し替わった状態。
Python まわりで助かった具体的な場面
Python 3.12 と 3.11 を行き来する必要があるプロジェクトで、以前はuv を使ったパッケージ管理と pyenv の組み合わせで運用していた。pyenv の shim 遅延が体感で気になっていたところに、mise + uv の組み合わせに変えたら体験が一段軽くなった。
具体的には、uv は「venv とパッケージ」、mise は「インタプリタのバージョン」を担当する分業。mise で 3.12 を入れておき、uv venv がそれを拾う。CI でも同じ .mise.toml を読ませればローカルと揃うので、「ローカルでは通るのに CI で落ちる」系の事故が減った。
ちなみに Go や Node.js の切り替えも体感は同じで、.mise.toml を 1 ファイル書けばチーム全員の環境が揃う。新しいメンバーが入ったとき、curl | sh と cd の 2 手でビルド環境が立ち上がる体験はかなり気持ちいい。
困ったところ、まだ試行錯誤中のところ
いいことばかりではない。半年使って踏んだ地雷をいくつか。
- プラグインの成熟度: メジャーどころ(Python, Node, Go, Ruby, Java)は問題ないが、マイナーな言語のプラグインは asdf 版を借りてくる形になる。たまに挙動が怪しい
- shebang が shim 前提のスクリプト:
#!/usr/bin/env pythonは問題ないが、#!/home/user/.asdf/shims/pythonのようにフルパスで書かれた古いスクリプトは壊れる。見つけ次第書き換えた - グローバルとローカルの優先順:
mise use -gで入れた版が、プロジェクトの.mise.tomlより優先されることが一度だけあった。原因は activate フックが走っていないサブシェルだったが、気づくまで 30 分溶かした
どれも致命的ではないが、asdf 時代には遭遇しなかった類の問題。ここは正直まだ慣れ切っていない。
移行を検討しているなら
個人的な結論として、新しいプロジェクトでは迷わず mise を選ぶ。既存の asdf プロジェクトも、半年ほど様子見してから順次移した。
チェックポイントは 3 つ。
- shim 遅延が気になるか(CI やリモート開発で顕著)
.mise.tomlに[env]と[tasks]を書く運用が合うか- 使っている言語のプラグインが mise コアでサポートされているか
3 番目が怪しければ、asdf のままでも十分戦える。焦って移す必要はないと思う。
自分はこの半年で 6 プロジェクトを mise に移したが、戻したくなった瞬間は一度もなかった。cd した瞬間に環境が切り替わる、あの 30ms の体験が、意外と生産性の底を上げている気がする。