Giter Site home page Giter Site logo

envrc's Introduction

Melpa Status Melpa Stable Status Build Status Support me

envrc.el - buffer-local direnv integration for Emacs

A GNU Emacs library which uses the direnv tool to determine per-directory/project environment variables and then set those environment variables on a per-buffer basis. This means that when you work across multiple projects which have .envrc files, all processes launched from the buffers "in" those projects will be executed with the environment variables specified in those files. This allows different versions of linters and other tools to be used in each project if desired.

How does this differ from direnv.el?

direnv.el repeatedly changes the global Emacs environment, based on tracking what buffer you're working on.

Instead, envrc.el simply sets and stores the right environment in each buffer, as a buffer-local variable.

From a user perspective, both are well tested and typically work fine, but the envrc.el approach feels cleaner to me.

Installation

Installable packages are available via MELPA: do M-x package-install RET envrc RET.

Alternatively, download the latest release or clone the repository, and install envrc.el with M-x package-install-file.

Usage

Add a snippet like the following at the bottom of your init.el:

(envrc-global-mode)

or

(add-hook 'after-init-hook 'envrc-global-mode)

or, if you're a use-package fan:

(use-package envrc
  :hook (after-init . envrc-global-mode))

Why must you enable the global mode late in your startup sequence like this? You normally want envrc-mode to be initialized in each buffer before other minor modes like flycheck-mode which might look for executables. Counter-intuitively, this means that envrc-global-mode should be enabled after other global minor modes, since each prepends itself to various hooks.

The global mode will only have an effect if direnv is installed and available in the default Emacs exec-path. (There is a local minor mode envrc-mode, but you should not try to enable this granularly, e.g. for certain modes or projects, because compilation and other buffers might not get set up with the right environment.)

