Giter VIP home page Giter VIP logo

buffer-env's Introduction

buffer-env — Buffer-local process environments

GNU ELPA MELPA

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

Basic setup

On the project side

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

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

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

On the Emacs side

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

(add-hook 'hack-local-variables-hook #'buffer-env-update)
(add-hook 'comint-mode-hook #'buffer-env-update)

In this way, any buffer potentially affected by directory-local variables, as well as Comint buffers, will also be affected by buffer-env. It is also possible to add buffer-env-update only to specific major-mode hooks, or call it interactively.

How this package works

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

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

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

  • .env: These files, used by Docker, Node.js and others, are simple lists of VARIABLE=value pairs. They are still executed as shell scripts (which dictates when and how quotes are to be used, for instance), but no export statements are needed.
  • guix.scm and manifest.scm: The development environment of the Guix package is loaded and exported to Emacs. Make sure you have entered guix shell at least once before to install the dependencies, otherwise you may block Emacs for a long time.
  • flake.nix and shell.nix: These files are used by the Nix package manager and are handled similarly to Guix.
  • pyproject.toml: If you are using Poetry, Hatch or PDM, buffer-env can infer the project environment from them.
  • *.ps1: Similar to a regular shell script, but interpreted by PowerShell.

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

Integration with other environment management mechanisms

Python virtualenvs

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

source path-to-virtualenv/bin/activate

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

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

