(Warning: beta-quality software. Feature complete, but tested only by the developer. Feedback is welcome, encouraged.)
This package allows you to use the Emacs project.el
system by
assigning properties to project directories which can be inherited as
buffer-local variables by all files within the project directory. It
is similar to the .dir-locals.el
mechanism already built-in to
Emacs, but with some important differences, which make it more similar
to the direnv CLI utility than .dir-locals.el
.
The key differences are:
- Any directory registered with
dir-local-env
will automatically be detected as a project directory by theproject.el
system (though you can register directories opting-out ofproject.el
). - There is no need to create a
.dir-locals.el
file in every single project directory. - Unlike the
dir-locals-class-alist
variable built-in to Emacs, thedir-local-env.el
mechanism can assign a unique set of properties directly to each project, rather than assigning properties on a per-class or per-major-mode basis. - Updates to the
dir-local-env
for a directory are applied immediately (with some caveats), so no need to first update the class variablesalist
and then re-assign the class to a directory, every time a property is modified.
The dir-locals-class-alist
, which can be set by the
dir-locals-set-class-variables
function, is the mechanism built-in
to Emacs for assigning directory-local variables to directories that
cannot store a .dir-locals.el
file. This class-based mechanism can
set variables for a directory inherited by all files within that
directory, and can set additional variables depending on the
major-mode of each file loaded from within that directory. You may
even assign multiple classes to each directory.
However this class-based mechanism still requires the creation of a class, and it requires the additional step of assigning the class to a directory. So the directory-local variables mechanism built-in to Emacs is not suitable for use cases where:
- you have many project directories, and you need each directory to have it’s own unique set of directory-local variables, since you would need to create one unique class for each project directory, populate each class with the correct properties, then assign each class to it’s respective directory.
- you need to make changes to the directory-local properties of a
project often. This would require creating an additional class for
the oft-changed properties, assigning that class to a directory,
calling the
hack-dir-local-variables
function, and then calling thehack-local-variables
function on each already-open buffer to inherit the updates.
With dir-local-env.el
, there are no classes of variables and no
filters based on major mode, you simply assign variables directly to a
directory, and all files within that directory inherit those variables
as buffer-local variables. This prevents the need for hacky solutions
like generating a unique class name from a directory path.
The dir-local-env
mechanism complements (not replaces) the
dir-local-env.el
, so directory-local variables may be set by both or
either. So it is still possible to use the dir-locals-class-alist
and dir-locals-set-class-variables
function while using
dir-local-env.el
.
- Provides a global minor mode
dir-local-env-global-minor-mode
- Compatible with Emacs’s
project.el
. When a directory local environment is registered for a directory, theproject.el
commands automatically see this directory as a project directory (disable by settingproject-dir-disabled
to non-~nil~). - sets “advice” on Emacs built-in process execution functions like
make-process
so as to alter theprocess-environment
andexec-path
variables on a per-project basis, especially useful for settingM-x compile
executable on a per-project basis. - Can be used to complement the
.dir-locals.el
system, rather than replace it. - Configuration syntax similar to the Emacs Lisp
(let* ...)
macro, for example:(defdir-local-env "/home/abcdef/projects/sort-of-cool-raytracer" ((exec-path '("/usr/local/optimized-gpu-thing/compiler", "/usr/bin"))) "/home/abcdef/projects/really-cool-compiler" ((project-dir-disabled t) ;; ^ this directory is thus ignored by `project.el' commands (favorite-color "red") (lucky-number 10001)))
- Easy to temporarily disable by setting the local
disable-dir-local-env
variable to non-~nil~. - “Idempotent” declaration semantics, meaning if you evaluate the
(defdir-local-env ...)
macro twice by accident, only the first invocation has any effect. To update the configuration, unregister the directory local environment and then re-evaluate the modified(defdir-local-env ...)
macro for that directory. - Provides Emacs commands to register directory-local environments,
and set variables, without using the declarative
defdir-local-env
macro:dir-local-env-register
registers a directory to have it’s own directory-local environment.dir-local-env-unregister
deletes a directory-local environment.dir-local-env-set
sets an environment variable in a registered directory-local environment.dir-local-env-show-all
shows a directory’s registered local environment and all variables set for that environment.
An Emacs Lisp implementation of direnv
Features for extracting environment variables from a shell process is
still experimental, and not at all easy to do (yet). But the
dlenv--split-null-delimited-string
function is provided so that you
might parse the output of the printenv -0;
shell command and produce
an environment data structure suitable for use with Emacs’s
process-environment
variable. Setting this variable in a dir-local
environment is similar to using direnv in the command line.
This also applies to projects computed by a functional package manager such as:
This is intended to be helpful when using the Emacs built-in M-x
compile
command, when one would like to execute a compiler via
project-specific directory PATH
environment variable defined by
directory-local environment variable mechanisms such as, direnv
. You
can cache the environment provided by direnv
into Emacs’s
process-environment
variable just for a particular project
directory, so that M-x compile
always executes a compiler in the
PATH
provided by direnv
.
This can theoretically also be helpful if you choose to install a Language Server Protocol (LSP) server using the Nix or Guix package managers, and would like to direct the eglot or lsp-mode systems to use a LSP server specific to a particular project directory.
Once the shell environment has been computed and you have a shell, you
may extract the environment using a command like printenv -0;
and
cache the result in a directory local process-environment
variable. From that point on, any time the M-x compile
command is
called on a file within that directory, the compiler and environment
variables defined by the Nix/Guix shell environment will be called.
Again, this is not exactly easy to do at this time, but it is hoped
that soon functionality to automate the process of extracting computed
process environments from functional package managers like Nix or
Guix, or from direnv
, will be implemented.
The defdir-local-env
macro and other commands like
dir-local-env-set
can apply changes to the directory local variables
immediately. Changes to process-environment
and exec-path
will be
seen immediately since dir-local-env-global-minor-mode
checks these
variables on each invocation of make-process
.
However for all other directory local variables it is still
necessary to call hack-dir-local-variables
on each buffer affected
by changes to the directory local environment after making updates to
other variables. This process might be automated in a later version by
adding advice functions to switch-to-buffer
that automatically call
hack-dir-local-variables
whenever user focus switches to a
directory. The architecture for how updates to the dir-local variables
might be applied has not been fully investigated yet.
There are, of course, other Emacs Lisp systems which allow you to assign properties to project directories in the manner of direnv.
- Sidecar Locals allows you to declare properties for a directory in a
file at the same level as the directory itself, rather than as a
file within the directory. You specify a list of paths in the
sidecar-local-paths-allow
to files that can be trusted to assign directory-local properties. - buffer-env is essentially an Emacs Lisp implementation of the direnv
tool, which loads project properties from a
.envrc
file, rather than from a.dir-locals.el
file, and can be used to set properties such asprocess-environment
andexec-path
, which is very useful for changing the compiler tool chain you are using for a particular project.