DEV Community

Jeff Dickey
Jeff Dickey

Posted on • Edited on

Beginner's Guide to rtx (mise)

NOTE: rtx has been renamed to mise. This post is still relevant but anytime you see "rtx" just replace it with "mise"

rtx is a tool that manages installations of programming language runtimes and other tools for local development.

If you are using pyenv, nvm, or asdf, you'll have a better experience with rtx. It's faster, easier to use, and generally has more features than any of those.

It's useful if you want to install a specific version of node or python or if you want to use different versions in different projects.

This guide will cover the 2 most commonly used languages in rtx: node and python, however you can still use this guide for other languages. Just change node and python out for java or ruby or whatever.

Demo

Background

Before we talk about rtx, lets cover some background. It's important to understand these in case something isn't working as expected.

~/.bashrc and ~/.zshrc

These are bash/zsh scripts that are loaded every time you start a new terminal session. We "activate" rtx here so that it is enabled and can modify environment variables when changing directories.

Not sure which one you're using? Try just running a command that doesn't exist and it should tell you:

$ some_invalid_command
bash: Unknown command: some_invalid_command
Enter fullscreen mode Exit fullscreen mode

Environment Variables

Environment variables are a bunch of strings that exist in your shell and get passed to every command that is run. We can use them in any language, in node we use them like this:

$ export MY_VAR=testing-123
$ node -e "console.log('MY_VAR: ', process.env.MY_VAR)"
MY_VAR: testing-123
Enter fullscreen mode Exit fullscreen mode

These are often used to change behavior of the commands we run. In essence, what rtx does is 2 things: manage installation of tools and manage their environment variables.

The latter is mostly invisible to you, but it's important to understand what it is doing, especially with one key environment variable: PATH.

PATH environment variable

PATH is a special environment variable. It is fundamental to how your shell works.

However, it also is just an environment variable like any other. We can display it the same way we did earlier with node:

$ node -e "console.log('PATH: ', process.env.PATH)"
Enter fullscreen mode Exit fullscreen mode

Or more commonly we can use echo (this is simplified, my real one has a lot more directories):

echo $PATH
/Users/jdx/bin:/opt/homebrew/bin:/usr/bin:/bin
Enter fullscreen mode Exit fullscreen mode

What is special about PATH is how bash/zsh use it. Any time you run a command like node or python, but also rm or mkdir and most other things, it needs to "find" where that command exists before it can run it.

This happens behind the scenes, but we can get the "real path" of these tools with which:

$ which rm
/bin/rm
$ which node
/opt/homebrew/bin/node
Enter fullscreen mode Exit fullscreen mode

If we look at the PATH I had above, we can see that /bin and /opt/homebrew/bin are included. bash/zsh will look at each directory in PATH (split on ":"), and check if there is an rm or node inside of each one. The first time it finds one, it returns it.

The way rtx works is it modifies PATH to include the paths to the expected runtimes, so once it is activated, you might see it include a directory like:

/Users/jdx/.local/share/rtx/installs/nodejs/18.0.0/bin
Enter fullscreen mode Exit fullscreen mode

That directory will contain node and npm binaries which will override anything that might be on the "system" (meaning something like /opt/homebrew/bin/node).

Installing rtx

See the rtx documentation for instructions on how to install, for macOS I suggest installing with Homebrew:

brew install rtx
rtx --version
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can also just download rtx as a single file with curl, then make it executable:

curl https://rtx.jdx.dev/rtx-latest-macos-arm64 > ~/bin/rtx
chmod +x ~/bin/rtx
rtx --version
Enter fullscreen mode Exit fullscreen mode

⚠️ Warning

Replace "macos" and "arm64" with "linux" or "x64" if using a different OS or architecture.
Also, this assumes that ~/bin is on PATH which won't be the case by default. Add export PATH="$PATH" to your ~/.bashrc or ~/.zshrc if it isn't already included. rtx -v will fail otherwise because it can't find rtx if it's just in some random directory not in PATH.

Activating rtx

