(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-envwill automatically be detected as a project directory by theproject.elsystem (though you can register directories opting-out ofproject.el). - There is no need to create a
.dir-locals.elfile in every single project directory. - Unlike the
dir-locals-class-alistvariable built-in to Emacs, thedir-local-env.elmechanism 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-envfor a directory are applied immediately (with some caveats), so no need to first update the class variablesalistand 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-variablesfunction, and then calling thehack-local-variablesfunction 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.elcommands automatically see this directory as a project directory (disable by settingproject-dir-disabledto non-~nil~). - sets “advice” on Emacs built-in process execution functions like
make-processso as to alter theprocess-environmentandexec-pathvariables on a per-project basis, especially useful for settingM-x compileexecutable on a per-project basis. - Can be used to complement the
.dir-locals.elsystem, 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-envvariable 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-envmacro:dir-local-env-registerregisters a directory to have it’s own directory-local environment.dir-local-env-unregisterdeletes a directory-local environment.dir-local-env-setsets an environment variable in a registered directory-local environment.dir-local-env-show-allshows 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-allowto 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
.envrcfile, rather than from a.dir-locals.elfile, and can be used to set properties such asprocess-environmentandexec-path, which is very useful for changing the compiler tool chain you are using for a particular project.