Graze 0.2 Released

i'm excited to annouce the first major update to my project graze, a declarative system for creating guix shell environments. this update adds a couple of things, but most importantly, a new command -- graze env -- which allows for the exporting of shell environment variables to other processes.

my motivation for making this came about after dealing with some problems in getting my shell environment to persist in emacs daemon sessions. i'd tried using a few of the different direnv packages for emacs, but to no avail. it was overall a bit of a pain, and i found myself yearning for something like what nix has with nix-direnv, but finding little that satisfied that same niche.

somehow, frustrated again with this as i tried to get haskell to do the haskell thing without requiring its own emacs session, i re-stumbled across buffer-env, which is a packaged providing emacs with direnv-like features, minus any dependency on direnv itself. being only like 300 lines of elisp its pretty easy to read through and understand, so i decided to just peak at the source code. it turned out the way it remains agnostic of any dependencies is by defining a list of "commands" for the environment, rather than running any specific files:

(defcustom buffer-env-command-alist
  `((,(rx "/.env" eos)
     . "set -a && >&2 . \"$1\" && env -0")
    (,(rx "/manifest.scm" eos)
     . "guix shell -m "$1" -- env -0")
    (,(rx "/guix.scm" eos)
     . "guix shell -D -f "$1" -- env -0")
    (,(rx "/flake.nix" eos)
     . "nix develop -c env -0")
    (,(rx "/shell.nix" eos)
     . "nix-shell \"$1\" --run \"env -0\"")))

holy moly all i need to do is define something like graze shell -- env -0 for a shell.scm and i'll be gucci. this is at least what i thought, but attempts at such were unsatisfactory. the main issue was that it was hard to hook this command manually into graze, since, rather than a shell environment in and of itself, it is just a launching-pad for guix shell. then i realized that all calls ended with the same env -0.

all this command does, really, is call env with a '\0' terminating each line rather than '\n'. it would be trivial to make using it a feature of graze itself.

rewriting the shell

there were two major issues i identified in the graze file that needed to be fixed. the first was changing the top of the script to not explicitly call shell. i wound up defining a simple environment variable that could be used to invoke a different command depending on whether the user called shell or env.

from this:
#!/usr/bin/env -S guile -e shell -s
!#

to this:
#!/usr/bin/env -S guile --no-auto-compile -e ${GRAZE_CMD} -s
!#

next was the issue of what i was calling. namely, i had defined shell as a variable, and was using a hacky setup that evaluated the variable and then errored out a bit after the user was done with the shell. it needed to be redefined as a function proper:

(define (shell args)
  (make-gshell
   #:pre-shell-hooks #f
   #:packages site-packages
   #:command #f))

the env command is very similar, only omitting the #:command, since that would be env -0:

(define (env args)
  (export-env
   #:pre-shell-hooks #f
   #:packages site-packages))

this breaks a lot of what worked on the first iteration, so some changes need to be made to the underlying code.

graze-guts

defining a new command means making it actually accessible from the program. below was the original invocation of graze shell:

(("graze" "shell")
 (format #f "evaluating shell.scm...")
 (if (get-shell-root)
   (execl (string-append (get-shell-root) "/shell.scm"))
   (format #f "ERROR: no shell.scm found!"))

which i spun into something a bit more robust:

(("graze" "env")
       (cond ((get-shell-root)
	      (setenv "GRAZE_CMD" "env")
	      (execl (string-append
		      (get-shell-root) "/shell.scm")))
	     (#t (format #f "ERROR: no shell.scm found!"))))
("graze" "shell")
       (format #f "evaluating shell.scm...")
       (cond ((get-shell-root)
	      (setenv "GRAZE_CMD" "shell")
	      (execl (string-append
		      (get-shell-root) "/shell.scm")))
	     (#t (format #f "ERROR: no shell.scm found!")))

and the command itself is defined here:

(define* (export-env #:key
		     (pre-shell-hooks #f)
		     (packages #f)
		     (options #f))
  (let ((env-invoke
	 (string-join (list
		       "guix shell"
		       (empty-or packages (make-package-list packages))
		       (empty-or options (make-shell-options options))
		       "-- env -0"))))
	(empty-or pre-shell-hooks (pre-shell-hooks))
	(system env-invoke)))

testing

though i don't program in rust anymore, i can recall how difficult it is to make it play nice with guix. it seemed like a good thing to test this new build on. being able to just run cargo from emacs would be a miracle. i started by defining a simple shell with some rust tools installed:

#!/usr/bin/env -S guile --no-auto-compile -e ${GRAZE_CMD} -s
!#

(use-modules (graze shell-utils))

(define site-packages
  (list
   'coreutils
   'bash
   'rust
   'rust-tokio
   'rust-cargo
   'rust-cargo-edit
   'rust:tools
   'rust-analyzer
   'gcc-toolchain))

(define (shell args)
  (make-gshell
   #:pre-shell-hooks #f
   #:packages site-packages
   #:command "env -0"))

(define (env args)
  (export-env
   #:pre-shell-hooks #f
   #:packages site-packages))

then, i set up buffer-env in emacs:

(use-package buffer-env)
(with-eval-after-load 'buffer-env
    (add-to-list 'buffer-env-command-alist
		 `(,(rx "/shell.scm" eos)
		   . "graze env")))
(add-hook 'hack-local-variables-hook #'buffer-env-update)
(add-hook 'comint-mode-hook #'buffer-env-update)

fingers crossed, i ran a M-x buffer-env-update shell.scm, and hoped for the best. a very "hell yeah" moment ensued: rust, rust analyzer, cargo, and tokio,,, not only all installed, but also working in an emacs deamon session. even rustic-cargo-run worked !

pure victory -- functioning rust

no fussing, not trying to track what was stored where, just a couple quick lines of code.

the other, smaller, updates

this was the most significant update, but not the other. the jump between graze 0.1 and 0.2 also included some refactoring of the help command and recursive searching for a shell.scm similar to how it works on nix. there was also a template added, but there's not much there yet ... stay tuned to see how this chunk evolves :D