Regarding interaction with the mode, see envrc-mode-map, and the commands envrc-reload, envrc-allow and envrc-deny. (There's also envrc-reload-all as a "nuclear" reset, for now!)

In particular, you can enable keybindings for the above commands by binding your preferred prefix to envrc-command-map in envrc-mode-map, e.g.

(with-eval-after-load 'envrc
  (define-key envrc-mode-map (kbd "C-c e") 'envrc-command-map))

Troubleshooting

If you find that a particular Emacs command isn't picking up the environment of your current buffer, and you're sure that envrc-mode is active in that buffer, then it's possible you've found code that runs a process in a temp buffer and neglects to propagate your environment to that buffer before doing so.

A couple of common Emacs commands that suffer from this defect are also patched directly via advice in envrc.elshell-command-to-string is a prominent example!

The inheritenv package was designed to handle this case in general.

Design notes

By default, Emacs has a single global set of environment variables used for all subprocesses, stored in the process-environment variable. direnv.el switches that global environment using values from direnv when the user performs certain actions, such as switching between buffers in different projects.

In practice, this is simple and mostly works very well. But there are some quirks, and it feels wrong to me to mutate the global environment in order to support per-directory environments.

Now, in Emacs we can also set process-environment locally in a buffer. If this value could be correctly maintained in all buffers based on their various respective .envrc files, then buffers across multiple projects could simultaneously be "connected" to the environments of their corresponding project directories. I wrote envrc.el to explore this approach.

envrc.el uses a global minor mode (envrc-global-mode) to hook into practically every buffer created by Emacs, including hidden and temporary ones. When a buffer is found to be "inside" an .envrc-managed project, process-environment is set buffer-locally by running direnv, the results of which are also cached indefinitely so that this is not too costly overall. Each buffer has a local minor mode (envrc-mode) with an indicator which displays whether or not a direnv is in effect in that buffer. (Hooking into every buffer is important, rather than just those with certain major modes, since separate temporary, compilation and repl buffers are routinely used for executing processes.)

This approach also has some trade-offs:

  • Buffers like *Help* will have envrc-mode enabled based on the directory of the buffer which caused them to be created initially, and then those buffers often live for a long time. If you launch programs from such buffers while working on a different project, the results might not be what you expect. I might exclude certain modes to minimise confusion, but users will always have to be aware of the fact that environments are buffer-specific.

  • There's a (very small) overhead every time a buffer is created, and that happens quite a lot.

  • direnv updates are not automatic. direnv.el re-executes direnv when switching between buffers that visit files in different directories, whereas envrc-mode caches the environment until the user refreshes it explicitly with envrc-reload.

Overall this approach works well in practice, and feels cleaner than trying to strategically modify the global environment.

It's also possible that there's a way to call direnv more aggressively by allowing it to see values of DIRENV_* obtained previously such that it becomes a no-op.


💝 Support this project and my other Open Source work via Patreon

💼 LinkedIn profile

✍ sanityinc.com

🐦 @sanityinc

envrc's People

Contributors

aaronjensen avatar accelbread avatar damiencassou avatar dependabot[bot] avatar grimpper avatar hpfr avatar ipvych avatar jellelicht avatar leungbk avatar mjoerg avatar picnoir avatar purcell avatar sellout avatar swflint avatar wbolster avatar whxvd avatar wyuenho avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

envrc's Issues

Dependency on direnv

Is the dependency on the direnv program really necessary? This dependency is not a big drawback, but I guess it is a small drawback...

Running direnv and parsing its output isn't any easier than sourcing the .envrc file in a shell and then parsing the output of env -0. And direnv's whitelisting mechanism is probably not documented, but it's not hard to reverse engineer either (alternatively, a similar mechanism to safe-local-variable-values could be used).

The more complicated part of direnv, which is to undo changes when leaving a directory, is irrelevant to this package.

Default environment when launch outside shell

Note really an issue.

I like the default Emacs environment way as stated in the Design Notes.

I use asdf-direnv and envrc for managing my environment. I have a default .envrc in my $HOME to manage the global tool versions. When I start Emacs in non terminal way (double click the App in macOS), that global .envrc is not being taken care by envrc (of course). I'd like to achieve that when I start Emacs outside a shell, it takes care of that default global environment in $HOME. Maybe I can solve this by using exec-path-from-shell, is there an envrc way to solve this (=select a default environment for Emacs when starting in GUI) ? Thanks!

I also tried these two ways:

  1. Run /usr/local/opt/emacs-mac/Emacs.app/Contents/MacOS/Emacs to start the GUI Emacs in my terminal, this way works the same as running a non GUI version of Emacs. envrc works perfectly but I have to keep a foreground shell command.
  2. Run /usr/local/opt/emacs-mac/Emacs.app/Contents/MacOS/Emacs & in my terminal, this way has an issue that the background process is run in a subshell by bash and my .profile is sourced again. Since my asdf and direnv setup is put in .bashrc (which is not sourced when the background process is launched), lots of PATH will be inserted before the PATH of parent shell and the default Emacs environment will get duplicate and unexpected PATH.

Add envrc-mode-hook

Hi,

first of all thanks for this package, it addresses exactly what I think was missing from direnv.el.

Do you think it would be possible to add a envrc-mode-hook to set up other per-user stuff after the mode is enabled? More importantly, do you think it would be something useful?

I have a font-lock related function for .envrc files that I'd like to run after envrc-global-mode. Of course I can wrap the call to envrc-global-mode in a defun where I call my font-lock utility as well, and use that.

So, basically, it's your call. :)

Feature Request in the README

This library is like the direnv.el package, but sets all environment variables buffer-locally, while direnv.el changes the global set of environment variables after each command.

Would it be possible to give a quick TLDR/nod at why this fact is important? It would be helpful to quickly understand how that is important without having to dig deep into other packages to put two in two together/cross compare them. This would be especially helpful for newcomers/non-emacs afficionados trying to make a decision about whether to use this over another package or not 🥲

Question about disabling .envrc loading on consult previews

I am using consult for preview when switching files in the minibuffer. The issue is that when I hit a file that is from not loaded .envrc environment, the environment gets loaded. This produces a delay in browsing the entries. I would like to somehow disable envrc when using this preview.

I am not sure if there is any support needed on either envrc or consult side, or not. I am using envrc-global-mode. I was thinking that easiest solution for me could be to just disable envrc-global-mode when using minibuffer, and reenable it afterwards. However, I was wondering if there could be a better solution than this one, provided by envrc logic in envrc-global-mode?

I noticed the current code of envrc checks for being in minibuffer, however, since the preview itself is not in the minibuffer, it's not hit.

Upload to NonGNU ELPA

envrc is the last package I use which is not in NonGNU ELPA (and therefore installable in Emacs without any configuration). It would be nice if it was uploaded.

Actually, uploading to GNU ELPA or even bundling with Emacs itself would be great. I think this would be an excellent addition to core Emacs, especially with the project.el support making projects much nicer.

project-compile not applying env when switching projects

Hello,

I am currently experiencing an issue with project-compile. I usually switch to a project by call project-compile directly.
This is my configuration:

(require 'envrc)
(setq envrc-debug t)
(envrc-global-mode)

em -Q --load config.el

When I call M-x project-compile and select another project that has a .envrc file, the environment does not get applied.

I have tried to debug the issue, but was unable to identify the root cause.

Thank you.

When started from the systemd unit, process-environment defaults to buffer local.

I hit a bug in emacs shell command processing: https://debbugs.gnu.org/cgi/bugreport.cgi?bug=52178 and it seems it's partly due to envrc (or inheritenv) in some specific conditions. When using the emacs provided systemd unit file to start an emacs daemon and calling envrc-global-mode, the process-environment variable defaults to buffer local while when run in a normal emacs instance or using --fg-daemon from a terminal, its only buffer-local for the envrc managed buffer, but still global for the rest.

I reproduced this in a clean emacs (on master) with the following init.el and evaluating (local-variable-p 'process-environment) to check the variable status.

;;; inheritenv.el --- Make temp buffers inherit buffer-local environment variables  -*- lexical-binding: t; -*-

;; Copyright (C) 2021  Steve Purcell

;; Author: Steve Purcell <[email protected]>
;; URL: https://github.com/purcell/inheritenv
;; Package-Requires: ((emacs "24.4"))
;; Version: 0.1-pre
;; Keywords: unix

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Environment variables in Emacs can be set buffer-locally, like many
;; Emacs preferences, which allows users to have different buffer-local
;; paths for executables in different projects, specified by a
;; ".dir-locals.el" file or via a "direnv" integration like
;; envrc (see https://github.com/purcell/envrc).

;; However, there's a fairly common pitfall when Emacs libraries run
;; background processes on behalf of a user: many such libraries run
;; processes in temporary buffers that do not inherit the calling
;; buffer's environment.  This can result in executables not being found,
;; or the wrong versions of executables being picked up.

;; An example is the Emacs built-in command
;; `shell-command-to-string'.  Whatever buffer-local `process-environment'
;; (or `exec-path') the user has set, that command will always use the
;; Emacs-wide default.  This is *specified* behaviour, but not *expected*
;; or *helpful*.

;; `inheritenv' provides a couple of tools for dealing with this
;; issue:

;; 1. Library authors can wrap code that plans to execute processes in
;;    temporary buffers with the `inheritenv' macro.
;; 2. End users can modify commands like `shell-command-to-string' using
;;    the `inheritenv-add-advice' macro.

;;; Code:

(require 'cl-lib)

;;;###autoload
(defun inheritenv-apply (func &rest args)
  "Apply FUNC such that the environment it sees will match the current value.
This is useful if FUNC creates a temp buffer, because that will
not inherit any buffer-local values of variables `exec-path' and
`process-environment'.
This function is designed for convenient use as an \"around\" advice.
ARGS is as for ORIG."
  (cl-letf* (((default-value 'process-environment) process-environment)
             ((default-value 'exec-path) exec-path))
    (apply func args)))


(defmacro inheritenv (&rest body)
  "Wrap BODY so that the environment it sees will match the current value.
This is useful if BODY creates a temp buffer, because that will
not inherit any buffer-local values of variables `exec-path' and
`process-environment'."
  `(inheritenv-apply (lambda () ,@body)))


(defmacro inheritenv-add-advice (func)
  "Advise function FUNC with `inheritenv-apply'.
This will ensure that any buffers (including temporary buffers)
created by FUNC will inherit the caller's environment."
  `(advice-add ,func :around 'inheritenv-apply))


(provide 'inheritenv)
;;; envrc.el --- Support for `direnv' that operates buffer-locally  -*- lexical-binding: t; -*-

;; Copyright (C) 2020  Steve Purcell

;; Author: Steve Purcell <[email protected]>
;; Keywords: processes, tools
;; Homepage: https://github.com/purcell/envrc
;; Package-Requires: ((seq "2") (emacs "24.4") (inheritenv "0.1"))
;; Package-Version: 0

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Use direnv (https://direnv.net/) to set environment variables on a
;; per-buffer basis.  This means that when you work across multiple
;; projects which have `.envrc` files, all processes launched from the
;; buffers "in" those projects will be executed with the environment
;; variables specified in those files.  This allows different versions
;; of linters and other tools to be installed in each project if
;; desired.

;; Enable `envrc-global-mode' late in your startup files.  For
;; interaction with this functionality, see `envrc-mode-map', and the
;; commands `envrc-reload', `envrc-allow' and `envrc-deny'.

;; In particular, you can enable keybindings for the above commands by
;; binding your preferred prefix to `envrc-command-map' in
;; `envrc-mode-map', e.g.

;;    (with-eval-after-load 'envrc
;;      (define-key envrc-mode-map (kbd "C-c e") 'envrc-command-map))

;;; Code:

;; TODO: special handling for DIRENV_* vars? exclude them? use them to safely reload more aggressively?
;; TODO: special handling for remote files
;; TODO: handle nil default-directory (rarely happens, but is possible)
;; TODO: limit size of *direnv* buffer
;; TODO: special handling of compilation-environment?
;; TODO: handle use of "cd" and other changes of `default-directory' in a buffer over time?
;; TODO: handle "allow" asynchronously?
;; TODO: describe env
;; TODO: click on mode lighter to get details
;; TODO: handle when direnv is not installed?
;; TODO: provide a way to disable in certain projects?
;; TODO: cleanup the cache?

(require 'seq)
(require 'json)
(require 'subr-x)
(require 'ansi-color)
(require 'cl-lib)
(require 'inheritenv)

;;; Custom vars and minor modes

(defgroup envrc nil
  "Apply per-buffer environment variables using the direnv tool."
  :group 'processes)

(defcustom envrc-debug nil
  "Whether or not to output debug messages while in operation.
Messages are written into the *envrc-debug* buffer."
  :type 'boolean)

(define-obsolete-variable-alias 'envrc--lighter 'envrc-lighter "2021-05-17")

(defcustom envrc-lighter '(:eval (envrc--lighter))
  "The mode line lighter for `envrc-mode'.
You can set this to nil to disable the lighter."
  :type 'sexp)
(put 'envrc-lighter 'risky-local-variable t)

(defcustom envrc-none-lighter '(" envrc[" (:propertize "none" face envrc-mode-line-none-face) "]")
  "Lighter spec used by the default `envrc-lighter' when envrc is inactive."
  :type 'sexp)

(defcustom envrc-on-lighter '(" envrc[" (:propertize "on" face envrc-mode-line-on-face) "]")
  "Lighter spec used by the default `envrc-lighter' when envrc is on."
  :type 'sexp)

(defcustom envrc-error-lighter '(" envrc[" (:propertize "error" face envrc-mode-line-error-face) "]")
  "Lighter spec used by the default `envrc-lighter' when envrc has errored."
  :type 'sexp)

(defcustom envrc-command-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "a") 'envrc-allow)
    (define-key map (kbd "d") 'envrc-deny)
    (define-key map (kbd "r") 'envrc-reload)
    map)
  "Keymap for commands in `envrc-mode'.
See `envrc-mode-map' for how to assign a prefix binding to these."
  :type 'keymap)
(fset 'envrc-command-map envrc-command-map)

(defcustom envrc-mode-map (make-sparse-keymap)
  "Keymap for `envrc-mode'.
To access `envrc-command-map' from this map, give it a prefix keybinding,
e.g. (define-key envrc-mode-map (kbd \"C-c e\") 'envrc-command-map)"
  :type 'keymap)

;;;###autoload
(define-minor-mode envrc-mode
  "A local minor mode in which env vars are set by direnv."
  :init-value nil
  :lighter envrc-lighter
  :keymap envrc-mode-map
  (if envrc-mode
      (envrc--update)
    (envrc--clear (current-buffer))))

;;;###autoload
(define-globalized-minor-mode envrc-global-mode envrc-mode
  (lambda () (unless (or (minibufferp) (file-remote-p default-directory))
               (envrc-mode 1))))

(defface envrc-mode-line-on-face '((t :inherit success))
  "Face used in mode line to indicate that direnv is in effect.")

(defface envrc-mode-line-error-face '((t :inherit error))
  "Face used in mode line to indicate that direnv failed.")

(defface envrc-mode-line-none-face '((t :inherit warning))
  "Face used in mode line to indicate that direnv is not active.")

;;; Global state

(defvar envrc--cache (make-hash-table :test 'equal :size 10)
  "Known envrc directories and their direnv results, as produced by `envrc--export'.")

;;; Local state

(defvar-local envrc--status 'none
  "Symbol indicating state of the current buffer's direnv.
One of '(none on error).")

;;; Internals

(defun envrc--lighter ()
  "Return a colourised version of `envrc--status' for use in the mode line."
  (pcase envrc--status
    (`on envrc-on-lighter)
    (`error envrc-error-lighter)
    (`none envrc-none-lighter)))

(defun envrc--find-env-dir ()
  "Return the envrc directory for the current buffer, if any.
This is based on a file scan.  In most cases we prefer to use the
cached list of known directories.
Regardless of buffer file name, we always use
`default-directory': the two should always match, unless the user
called `cd'"
  (let ((env-dir (locate-dominating-file default-directory ".envrc")))
    (when env-dir
      ;; `locate-dominating-file' appears to sometimes return abbreviated paths, e.g. with ~
      (setq env-dir (expand-file-name env-dir)))
    env-dir))

(defun envrc--cache-key (env-dir process-env)
  "Get a hash key for the result of invoking direnv in ENV-DIR with PROCESS-ENV."
  (mapconcat 'identity (cons env-dir process-env) "\0"))

(defun envrc--update ()
  "Update the current buffer's environment if it is managed by direnv.
All envrc.el-managed buffers with this env will have their
environments updated."
  (let ((env-dir (envrc--find-env-dir)))
    (if env-dir
        (let* ((cache-key (envrc--cache-key env-dir process-environment))
               (result (pcase (gethash cache-key envrc--cache 'missing)
                         (`missing (let ((calculated (envrc--export env-dir)))
                                     (puthash cache-key calculated envrc--cache)
                                     calculated))
                         (cached cached))))
          (envrc--apply (current-buffer) result)
          ;; We assume direnv and envrc's use of it is idempotent, and
          ;; add a cache entry for the new process-environment on that
          ;; basis.
          (puthash (envrc--cache-key env-dir process-environment) result envrc--cache))
      (envrc--apply (current-buffer) 'none))))

(defmacro envrc--at-end-of-special-buffer (name &rest body)
  "At the end of `special-mode' buffer NAME, execute BODY.
To avoid confusion, `envrc-mode' is explicitly disabled in the buffer."
  (declare (indent 1))
  `(with-current-buffer (get-buffer-create ,name)
     (unless (derived-mode-p 'special-mode)
       (special-mode))
     (when envrc-mode (envrc-mode -1))
     (goto-char (point-max))
     (let ((inhibit-read-only t))
       ,@body)))

(defun envrc--debug (msg &rest args)
  "A version of `message' which does nothing if `envrc-debug' is nil.
MSG and ARGS are as for that function."
  (when envrc-debug
    (envrc--at-end-of-special-buffer "*envrc-debug*"
      (insert (apply 'format msg args))
      (newline))))

(defun envrc--directory-path-deeper-p (a b)
  "Return non-nil if directory path B is deeper than directory path A."
  (string-prefix-p (file-name-as-directory a) (file-name-as-directory b)))

(defun envrc--deepest-paths-first (paths)
  "Sort PATHS such that the deepest paths in a hierarchy appear first."
  (sort paths
        (lambda (a b) (or (envrc--directory-path-deeper-p b a)
                          (string< a b)))))

(defun envrc--export (env-dir)
  "Export the env vars for ENV-DIR using direnv.
Return value is either 'error, 'none, or an alist of environment
variable names and values."
  (unless (file-exists-p (expand-file-name ".envrc" env-dir))
    (error "%s is not a directory with a .envrc" env-dir))
  (message "Running direnv in %s..." env-dir)
  (let ((stderr-file (make-temp-file "envrc"))
        result)
    (unwind-protect
        (let ((default-directory env-dir))
          (with-temp-buffer
            (let ((exit-code (envrc--call-process-with-default-exec-path "direnv" nil (list t stderr-file) nil "export" "json")))
              (envrc--debug "Direnv exited with %s and output: %S" exit-code (buffer-string))
              (if (zerop exit-code)
                  (progn
                    (message "Direnv succeeded in %s" env-dir)
                    (if (zerop (buffer-size))
                        (setq result 'none)
                      (goto-char (point-min))
                      (setq result (let ((json-key-type 'string)) (json-read-object)))))
                (message "Direnv failed in %s" env-dir)
                (setq result 'error))
              (envrc--at-end-of-special-buffer "*envrc*"
                (insert "==== " (format-time-string "%Y-%m-%d %H:%M:%S") " ==== " env-dir " ====\n\n")
                (let ((initial-pos (point)))
                  (insert-file-contents (let (ansi-color-context)
                                          (ansi-color-apply stderr-file)))
                  (goto-char (point-max))
                  (add-face-text-property initial-pos (point) (if (zerop exit-code) 'success 'error)))
                (insert "\n\n")
                (unless (zerop exit-code)
                  (display-buffer (current-buffer)))))))
      (delete-file stderr-file))
    result))

;; Forward declaration for the byte compiler
(defvar eshell-path-env)

(defun envrc--merged-environment (process-env pairs)
  "Make a `process-environment' value that merges PROCESS-ENV with PAIRS.
PAIRS is an alist obtained from direnv's output.
Values from PROCESS-ENV will be included, but their values will
be masked by Emacs' handling of `process-environment' if they
also appear in PAIRS."
  (append (mapcar (lambda (pair)
                    (if (cdr pair)
                        (format "%s=%s" (car pair) (cdr pair))
                      ;; Plain env name is the syntax for unsetting vars
                      (car pair)))
                  pairs)
          process-env))

(defun envrc--clear (buf)
  "Remove any effects of `envrc-mode' from BUF."
  (with-current-buffer buf
    (kill-local-variable 'exec-path)
    (kill-local-variable 'process-environment)
    (kill-local-variable 'eshell-path-env)))


(defun envrc--apply (buf result)
  "Update BUF with RESULT, which is a result of `envrc--export'."
  (with-current-buffer buf
    (setq-local envrc--status (if (listp result) 'on result))
    (if (memq result '(none error))
        (progn
          (envrc--clear buf)
          (envrc--debug "[%s] reset environment to default" buf))
      (envrc--debug "[%s] applied merged environment" buf)
      (setq-local process-environment (envrc--merged-environment process-environment result))
      (let ((path (getenv "PATH"))) ;; Get PATH from the merged environment: direnv may not have changed it
        (setq-local exec-path (parse-colon-path path))
        (when (derived-mode-p 'eshell-mode)
          (setq-local eshell-path-env path))))))

(defun envrc--update-env (env-dir)
  "Refresh the state of the direnv in ENV-DIR and apply in all relevant buffers."
  (envrc--debug "Invalidating cache for env %s" env-dir)
  (cl-loop for k being the hash-keys of envrc--cache
           if (string-prefix-p (concat env-dir "\0") k)
           do (remhash k envrc--cache))
  (envrc--debug "Refreshing all buffers in env  %s" env-dir)
  (dolist (buf (envrc--mode-buffers))
    (with-current-buffer buf
      (when (string= (envrc--find-env-dir) env-dir)
        (envrc--update)))))

(defun envrc--mode-buffers ()
  "Return a list of all live buffers in which `envrc-mode' is enabled."
  (seq-filter (lambda (b) (and (buffer-live-p b)
                               (with-current-buffer b
                                 envrc-mode)))
              (buffer-list)))

(defmacro envrc--with-required-current-env (varname &rest body)
  "With VARNAME set to the current env dir path, execute BODY.
If there is no current env dir, abort with a user error."
  (declare (indent 1))
  (cl-assert (symbolp varname))
  `(let ((,varname (envrc--find-env-dir)))
     (unless ,varname
       (user-error "No enclosing .envrc"))
     ,@body))

(defun envrc--call-process-with-default-exec-path (&rest args)
  "Like `call-process', but ensures the default variable `exec-path' is used.
This ensures the globally-accessible \"direnv\" binary is
consistently available.  ARGS is as for `call-process'."
  (let ((exec-path (default-value 'exec-path)))
    (apply 'call-process args)))

(defun envrc-reload ()
  "Reload the current env."
  (interactive)
  (envrc--with-required-current-env env-dir
    (envrc--update-env env-dir)))

(defun envrc-allow ()
  "Run \"direnv allow\" in the current env."
  (interactive)
  (envrc--with-required-current-env env-dir
    (let* ((default-directory env-dir)
           (exit-code (envrc--call-process-with-default-exec-path "direnv" nil (get-buffer-create "*envrc-allow*") nil "allow")))
      (if (zerop exit-code)
          (envrc--update-env env-dir)
        (display-buffer "*envrc-allow*")))))

(defun envrc-deny ()
  "Run \"direnv deny\" in the current env."
  (interactive)
  (envrc--with-required-current-env env-dir
    (let* ((default-directory env-dir)
           (exit-code (envrc--call-process-with-default-exec-path "direnv" nil (get-buffer-create "*envrc-deny*") nil "deny")))
      (if (zerop exit-code)
          (envrc--update-env env-dir)
        (display-buffer "*envrc-deny*")))))

(defun envrc-reload-all ()
  "Reload direnvs for all buffers.
This can be useful if a .envrc has been deleted."
  (interactive)
  (envrc--debug "Invalidating cache for all envs")
  (clrhash envrc--cache)
  (dolist (buf (envrc--mode-buffers))
    (with-current-buffer buf
      (envrc--update))))



;;; Propagate local environment to commands that use temp buffers

(defun envrc-propagate-environment (orig &rest args)
  "Advice function to wrap a command ORIG and make it use our local env.
This can be used to force compliance where ORIG starts processes
in a temp buffer.  ARGS is as for ORIG."
  (if envrc-mode
      (inheritenv (apply orig args))
    (apply orig args)))

(advice-add 'shell-command-to-string :around #'envrc-propagate-environment)
(advice-add 'org-babel-eval :around #'envrc-propagate-environment)


;;; Major mode for .envrc files

(defvar envrc-file-extra-keywords
  '("MANPATH_add" "PATH_add" "direnv_layout_dir" "direnv_load" "dotenv"
    "expand_path" "find_up" "has" "join_args" "layout" "load_prefix"
    "log_error" "log_status" "path_add" "rvm" "source_env" "source_up"
    "use" "use_guix" "use_nix" "user_rel_path" "watch_file")
  "Useful direnv keywords to be highlighted.")

;;;###autoload
(define-derived-mode envrc-file-mode
  sh-mode "envrc"
  "Major mode for .envrc files as used by direnv.
\\{envrc-file-mode-map}"
  (font-lock-add-keywords
   nil `((,(regexp-opt envrc-file-extra-keywords 'symbols)
          (0 font-lock-keyword-face)))))

;;;###autoload
(add-to-list 'auto-mode-alist '("\\.envrc\\'" . envrc-file-mode))

(provide 'envrc)
;;; envrc.el ends here
;;; inheritenv.el --- Make temp buffers inherit buffer-local environment variables  -*- lexical-binding: t; -*-

;; Copyright (C) 2021  Steve Purcell

;; Author: Steve Purcell <[email protected]>
;; URL: https://github.com/purcell/inheritenv
;; Package-Requires: ((emacs "24.4"))
;; Version: 0.1-pre
;; Keywords: unix

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Environment variables in Emacs can be set buffer-locally, like many
;; Emacs preferences, which allows users to have different buffer-local
;; paths for executables in different projects, specified by a
;; ".dir-locals.el" file or via a "direnv" integration like
;; envrc (see https://github.com/purcell/envrc).

;; However, there's a fairly common pitfall when Emacs libraries run
;; background processes on behalf of a user: many such libraries run
;; processes in temporary buffers that do not inherit the calling
;; buffer's environment.  This can result in executables not being found,
;; or the wrong versions of executables being picked up.

;; An example is the Emacs built-in command
;; `shell-command-to-string'.  Whatever buffer-local `process-environment'
;; (or `exec-path') the user has set, that command will always use the
;; Emacs-wide default.  This is *specified* behaviour, but not *expected*
;; or *helpful*.

;; `inheritenv' provides a couple of tools for dealing with this
;; issue:

;; 1. Library authors can wrap code that plans to execute processes in
;;    temporary buffers with the `inheritenv' macro.
;; 2. End users can modify commands like `shell-command-to-string' using
;;    the `inheritenv-add-advice' macro.

;;; Code:

(require 'cl-lib)

;;;###autoload
(defun inheritenv-apply (func &rest args)
  "Apply FUNC such that the environment it sees will match the current value.
This is useful if FUNC creates a temp buffer, because that will
not inherit any buffer-local values of variables `exec-path' and
`process-environment'.
This function is designed for convenient use as an \"around\" advice.
ARGS is as for ORIG."
  (cl-letf* (((default-value 'process-environment) process-environment)
             ((default-value 'exec-path) exec-path))
    (apply func args)))


(defmacro inheritenv (&rest body)
  "Wrap BODY so that the environment it sees will match the current value.
This is useful if BODY creates a temp buffer, because that will
not inherit any buffer-local values of variables `exec-path' and
`process-environment'."
  `(inheritenv-apply (lambda () ,@body)))


(defmacro inheritenv-add-advice (func)
  "Advise function FUNC with `inheritenv-apply'.
This will ensure that any buffers (including temporary buffers)
created by FUNC will inherit the caller's environment."
  `(advice-add ,func :around 'inheritenv-apply))


(provide 'inheritenv)
;;; envrc.el --- Support for `direnv' that operates buffer-locally  -*- lexical-binding: t; -*-

;; Copyright (C) 2020  Steve Purcell

;; Author: Steve Purcell <[email protected]>
;; Keywords: processes, tools
;; Homepage: https://github.com/purcell/envrc
;; Package-Requires: ((seq "2") (emacs "24.4") (inheritenv "0.1"))
;; Package-Version: 0

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Use direnv (https://direnv.net/) to set environment variables on a
;; per-buffer basis.  This means that when you work across multiple
;; projects which have `.envrc` files, all processes launched from the
;; buffers "in" those projects will be executed with the environment
;; variables specified in those files.  This allows different versions
;; of linters and other tools to be installed in each project if
;; desired.

;; Enable `envrc-global-mode' late in your startup files.  For
;; interaction with this functionality, see `envrc-mode-map', and the
;; commands `envrc-reload', `envrc-allow' and `envrc-deny'.

;; In particular, you can enable keybindings for the above commands by
;; binding your preferred prefix to `envrc-command-map' in
;; `envrc-mode-map', e.g.

;;    (with-eval-after-load 'envrc
;;      (define-key envrc-mode-map (kbd "C-c e") 'envrc-command-map))

;;; Code:

;; TODO: special handling for DIRENV_* vars? exclude them? use them to safely reload more aggressively?
;; TODO: special handling for remote files
;; TODO: handle nil default-directory (rarely happens, but is possible)
;; TODO: limit size of *direnv* buffer
;; TODO: special handling of compilation-environment?
;; TODO: handle use of "cd" and other changes of `default-directory' in a buffer over time?
;; TODO: handle "allow" asynchronously?
;; TODO: describe env
;; TODO: click on mode lighter to get details
;; TODO: handle when direnv is not installed?
;; TODO: provide a way to disable in certain projects?
;; TODO: cleanup the cache?

(require 'seq)
(require 'json)
(require 'subr-x)
(require 'ansi-color)
(require 'cl-lib)
(require 'inheritenv)

;;; Custom vars and minor modes

(defgroup envrc nil
  "Apply per-buffer environment variables using the direnv tool."
  :group 'processes)

(defcustom envrc-debug nil
  "Whether or not to output debug messages while in operation.
Messages are written into the *envrc-debug* buffer."
  :type 'boolean)

(define-obsolete-variable-alias 'envrc--lighter 'envrc-lighter "2021-05-17")

(defcustom envrc-lighter '(:eval (envrc--lighter))
  "The mode line lighter for `envrc-mode'.
You can set this to nil to disable the lighter."
  :type 'sexp)
(put 'envrc-lighter 'risky-local-variable t)

(defcustom envrc-none-lighter '(" envrc[" (:propertize "none" face envrc-mode-line-none-face) "]")
  "Lighter spec used by the default `envrc-lighter' when envrc is inactive."
  :type 'sexp)

(defcustom envrc-on-lighter '(" envrc[" (:propertize "on" face envrc-mode-line-on-face) "]")
  "Lighter spec used by the default `envrc-lighter' when envrc is on."
  :type 'sexp)

(defcustom envrc-error-lighter '(" envrc[" (:propertize "error" face envrc-mode-line-error-face) "]")
  "Lighter spec used by the default `envrc-lighter' when envrc has errored."
  :type 'sexp)

(defcustom envrc-command-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "a") 'envrc-allow)
    (define-key map (kbd "d") 'envrc-deny)
    (define-key map (kbd "r") 'envrc-reload)
    map)
  "Keymap for commands in `envrc-mode'.
See `envrc-mode-map' for how to assign a prefix binding to these."
  :type 'keymap)
(fset 'envrc-command-map envrc-command-map)

(defcustom envrc-mode-map (make-sparse-keymap)
  "Keymap for `envrc-mode'.
To access `envrc-command-map' from this map, give it a prefix keybinding,
e.g. (define-key envrc-mode-map (kbd \"C-c e\") 'envrc-command-map)"
  :type 'keymap)

;;;###autoload
(define-minor-mode envrc-mode
  "A local minor mode in which env vars are set by direnv."
  :init-value nil
  :lighter envrc-lighter
  :keymap envrc-mode-map
  (if envrc-mode
      (envrc--update)
    (envrc--clear (current-buffer))))

;;;###autoload
(define-globalized-minor-mode envrc-global-mode envrc-mode
  (lambda () (unless (or (minibufferp) (file-remote-p default-directory))
               (envrc-mode 1))))

(defface envrc-mode-line-on-face '((t :inherit success))
  "Face used in mode line to indicate that direnv is in effect.")

(defface envrc-mode-line-error-face '((t :inherit error))
  "Face used in mode line to indicate that direnv failed.")

(defface envrc-mode-line-none-face '((t :inherit warning))
  "Face used in mode line to indicate that direnv is not active.")

;;; Global state

(defvar envrc--cache (make-hash-table :test 'equal :size 10)
  "Known envrc directories and their direnv results, as produced by `envrc--export'.")

;;; Local state

(defvar-local envrc--status 'none
  "Symbol indicating state of the current buffer's direnv.
One of '(none on error).")

;;; Internals

(defun envrc--lighter ()
  "Return a colourised version of `envrc--status' for use in the mode line."
  (pcase envrc--status
    (`on envrc-on-lighter)
    (`error envrc-error-lighter)
    (`none envrc-none-lighter)))

(defun envrc--find-env-dir ()
  "Return the envrc directory for the current buffer, if any.
This is based on a file scan.  In most cases we prefer to use the
cached list of known directories.
Regardless of buffer file name, we always use
`default-directory': the two should always match, unless the user
called `cd'"
  (let ((env-dir (locate-dominating-file default-directory ".envrc")))
    (when env-dir
      ;; `locate-dominating-file' appears to sometimes return abbreviated paths, e.g. with ~
      (setq env-dir (expand-file-name env-dir)))
    env-dir))

(defun envrc--cache-key (env-dir process-env)
  "Get a hash key for the result of invoking direnv in ENV-DIR with PROCESS-ENV."
  (mapconcat 'identity (cons env-dir process-env) "\0"))

(defun envrc--update ()
  "Update the current buffer's environment if it is managed by direnv.
All envrc.el-managed buffers with this env will have their
environments updated."
  (let ((env-dir (envrc--find-env-dir)))
    (if env-dir
        (let* ((cache-key (envrc--cache-key env-dir process-environment))
               (result (pcase (gethash cache-key envrc--cache 'missing)
                         (`missing (let ((calculated (envrc--export env-dir)))
                                     (puthash cache-key calculated envrc--cache)
                                     calculated))
                         (cached cached))))
          (envrc--apply (current-buffer) result)
          ;; We assume direnv and envrc's use of it is idempotent, and
          ;; add a cache entry for the new process-environment on that
          ;; basis.
          (puthash (envrc--cache-key env-dir process-environment) result envrc--cache))
      (envrc--apply (current-buffer) 'none))))

(defmacro envrc--at-end-of-special-buffer (name &rest body)
  "At the end of `special-mode' buffer NAME, execute BODY.
To avoid confusion, `envrc-mode' is explicitly disabled in the buffer."
  (declare (indent 1))
  `(with-current-buffer (get-buffer-create ,name)
     (unless (derived-mode-p 'special-mode)
       (special-mode))
     (when envrc-mode (envrc-mode -1))
     (goto-char (point-max))
     (let ((inhibit-read-only t))
       ,@body)))

(defun envrc--debug (msg &rest args)
  "A version of `message' which does nothing if `envrc-debug' is nil.
MSG and ARGS are as for that function."
  (when envrc-debug
    (envrc--at-end-of-special-buffer "*envrc-debug*"
      (insert (apply 'format msg args))
      (newline))))

(defun envrc--directory-path-deeper-p (a b)
  "Return non-nil if directory path B is deeper than directory path A."
  (string-prefix-p (file-name-as-directory a) (file-name-as-directory b)))

(defun envrc--deepest-paths-first (paths)
  "Sort PATHS such that the deepest paths in a hierarchy appear first."
  (sort paths
        (lambda (a b) (or (envrc--directory-path-deeper-p b a)
                          (string< a b)))))

(defun envrc--export (env-dir)
  "Export the env vars for ENV-DIR using direnv.
Return value is either 'error, 'none, or an alist of environment
variable names and values."
  (unless (file-exists-p (expand-file-name ".envrc" env-dir))
    (error "%s is not a directory with a .envrc" env-dir))
  (message "Running direnv in %s..." env-dir)
  (let ((stderr-file (make-temp-file "envrc"))
        result)
    (unwind-protect
        (let ((default-directory env-dir))
          (with-temp-buffer
            (let ((exit-code (envrc--call-process-with-default-exec-path "direnv" nil (list t stderr-file) nil "export" "json")))
              (envrc--debug "Direnv exited with %s and output: %S" exit-code (buffer-string))
              (if (zerop exit-code)
                  (progn
                    (message "Direnv succeeded in %s" env-dir)
                    (if (zerop (buffer-size))
                        (setq result 'none)
                      (goto-char (point-min))
                      (setq result (let ((json-key-type 'string)) (json-read-object)))))
                (message "Direnv failed in %s" env-dir)
                (setq result 'error))
              (envrc--at-end-of-special-buffer "*envrc*"
                (insert "==== " (format-time-string "%Y-%m-%d %H:%M:%S") " ==== " env-dir " ====\n\n")
                (let ((initial-pos (point)))
                  (insert-file-contents (let (ansi-color-context)
                                          (ansi-color-apply stderr-file)))
                  (goto-char (point-max))
                  (add-face-text-property initial-pos (point) (if (zerop exit-code) 'success 'error)))
                (insert "\n\n")
                (unless (zerop exit-code)
                  (display-buffer (current-buffer)))))))
      (delete-file stderr-file))
    result))

;; Forward declaration for the byte compiler
(defvar eshell-path-env)

(defun envrc--merged-environment (process-env pairs)
  "Make a `process-environment' value that merges PROCESS-ENV with PAIRS.
PAIRS is an alist obtained from direnv's output.
Values from PROCESS-ENV will be included, but their values will
be masked by Emacs' handling of `process-environment' if they
also appear in PAIRS."
  (append (mapcar (lambda (pair)
                    (if (cdr pair)
                        (format "%s=%s" (car pair) (cdr pair))
                      ;; Plain env name is the syntax for unsetting vars
                      (car pair)))
                  pairs)
          process-env))

(defun envrc--clear (buf)
  "Remove any effects of `envrc-mode' from BUF."
  (with-current-buffer buf
    (kill-local-variable 'exec-path)
    (kill-local-variable 'process-environment)
    (kill-local-variable 'eshell-path-env)))


(defun envrc--apply (buf result)
  "Update BUF with RESULT, which is a result of `envrc--export'."
  (with-current-buffer buf
    (setq-local envrc--status (if (listp result) 'on result))
    (if (memq result '(none error))
        (progn
          (envrc--clear buf)
          (envrc--debug "[%s] reset environment to default" buf))
      (envrc--debug "[%s] applied merged environment" buf)
      (setq-local process-environment (envrc--merged-environment process-environment result))
      (let ((path (getenv "PATH"))) ;; Get PATH from the merged environment: direnv may not have changed it
        (setq-local exec-path (parse-colon-path path))
        (when (derived-mode-p 'eshell-mode)
          (setq-local eshell-path-env path))))))

(defun envrc--update-env (env-dir)
  "Refresh the state of the direnv in ENV-DIR and apply in all relevant buffers."
  (envrc--debug "Invalidating cache for env %s" env-dir)
  (cl-loop for k being the hash-keys of envrc--cache
           if (string-prefix-p (concat env-dir "\0") k)
           do (remhash k envrc--cache))
  (envrc--debug "Refreshing all buffers in env  %s" env-dir)
  (dolist (buf (envrc--mode-buffers))
    (with-current-buffer buf
      (when (string= (envrc--find-env-dir) env-dir)
        (envrc--update)))))

(defun envrc--mode-buffers ()
  "Return a list of all live buffers in which `envrc-mode' is enabled."
  (seq-filter (lambda (b) (and (buffer-live-p b)
                               (with-current-buffer b
                                 envrc-mode)))
              (buffer-list)))

(defmacro envrc--with-required-current-env (varname &rest body)
  "With VARNAME set to the current env dir path, execute BODY.
If there is no current env dir, abort with a user error."
  (declare (indent 1))
  (cl-assert (symbolp varname))
  `(let ((,varname (envrc--find-env-dir)))
     (unless ,varname
       (user-error "No enclosing .envrc"))
     ,@body))

(defun envrc--call-process-with-default-exec-path (&rest args)
  "Like `call-process', but ensures the default variable `exec-path' is used.
This ensures the globally-accessible \"direnv\" binary is
consistently available.  ARGS is as for `call-process'."
  (let ((exec-path (default-value 'exec-path)))
    (apply 'call-process args)))

(defun envrc-reload ()
  "Reload the current env."
  (interactive)
  (envrc--with-required-current-env env-dir
    (envrc--update-env env-dir)))

(defun envrc-allow ()
  "Run \"direnv allow\" in the current env."
  (interactive)
  (envrc--with-required-current-env env-dir
    (let* ((default-directory env-dir)
           (exit-code (envrc--call-process-with-default-exec-path "direnv" nil (get-buffer-create "*envrc-allow*") nil "allow")))
      (if (zerop exit-code)
          (envrc--update-env env-dir)
        (display-buffer "*envrc-allow*")))))

(defun envrc-deny ()
  "Run \"direnv deny\" in the current env."
  (interactive)
  (envrc--with-required-current-env env-dir
    (let* ((default-directory env-dir)
           (exit-code (envrc--call-process-with-default-exec-path "direnv" nil (get-buffer-create "*envrc-deny*") nil "deny")))
      (if (zerop exit-code)
          (envrc--update-env env-dir)
        (display-buffer "*envrc-deny*")))))

(defun envrc-reload-all ()
  "Reload direnvs for all buffers.
This can be useful if a .envrc has been deleted."
  (interactive)
  (envrc--debug "Invalidating cache for all envs")
  (clrhash envrc--cache)
  (dolist (buf (envrc--mode-buffers))
    (with-current-buffer buf
      (envrc--update))))



;;; Propagate local environment to commands that use temp buffers

(defun envrc-propagate-environment (orig &rest args)
  "Advice function to wrap a command ORIG and make it use our local env.
This can be used to force compliance where ORIG starts processes
in a temp buffer.  ARGS is as for ORIG."
  (if envrc-mode
      (inheritenv (apply orig args))
    (apply orig args)))

(advice-add 'shell-command-to-string :around #'envrc-propagate-environment)
(advice-add 'org-babel-eval :around #'envrc-propagate-environment)


;;; Major mode for .envrc files

(defvar envrc-file-extra-keywords
  '("MANPATH_add" "PATH_add" "direnv_layout_dir" "direnv_load" "dotenv"
    "expand_path" "find_up" "has" "join_args" "layout" "load_prefix"
    "log_error" "log_status" "path_add" "rvm" "source_env" "source_up"
    "use" "use_guix" "use_nix" "user_rel_path" "watch_file")
  "Useful direnv keywords to be highlighted.")

;;;###autoload
(define-derived-mode envrc-file-mode
  sh-mode "envrc"
  "Major mode for .envrc files as used by direnv.
\\{envrc-file-mode-map}"
  (font-lock-add-keywords
   nil `((,(regexp-opt envrc-file-extra-keywords 'symbols)
          (0 font-lock-keyword-face)))))

;;;###autoload
(add-to-list 'auto-mode-alist '("\\.envrc\\'" . envrc-file-mode))

(provide 'envrc)
;;; envrc.el ends here
(envrc-global-mode)

Asynchronous direnv calls

I have hacked Envrc (gist) to call direnv asynchronously (with make-process and a sentinel) because as a Guix user using Envrc to establish a guix environment can block for quite a while as files are downloaded and binaries are built. I imagine the same problem happens when using direnv to establish a nix shell environment. The simplest solution is to manually establish the environment once to let the software be installed before using Envrc, but this doesn't feel great as it could be done automatically.

Should I polish this feature so that it could be included, or do you think it doesn't fit with the project?

Make Info-mode respect INFOPATH

Probably outside of envrc scope, but maybe you know a clean solution. How to make Emacs' info command to be aware of info pages provided via INFOPATH by project's .envrc?

Make "direnv" location customizable

From

(defun envrc--call-process-with-global-env (&rest args)
  "Like `call-process', but always use the global process environment.
In particular, we ensure the default variable `exec-path' and
`process-environment' are used.  This ensures the
globally-accessible \"direnv\" binary is consistently available.
ARGS is as for `call-process'."
  ...)

the lack of this feature seems intentional, but I can’t quite make out why.

I configure emacs as much as possible to reference binaries directly in the Nix store, so they’re not generally in either my $PATH or exec-path. However, envrc forces me to have direnv in my exec-path.

So, I’d like to be able to change it or at least understand why that’s a bad idea (and then maybe still change it).

Org Babel PATH

I would've expected an org-babel shell block to inherit the buffer local process-envronment and allow me to use packages which are brought in by my .envrc (which references a shell.nix via use nix). This doesn't appear to be the case.

I'm guessing this is an issue on the Org side, but I don't belive it was an issue previously with direnv.

Wrong type argument: integer-or-marker-p, nil

I got Wrong type argument: integer-or-marker-p, nil when starting emacs. Not sure how to get a backtrace.

emacs --debug-init doesn't catch it.

I'm using (envrc-global-mode) and I tried loading it after lsp-mode.

Any ideas?

Duplicate messages in the *envrc* buffer

When visiting a file in a directory with a .envrc file I see the following duplicate messages in the *envrc* buffer.

==== 2022-08-26 07:14:27 ==== /home/doolio/projects/learning/python/ ====

direnv: loading ~/projects/learning/python/.envrc
direnv: export +VIRTUAL_ENV ~PATH

==== 2022-08-26 07:14:27 ==== /home/doolio/projects/learning/python/ ====

direnv: loading ~/projects/learning/python/.envrc
direnv: export +VIRTUAL_ENV ~PATH

It doesn't appear to cause any issues but is this expected behaviour or a misconfiguration on my part. The relevant use-package form from my init.el is as follows. I can provide the full macro expansion of this form if necessary.

(use-package envrc                      ; Buffer-local direnv integration.
  :if (executable-find "direnv")
  :demand t
  :config (envrc-global-mode))

The contents of my .envrc are as follows:

layout python python3

Thanks for your time.

Add defvar envrc-venv-name

Would it be possible to add a envrc-venv-name variable and ideally for its value to just be the pertinent part? That is say direnv exports VIRTUAL_ENV with a value of /home/doolio/projects/learning/python/atbs/.direnv/python-3.9.2 then envrc-venv-name would have the value python-3.9.2?

I'm hoping that I could then somehow add that information to my eshell prompt like I can do in a bash terminal.

Thanks for your time.

Support .env

direnv supports both .envrc and .env. .env files are supported by various projects like:

It would be great if this package could support that as there was no need to support two different formats in the same project.

dired-do-async-shell-command does not have direnv environment

I see that support for async-shell-command was added in #52, but it
seems this doesn't work for & in dired buffers. Similarly, I don't
see the environment propagated when using ! in dired buffers and
adding a trailing & (perhaps they go through the same code paths).

Question about other buffer local variables

Hi!

I wanted to ask if it is also (easily?) possible to set other variables in Emacs
depending on the local environment with envrc. For example, when using
different versions of the jdtls language server in Java, the corresponding
minor mode lsp-java needs to know about the installation directories via
lsp-java-server-install-dir, which differ for different versions of the
language server.

At the moment, I use directory-local environment variables and a "set the
variable in the major mode hook", e.g.,

   (after! cc-mode
     (defun my-set-lsp-path ()
       (setq lsp-java-server-install-dir (getenv "JDTLS_PATH")))
     (add-hook 'java-mode-hook #'my-set-lsp-path))

That leads to problems when the environment changes. Of course, it would be much
easier to tell envrc to set lsp-java-server-install-dir along with the
exec-path. Then, I could just use envrc-reload to update the environment,
like I usually to with exec-path.

Thanks!

Make a new release

Put 0.3 tag, please, so downstream package managers and distributions can update the version of the package.

The emacs extra dependencies path is stripped from the global 'exec-path

I'm using nix-community/emacs-overlay.

exec-path is a variable defined in ‘C source code’.
Its value is
("/usr/bin/" "/bin/" "/usr/sbin/" "/sbin/" "/usr/bin/" "/bin/" "/usr/sbin/" "/sbin/")
Original value was 
("/usr/bin" "/bin" "/usr/sbin" "/sbin" "/usr/bin" "/bin" "/usr/sbin" "/sbin" "/nix/store/4h8k46h3fn0p5cy0pi6hgzr8ksvh4bxg-emacs-unstable-emacs-27.1/libexec/emacs/27.1/x86_64-apple-darwin19.6.0")
Local in buffer *vterm test-direnv-project*; global value is 
("/nix/store/1aaj4df0rgydf7dw11wylp5r1j3i74a8-emacs-packages-deps/bin" "/usr/bin" "/bin" "/usr/sbin" "/sbin" "/nix/store/4h8k46h3fn0p5cy0pi6hgzr8ksvh4bxg-emacs-unstable-emacs-27.1/libexec/emacs/27.1/x86_64-apple-darwin19.6.0")

Purpose of *envrc-allow* and *envrc-deny* buffers

What is the purpose of these special buffers? They are just empty buffers for me or am I missing something. Any direnv related messages are relayed to me via the *envrc* buffer and/or the echo area. Couldn't the *envrc* buffer serve as the DESTINATION argument of call-process?

`shell-command-to-string` and `call-process` don't work in an envrc world

I'm trying to get rust-mode working on a project I'm hacking on. This project is set up using direnv to use a nix env that uses cargo from nixpkgs-mozilla. When I run rust-test from within a file in the project, it errors out with something like this backtrace:

Debugger entered--Lisp error: (file-missing "Searching for program" "No such file or directory" "cargo")
  call-process("cargo" nil t nil "locate-project")
  rust-buffer-project()
  rust-update-buffer-project()

The offending lines in rust-buffer-project can be simplified to something like (with-temp-buffer (call-process rust-cargo-bin nil t nil "locate-project")).

This seemed like a lot of work to go through in order just to capture stdout of a process, so I looked around and found shell-command-to-string, but (shell-command-to-string "cargo locate-project") produces more-or-less the same behavior.

Although shell-command-to-string and call-process break, shell-command and compile work fine. I'm not 100% sure why. I'm guessing that it has to do with which buffer is "current" when the final calls to do the exec happen, but I haven't traced all the way through both functions to figure out exactly what code paths I'm hitting.

I guess my question is, what is the best practice here? Should people writing call-process be writing to an output buffer? Is it fair to complain to authors of packages that don't do that? It seems like mandating that developers copy process-environment over manually would be bad (especially since process-environment is described as "may be risky if used as a file-local variable").

Run from before-hack-local-variables-hook

Adding envrc--update to before-hack-local-variables-hook instead of enabling envrc-mode seems to work fine, when combined with inheritenv, and would eliminate the overhead associated to every buffer creation, if I understand things correctly. It also seems more consistent to add envrc variables only to buffers that receive dir-local variables.

Should this mode of operation be officially supported?

Then only downside I see in this approach is that it's now unavoidable that envrc--update runs after the major mode hooks. A way around this issue would be to provide a macro on the lines of

(defmacro after-hack-local-variables (&rest body)
  "Arrange for BODY to run after buffer-local variables are set."
  `(add-hook 'hack-local-variables-hook (lambda () ,@body) nil 'local))

Clarification on when envrc.el should be loaded?

Hi Steve, I'm not an emacs savy by any means; while reading about when should envrc.el be loaded, more specifically this part here:

It's probably wise to do this late in your startup sequence: you normally want envrc-mode to be initialized in each buffer before other minor modes like flycheck-mode which might look for executables. Counter-intuitively, this means that envrc-global-mode should be enabled after other global minor modes, since each prepends itself to various hooks.

Could you suggest a hook users could use to abide by this recommendation? Looking at Emacs startup summary window-setup-hook runs right after emacs-startup-hook so that's the one we should use?

Thanks!

`void-function inheritenv` when `envrc-propagate-environment` is called

I get this error when envrc calls envrc-propagate-environment:

Debugger entered--Lisp error: (void-function inheritenv)
  inheritenv("/home/hlissner/projects/org/notes/daily/2020-11-14...")
  envrc-propagate-environment(#<subr shell-command-to-string> "/run/current-system/sw/bin/find -L \"/home/hlissner...")
  apply(envrc-propagate-environment #<subr shell-command-to-string> "/run/current-system/sw/bin/find -L \"/home/hlissner...")

Only way around it is to recompile envrc (via M-x byte-recompile-directory) after I've loaded inheritenv in the current session (with M-x load-library RET inheritenv).

I believe the cause is that this package doesn't load inheritenv at compile time, so the macro is unavailable to be expanded, causing it to be treated as a function call.

A (eval-when-compile (require 'inheritenv)) at the top of the package would fix it.

`envrc-reload` should do `direnv reload`

I'm working on a project packaged with Nix. My development environment includes an .envrc file which contains use flake, which is defined by https://github.com/nix-community/nix-direnv.

Periodically when I pull from master, new dependencies are introduced. Recompiling the project fails until I regenerate the Nix environment. The command direnv reload definitely causes this regeneration to happen. direnv allow does not. I'm not sure if this is because nix-direnv tries to be efficient and caches the Nix environment (per https://github.com/nix-community/nix-direnv#tracked-files) or if this is typical for direnv usage in general.

envrc does not offer a way to invoke direnv reload. There are functions envrc-reload and envrc-reload-all, but they do not directly invoke direnv reload. Instead, they do direnv export. direnv export also does not cause the Nix environment to be rebuilt. I'm not sure if it's supposed to? However, I feel that the name reload in these functions is a little confusing since it appears to parallel functions like envrc-allow (-> direnv allow) and envrc-deny (-> direnv deny).

I'd like a way to refresh my envrcs when I need to. I'm not exactly sure if that means fixing export to regenerate the environment when necessary, adding some functionality to envrc to call direnv reload somehow, or something else entirely. Your opinion eagerly solicited.

This is still a little fuzzy for me, so thanks for bearing with me. If this isn't sufficiently clear, let me know and I will try to put together a minimal repo to play with.

Thanks for your work on envrc! It's a lifesaver!

direnv.el comparison notes in readme are partially incorrect

👋🏼 direnv.el author here.

the current readme for this project makes these claims about direnv.el:

  • When switching to a buffer that is not "inside" a project with an
    .envrc file, the buffer will see the last project's environment. I
    would prefer it to see the default Emacs environment.

  • When direnv fails to execute in the course of switching to a
    buffer in a new project with an .envrc file (e.g. because that
    .envrc file is disallowed), buffers in the new project will see the
    environment variables from the previous project.

both claims are not (entirely) correct:

  • direnv.el unloads environments (reverting to default emacs env) when switching to a buffer outside a direnv-managed directory. only when switching to a buffer that is not visiting a file (and hence not associated to a directory) the last used environment is retained; this is by design and very useful for things like scratch buffers, but also compilation buffers, etc.

  • when direnv.el fails to execute direnv when switching buffers you will indeed see a warning/error, but the ‘active’ environment is unloaded so there's no accidental carrying over of env vars from an unrelated project. this is standard direnv behaviour, and direnv.el inherits that.

Peculiar error when saving .envrc

Hi,

I am using this package, and have set it up as mentioned in the README. Whenever I try to write a .envrc file using Emacs, I get the error message "peculiar error" in the echo area, and I am unable to save the .envrc file. I can write the file normally using other editors, and then envrc properly loads the environment as mentioned in the .envrc file. Any suggestions? Thanks!

envrc runs direnv synchronously

Some .envrc files are slow to execute. E.g., anything with use flake or use nix could take quite a while before completing. This currently locks all of Emacs. I should still be able to work on other projects and even track the *envrc* buffer on the current project. I think this is more important than preventing any use of the environment before it has switched over.

A more advanced handler (beyond simply going async) could perhaps explicitly block on the async process until it either 1. completes (and then silently unblocks) or 2. observes “direnv: (…) is taking a while to execute. Use CTRL-C to give up.” in the output (and then brings *envrc* to the front and processing continues async).

But I’d be more than happy with just a fire-and-forget.

envrc not recognizing .envrc file in a parent directory

I have the following envrc-mode config

;; envrc
(use-package envrc
  :ensure t
  :init (envrc-global-mode))

When I startup emacs I seem to be in envrc-mode.

However, if I open a file that is nested in a directory where one of the parent directories contains an .envrc file, that env is not activated with respect to that file. Running (envrc-reload) in the file buffer does activate the environment.

Overriding exec-path changes from other hooks

This sounds familiar to some of the other issues reported.

In summary, envrc will override the changes that hooks might do to the exec-path.

In my case, I do use also add-node-modules-path, which will add some elements to the exec-path. But this seemed not to have any effect if envrc is enabled.

Looking at the code, envrc is very careful to merge emacs PATH and the value read from the .envrc file. But it is looking at the PATH environment variable, not the exec-path. So the exec-path is effectively reset.

https://github.com/purcell/envrc/blob/master/envrc.el#L288-L289

Should the merged environment consider exec-path too, or would it break things?

dap-mode cpptools not picking up direnv environment

Hi. I'm running a Nix Shell using

            (dap-register-debug-template
             "(gdb) cae Debug ATLAS"
             (list :name "(gdb) cae Debug ATLAS"
                   :type "cppdbg"
                   :request "launch"
                   :program "${workspaceFolder}/build/debug/src/xxs/atlas"
                   :args ["-c" "${workspaceFolder}/../flx/flx/PricingEngineServer/config/xxscfg_DTH_server_debug.xml"]
                   :dap-server-path dap-cpptools-debug-program
                   :stopAtEntry nil
                   :cwd "${workspaceFolder}"
                   :environment [(:name "LD_LIBRARY_PATH" :value "~/src/atlas/build/debug/src/soapif:~/src/atlas/build/debug/src/xxnull:~/src/atlas/build/debug/src/xxaudit:~/src/atlas/build/debug/src/xxcrypto:~/src/atlas/build/debug/src/flxatptestse:~/src/atlas-libraries:~/src/atlas/build/debug/_deps/pgpsdk-src/libraries/release/:~/src/atlas/build/debug/_deps/grpc-src/lib:~/src/atlas/build/debug/_deps/opentelemetry-src/lib")]
                   :externalConsole nil
                   :MIMode "gdb"
                   :setupCommands [(:description "Enable pretty-printing for gdb" :text "-enable-pretty-printing" :ignoreFailures t)]
                   :preLaunchTask "Build ATLAS"
                   :cleanup-function (lambda (sesh)
                                       (when (not (dap--session-running sesh))
                                         (kill-buffer (dap--debug-session-output-buffer sesh))))))

and my shell.nix is:

{ pkgs ? import <nixpkgs> {} }:

with pkgs;

mkShell rec {
  buildInputs = [
    boost
    cmake
    stdenv
    zlib
    unixODBC
    freetds
    pkg-config
    minizip
    nss
    jemalloc
    xalanc
    xercesc
    gdb
    curl
    gcc13
    libxml2
    abseil-cpp
    opentelemetry-cpp
    autoconf
  ];

  shellHook = ''
      export ATLAS_ROOT=/home/st/src/atlas
      export BUILD_DIR=''${ATLAS_ROOT}/build/debug
      export LD_LIBRARY_PATH=''${BUILD_DIR}/src/soapif:''${BUILD_DIR}/src/xxnull:''${BUILD_DIR}/src/xxaudit:''${BUILD_DIR}/src/xxcrypto:''${BUILD_DIR}/src/flxatptestse:''${ATLAS_ROOT}/../atlas-libraries:''${BUILD_DIR}/_deps/pgpsdk-src/libraries/release/:''${BUILD_DIR}/_deps/grpc-src/lib:''${BUILD_DIR}/_deps/opentelemetry-src/lib:''${BUILD_DIR}/src/xxcrash64
      export COMPILE_COMMANDS_JSON=''${BUILD_DIR}/compile_commands.json
      export PATH=''${BUILD_DIR}/src/xxs:''${ATLAS_ROOT}:''${PATH}
      export LDFLAGS="''${LDFLAGS} -Wl,--undefined-version"
      export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath buildInputs}:''$LD_LIBRARY_PATH"
      export LD_LIBRARY_PATH="${pkgs.stdenv.cc.cc.lib.outPath}/lib:''$LD_LIBRARY_PATH"
  '';
}

but dap-mode is not picking up the LD_LIBRARY_PATH.

Janky interaction with lsp-mode

Configured with

(use-package envrc
  :after (lsp-mode flycheck)
  :init (envrc-global-mode))

envrc works for the first file I open in a workspace, but when I open another file in the same workspace, lsp-mode complains that the configured language-server command is not present on the path. Is there a way I can get this working?

Missing `diff-mode` dependency for faces

The envrc--summarise-changes function uses faces diff-changed, diff-added, and diff-removed, but envrc does not require diff-mode. This causes messages upon startup of the form

Invalid face reference: diff-changed [2 times]
Invalid face reference: diff-added
Invalid face reference: diff-changed [2 times]
...

if diff-mode was not previously loaded (on Emacs 29.1).

Possible to propagate environment when jumping to definitions in libraries?

I have the following situation

  • I'm in a rust buffer, using doom emacs with lsp and envrc, and everything works together wonderfully. I happen to have the language server installed via my direnv-managed environment, rather than globally or managed by emacs/lsp.
  • If I use lsp to jump to the definition of a function in the stdlib (or in a 3rd party library), the definition is located in a file that's outside my project root, because it's not my code.
  • Therefore envrc does not propagate my environment for that file, and therefore my language server is not available to lsp.

I would like the behavior to be something similar to how envrc handles buffers like *Help*, i.e. I would like envrc to hook into jump-to-definition with something that says "if the place I jump to has no .envrc file, keep using the one that I was just in"

Could envrc execute emacs lisp ?

Hello,

I am wondering if envrc could execute emacs lisp after loading other environment variables.

My use-case is a combination of emacs, agda and Nix.

I have a nix project which fetch agda with direnv.
The problem is that the agda mode package comes with the agda binary and the version must be matching.

I would like to automatically execute something like :

(add-load-path!
   (file-name-directory (shell-command-to-string "agda-mode locate")))
(require 'agda2)

I tried with dir-locals.el but it seems to be loaded before envrc.

Has anyone had a similar problem?

async-shell-command support?

Hi @purcell

I wonder why we don't propagate env for async-shell-command. Sometimes I use async-shell-command to call pip install but the pip version is not correct

And this help me

(advice-add 'async-shell-command :around #'envrc-propagate-environment)

Thanks

eshell can't load .envrc via Tramp

If I try to use 'envrc-allow` in an eshell which is on another machine via Tramp with ssh, direnv is unable to find the .envrc:

direnv: error .envrc file not found

If I use a copy of this directory locally, it successfully finds .envrc. My guess is that something with the execution of the direnv command isn't wrapping it correctly with TRAMP. I'm vaguely aware that something along these lines was improved with Emacs 27, but I'm on 27.2, so there might be additional work here?

Envrc mode enabled, but Racket application relying on SDL2 only works when Emacs is started from within project dir

Sorry for the long title. I might be doing something wrong here, but I'm trying out a small project using SDL2 for graphics, with my .envrc containing only use nix, and the shell.nix being the following:

with import <nixpkgs> { };

with pkgs;
mkShell {
  buildInputs = [ SDL2 ];
  LD_LIBRARY_PATH = lib.makeLibraryPath [ SDL2 libGL ];
}

The project itself is using Racket (if you need a minimal example, I can of course provide one on demand; I just wanted to make sure I'm not expecting something unreasonable from envrc). Running emacs from within the project directory, I can start the application just fine via Emacs. If my Emacs has been started outside, however, it complains about not being able to load libSDL2.so. The *envrc* buffer prints out the successful block of message, and I get Direnv succeeded in ... in the message buffer.
What's more, the LD_LIBRARY_PATH variable (which is crucial for Racket's sdl2 package) is set to the correct value when I M-x getenv LD_LIBRARY_PATH.

Is this an issue or expected behavior? If it's an issue (not necessarily with envrc), I'll glady try and find the reason, but probably need some hints :)

Unable to use in a git repository

I installed direnv and git using nix-darwin and home-manager. By setting PATH, envrc found direnv. However, if my .envrc directories use git, an error occurs:

direnv: loading /Volumes/Extra/Code/sunman-programmable/.envrc
direnv: using flake
error:
       … while fetching the input 'git+file:///Volumes/Extra/Code/sunman-programmable'

       error: getting the HEAD of the Git tree '/Volumes/Extra/Code/sunman-programmable' failed with exit code 1:
       error: executing 'git': No such file or directory
       
direnv: nix-direnv: Evaluating current devShell failed. Falling back to previous environment!
direnv: export +NIX_DIRENV_DID_FALLBACK ~PATH ~XPC_SERVICE_NAME

my config:

(when (eq system-type 'darwin)
  ;; 加载你的 Nix shell 配置文件
  (defun my/load-nix-shell-env ()
    "Load the environment variables from the nix shell."
    (interactive)
    (let ((path-from-shell (replace-regexp-in-string
                            "[ \t\n]*$"
                            ""
                            (shell-command-to-string "/run/current-system/sw/bin/bash --login -i -c 'echo $PATH'"))))
      (setenv "PATH" path-from-shell)
      (setq eshell-path-env path-from-shell)
      (setq exec-path (split-string path-from-shell path-separator))))

  ;; 调用这个函数来加载 Nix shell 环境
  (my/load-nix-shell-env))

(use-package envrc
  :hook (after-init . envrc-global-mode))

Major mode for .envrc files

The direnv.el package has the following:

;;;###autoload
(define-derived-mode direnv-envrc-mode
  sh-mode "envrc"
  "Major mode for .envrc files as used by direnv.

Since .envrc files are shell scripts, this mode inherits from sh-mode.
\\{direnv-envrc-mode-map}")

;;;###autoload
(add-to-list 'auto-mode-alist '("\\.envrc\\'" . direnv-envrc-mode))

I feel like this package should include a major mode for .envrc files as well.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.