In order for rtx to work it should* be activated. To do this, modify your ~/.bashrc or ~/.zshrc and add "$(rtx activate bash)" or "$(rtx activate zsh)". You generally want to put this near the bottom of that file because otherwise rtx might not be on PATH if it's added earlier in the file.

In other words, you might have the following in your ~/.bashrc:

export PATH="$HOME/bin:$PATH"
eval "$(rtx activate bash)"
Enter fullscreen mode Exit fullscreen mode

If you swap those around it won't work since rtx won't be included in PATH.

Once you modify this file, you'll need to run source ~/.bashrc or source ~/.zshrc in order for the changes to take effect (or just open a new terminal window).

You can verify it is working by running rtx doctor. You should see a message saying "No problems found". If it shows an error, follow the instructions shown.

$ rtx doctor
rtx version:
  1.21.2 macos-arm64 (built 2023-03-04)

shell:
  /opt/homebrew/bin/fish
  fish, version 3.6.0

No problems found
Enter fullscreen mode Exit fullscreen mode

(*This isn't strictly true, you can use rtx with shims or with rtx exec, but generally this is the way you'll want to use rtx.)

Installing Node.js

Now that rtx is installed and activated we can install node with it, to do this simply run the following:

rtx install nodejs@18
Enter fullscreen mode Exit fullscreen mode

At this point node is installed, but not yet on PATH so we can't just call node and run it. To use it we can tell rtx to execute it directly:

rtx exec nodejs@18 -- npm install
rtx exec nodejs@18 -- node ./app.js
Enter fullscreen mode Exit fullscreen mode

In these commands we're calling rtx, telling it to use nodejs@18, then we say -- which tells rtx to stop listening to arguments and everything after that will be executed as a new command in the environment that rtx created. (If that isn't obvious just keep going, it'll likely make more sense later.)

Also note that nodejs@18 sets up both the node and npm binaries so that npm install and node ... both use the same version of node from rtx.

This is fine for ad-hoc testing or one-off tasks, but it's a lot to type. What we want to do now is make nodejs@18 the default version that will be used when running node without prefixing it with rtx exec nodejs@18 --.

Make nodejs@18 the global default

To make it the default, run the following:

rtx use --global nodejs@18
Enter fullscreen mode Exit fullscreen mode

What this does is modify the global config (as of this writing that defaults to ~/.tool-versions, but it will eventually be ~/.config/rtx/config.toml).

You can also edit this file manually. It looks like this for ~/.tool-versions:

nodejs 18
Enter fullscreen mode Exit fullscreen mode

Or this for ~/.config/rtx/config.toml (you can use this today if you want, it's just that rtx use --global won't write to this by default currently):

[tools]
nodejs = '18'
Enter fullscreen mode Exit fullscreen mode

Now node is on PATH and we can just run the following:

npm install
node ./app.js
Enter fullscreen mode Exit fullscreen mode

Let's do a bit more digging to see what is actually going on here. If we run echo $PATH we can see that our PATH has a new entry in it (simplified output):

$ echo $PATH
/Users/jdx/.local/share/rtx/installs/nodejs/18.14.2/bin:/opt/homebrew/bin:/usr/bin:/bin
Enter fullscreen mode Exit fullscreen mode

If we look inside that "rtx/install/nodejs" directory we see the following:

ls /Users/jdx/.local/share/rtx/installs/nodejs/18.14.2/bin
corepack    node        npm     npx
Enter fullscreen mode Exit fullscreen mode

These are the commands that node provides. Because this is first in our PATH, these are what bash/zsh will execute when we run them.

Here are some other rtx commands with example output that can be helpful to see what is going on:

  • show the location of an rtx bin
$ rtx which node 
/Users/jdx/.local/share/rtx/installs/nodejs/18.14.2/bin/node
Enter fullscreen mode Exit fullscreen mode
  • get the current version this bin points to
$ rtx which node --version
18.14.2
Enter fullscreen mode Exit fullscreen mode
  • show the directory the current version of nodejs is installed to
$ rtx where nodejs
/Users/jdx/.local/share/rtx/installs/nodejs/18.14.2
Enter fullscreen mode Exit fullscreen mode

Make nodejs@18 the local default

