HOME|RESUME|EXTRA

GNU Guix: (Interactive) manifests

1 What’s a manifest and where can I get one?

GNU Guix has, besides all the other cool features, a (you guessed it) cool feature that allows users to specify what packages they want to have installed in a given profile by using Scheme code. And since Scheme is not just a weird configuration format, but a complete programming language, you can do whatever you want with it.

The file you’re feeding into Guix must contain a “manifest”. It’s just Scheme code that will evaluate to a manifest. What’s a manifest? A list of packages in Guix’s own special format (not just a list of strings).

How would you create a manifest?

The simplest way is by writing a Scheme file in which you plug a list of strings into specifications->manifest:

(use-modules (gnu)) ;; specifications->manifest is located inside this module

(specifications->manifest '("emacs" "gcc-toolchain"))

Supposing you saved this file as manifest.scm, you can now feed it into Guix with guix package -m manifest.scm and that will configure your profile to contain only the packages written in your manifest.

2 How to make it even cooler

What’s so great about configuring you system with code is that you can do stuff in your code. It’s not just a static declaration.

Let’s say you have multiple machines, each of which you use for a different purpose. On one of them, the “office box”, you need LibreOffice and GNU Cash. On another one, the “writing desk”, you need Emacs, Org mode and a LaTeX distribution. And you also have a “code box” because you like programming, don’t you?

Everything other than the specific software needed to provide the flavors to each computer (office, writing, code) you’ll want to have in common. You also want to have an easy setup, so if you buy a new laptop instead of your current “code box”, you don’t have to remember all the little programs you needed to take sexy screenshots for r/unixporn.

Guix is here to save you!

Let’s write some code that will generate a manifest interactively, allowing us to select “classes” of software. We’ll begin by defining the structure that holds our packages and categories of packages. Good thing Scheme allows creating dynamic structures easily with its lists:

(define groups
  `((programming . ((c . ("gcc-toolchain"
                          "clang"
                          "gdb"
                          "ccls"))
                    (guile . ("guile"))
                    (ocaml . ("opam"))
                    (python . ("python"))
                    (php . ("php"))
                    (elisp . ("emacs"))
                    (haskell . ("ghc"
                                "ghcid"))))
    (fonts . ("fontconfig"
              "font-adobe-source-code-pro"
              "font-gnu-unifont"
              "font-fira-code"
              "font-google-noto"
              "font-dejavu"
              "font-liberation"
              "font-awesome"))
    (emacs . ("emacs"
              "emacs-f"
              "emacs-s"
              "emacs-dash"
              "emacs-telega"
              "emacs-htmlize"))
    (mail . ("offlineimap"
             "mu"))
    (vim . ("neovim"
            "python-pynvim"
            "node"))
    (latex . ("texlive"
              "biber"))
    (office . ("libreoffice"))))

That is an excerpt from my own setup. As you can see, we put related software into categories such as fonts or emacs and we can also have subcategories (nesting is unlimited!) like haskell or c in programming.

The problem that arises now is how to process this structure.

I managed to hack two mutually recursive functions together that:

  • shows you the groups you can choose from
  • lets you write a list of groups
  • traverses those groups and:
    • if the group contains subgroups, do the same thing all over again for those
    • else take each string from the group and append it to a list

There probably is a better way to do this, but here’s my solution:

;; TODO: come up with a meaningful name for foo1 and foo
(define (foo1 group group-name)
  (format #t "~%Enter a list with the tools you want to set up from the `~a' group.~%" group-name)
  (format #t "Here are the available tools: ~a.~%" (map car group))
  (newline)
  (letrec ((selected (read))
           (loop (lambda (l acc)
                   (if (null? l)
                       acc
                       (loop (cdr l) (append acc (foo (assoc-ref group (car l)) (car l))))))))
    (loop (if (equal? selected 'all)
              (map car group)
              selected)
          '())))

(define (foo group group-name)
  (if (list? (car group))
      (foo1 group group-name)
      group))

(You’ll need to import (ice-9 format) for the format function.)

After loading this, try running (foo1 groups "all"). It should give you the prompt to select groups and after you go through all that, it should return a list of strings representing the names of all the packages you want installed.

To turn this into the correct representation for Guix, remember our good friend specifications->manifest.

(define to-install (foo1 groups "all"))

(specifications->manifest to-install)

If you put all this stuff into a file, you can feed it into guix package -m just like before. Since Guix and Scheme are cool they will evaluate your whole script and prompt you to say what groups of software you want installed, just like you told them to.

2.1 Edit <2020-07-11 Sat>

I talked to a friend about this and he told me that it would be useful to make this accept packages too (package objects, not strings) in the groups.

It turns out that this is extremely easy to implement. Just drop the package objects where you want them in the groups and replace the last line:

(specifications->manifest to-install)

with:

(concatenate-manifests
 (list (specifications->manifest (filter string? to-install))
       (packages->manifest (filter package? to-install))
       (packages->manifest (filter inferior-package? to-install))))

For this you’ll need to also include the (guix packages), (guix profiles) and (guix inferior) modules.