2015-12-06

Practical direnv

direnv is a nifty little shell tool I came across recently which looks extremely useful for developers who work across multiple languages or projects.

direnv automatically update environment variables when you enter/exit particular directory trees. Much as you might have HOME and PATH variables set globally in your .profile/.bashrc/etc file, direnv allows you to control these on a per-directory basis.

This post explores a few simple examples for usage in Python, Go and Java; see below for further resources.

For Mac users, installation is easiest using Homebrew (brew install direnv).

Automatic Python virtualenv switching

For Python projects virtualenv provides separation of dependencies, but more than once I’ve found myself forgetting to activate/deactivate when moving around between projects. Direnv again makes this easy and foolproof:

layout python

When you enter the folder subtree direnv will automatically pick up an existing virtualenv environment and activate it.

For example:

$ mkdir a b
$ echo "layout python" > a/.envrc
$ echo "layout python" > b/.envrc
$ cd a
    direnv: error .envrc is blocked. Run `direnv allow` to approve its content.

This warning occurs because direnv has detected a new or changed .envrc file - a good security precaution.

$ direnv allow
    direnv: loading .envrc
    Running virtualenv with interpreter /usr/local/bin/python
    New python executable in /private/tmp/a/.direnv/python-2.7.10/bin/python2.7
    Also creating executable in /private/tmp/a/.direnv/python-2.7.10/bin/python
    Installing setuptools, pip, wheel...done.
    direnv: export +VIRTUAL_ENV ~PATH
$ pip install pycrypto
    Collecting pycrypto
    Installing collected packages: pycrypto
    Successfully installed pycrypto-2.6.1
    

We now have a virtualenv inside a with pycrypto installed:

$ pip freeze
    pycrypto==2.6.1
    wheel==0.24.0

Changing directory to b is enough to automatically switch virtualenv.

$ cd ../b
    direnv: error .envrc is blocked. Run `direnv allow` to approve its content.
$ direnv allow
    [SNIP]
    direnv: export +VIRTUAL_ENV ~PATH
$ pip freeze
    wheel==0.24.0

Subsequent changes of directory won’t prompt for direnv allow, and allows seamless changes of virtualenv:

$ cd ../a
    direnv: loading .envrc
    direnv: export +VIRTUAL_ENV ~PATH
$ pip freeze
    pycrypto==2.6.1
    wheel==0.24.0
    

Automatic setting of GOPATH

If you’re developing in Go, direnv can help automatically maintain a separate GOPATH for each independent project. This can be useful where you’re using different versions of dependencies across projects, and wish to maintain some separation - without remembering to change GOPATH yourself every time you switch project.

Quite simply, place the following in .envrc at the root of each Go project:

layout go

Direnv will do the rest automatically; setting GOPATH and also updating PATH to include $GOPATH/bin.

Different versions of a tool for different projects

While I’m almost always working with Java 8, for legacy reasons I’m also working on a Java 7 codebase at the moment. There’s nothing intrinsically wrong with using JDK 1.8 for JDK 1.7 development, but there are some risky areas I’d rather avoid. Additionally we have a need to use a fixed Gradle version rather than using the system one (or, curiously, the Gradle wrapper).

So, I set my general purpose JDK to 1.8 by placing this in my home ~/.envrc file:

# On OS X, discover JAVA_HOME for a major JDK version by using the java_home utility
export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)

# direnv helper to add this to PATH. export PATH=...:$PATH also works.
PATH_add $JAVA_HOME/bin

And override the Java and Gradle version specifically for the legacy project by placing this in my ~/projects/something/.envrc:

export JAVA_HOME=$(/usr/libexec/java_home -v 1.7)
# PWD is always set to the directory in which this .envrc file was found
# In this case, our legacy version of gradle is in ~/projects/something/legacy-gradle
export GRADLE_HOME=$PWD/legacy-gradle

# Update paths as previously
PATH_add $JAVA_HOME/bin
PATH_add $GRADLE_HOME/bin

Now, for working with Java anywhere inside my home folder, direnv ensures that I’m using Java 1.8 automatically. However, if I run Java, Gradle, or check the JAVA_HOME/GRADLE_HOME environment variables anywhere under ~/projects/something, it’s going to be the correct, older version.

Note: this example could be refactored into a custom use function, but I wanted to start with something that would be reasonably familiar looking to most *nix users.

Further reading

  • direnv.net is an OK starting point, but I felt didn’t do enough to show the power of the tool.
  • The direnv man page provides some other simple examples.
  • The direnv standard library provides convenience wrappers for Python, Go, Node, Ruby, Perl and Nix, and is worth looking at.
  • Rachid Belaid has done a more extensive writeup specifically around using direnv for Go development.

Previous post
iOS App Transport Security in dev environments App Transport Security, introduced with iOS 9, is a great step towards improving the security of all apps by forcing use of HTTPS for network
Next post
JUnit integration testing with Docker and Testcontainers This is the first of a couple of related posts; also see Fun with Disque, Java and Spinach and Better JUnit Selenium testing with Docker and