Let's say we had a project where we wanted to use version 16.x of node instead. We can use rtx to have node and npm point to that version instead when in that directory:

cd ~/src/myproj
rtx use nodejs@16
Enter fullscreen mode Exit fullscreen mode

Note that by default we don't have to install nodejs@16 ahead of time. rtx will prompt if it is not installed (see the demo in this article to see how that looks).

Now if we're in ~/src/myproj, then rtx will make node point to v16 and if we're anywhere else it will use v18.

Upgrading node versions

If you want to use a new major version of node, then set it with rtx use --global nodejs@20 or rtx use nodejs@20. You can also use nodejs@lts for LTS version or nodejs@latest for the latest version.

If you just want to update to a new minor or patch version (18.1.0 or 18.0.1, for example), then all you need to do is run rtx install so long as nodejs 18 is what is in ~/.tool-versions.

You can remove old versions no longer referenced with rtx prune.

Installing Python

Now let's look at installing python which is a bit more complex than node and has some unique quirks.

Make python@latest the global default

We can default python to the latest version of by using the following:

rtx use --global python@latest
python --version
pip --version
Enter fullscreen mode Exit fullscreen mode

This will create many new bins we can call like python, python3, python3.11, pip, pip3, and pip3.11.

Make python@3.11 the local default

We can also set local versions just like with node:

rtx use python@3.11 
python --version
pip --version
Enter fullscreen mode Exit fullscreen mode

Multiple python versions

With python we can use multiple versions:

rtx use --global python@3.11 python@3.10
python --version
python3.10 --version
Enter fullscreen mode Exit fullscreen mode

This will make python and python3 use v3.11 and we can use v3.10 by running python3.10 or pip3.10.

Managing virtualenv with rtx

We can have rtx automatically setup a virtualenv (virtualenvs themselves are out of scope for this article) by using the following .rtx.toml:

[tools]
python = {version='3.10', virtualenv='.venv'}
Enter fullscreen mode Exit fullscreen mode

Whenever inside of this directory, the .venv virtualenv will be created if it does not exist and rtx will automatically activate the virtualenv.

Arbitrary environment variables with rtx

One of rtx's most loved features is the ability to set arbitrary env vars in different directories. This replaces what people commonly use dotenv and direnv for.

To do this, you need to use .rtx.toml instead of .tool-versions since the latter could not support this syntax. Just create this file in any directory you want the environment variables to take effect:

[env]
NODE_ENVIRONMENT = "production"
S3_BUCKET = "my_s3_bucket"
AWS_ACCESS_KEY_ID = "..."
AWS_SECRET_ACCESS_KEY = "..."
Enter fullscreen mode Exit fullscreen mode

As long as rtx is activated, these environment variables will be setup whenever inside of that directory.

Shims

Shims are an optional feature that can be used in some cases in rtx where things don't work as expected. See the rtx docs for more on how shims work.

If you want to integrate rtx with your IDE you'll likely want to use shims for that (but I recommend keeping rtx activate for usage in the terminal unless you have a unique setup where this doesn't work well).

Comparisons to other tools

Let's examine other ways to install node/python and see how they compare:

Node/Python Official .pkg

For macOS, if you go the official node and python websites they'll offer a .pkg installer to install node and python. I don't recommend these for a few reasons:

  • No ability to update. If you want to get the latest version, you need to go to the website and reinstall. That should ideally be a single CLI command.
  • No ability to use multiple versions. If you have multiple projects that need different versions you won't be able to use this method.
  • Can conflict with PATH. If you have node or python installed by some other means, this can either override that install or not work because it is being overridden. Using rtx makes it easy to control when languages should and should not override the system versions.

How is rtx different than asdf?

rtx is very similar to asdf and behaves as a drop-in replacement for almost any use-case. See the docs for more details on this. rtx can be thought of as a clone of asdf with extra features.

Under the hood, rtx uses asdf plugins so the logic for actually installing node and python is the same for both asdf and rtx. That's true today at least, rtx may diverge and fork asdf plugins to enable rtx-specific behavior if needed.

However it is much faster, has better UX, and it has extra features like the ability to modify arbitrary env vars (FOO=bar), or manage Python virtualenvs.

