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
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
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)"
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
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
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
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
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
⚠️ 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. Addexport PATH="$PATH"
to your~/.bashrc
or~/.zshrc
if it isn't already included.rtx -v
will fail otherwise because it can't findrtx
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)"
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
(*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
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
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
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
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'
Now node is on PATH and we can just run the following:
npm install
node ./app.js
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
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
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
- get the current version this bin points to
$ rtx which node --version
18.14.2
- show the directory the current version of nodejs is installed to
$ rtx where nodejs
/Users/jdx/.local/share/rtx/installs/nodejs/18.14.2
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
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
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
Multiple python versions
With python we can use multiple versions:
rtx use --global python@3.11 python@3.10
python --version
python3.10 --version
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'}
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 = "..."
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)
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! 🫠😅😶🌫️
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.
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?
Looks super cool. Where does RTX get its software from? Which software is supported?
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...