Use mise to manage versions for all languages

Sunday, June 16, 2024 / , , , ,
Simplified ChineseEnglish
Summary:
nvm, pyenv, rustup, and the openjdk in Homebrew—each language had its own version management. Recently I tried mise and replaced them all with a single tool, and `.zshrc` became much cleaner.

The programming languages on my computer have always been managed separately. For example, I use nvm for Node, pyenv for Python, and rustup for Rust, while Go and Java are installed directly with Homebrew. Each one has its own commands, its own installation directory, and its own initialization code to add to .zshrc.

Over time, .zshrc accumulated a pile of things like this:

Shell
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

Since nvm is notoriously slow, I used the oh-my-zsh nvm plugin and enabled lazy mode so that it loads only when node is first used. pyenv's two eval lines also have to run every time a terminal is opened.

The worst part was Java. Since some projects require Java 17 and Gradle 7, I installed openjdk@17 and gradle@7 with Homebrew, and then JAVA_HOME had to be hardcoded to a path in the Homebrew Cellar, and the path even contained a specific version number 17.0.7. So as long as brew upgrade bumps it by one minor version, that path breaks, and I have to come back and change it manually.

mise

Until I discovered mise. It was previously called rtx and was renamed to mise earlier this year. It is written in Rust and can be thought of as a faster asdf. A single tool can manage the versions of languages like Node, Python, Go, Rust, and Java, and can also install some command-line tools for development.

Compared with tools like nvm and pyenv, mise has the following advantages:

  • One tool manages all languages. The commands are all the same: mise use, mise install, mise ls, and there's no need to remember each tool's own usage.
  • Fast. By default it does not use shims. Instead, before each prompt is displayed, it works out which versions to use based on the current directory and directly changes PATH. There is no extra overhead when running commands, and this step itself is also very fast.
  • Switch versions by directory. Put a config file in the project directory; the version switches automatically when you enter the directory and switches back when you leave.
  • Compatible with old version files. mise can directly read the existing .nvmrc, .python-version, .tool-versions files in a project.

Installation

I installed it with Homebrew.

Shell
brew install mise

Then activate it in ~/.zshrc and add completion as well.

Shell
eval "$(mise activate zsh)"
# mise completion
source <(mise completion zsh)

mise activate registers a hook in zsh. Before each prompt appears, it checks the current directory and puts the path for the corresponding version into PATH.

Global versions

Global versions are written in ~/.config/mise/config.toml. You can edit this file directly, or use mise use -g, and mise will install them automatically and write them into the config.

Shell
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

The final config file looks something like this.

TOML
[tools]
node = "lts"
python = "3.12"
go = "latest"
rust = "stable"
java = "17"
gradle = "7"
bun = "latest"
pnpm = "9"
"npm:wrangler" = "3"

Versions here can be very specific, for example 3.12.4, or you can write just a prefix; 3.12 means the latest one in 3.12, latest means the latest version. For example, for Rust, writing stable will automatically use the stable version.

Java is also managed by mise, so there is no need to hardcode JAVA_HOME anymore; mise sets it automatically when switching Java versions. The same goes for Gradle—just write it into the config—and the gradle7 alias pointing to the Cellar can also be deleted.

Besides languages themselves, some development command-line tools can also be handed over to mise. For example, pnpm and bun, as well as the npm: prefix notation, which means a command-line tool installed with npm; that is how I installed Cloudflare's wrangler. This way, the versions of these tools are also written into the same file, and there is no longer any need for npm install -g.

Project versions

When a project needs a specific version, run mise use in the project directory, without -g.

Shell
cd some-project
mise use node@18

It will generate a .mise.toml.

TOML
[tools]
node = "18"

After that, as soon as you enter this directory, node will be version 18, and when you leave the directory it will revert to the global version. This file can also be committed to the repository; if someone else uses mise, they can enter the directory and run mise install to install the same versions.

If the project already has a .nvmrc or .python-version, there is nothing to do; mise will simply follow the version in it.

Environment variables

.mise.toml can also set environment variables: they are set automatically when you enter the directory and unset automatically when you leave, a bit like direnv.

TOML
[env]
NODE_ENV = "development"
DATABASE_URL = "postgres://localhost/dev"

Using it in editors

mise activate works via a shell hook, so it only works in interactive terminals. If an editor is opened from the Dock, or a program does not go through a shell, it will not get mise's PATH.

In this case you can use shim mode; add this line to ~/.zprofile.

Shell
eval "$(mise activate zsh --shims)"

It will add ~/.local/share/mise/shims to PATH. Inside are small programs named after the commands; when they are called, mise decides which version to use. However, at the moment I mainly open editors from the terminal, so I have not enabled it yet.

Cleaning up old tools

After switching everything to mise, the old tools can be removed.

Shell
brew uninstall nvm
rm -rf ~/.nvm

.zshrc no longer has its nvm plugin, pyenv initialization, or hardcoded JAVA_HOME or gradle7; only the mise activate line remains.

After installation, you can use mise ls to see which versions are currently installed and which one is in use.

Shell
mise ls

If you want to update, mise upgrade will upgrade tools whose config specifies latest, lts or a prefix version to the latest matching version. If you run into strange problems, first run mise doctor, which checks whether mise is activated and whether there are any problems with the config file.

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...