Beginner's Guide to rtx (mise)
Tags: python, node, asdf, rtx
Published: 2023-03-04 19:37:48

---
title: Beginner's Guide to rtx (mise)
published: true
description: Learn how to install and use node and python for local development
tags: python,node,asdf,rtx
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-03-04 17:07 +0000
---

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

[rtx](https://github.com/jdxcode/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

{% embed https://asciinema.org/a/564654 %}

## 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:

```sh-session
$ 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:

```sh-session
$ 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):

```sh-session
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`:

```sh-session
$ 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](https://rtx.jdx.dev) 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. 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)"
```

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](#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](https://asciinema.org/a/564654) 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:

```toml
[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.