Emacs literate config with Guix home

My literate config didn’t work in guix home and as it turned out (org-babel-load-file (expand-file-name "~/.emacs.d/config.org")) isn’t working on Guix as config.org is a symlink into the store and because the timestamp is 1970 config.el will not get updated.

I decided to advice org-babel-load-file so that tangling adds a comment with the guix store hash of the original file on top of the tangled file and every time org-babel-load-file gets triggered it will compare the hashes.

The end of my current init.el:

;; Add personal modules directory to load path
(add-to-list 'load-path (expand-file-name "lisp" user-emacs-directory))

;; Load the Guix aware org-babel advice
(require 'guix-org-hash)

;; This is the actual config file
(org-babel-load-file (expand-file-name "~/.emacs.d/config.org"))
;;; guix-org-hash.el - Guix aware org-babel-load-file advice -*- lexical-binding: t; -*-

(require 'subr-x)  ;; for when-let, string-trim, etc.

(defun guix--store-hash (path)
  "Extract the Guix store hash from PATH, if it points into /gnu/store.
Return nil otherwise."
  (when (string-match "^/gnu/store/\\([a-z0-9]+\\)-" path)
    (match-string 1 path)))

(defun guix--read-embedded-hash (el-file)
  "Read embedded Guix hash comment from EL-FILE, or nil if not present."
  (when (file-exists-p el-file)
    (with-temp-buffer
      (insert-file-contents el-file nil 0 200) ;; just the first few lines
      (when (re-search-forward "^;; Guix-hash: \\([a-z0-9]+\\)" nil t)
        (match-string 1)))))

(defun guix--maybe-retangle (org-file el-file)
  "Re-tangle ORG-FILE into EL-FILE if Guix store hash changed."
  (let* ((truename (file-truename org-file))
         (store-hash (guix--store-hash truename))
         (old-hash (guix--read-embedded-hash el-file)))
    (when (and store-hash (not (equal store-hash old-hash)))
      (message "[Guix] Re-tangling %s (hash %s → %s)"
               org-file (or old-hash "none") store-hash)
      (require 'org)
      (require 'ob-tangle)
      (org-babel-tangle-file org-file el-file)
      (with-current-buffer (find-file-noselect el-file)
        (goto-char (point-min))
        (insert (format ";; Guix-hash: %s\n" store-hash))
        (save-buffer))
      t))) ;; return t if retangled

(defun guix--babel-load-file-around (orig-fun org-file &rest args)
  "Advice around `org-babel-load-file' that is aware of Guix store hashes."
  (let* ((el-file (concat (file-name-sans-extension org-file) ".el")))
    (unless (guix--maybe-retangle org-file el-file)
      ;; if no retangle triggered, fall back to standard behavior
      (message "[Guix] Using cached %s" el-file))
    (apply orig-fun org-file args)))

(advice-add 'org-babel-load-file :around #'guix--babel-load-file-around)

(provide 'guix-org-hash)
2 Likes