001

Java, Python and Node versions on a Mac with SDKMAN, pyenv and nvm

SDKMAN, pyenv and nvm install several Java, Python and Node versions side by side. A small file in each project selects the version, and the shell switches to it when you cd into the folder.

problem

Different projects need different Java, Python and Node versions. Without a version manager you have one version per language, and you reinstall runtimes by hand every time you change project.

I work on Java, Python and Node.js projects, and they need different runtime versions. Some use an old LTS release and some use the current one. I use one version manager per language: SDKMAN for Java, pyenv for Python and nvm for Node.js. This note shows how to install each one on a Mac, how to switch versions, and how to pin a version to a project.


Why not use the system runtimes

macOS includes some runtimes, but they are for the operating system. They change with the macOS version, they are often old, and you need sudo to modify them. There is also only one version of each, so two projects that need different versions cannot both run.

A version manager installs any number of versions in your home folder and switches between them without sudo.


Java with SDKMAN

SDKMAN installs and switches JDK versions. It also manages Gradle, Maven, Kotlin and the Spring Boot CLI. It works on macOS and Linux.

Install

curl -s "https://get.sdkman.io" | bash

Restart the terminal or run source ~/.zshrc, then check the installation:

sdk version

Install a JDK

List the available Java distributions:

sdk list java

The list includes Temurin (Eclipse Adoptium), Corretto (Amazon), Zulu (Azul), GraalVM and others. I use Temurin for most projects. I choose another distribution only when a project needs a specific JVM. Install a version by its identifier:

# Install Java 21 (LTS), Temurin distribution
sdk install java 21.0.3-tem

# Install Java 17 (LTS), Temurin distribution
sdk install java 17.0.11-tem

Switch versions

# Use 21 in the current shell session only
sdk use java 21.0.3-tem

# Set 21 as the global default
sdk default java 21.0.3-tem

# Check what's active
java -version

Per-project pinning

Add a .sdkmanrc file to the project root:

# .sdkmanrc
java=17.0.11-tem

Run sdk env in that folder to activate the version. If you set sdkman_auto_env=true in ~/.sdkman/etc/config, SDKMAN switches the version every time you cd into the folder.


Python with pyenv

pyenv is the most common version manager for Python. It puts small wrapper scripts in front of the python and python3 commands, and the wrappers run the active version.

Install

On a Mac, install it with Homebrew:

brew install pyenv

Add these lines to ~/.zshrc:

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

Restart the terminal, then check the installation:

pyenv --version

Install a Python version

# See all available versions
pyenv install --list

# Install specific versions
pyenv install 3.12.3
pyenv install 3.11.9

Switch versions

# Set global default
pyenv global 3.12.3

# Set for the current shell only
pyenv shell 3.11.9

# Verify
python --version

Per-project pinning

# Inside the project directory
pyenv local 3.11.9

The command creates a .python-version file in the folder. pyenv reads this file every time you run Python inside the folder.

# .python-version
3.11.9

Node.js with nvm

nvm works like pyenv, for Node.js. It keeps several Node versions installed and switches between them for a project or for a shell session.

Install

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

The installer adds its setup lines to ~/.zshrc. Restart the terminal, then check the installation:

nvm --version

Install a Node version

# List available versions (LTS only)
nvm ls-remote --lts

# Install specific versions
nvm install 20          # installs latest 20.x (LTS Iron)
nvm install 18          # installs latest 18.x (LTS Hydrogen)
nvm install --lts       # installs the latest active LTS

Switch versions

# Use a version in the current session
nvm use 20

# Set the default for all new shells
nvm alias default 20

# Verify
node --version
npm --version

Per-project pinning

Create a .nvmrc file in the project root:

# .nvmrc
20

nvm use without arguments reads this file. nvm does not switch on cd by itself. To switch automatically, add this hook to ~/.zshrc:

autoload -U add-zsh-hook

load-nvmrc() {
  local nvmrc_path
  nvmrc_path="$(nvm_find_nvmrc)"

  if [ -n "$nvmrc_path" ]; then
    local nvmrc_node_version
    nvmrc_node_version=$(nvm version "$(cat "${nvmrc_path}")")

    if [ "$nvmrc_node_version" = "N/A" ]; then
      nvm install
    elif [ "$nvmrc_node_version" != "$(nvm version)" ]; then
      nvm use
    fi
  elif [ -n "$(PWD=$OLDPWD nvm_find_nvmrc)" ] && [ "$(nvm version)" != "$(nvm version default)" ]; then
    echo "Reverting to nvm default version"
    nvm use default
  fi
}

add-zsh-hook chpwd load-nvmrc
load-nvmrc

Using the three tools together

With the three tools installed, every project works the same way:

  1. Clone the repository.
  2. cd into it.
  3. If the project has .sdkmanrc, .python-version or .nvmrc, the shell switches to those versions.
  4. If it has none of them, set the versions once and commit the files.

Commit the version files. They tell everyone who clones the repository which runtime to use, and most CI systems read them. GitHub Actions can read the version from .nvmrc and .python-version directly.


Quick reference

SDKMAN Java
config file .sdkmanrc
auto-switch sdk env · or sdkman_auto_env=true
pyenv Python
config file .python-version
auto-switch automatic on cd
nvm Node.js
config file .nvmrc
auto-switch requires zsh hook
operation
SDKMAN
pyenv
nvm
list installed
sdk list java
pyenv versions
nvm ls
install version
sdk install java <ver>
pyenv install <ver>
nvm install <ver>
switch global
sdk default java <ver>
pyenv global <ver>
nvm alias default <ver>
switch session
sdk use java <ver>
pyenv shell <ver>
nvm use <ver>
pin to project
.sdkmanrc
.python-version
.nvmrc