Originally published at 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, 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 usedcat <<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
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.
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.
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
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
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:
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.
Top comments (1)
This was a showstopper for me
github.com/direnv/direnv/issues/408
As the activate script generated by
python3 -m venv .....blah blah ...
Modifies PS1