asdf is burdened by being written in bash which is very slow and greatly limits their capabilities. asdf can never be as fast or feature-complete as rtx just because it's written in bash. They would need a ground-up rewrite: which is rtx.

So far as I'm aware, there should be no use-case where asdf is a better fit than rtx.

How is rtx different than Homebrew?

Tools like Homebrew are great (it's how I install most of my tools on macOS), but it won't have the latest version of tools when they come out since there is a manual process to update them.

Homebrew also does not let you use different versions in different directories (at least, not without manually modifying PATH). It doesn't let you install a specific version (e.g.: nodejs-18.0.0 instead of nodejs-18.0.1). It has some support for different major versions in some languages like: brew install nodejs@18.

Use homebrew if you just want to use the latest version of the tool and don't need different versions in different directories. rtx requires a bit more yak-shaving that isn't necessary for a lot of tools.

How is rtx different than apt-get/yum/dnf?

Most Linux distros strive to maintain compatibility and do not allow new software into existing distributions. Because of this, they do not even include node since the versions change too fast and they go end-of-life when the distribution is still under LTS.

Python is included but it's often old or very old.

If python is just some system dependency you don't interact with much this is fine and ideal for compatibility reasons. However if you're writing python yourself, you'll want to use a much more modern version.

Multiple versions is also not terribly well supported by these mechanisms. It can be done, but in a seamless way where you just change directory and it magically has the right language versions in different directories.

You should install python in your Linux packages, but don't develop python projects using that version.

How is rtx different than nvm/pyenv?

These are the most well known tools for switching between node and python versions. They function well, but they're very slow.

These also only work for a single language where rtx can be used to work with any language. It's only one tool to setup and if you want to pick up a different language you don't need to figure out a new tool.

Top comments (5)

Collapse
 
buildbackbuehler profile image
Zachary Meyer

RTX is a godsend, but I could use some help (further) reeling my Mac in. I went with installing RTX via binary, which would more-or-less enable it to become the packman-to-rule-them-all, no?

Out of noob fear & aging internet info (truly wisdoom, wisdumb) I'd been using what I knew for so long. But I am always finding myself with errors and fragmented symlinks no matter how I configure things because my hierarchy is:

brew -> git/random dependencies for casks -> useful formulae
rtx -> node/python (a lot of the aforementioned useful formulae are unknown to this env. thus, redundancies x100)

I can't help but imagine things would run smoothly if I took the leap of faith and had it all under rtx. Except, what would that look like? Unsure if I would/should then strive to simply use "rtx install" for brew/git/node/npm/pnpm/python/pipenv/ruby or if I'll need to have a dependency tree of sorts? Because there's Docker, Rust/Cargo, too. If I can just have a monorepo ala pnpm thanks to RTX, I will cry tears of joy

The end uses tend to be, Stable Diffusion (Python/Pip/Pytorch/Apple Metal), Web Apps (nextjs@pnpm), for remote full-stack dev'ing using Docker Containers w/ Ruby, Rust and React or whatever via Ansible. It makes my brain melt...organized chaos! 🫠😅😶‍🌫️

Collapse
 
astrojuanlu profile image
Juan Luis Cano Rodríguez

I've been using rtx for 20 minutes and I'm loving it already. For many years I've avoided pyenv (and asdf) because shims are so damn problematic... this feels at the perfect level of abstraction, avoids too much magic, is snappy, and worked perfectly in the first attempt. You gained a loyal fan.

Collapse
 
sachajw profile image
Sacha Wharton • Edited

Such a great article! Thank you for taking to the time to put this out there. Much appreciated!

I assume this means I can drop SDKMan for managing Java versions?

Collapse
 
trevdev profile image
Trev

Looks super cool. Where does RTX get its software from? Which software is supported?

Collapse
 
jdxcode profile image
Jeff Dickey

it depends on the plugin, for node it uses node-build under the hood and for python it uses python-build from pyenv. Other plugins might use GitHub releases.

It supports anything asdf supports which is anything popular. There are hundreds of just the built-in plugins: github.com/jdxcode/rtx/blob/main/s...