#+title: buffer-env --- Buffer-local process environments

#+html: <p align="center">
#+html: <a href="http://elpa.gnu.org/packages/buffer-env.html"><img alt="GNU ELPA" src="https://elpa.gnu.org/packages/buffer-env.svg"/></a>
#+html: <a href="https://melpa.org/#/buffer-env"><img alt="MELPA" src="https://melpa.org/packages/buffer-env-badge.svg"/></a>
#+html: </p>

With this package, you can teach Emacs to call the correct version of
external programs such as linters, compilers and language servers on a
/per-project/ basis.  Thus you can work on several projects in
parallel with no undue interference and switch seamlessly between
them.

** Basic setup

*** On the project side
Your project settings should go into a shell script named =.envrc=
which exports a suitable =PATH=, as well as any other desired
environment variables.  Place this script at the root directory of
your project.

This follows the approach of the popular [[https://direnv.net/][direnv]] program, and is mostly
compatible with it.  However, buffer-env is entirely independent of
direnv so it is not possible to use direnv-specific features in the
=.envrc= scripts --- at least not directly.

Alternatively, it is possible to configure buffer-env to directly
support other environment setup methods, such as Python virtualenvs or
=.env= files.  See below for details.

*** On the Emacs side
The usual way to activate this package in Emacs is by including the
following in your init file:

#+begin_src emacs-lisp
  (add-hook 'hack-local-variables-hook 'buffer-env-update)
#+end_src

In this way, any buffer potentially affected by [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html][directory-local
variables]] will also be affected by buffer-env.  It is also possible to
add =buffer-env-update= only to specific major-mode hooks, or call it
interactively.

** How this package works
When called interactively, =buffer-env-update= asks for a script file
and executes it (in the sense detailed below).  The role of the script
is to set some environment variables.  Then the Emacs variables
=process-environment= and =exec-path= are made buffer-local and their
values are set so as to replicate the environment defined by the
script.

When =buffer-env-update= is called from a hook, a file named
=buffer-env-script-name= is looked up in the current directory or one
of its parents.  If found, the same procedure as in the interactive
case takes place.  Otherwise, nothing happens.

It remains to clarify what “executing a script” means in the
paragraphs above.  Normally, it simply means to execute the script as
a shell script and collect all the exported variables.  However,
certain script names are treated specially.  These are:

- =.env=: These files, used by Docker, Node.js and others, are simple
  lists of =VARIABLE=value= pairs.  They are still executed as shell
  scripts (which dictates when and how quotes are to be used, for
  instance), but no =export= statements are needed.
- =guix.scm=: The development environment of the Guix package is
  loaded and exported to Emacs.  Make sure you have entered =guix
  shell= at least once before to install the dependencies, otherwise
  you may block Emacs for a long time.

For instructions on how to extend this list, see the documentation of
the variable =buffer-env-commands=.

** Integration with other environment management mechanisms

*** Python virtualenvs
In most cases, the easiest way to interface with Python virtualenvs is
to create an =.envrc= file with the following contents:

#+begin_src bash
  source path-to-virtualenv/bin/activate
#+end_src

You can also call =buffer-env-activate= interactively and select the
=activate= script directly.

However, if you want to avoid writing =.envrc= scripts and you create
virtualenvs in a predictable place, say in a =.venv= directory at the
root of each project, you can say

#+begin_src emacs-lisp
  (setq buffer-env-script-name ".venv/bin/activate")
#+end_src

or a variation thereof.  Note that it is also possible to provide an
absolute path for =buffer-env-script-name=, and it is possible to specify it
as a buffer- or directory-local variable.

*** .env files
To load the environment defined by a =.env= file, you can select it
interactively with =buffer-env-update=.  To automate the process, set
=buffer-env-script-name= to =".env"=, either globally, dir-locally or
buffer-locally.

*** Direnv
Buffer-env is /mostly/ compatible with direnv; specifically, it assumes
=.envrc= is a regular shell script, so you can't directly use anything
from direnv's library of helper functions.  A workaround is to use the
following configuration:

#+begin_src emacs-lisp
  (with-eval-after-load 'buffer-env
    (add-to-list 'buffer-env-commands '(".envrc" . "direnv exec . env -0")))
#+end_src

If you need tighter integration with direnv, you may want to check out
the [[https://github.com/purcell/envrc][envrc]] package.

** Compatibility issues
Most Emacs packages are not written with the possibility of a
buffer-local process environment in mind.  This leads to issues with a
few commands; specifically, those which start an external process
after switching to a different buffer or remote directory.  Examples
include:

- =shell=, =project-shell= (=C-x p s=) and other REPLs;
- =compile= and =project-compile= (=C-x p c=) in Emacs 27 and older;
- =async-shell-command= (=M-&=).

Fortunately, the problem has an easy fix provided by the [[https://github.com/purcell/inheritenv][inheritenv]]
package, which see.

Alternatively, if you speak Elisp and want to keep your configuration
lean, you can just copy the function below and apply it as an
=:around= advice to any affected commands.

#+begin_src emacs-lisp
  (defun buffer-env-inherit (fn &rest args)
    "Call FN with ARGS using the buffer-local process environment.
  Intended as an advice around commands that start a process after
  switching buffers."
    (cl-letf (((default-value 'process-environment) process-environment)
              ((default-value 'exec-path) exec-path))
      (apply fn args)))
#+end_src

*** Non-Unix-like systems
Currently, this package assumes your system shell is POSIX.  For other
types of operating system, I would need to know what the appropriate
value of =buffer-env-commands= is.  Drop me a line if you can help.

** Related packages
This package is essentially a knockoff of the [[https://github.com/purcell/envrc][envrc]] package by Steve
Purcell.  The main difference is that envrc depends on and tightly
integrates with the [[https://direnv.net/][direnv]] program, while buffer-env is minimalist and
has no extra dependencies.

For a comparison of the buffer-local approach to environment variables
with the global approach used by most of the similar packages, see
[[https://github.com/purcell/envrc#design-notes][envrc's design notes]].

There is a large number of Emacs packages interfacing with the Python
virtualenv system.  They all seem to take the global approach and,
therefore, the comparisons and caveats in the envrc design notes also
apply, mutatis mutandis.

** Contributing
Discussions, suggestions and code contributions are welcome! Since
this package is part of GNU ELPA, contributions require a copyright
assignment to the FSF.
