TechQuant Blog

mise でランタイムを一元管理する — asdf から乗り換えてわかった良し悪し

7分で読める

ある日、プロジェクトを切り替えた瞬間に python --version が変わる。nodego も、.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 testmise run deploy で呼べる。depends で依存を書けるから、雑な Makefile の置き換えに十分使える。

以前紹介したPython プロジェクトの Makefile 自動化では make testmake 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 | shcd の 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 つ。

  1. shim 遅延が気になるか(CI やリモート開発で顕著)
  2. .mise.toml[env][tasks] を書く運用が合うか
  3. 使っている言語のプラグインが mise コアでサポートされているか

3 番目が怪しければ、asdf のままでも十分戦える。焦って移す必要はないと思う。

自分はこの半年で 6 プロジェクトを mise に移したが、戻したくなった瞬間は一度もなかった。cd した瞬間に環境が切り替わる、あの 30ms の体験が、意外と生産性の底を上げている気がする。