Emacs, Lisp, and Guix
mituuz
A while ago I was bitten by the emacs bug. I went in just wanting to test Org Mode, but after installing Doom Emacs it became apparent that there was no going back.
Soon I was migrating my recipes to org mode, writing my sql in org mode and migrated some spreadsheets to org tables (queriable using sqlite in memory).
I had heard the tales about lisp being awesome for editor configuration and agree wholeheartedly. Though I haven’t written anything overly complex.
emacs-lisp has great documentation and epic integration available from the get go. You can easily write and evaluate functions, run them directly in the editor and emacs even comes with a test framework ert. I enjoyed writing tests/outputs directly to a single code block which included multiple inputs to the same function, this allowed really fast iteration and the test cases were already done.
All of this led me to try out multiple lisp and scheme implementations, from which I discovered Guile, and after a while a friend of mine linked me to Guix.
Guix, Nix?
I’ve been running Headscale on a Nixos server for several months and have done some experimentation using Nix. One thing that I’ve really enjoyed is using Nix to build dependencies for docker images and installing packages where I want the exact same version on different machines.
I really like the idea of declarative configs and temporary environments and shells, but never really got around to starting the migration.
I’m running arch and do have both Nix and Guix package managers installed. Both of these support home manager style configuration where you can configure user’s whole environment declaratively. This includes programs, services, env variables, config files and pretty much anything you can imagine (and have the skills/patience for).
For me Guix won because of well, lisp of course. There is also the focus on free software which I like. It has to be acknowledged that the Guix ecosystem is noticeably smaller than Nix’s (though I haven’t yet noticed this).
Guix uses Guile as the configuration language, which I’m already familiar with and which I enjoy.
RIP fish
fish has been my shell of choice for a long time; I really enjoy the default feel without having to touch any configs at all.
It does have downsides - a large amount of docs and software only provide bash scripts to do things.
I had some issues setting up fish on a foreign install. It seemed that the path variables weren’t correct and I (obviously) couldn’t source the .profile which is bash compatible. Instead of fighting, I pivoted.
With plugins, zsh can get close enough to fish that I won’t be missing any functionality. Previously when I used zsh I had some issues between machines but with Guix handling the whole shebang now, It just works™.
(define (zsh-plugins)
(plain-file "zsh-plugins"
"
source $HOME/.guix-home/profile/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh
source $HOME/.guix-home/profile/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
source $HOME/.guix-home/profile/share/zsh/plugins/zsh-history-substring-search/zsh-history-substring-search.zsh
bindkey '^[[A' history-substring-search-up
bindkey '^[[B' history-substring-search-down
eval \"$(starship init zsh)\"
...
"))
(home-environment
(packages (list
...
;; Shell
zsh
zsh-autosuggestions
zsh-completions
zsh-syntax-highlighting
zsh-history-substring-search
starship
...))
(services
(append (list
(service home-zsh-service-type
(home-zsh-configuration
(xdg-flavor? #t)
;; These write the actual config files to ~/.config/zsh/
;; Build .zshrc from several snippets
;; Load from an actual file
(zshrc (list (local-file "zsh/aliases.zsh")
;; Source from a function
(zsh-plugins)
;; Write directly
(plain-file "zoxide"
"eval \"$(zoxide init zsh)\"")
(plain-file "guix-profile"
"emulate sh -c '. ~/.profile'")))
(zprofile (list (plain-file "guix-profile"
"emulate sh -c '. ~/.profile'"))))))
...
%base-home-services)))
More config files
I initially embedded most of my config files directly to the home config as
separate functions. The only extra hoop to jump was that I had to escape double
quotes in the string blocks, but there are nicer ways by using dotfiles and
local-file.
(define (ideavim)
(plain-file "ideavim"
"
source ~/.vimrc
"))
(define (vimrc)
(plain-file "vimrc"
"
let mapleader = \" \"
"))
(define (gitconfig)
(plain-file "config"
"
[push]
default = simple
autoSetupRemote = true
"))
;; These create files to home
(service home-files-service-type
`((".ideavimrc" ,(ideavim))
(".vimrc" ,(vimrc))))
;; This creates files to .config/
(service home-xdg-configuration-files-service-type
`(("git/config" ,(gitconfig))))
;; Alternatively, you can automatically create dotfiles
;; sourced from a single directory
;; This supports both XDG and plain format
;; dotfiles/.gitconfig
;; dotfiles/.config/git/config
(service home-dotfiles-service-type
(home-dotfiles-configuration
(directories '("./dotfiles"))))
Pinning commits
One thing that I missed from Nix was including the commit in the config files to ensure that any machines would fetch the exact same software.
It’s easy to pin a commit by defining a channel as shown below - just remove the branch. This way the channel introduction is handled by default (custom channels are recommended to have introduction, which verifies the channel)
guix describe -f channels
(list (channel
(name 'guix)
(url "https://git.guix.gnu.org/guix.git")
(branch "master")
(commit "1cbf34833b403e56dabee4c56c365fb5b80277bc")
(introduction
(make-channel-introduction
"9edb3f66fd807b096b48283debdcddccfea34bad"
(openpgp-fingerprint
"BBB0 2DDF 2CEA F6A8 0D1D E643 A2A0 6DF2 A33A 54FA")))))
The actual commit pin looks like this
(home-environment
...
(services
(append (list
...
;; This directly creates .config/guix/channels.scm
(service home-channels-service-type
(list (channel
(name 'guix)
(url "https://codeberg.org/guix/guix.git")
(commit "f5358353c80de5c3e8034dffdaf2b9570246b90c")
(introduction
(make-channel-introduction
"9edb3f66fd807b096b48283debdcddccfea34bad"
(openpgp-fingerprint
"BBB0 2DDF 2CEA F6A8 0D1D E643 A2A0 6DF2 A33A 54FA")))))))
%base-home-services)))
Updating pinned commits
I decided to write a guile script which I can use to replace the old commit with the actual latest master commit from the Guix repository using a simple regex replace.
#!/usr/bin/env guile
!#
(use-modules (ice-9 popen)
(ice-9 regex)
(ice-9 rdelim)
(ice-9 textual-ports))
(define (get-commit-hash)
(let* ((port (open-input-pipe "git ls-remote https://codeberg.org/guix/guix.git master"))
(str (read-line port)))
(close-pipe port)
(let ((res (string-split str #\tab)))
(car res))))
(define (update-commit config-file)
(unless (file-exists? config-file)
(display "No config file available!")
(exit 1))
(let* ((pattern "\\(commit \"[a-f0-9]{40}\"\\)")
(regex (make-regexp pattern))
(commit-hash (get-commit-hash))
(content (call-with-input-file config-file get-string-all)))
(with-output-to-file config-file
(lambda ()
(display (regexp-substitute/global
#f regex content 'pre
(string-append "(commit \"" commit-hash "\")")
'post))))
(display (string-append "Found and wrote commit: " commit-hash))))
(update-commit "home.scm")
Conclusion
I’ve just started my Guix adventure and know that I have a lot to learn and discover, but I’ve enjoyed it very much. I think the documentation is really good.
I’m looking forward to trying out custom packages, slowly migrating more and more things to my config, and creating custom dev environments (at least for Android and QEMU).
There is a lot more to go through, and I would like to properly try out the Guix System (OS) too.
I would highly suggest trying out Guix or Nix, and also Lisp. That pesky Emacs bug sure has some nasty siblings.