direnv — Take Control of your Development Environment

Custom Development

Think of direnv as your per-directory .bashrc

If you aren’t familiar with direnv, 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. However, it’s only installed via Homebrew. 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, but here’s a short summary for Ubuntu and bash:

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

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:

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

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

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:

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

Example #3 — Node via nodenv

Using nodenv follows the pyenv pattern above. If you really want separate node.js installations, the Node section of the direnv wiki shows how to use nvm to achieve this.

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

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:

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

Example #4 — Ruby via rbenv

Using rbenv follows the pyenv pattern above.

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

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:

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

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 directly, the process is almost identical.

Here’s how I installed goenv:

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:

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

Homebrew

Now, back to the original problem, Homebrew. Just to be complete, you need a few packages (in Ubuntu) to install Homebrew:

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

Now, the installation is pretty straight-forward:

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:

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

And here is the .envrc from brew shellenv:

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.