(setq buffer-env-script-name ".venv/bin/activate")
;; alternatively, try to find a .envrc file first
(setq buffer-env-script-name '(".envrc" ".venv/bin/activate"))

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

.env files

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

Direnv

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

(with-eval-after-load 'buffer-env
  (add-to-list 'buffer-env-command-alist '("/\\.envrc\\'" . "direnv exec . env -0")))

If you need tighter integration with direnv, you may want to check out the envrc package.

Compatibility issues

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

  • compile and project-compile (C-x p c) in Emacs 27 and older,
  • async-shell-command (M-&).

Fortunately, the problem has an easy fix provided by the inheritenv package, which see.

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

(eval-when-compile (require 'cl-lib))
(defun buffer-env-inherit (fn &rest args)
  "Call FN with ARGS using the buffer-local process environment.
Intended as an advice around commands that start a process after
switching buffers."
  (cl-letf (((default-value 'process-environment) process-environment)
            ((default-value 'exec-path) exec-path))
    (apply fn args)))

Related packages

This package is essentially a knockoff of the envrc package by Steve Purcell. The main difference is that envrc depends on and tightly integrates with the direnv program, while buffer-env is minimalist and has no extra dependencies.

For a comparison of the buffer-local approach to environment variables with the global approach used by most of the similar packages, see envrc’s design notes.

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

Contributing

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

buffer-env's People

Contributors

astoff avatar faerryn avatar flogth avatar manuel-uberti avatar phikal 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

Watchers

 avatar  avatar  avatar

buffer-env's Issues

How to inherit environment from manifest.scm to external applications

When editing org documents I sometimes run python code in a python interpreter:

  • First I open the code editor in a source block C-c '
  • then I start python console C-c C-p
  • then I send the code to the interpreter C-c C-C

contrary to only running C-c C-c inside the source block this does not inherit the buffer environment. Is there a way to achieve this (easily)?

Another issue I stumbled onto was running the export command asynchronously

  • Open export menu C-c C-e
  • select async C-a
  • select en export option e.g. latex l o

this fails due to missing dependencies. Is there a possible workaround using buffer-env?

Support specifying a command directly

First, thank you for this package. It's almost exactly what I was need, and I was about to resign myself to writing something crappier by hand. 😄

I would like to suggest an option to override the “search upward for a file with a known name” feature, and to specify a command directly. My use-case is that I use rtx, a clone of asdf, which would have the same concerns. I think it would also apply to direnv users.

rtx already knows how to search for configuration files, and it recognizes a variety of different file names—actually an unbounded list of file names, because plugins can add their own. I'd rather not try to replicate all the different file names possible, and it's not uncommon for multiple config files could occur in the same directory. I would always want to run one command, rtx exec -- env -0, regardless what directory the buffer is in or what other files exist.

I realize I could create a .envrc file in my home directory that uses source $(rtx env) to set the environment, and let buffer-env-update find it—and I think that will be my work-around for now (Edit: I don't think this would actually work. See the comment below.)—but it seems wasteful to search all the way up to my home directory, just to do it again inside rtx. Also, when editing files outside my home directory, I would still want to use the environment from rtx. (It has global configuration.) It also feels a little silly to set the environment via bash when rtx already provides it via rtx exec—and probably more efficiently, since it's a compiled program.

I don't know what the best interface would be to support this option. I can think of a few alternatives:

  1. Allow buffer-env-script-name and the file argument to buffer-env-update to be nil, and allow nil as a key in buffer-env-commands.

    • Pros: Uses the existing interface. Still allows files to be sourced interactively.
    • Cons: Not obvious what nil indicates. Only allows one command to be defined.
  2. Add a buffer-env-treat-as-command flag, which would cause buffer-env-script-name and the file argument to buffer-env-update to be interpreted instead as literal commands.

    • Pros: Allows multiple commands and interactive commands.
    • Cons: It would be confusing that buffer-env-script-name might be a script. No way to interactively source files if flag is set.
  3. Deprecate buffer-env-script-name in favor of a new buffer-env-sources list accepting a mix of strings and type-string pairs, where type is one of file or command, and bare strings are treated as files. Commands would be run unconditionally; files would be searched and evaluated as normal. The file argument to buffer-env-update would accept a string or type-string pair, or else might learn an optional :type keyword argument, or both.

    • Pros: Most flexible, I think. Allows multiple commands. Allows sourcing file and running commands interactively. Leaves space for more types in the future.
    • Cons: Bigger change. Possibly more complex to implement. More to explain.

Adding buffer-env-update to hack-local-variables-hook does not affect buffers without files

The recommended configuration says to add buffer-env-update to hack-local-variables-hook, but this only works for buffers with an associated file. In all other cases nothing happens, which is confusing and breaks the transparency that I think would be desirable.

This can be remedied by ensuring that hack-dir-local-variables-non-file-buffer is called, e.g. by adding

(add-hook 'comint-mode-hook #'hack-dir-local-variables-non-file-buffer)

or

(add-hook 'comint-mode-hook #'buffer-env-update)

to the user configuration, e.g. if you want buffer-env to work in a shell or REPL. But this approach requires manually supporting every non-file major-mode.

Assuming I didn't make some mistake here, do you see any good way around this. If not, I think adding a minor mode that initializes buffer-env would be useful?

Setting buffer-env-command locally

Hi,

in a little Java project of mine I want to override the value of buffer-env-command I have globally set to direnv exec . env -0 (the .envrc files I work with are full of crazy things):

(setq-default buffer-env-command "direnv exec . env -0")
(add-hook 'hack-local-variables-hook #'buffer-env-update)

What I am doing in my Java project is the following in .dir-locals.el:

((nil . ((buffer-env-command . ">&2 . \"$0\" && env -0")
         (buffer-env-script-name . "env/bin/activate")
         (python-shell-virtualenv-root . "./env"))))

However, the activate script is not run. It is run if I do the following, though:

((nil . ((eval (setq buffer-env-command ">&2 . \"$0\" && env -0"))
         (buffer-env-script-name . "env/bin/activate")
         (python-shell-virtualenv-root . "./env"))))

Which is not what I want because in this way I am losing the global setting for direnv. What can I do?

`format-prompt` not defined in Emacs 27

Hi,

you use the function format-prompt, even though "Package-Requires" indicates that Emacs 27.1 is the minimal version. It seems nonsensical to me to raise the version just because of one function (27.1 is probably still the most widely used version). The simplest solution in this case would be to replace it with equivalent, backwards compatible code. An alternative would be to use the compat library, if you intend to make use of other functionality that is defined in Emacs 28.1+ in the future.

Support pyproject.toml

For Python projects using a a fully featured project manager like Poetry, Hatch or PDM, it would be nice to extract the project environment directly from the corresponding pyproject.toml file.

The following code, which can be pasted in the user configuration, seems to work but need more testing. Feedback is welcome!

(setq buffer-env-script-name '(".envrc" "pyproject.toml"))
(with-eval-after-load 'buffer-env
  (add-to-list 'buffer-env-commands
               '("pyproject.toml" . "\
BACKEND=$(sed -nr 's/^build-backend\s*=\s*\"([a-z]+).*/\\1/p' \"$0\")
case $BACKEND in
  poetry|pdm) $BACKEND run env -0;;
  hatchling) hatch run env -0;;
  *) echo >&2 \"Unsupported build backend '$BACKEND'.\" && exit 1;;
esac")))

Fails to activate virtualenv interactively

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

Hi,

buffer-env-update fails to set up .venv/vin/activate at all.

I am using the version of buffer-env currently in elpa.

What should I try to debug this further?

GNU Emacs 28.2 (build 1, x86_64-redhat-linux-gnu, GTK+ Version 3.24.38, cairo version 1.17.8) of 2023-06-05

Here is the relevant config for buffer-env in my init.el:

;;; Handle Python virtualenvs and Guix manifests.
(unless (package-installed-p 'buffer-env)
  (package-install 'buffer-env))

I then attempt to run buffer-env-update on .venv/bin/activate interactively in my project directory where I created the virtualenv but that fails.

I use virtualenvs successfully on a daily basis with pyvenv but would like to use buffer-env instead.

`cl-letf` warning

Hi Augusto,

this is just a minor thing, maybe something you are already aware of, but if I use the snippet in the README to set up the buffer-local process environment, I get a warning. Namely, this:

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

Results in the following message when Flymake is active:

‘((default-value 'process-environment) process-environment)’ is a malformed function

The message disappears if I place this at the beginning of my init.el:

(eval-when-compile
  (require 'cl-lib))

Also, I've started to notice this message recently, but I wasn't able to pin down the exact Emacs master commit that triggers it. As of now, I am using Emacs built on 3425690 commit if it helps.

Temporarily disable hack-local-variables-hook? And misc org-babel issues.

Here's my config:

(setup (:pkg buffer-env)
  (:option buffer-env-script-name "manifest.scm")
  (add-hook 'comint-mode-hook #'hack-dir-local-variables-non-file-buffer)
  (add-hook 'hack-local-variables-hook #'buffer-env-update))

However, inside the file I'm editing, hack-local-variables-hook is set to (outline-apply-default-state t).

I'm editing an org-file that tangles the manifest.scm, but even as I'm editing the babel block that produces the file, it is attempting to run a bad manifest. For now, I'm just going to output to manifest2.scm and delete the manifest.

I've tried the following from within the org bufffer.

(setq hack-locals-old hack-local-variables-hook)
(setq hack-local-variables-hook nil)

Is there any way to temporarily disable this within a buffer/project?

Saving buffer-env-safe-files: wrong-type-argument listp t

Hi,

I edited the .envrc of one of my projects today and run direnv allow to make sure the changes are loaded.

However, the first time I act on this project (e.g., C-x p p -> Select project -> RET -> v (for VC-Dir)), I get the following prompt:

Mark current version of ‘/home/manuel/code/eerlijewoz/cockpit/.envrc’ as safe to execute? (y or n)

And when I hit y I get:

Debugger entered--Lisp error: (wrong-type-argument listp t)
  custom-save-variables()
  custom-save-all()
  customize-save-variable(buffer-env-safe-files (("/home/manuel/code/eerlijewoz/cockpit/.envrc" . "ffe8ad77e9d2affb4445c4a83d830d4606860e1d5ba86aab99...")))
  buffer-env--authorize("/home/manuel/code/eerlijewoz/cockpit/.envrc")
  buffer-env-update()
  hack-local-variables-apply()
  hack-dir-local-variables-non-file-buffer()
  project--value-in-dir(project-vc-merge-submodules "~/code/eerlijewoz/cockpit/")
  project--vc-merge-submodules-p("~/code/eerlijewoz/cockpit/")
  project-try-vc("~/code/eerlijewoz/cockpit/")
  project--find-in-directory("~/code/eerlijewoz/cockpit/")
  project-current(t)
  project-find-file(nil)
  funcall-interactively(project-find-file nil)
  project-switch-project("~/code/eerlijewoz/cockpit/")
  funcall-interactively(project-switch-project "~/code/eerlijewoz/cockpit/")
  command-execute(project-switch-project)

This is how I set up buffer-env in my init.el:

(add-hook 'hack-local-variables-hook #'buffer-env-update)
(with-eval-after-load 'buffer-env
  (push '(".envrc" . "direnv exec . env -0") buffer-env-commands))

(defun mu-buffer-env-inherit (fn &rest args)
  "Call FN with ARGS using the buffer-local process environment."
  (cl-letf (((default-value 'process-environment) process-environment)
            ((default-value 'exec-path) exec-path))
    (apply fn args)))
(advice-add #'mu-buffer-env-inherit :around #'project-shell)
(advice-add #'mu-buffer-env-inherit :around #'shell)

Alternative to Customize?

Hi,

first of all thanks for this package and for making it available on GNU ELPA. It works as expected with direnv.

However, why did you choose to go the Customize way with buffer-env-safe-files? I usually do not use Customize, so I specifically don't put any settings related to it neither in my init.el nor in a different custom file.

Shouldn't something like this be enough for the purpose buffer-env-safe-files covers?

(setq-default buffer-env-safe-files '("/path-to/your/.envrc"))

Of course I may be missing something or not getting it entirely. Note that I am also fine with loading a custom.el file with buffer-env-safe-files in it, so this is not a bug report. Just plain curiosity.

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.