direnv -- Take Control of your Development Environment
Tags: direnv, environment, bash
Published: 2020-01-24 17:01:33

*Originally published at [www.pinnsg.com/direnv-take-control-of-your-development-environment/](https://www.pinnsg.com/direnv-take-control-of-your-development-environment/)*

### Think of `direnv` as your per-directory `.bashrc`

If you aren't familiar with [direnv](https://direnv.net), you need to be. It's a great tool to help you stay organized.  In any given directory, you create an `.envrc` with your environment or script setup, and that is activated when you change to a directory. For example, if you have a setup script in a project directory that exports several environment variables, name that script `.envrc`.

For reference, I never install global Python libraries when I set up a new machine, I always use `direnv`. It ensures that I have the proper versions installed per project, which is key.

So, why write this? Recently, I had the opportunity to try out [AWS SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install-linux.html). However, it's only installed via [Homebrew](https://docs.brew.sh/Homebrew-on-Linux). I grow weary of all the tools that want to you append just this little snippet to the end of your `.bashrc`. This is where `direnv` shines.  



## Installing `direnv`

Refer to the [installation documents](https://github.com/direnv/direnv/blob/master/docs/installation.md), but here's a short summary for `Ubuntu` and `bash`:

```sh
sudo apt install -y direnv
cat <<'EOF' >>.bashrc
eval "$(direnv hook bash)"
EOF
exec bash
```

* Note #1: By enclosing EOF in single quotes (`cat <<'EOF'`), I'm suppressing expansion.  Therefore, the exact string is appended to `.bashrc` (in this case).  If I had simply used `cat <<EOF >>.bashrc`, then the eval would be executed *before* appending to `.bashrc`, which is not what we want.
* Note #2: The command `exec bash` reloads `.bashrc`

## Example #1 -- Python via `virtualenv`

Let's start with the easy case. Say you have a python project, and that project comes with a `requirements.txt`. If you try and maintain your python dependencies in your global packages, it will not be long before some of the various projects have package conflicts. Why not install them in the project directory (a la `node_modules`, if you will)? `direnv` makes that easy.

The simplest method for using `python` and `direnv` is using [virtualenv](https://virtualenv.pypa.io/en/latest/). This package is typically included in most python installations or is available in the appropriate package repository. If not, here's how to install it on Ubuntu, for example:

```sh
sudo apt install -y python-virtualenv
```

Now that the pre-requisites are installed, we can set up our development environment for our project. In this case, I want the latest Python3 in an isolated environment so I can install the `requirements.txt` files specifically for this project. Here are the commands:

```sh
sudo apt install -y python-virtualenv
mkdir python-test-3 && cd $_
echo "layout python3" > .envrc
direnv allow
which python
python --version
cat <<EOF >requirements.txt
cryptography==2.3
PyJWT==1.6.4
EOF
pip install -r requirements.txt
find . -name *PyJWT* -type d
cd ..
mkdir python-test-2 && cd $_
echo "layout python2" > .envrc
direnv allow
which python
python --version
find . -name *PyJWT* -type d
```

![install-python](https://mcpubbucket.s3-external-1.amazonaws.com/virtualenv-versions.gif)

As you can see from the `find` commands, the packages are installed in the `.direnv` subdirectory in your project.

## Example #2 -- Python via `pyenv`

Given the remaining examples are `rbenv`, `nodenv` and `goenv`, it's worthwhile to also to check out [pyenv](https://github.com/pyenv/pyenv-installer) as an alternative installation method for python. Whereas `virtualenv` leverages the existing python installation, `pyenv` downloads python releases and installs them in `~/.pyenv/versions`.

First, install `pyenv` and a couple of versions of python along with various prerequisites. Here are our commands:

```sh
sudo apt install libreadline-dev libbz2-dev libsqlite3-dev libssl1.0
curl -fsSL https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash
cat <<'EOF' >>.bashrc
export PATH="$HOME/.pyenv/bin:$PATH"
eval "$(pyenv init -)"
EOF
exec bash
pyenv install 3.7.6
pyenv install 2.6.9
```

**NOTE**: For this example, I'm installing an older version of python (2.6.9) and using older SSL libraries.  For that reason, I'm installing the 1.0 version of `libssl` (the above `sudo apt install ... libssl1.0` ).  Make sure you choose the appropriate SSL library for your requirements!

Now, here's how to setup your projects:

```sh
python2 --version
python3 --version
mkdir python-3.7 && cd $_
echo 'export PYENV_VERSION=3.7.6' > .envrc
direnv allow
python --version
cd .. && clear
mkdir python-2.6 && cd $_
echo 'export PYENV_VERSION=2.6.9' > .envrc
direnv allow
python --version
cd ../python-3.7 && clear
python --version
```

As you can see from the gif below, we start out with python2 with version 2.7.17, and python3 version 3.6.9.  However, using `pyenv`, we now have directory-specific versions installed of 3.7.6 and 2.6.7.

![images/pyenv-versions.gif](https://mcpubbucket.s3-external-1.amazonaws.com/pyenv-versions.gif)

## Example #3 -- Node via `nodenv`

Using [nodenv](https://github.com/nodenv/nodenv) follows the `pyenv` pattern above.  If you really want separate node.js installations, the [Node](https://github.com/direnv/direnv/wiki/Node) section of the `direnv` wiki shows how to use [nvm](https://github.com/creationix/nvm) to achieve this.

First, install `nodenv` and a couple of versions of node. Here are our commands:

```sh
curl -fsSL https://github.com/nodenv/nodenv-installer/raw/master/bin/nodenv-installer | bash
cat <<'EOF' >>.bashrc
export PATH="$HOME/.nodenv/bin:$PATH"
eval "$(nodenv init -)"
EOF
exec bash
nodenv install 10.18.1
nodenv install 8.17.0
```

Now, here's how to setup your projects:

```sh
mkdir node-test-10 && cd $_
echo 'export NODENV_VERSION=10.18.1' > .envrc
direnv allow
node --version
cd .. && clear
mkdir node-test-8 && cd $_
echo 'export NODENV_VERSION=8.17.0' > .envrc
direnv allow
node --version
cd ../node-test-10 && clear
node --version
```

**NOTE**: You may also want to add `layout node` to your `.envrc` to add `$PWD/node_modules/.bin` to your PATH when you are in this directory.

![images/nodenv-versions.gif](https://mcpubbucket.s3-external-1.amazonaws.com/nodenv-versions.gif)

## Example #4 -- Ruby via `rbenv`

Using [rbenv](https://github.com/rbenv/rbenv) follows the `pyenv` pattern above.

First, install `rbenv` and a couple of versions of ruby along with the prerequisites. Here are our commands:

```sh
sudo apt install -y autoconf bison build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm5 libgdbm-dev
curl -fsSL https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-installer | bash
cat <<'EOF' >>.bashrc
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"
EOF
exec bash
rbenv install 2.6.5
rbenv install 2.2.5
```

Now, here's how to setup your projects:

```sh
mkdir ruby-test-2.6 && cd $_
echo 'export RBENV_VERSION=2.6.5' > .envrc
direnv allow
ruby --version
cd .. && clear
mkdir ruby-test-2.4 && cd $_
echo 'export RBENV_VERSION=2.4.4' > .envrc
direnv allow
ruby --version
cd ../ruby-test-2.6 && clear
ruby --version
```

![images/rbenv-versions.gif](https://mcpubbucket.s3-external-1.amazonaws.com/rbenv-versions.gif)

## Example #5 -- Go via `goenv`

You guessed it, you can do the same with Go. However, I didn't find a `goenv-installer` based on `rbenv-installer`, so I quickly converted `rbenv-installer` to handle go (the principle is the same). Whether you use my first-pass installer or install [goenv](https://github.com/syndbg/goenv) directly, the process is almost identical.

Here's how I installed `goenv`:

```sh
curl -fsSL https://github.com/drmikecrowe/goenv-installer/raw/master/bin/goenv-installer | bash
cat <<'EOF' >>.bashrc
export PATH="$HOME/.goenv/bin:$PATH"
eval "$(goenv init -)"
EOF
goenv install 1.13
goenv install 1.10
```

Now, here's how to setup your projects:

```sh
mkdir go-test-1.13 && cd $_
echo 'export GOENV_VERSION=1.13.0' > .envrc
direnv allow
go version
cd .. && clear
mkdir go-test-1.10 && cd $_
echo 'export GOENV_VERSION=1.10.0' > .envrc
direnv allow
go version
cd ../go-test-1.13 && clear
go version
```

![images/goenv-versions.gif](https://mcpubbucket.s3-external-1.amazonaws.com/goenv-versions.gif)

## Homebrew

Now, back to the original problem, [Homebrew](https://docs.brew.sh/Homebrew-on-Linux). Just to be complete, you need a few packages (in Ubuntu) to install `Homebrew`:

```sh
sudo apt-get install -y build-essential curl file git
```

Now, the installation is pretty straight-forward:

```sh
sh -c "$(curl -fsSL https://raw.githubusercontent.com/Linuxbrew/install/master/install.sh)"
```

However, here's where we branch from the installation instructions. I don't want Brew always in my path. I'd prefer it enabled for selective projects. So, let's use the same approach as before. To do that, rather than update our `.bash_profile` or `.profile`, let's tweak the last step of the brew installation for our directory-specific `.envrc`.

Note: Homebrew installs in one of two locations: `/home/linuxbrew` or `$HOME/.linuxbrew` -- Adjust the command below accordingly. For me, it was the former:

```sh
mkdir brew-project && cd $_
/home/linuxbrew/.linuxbrew/bin/brew shellenv > .envrc
direnv allow
```

This command executes `brew shellenv`, which outputs all the revised environment variables Homebew wants to load.  It stores those variables in `.envrc`, which is subsequently loaded whenever we enter this directory. So, rather than always being loaded, it is now loaded in the projects where I need it.

So, here's that process in action:

![images/homebrew.gif](https://mcpubbucket.s3-external-1.amazonaws.com/homebrew.gif)

And here is the `.envrc` from `brew shellenv`:

```sh
export HOMEBREW_PREFIX="/home/linuxbrew/.linuxbrew";
export HOMEBREW_CELLAR="/home/linuxbrew/.linuxbrew/Cellar";
export HOMEBREW_REPOSITORY="/home/linuxbrew/.linuxbrew/Homebrew";
export PATH="/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin${PATH+:$PATH}";
export MANPATH="/home/linuxbrew/.linuxbrew/share/man${MANPATH+:$MANPATH}:";
export INFOPATH="/home/linuxbrew/.linuxbrew/share/info${INFOPATH+:$INFOPATH}";
```


# Conclusion

With this final example, you now see how `.envrc` is truly the per-directory `.bashrc`. Rather than reaching for `.bashrc` when you add a new development system, consider if this is _always_ needed or if you want to selectively use it. The more I use `direnv`, the more power I find it has.