direnv: Directory-specific environments
Sunday, June 3rd, 2018
Over the course of a single day I might work on a dozen different admin or development projects. In the morning I could be hacking on some Zabbix monitoring scripts, in the afternoon on auto-generated documentation and in the evening on a Python or C project.
I try to keep my system clean and my projects as compartmentalized as possible, to avoid library version conflicts and such. When jumping from one project to another, the requirements of my shell environment can change significantly. One project may require
/opt/nim/bin to be in my
PATH. Another project might require a Python VirtualEnv to be active, or to have
GOPATH set to the correct value. All in all, switching from one project to another incurs some overhead, especially if I haven't worked on it for a while.
Wouldn't it be nice if we could have our environment automatically set up simply by changing to the project's directory? With direnv we can.
direnv is an environment switcher for the shell. It knows how to hook into bash, zsh, tcsh, fish shell and elvish to load or unload environment variables depending on the current directory. This allows project-specific environment variables without cluttering the ~/.profile file.
Before each prompt, direnv checks for the existence of a ".envrc" file in the current and parent directories. If the file exists (and is authorized), it is loaded into a bash sub-shell and all exported variables are then captured by direnv and then made available to the current shell.
It's easy to use. Here's a quick guide:
Install direnv (I'm using Ubuntu, but direnv is available for many Unix-like systems):
fboender @ jib ~ $ sudo apt install direnv
You'll have to add direnv to your
.bashrc in order for it to work:
fboender @ jib ~ $ tail -n1 ~/.bashrc eval "$(direnv hook bash)"
In the base directory of your project, create a
.envrc file. For example:
fboender @ jib ~ $ cat ~/Projects/fboender/foobar/.envrc #!/bin/bash # Settings PROJ_DIR="$PWD" PROJ_NAME="foobar" VENV_DIR="/home/fboender/.pyenvs" PROJ_VENV="$VENV_DIR/$PROJ_NAME" # Create Python virtualenv if it doesn't exist yet if [ \! -d "$PROJ_VENV" ]; then echo "Creating new environment" virtualenv -p python3 $PROJ_VENV echo "Installing requirements" $PROJ_VENV/bin/pip3 install -r ./requirements.txt fi # Emulate the virtualenv's activate, because we can't source things in direnv export VIRTUAL_ENV="$PROJ_VENV" export PATH="$PROJ_VENV/bin:$PATH:$PWD" export PS1="(`basename \"$VIRTUAL_ENV\"`) $PS1" export PYTHONPATH="$PWD/src"
This example automatically creates a Python3 virtualenv for the project if it doesn't exist yet, and installs the dependencies. Since we can only export environment variables directly, I'm emulating the virtualenv's
bin/activate script by setting some Python-specific variables and exporting a new prompt.
Now when we change to the project's directory, or any underlying directory, direnv tries to activate the environment:
fboender @ jib ~ $ cd ~/Projects/fboender/foobar/ direnv: error .envrc is blocked. Run `direnv allow` to approve its content.
This warning is to be expected. Running random code when you switch to a directory can be dangerous, so direnv wants you to explicitly confirm that it's okay. When you see this message, you should always verify the contents of the
We allow the
.envrc, and direnv starts executing the contents. Since the python virtualenv is missing, it automatically creates it and installs the required dependencies. It then sets some paths in the environment and changes the prompt:
fboender @ jib ~ $ direnv allow direnv: loading .envrc Creating new environment Already using interpreter /usr/bin/python3 Using base prefix '/usr' New python executable in /home/fboender/.pyenvs/foobar/bin/python3 Also creating executable in /home/fboender/.pyenvs/foobar/bin/python Installing setuptools, pkg_resources, pip, wheel...done. Installing requirements Collecting jsonxs (from -r ./requirements.txt (line 1)) Collecting requests (from -r ./requirements.txt (line 2)) Using cached https://files.pythonhosted.org/packages/49/df/50aa1999ab9bde74656c2919d9c0c085fd2b3775fd3eca826012bef76d8c/requests-2.18.4-py2.py3-none-any.whl Collecting tempita (from -r ./requirements.txt (line 3)) Collecting urllib3<1.23,>=1.21.1 (from requests->-r ./requirements.txt (line 2)) Using cached https://files.pythonhosted.org/packages/63/cb/6965947c13a94236f6d4b8223e21beb4d576dc72e8130bd7880f600839b8/urllib3-1.22-py2.py3-none-any.whl Collecting chardet<3.1.0,>=3.0.2 (from requests->-r ./requirements.txt (line 2)) Using cached https://files.pythonhosted.org/packages/bc/a9/01ffebfb562e4274b6487b4bb1ddec7ca55ec7510b22e4c51f14098443b8/chardet-3.0.4-py2.py3-none-any.whl Collecting certifi>=2017.4.17 (from requests->-r ./requirements.txt (line 2)) Using cached https://files.pythonhosted.org/packages/7c/e6/92ad559b7192d846975fc916b65f667c7b8c3a32bea7372340bfe9a15fa5/certifi-2018.4.16-py2.py3-none-any.whl Collecting idna<2.7,>=2.5 (from requests->-r ./requirements.txt (line 2)) Using cached https://files.pythonhosted.org/packages/27/cc/6dd9a3869f15c2edfab863b992838277279ce92663d334df9ecf5106f5c6/idna-2.6-py2.py3-none-any.whl Installing collected packages: jsonxs, urllib3, chardet, certifi, idna, requests, tempita Successfully installed certifi-2018.4.16 chardet-3.0.4 idna-2.6 jsonxs-0.6 requests-2.18.4 tempita-0.5.2 urllib3-1.22 direnv: export +PYTHONPATH +VIRTUAL_ENV ~PATH (foobar) fboender @ jib ~/Projects/fboender/foobar (master) $
I can now work on the project without having to manually switch anything. When I'm done with the project and change to a different dir, it automatically unloads:
(foobar) fboender @ jib ~/Projects/fboender/foobar (master) $ cd ~ direnv: unloading fboender @ jib ~ $
And that's about it! You can read more about direnv on its homepage.