Literate Guix configuration

2025/06/22

Intro

This file is a literate configuration of a Guix and Emacs system. Tangling it (C-c C-v t) produces home.scm , which could be then installed as usual.

Using this template you’ll be able to structure a home-environment definition in a way that is convenient to you. Besides regular noweb features, a new source code block language is implemented, allowing templated configuration files (see example below). Emacs configuration is included in the home-environment as a package (see the definition), which is built by tangling this org file.

A couple settings to ease the use in emacs:

#+PROPERTY: header-args :noweb-prefix no :noweb yes :eval no
#+PROPERTY: header-args:elisp :tangle config.el
#+PROPERTY: header-args:cfg :eval yes :cache yes :wrap "src text :noweb-ref guix-file"
# Local Variables:
# eval: (add-hook 'after-save-hook (lambda () (org-babel-tangle nil nil "scheme")) 0 t)
# eval: (add-hook 'org-babel-pre-tangle-hook #'org-babel-execute-buffer 0 t)
# End:

Template

Main template which tangles to home.scm.

#+begin_src scheme :tangle home.scm
(use-modules
 (gnu)
 (gnu services)
 (gnu home services)
 (gnu home services desktop)
 (guix gexp)
 (guix git-download)
 (guix build-system copy)
 (guix packages)
 (ice-9 receive))

(define (pkg x)
  (if (string? x)
      (receive (package out)
          (specification->package+output x)
        (if (string= "out" out) package
            (list package out)))
      x))

(home-environment
 (services
  (cons*
   (simple-service
    'files home-files-service-type
    `((".guile" ,%default-dotguile)
      (".config/guix/channels.scm"
       ,(plain-file
         "channels.scm"
         (simple-format #f "~s" '(list <<guix-channel>>))))
      <<guix-file>>))
   <<guix-srv>>
   %base-home-services))

   (packages (map pkg (list <<guix-pkg>>))))

Channels

guix

#+begin_src scheme :noweb-ref guix-channel
(channel
 (name 'guix)
 (url "https://git.guix.gnu.org/guix.git")
 (branch "master")
 (introduction
  (make-channel-introduction
   "9edb3f66fd807b096b48283debdcddccfea34bad"
   (openpgp-fingerprint
    "BBB0 2DDF 2CEA F6A8 0D1D  E643 A2A0 6DF2 A33A 54FA"))))

Example content

package

#+begin_src scheme :noweb-ref guix-pkg
"emacs-next" "emacs-org"

service

#+begin_src scheme :noweb-ref guix-srv
(service home-dbus-service-type)

simple files

#+begin_src scheme :noweb-ref guix-file
("plain" ,(plain-file "file" "hello, world"))
("local" ,(local-file "." "file" #:recursive? #t))

templated files

A cfg source block with :dest parameter will produce a result, which when wrapped using :wrap "src scheme :noweb-ref guix-file", would be weaved automatically in the main template.

#+begin_src cfg :dest "tmpl"
this template gets converted to mixed-text-file
ANT unicode characters mark scheme code
some-program = 🐜 (pkg "msmtp") 🐜/bin/msmtp
("tmpl"
,(mixed-text-file
  "cfg-file"
"this template gets converted to mixed-text-file\n"
"ANT unicode characters mark scheme code\n"
"some-program = " (pkg "msmtp") "/bin/msmtp"))

Emacs config

ob-cfg.el

A helper script that renders cfg source code blocks.

;; -*- lexical-binding: t; -*-
(require 'rx)
(defun org-babel-execute:cfg (body params)
  "Wrap BODY in a (mixed-text-file). If PARAMS contains :file, use its
contents instead of BODY. If PARAMS contain :chmod, additionally wrap
with (chmod-computed-file)."
  (let-alist params
    (when .:file
      (with-temp-buffer
        (insert-file-contents-literally .:file nil)
        (setf body (buffer-string))))

    (concat
     (if .:dest (format "(%S\n," .:dest) "")
     (if .:chmod "(chmod-computed-file " "")
     "(mixed-text-file\n  \"cfg-file\"\n"
     ;; split string into parts 'string-part🐜scheme-part🐜'
     ;; - output each line of 'string-part' quoted.
     ;; - output 'scheme-part' as is
     ;; - note: add an empty 'scheme-part' at the end of input string
     ;;   so that the last 'string-part' is matched
     (replace-regexp-in-string
      (rx (minimal-match
           (group (zero-or-more anything))
           ?🐜 (group (zero-or-more anything)) ?🐜))
      (lambda (e)
        (let ((string-part (match-string 1 e))
              (scheme-part (match-string 2 e)))
          (concat
           (if (string-empty-p string-part) ""
             (string-join
              (mapcar
               (lambda (l) (prin1-to-string l nil '((escape-newlines . t))))
               (string-lines string-part nil t))
              "\n"))
           scheme-part)))
      (concat body "🐜🐜") t t)
     ")"
     (if .:chmod (format " #o%o)" (org-babel-interpret-file-mode .:chmod)) "")
     (if .:dest ")" ""))))

config package

#+begin_src scheme :noweb-ref emacs-config :noweb yes
(package
  (name "emacs-config")
  (version "0")
  (source (local-file "." "src" #:recursive? #t))
  ;; you really should put the files in git and enable filtering
  ;; #:select? (git-predicate ".")
  (build-system copy-build-system)
  (arguments
    (list
     #:install-plan ''(("." "" #:include (".el")))
     #:phases
     #~(modify-phases %standard-phases
         (add-before 'install 'tangle
           (lambda* (#:key inputs #:allow-other-keys)
             (substitute* "literate.org"
               (("\\(guix[/]pkg '([^ )]+)" _ pkg)
                (format #f "(concat ~s"
                        (or (assoc-ref inputs pkg)
                            (error (format #f "~a is required for emacs config but not present in inputs" pkg))))))

             (invoke "emacs" "-Q" "-l" "ob-cfg.el" "--batch" "--eval"
                     (simple-format #f "~s"
                                    '(progn
                                      (require 'ob-tangle)
                                      (setq org-confirm-babel-evaluate nil)
                                      (with-current-buffer
                                       (find-file-noselect "literate.org")
                                       (org-babel-tangle nil "early-init.el" "elisp")))))
             (delete-file "literate.org"))))))
  (inputs (map pkg '("msmtp")))
  (native-inputs (map pkg '("emacs-org" "emacs-next-minimal")))
  (description "emacs config")
  (home-page #f)
  (synopsis #f)
  (license #f))
#+begin_src scheme :noweb-ref guix-file
(".config/emacs" ,<<emacs-config>>)

config parts

#+begin_src text :noweb-ref emacs-dep
"msmtp"
#+begin_src elisp
(load (concat user-emacs-directory "ob-cfg.el"))
(require 'sendmail)
(setq sendmail-program (guix/pkg 'msmtp "/bin/msmtp"))