Modifying Guix packages using inheritance

Screenshot of a VSCode showing guix edit

Figure 1: Guix edit output shown in VSCode

We're going to start playing with package definitions, inheriting existing package recipes and altering them. With inheritance we can accomplish what we've done with transformations, but using concise package definitions. That's going to lead us towards being able to make more complex changes to packages later on.

We're also going to explore the Perfect Setup - not Emacs - but instead Visual Studio Code for editing package and viewing packages. While I personally use Vim and Tmux for packaging, we'll show how to use VS Code as it's easy to get started with. As we explore packaging more we'll be spending far too much time messing with tools!

This post is one of a series on Guix packaging, so far we've covered:

Viewing package definitions

Package definitions (sometimes called recipes) are how Guix knows the steps to build a particular package. The Guix project contains a lot of functionality to assist us creating packages, environments, services and whole Linux systems. It also contains over 27,000 packages. All the recipes for those packages (some recipes create more than one package) are stored under gnu/packages - the directory structure is because they are part of the 'gnu system'.

The package recipes are also stored locally on your machine - downloading them is one of the things that guix pull does. We'll be altering existing packages definitions from Guix's archive, so it's worthwhile becoming familiar with them - the more we read, the more patterns we see.

To see those package recipe use the guix edit command:

$ guix edit hello
$ guix edit mutt
$ guix edit vim

We're shown the specific definition that is available on our system in the local copy of the Guix archive. It's a bit of a funny command because you can't actually edit the package definitions since they're in the Guix store.

Guix edit opens the package in whatever your EDITOR is set to.

If you're using VSCode then Zimoun has a nice wrapper script that lets you open them correctly. Grab it from his advanced-packages-2023 repository and place it somewhere in your $PATH. For me that's:

cd ~/bin
curl --remote-name https://gitlab.com/zimoun/advanced-packages-2023/-/raw/main/vscode-wrapper
chmod u+x vscode-wrapper
export EDITOR=vscode-wrapper

The package definitions are pretty easy to understand, it's a very structured DSL. They can become more complex when manipulating the build process itself - but the basics are straightforward.

Package inheritance

Each package recipe creates a package instance in Guix. We have the ability to inherit an existing package and edit, remove and change parts of that definition, creating our own packages. This is often used to create packages with slightly different library inputs, or to change the way that a package builds.

For example, the vim-full package (guix edit vim-full) inherits the Vim package:

(define-public vim-full
  (package
    (inherit vim)
    (name "vim-full")
    (arguments
     `(#:configure-flags
            (list (string-append "--with-lua-prefix="
                        (assoc-ref %build-inputs "lua"))
    ;; the recipe continues after this changing how the package is built

This definition creates a package variant called vim-full. It's built using the same source that was downloaded for the vim package, but the variant uses different configure flags and adds libraries. The output is a different package, with a different name and capabilities - but sharing the same source download and other elements.

We create our own local packages in exactly the same way, they inherit from the Guix archive's package. We build those packages and install them locally: there's a separate post about package building (guix build command) if you need a reminder.

Lets create a directory for our experiments and create a package definition - for me that is:

$ cd <some/location>
$ mkdir guixplay

$ touch local-packages.scm

In that file add the following code:

 1 (define-module (local-packages)
 2   #:use-module (guix download)
 3   #:use-module (guix packages)
 4   #:use-module (gnu packages calcurse))  ;; contains calcurse
 5 
 6 (define calcurse-4.8
 7   (package
 8     (inherit calcurse)
 9     (version "4.8.1")
10     (source
11       (origin
12         (method url-fetch)
13           (uri (string-append "https://calcurse.org/files/calcurse-"
14                               version ".tar.gz"))
15                (sha256
16                  (base32 "0lappv4slgb5spyqbh6yl5r013zv72yqg2pcl30mginf3wdqd8k9"))))))

The first line creates a Guile Scheme module called local-packages. The location and filename where the package definition is stored, and the name within the define-module are connected.

Guile Scheme organises code in modules, each file is a separate module. The important thing to know is that each module name and the file name have to be the same. If the file is called test.scm then the definition will be (define module (test)). If there are hyphens then it's the same: rust-keyring.scm is defined as (define-module (rust-keyring)). Finally, if there's a path it's shown as separate words, so if the path is packages/test-games.scm then the module is (define-module (packages test-games)).

In lines 2-3 we use various Guix modules so that we can define a package and download source code. There are a lot of modules in Guix, they give us all sorts of capabilities to create packages, services and systems.

We also need the calcurse module itself (line 4). This contains the definition of Calcurse in Guix, which we need so we can inherit from it. We know it's the calcurse module by using guix edit calcurse and seeing the name and the path of the file it opens for us: for me that's gnu/packages/calcurse.scm so I then know the module is (gnu packages calcurse).

To build the package itself we create a build shell - I generally do this in a different tmux split:

$ guix shell --container --nesting --network coreutils

[env]$ guix build --load-path=./ calcurse@4.8.1

Normally when we use guix build it searches for packages in a set of standard locations. We use the --load-path option to prepend a directory (our current one) to the module load path. Consequently, it can then find the definition for the variable called calcurse at version 4.8.1.

The build will fail with something like this:

sha256 hash mismatch for /gnu/store/pqcswn8yvgs1xfnxzrsaq84k32f0bg0c-calcurse-4.8.1.tar.gz:
  expected hash: 0lappv4slgb5spyqbh6yl5r013zv72yqg2pcl30mginf3wdqd8k9
    actual hash:   02l3spj2ai97b87winz3kvsxvf66g74lm46b7kcbhsgx2iqb6syq
    hash mismatch for store item '/gnu/store/pqcswn8yvgs1xfnxzrsaq84k32f0bg0c-calcurse-4.8.1.tar.gz'

Every package in Guix has a hash of the source file that's used to build it - that way we know that we're getting the correct upstream source and no-one has messed with it. There is a guix hash command that we could use if we had downloaded a copy of the source. It's easier to just let it fail on the first build and then edit our local package definition to have the correct hash.

Go back into local-packages.scm and change the hash to be the correct one. For me this looks like:

(sha256
  (base32
    "02l3spj2ai97b87winz3kvsxvf66g74lm46b7kcbhsgx2iqb6syq"))))))

Now rebuild it and it should successfully complete the build!

We can change the package version to be whatever we want, and even the name. For example:

 1 (define-module (local-packages)
 2   #:use-module (guix packages)
 3   #:use-module (guix download)
 4   #:use-module (gnu packages calcurse))  ;; contains calcurse
 5 
 6 (define-public calcurse-4.8
 7   (package
 8     (inherit calcurse)
 9     (name "calcurse-futurile")
10     (version "4.8.1-futurile")
11     (source
12       (origin
13         (method url-fetch)
14           (uri (string-append "https://calcurse.org/files/calcurse-"
15                               version ".tar.gz"))
16                (sha256
17                  (base32
18                    "02l3spj2ai97b87winz3kvsxvf66g74lm46b7kcbhsgx2iqb6syq"))))))

In this case there's a new field in the record (name "calcurse-futurile"), so to build it:

guix build --load-path=./ calcurse-futurile

To install it do:

[env]$ guix package --load-path=./ --install calcurse-futurile

[env]$ guix package --list-installed
calcurse-futurile  4.8.1-futurile  out  /gnu/store/zzaip31pjqfhlg[...]bmbs9-calcurse-futurile-4.8.1-futurile

As we can see both the package name and the package version have been changed.

Underneath the hood the package recipe is a Guile Record with some additions to make it easy to use. Guix uses Records for all sorts of types, like packages and the origin of source code. We inherit an existing variable by specifying inherit as the first field in the record declaration - it has to be the first field.

It's sometimes confusing that the code uses the variable, while Guix's CLI commands use the package's name: in a lot of cases (but not all) the variable and the name are the same thing anyway. The variable is the one after the define public part, so calcurse-4.8 in the package above. The variable is used within Guile Scheme code, for example when inheriting a package.

As an illustration lets inherit our calcurse-4.8 variable and change some of the fields like the description:

1 (define-public calcurse-4-test
2   (package
3     (inherit calcurse-4.8)
4     (name "calcurse-altered")
5     (synopsis "Curses text-based CalDav and ToDo application")
6     (description "Curses text-bases application for Calendaring and ToDo.")))

As we can see we're creating a new variable called calcurse-4-test (line 1). This is done by inheriting the package we created earlier - it's variable was calcurse-4.8. The actual name that we use in the Guix command is the one in the name field calcurse-altered - so we can build and install it like so:

[env]$ guix build --load-path=./ calcurse-altered

# show information about it, or install it
[env]$ guix package --load-path=./ --show=calcurse-altered
name: calcurse-altered
version: 4.8.1-futurile
outputs:
+ out: everything
systems: x86_64-linux i686-linux
dependencies: ncurses@6.2.20210619 tzdata@2022a
location: ./local-packages.scm:22:2
homepage: https://www.calcurse.org
license: FreeBSD
synopsis: Curses text-based CalDav and ToDo application
description: Curses text-bases application for Calendaring and ToDo.

Sometimes, we make a change to a package and when we go to build it Guix will tell us it already has the build (we're just repeating ourselves!). If we want to do a build anyway, we can either delete the build we have (using guix gc) or use the --check option to guix build:

# go into our build shell
$ guix shell --container --network --nesting coreutils

# try to build calcurse-futurile - guix responds by showing us the existing build
[env]$ guix build --load-path=./ calcurse-futurile
/gnu/store/zzaip31pjqfhlg5zx1992v59lyvbmbs9-calcurse-futurile-4.8.1-futurile

# delete the build from the store
[env]$ guix gc --delete /gnu/store/zzaip31pjqfhlg5zx1992v59lyvbmbs9-calcurse-futurile-4.8.1-futurile

# OR: repeat the build and check if it's the same as the previous build
[env]$ guix build --load-path=./ --check calcurse-futurile

When we do this the build is very fast because it will graft the package without doing a full build. Grafts are a really interesting capability as they allow the 'replacement' in a package definition. They're very important for security updates, and the linked blog is worth reading.

Nonetheless, when we're playing with packages we want it to do a complete rebuild, and we don't want the final grafts done as this slows down the build. For both those situations we can provide the --no-grafts option:

# repeat a build using --check and --no-grafts
[env]$ guix build --load-path=./ --check --no-grafts calcurse-futurile


# OR: delete the build from the store and do a full rebuild with --no-grafts
[env]$ guix gc --delete /gnu/store/zzaip31pjqfhlg5zx1992v59lyvbmbs9-calcurse-futurile-4.8.1-futurile
[env]$ guix gc --delete /gnu/store/3c9rfznc770din5687gpvrjh54lnx5ls-calcurse-futurile-4.8.1-futurile

[env]$ guix build --load-path=./ --no-grafts calcurse-futurile

The Perfect Setup (VSCode)

At this point, we should talk about tools and the development experience when working with Guix packages.

The Guix manual describes The Perfect Setup as being to use Emacs for Guile and Guix hacking. There's no-doubt that the Emacs tools for Guile are the most advanced, so if Emacs is your bag go for it.

Screenshot of a video rebuilding the Guix Cbonsai package

Figure 2 (click to enlarge!): installing Scheme LSP

If you don't use Emacs already or don't have a strong preference for an editor then I suggest Visual Studio Code as an easy on-ramp. While I personally use Vim - for getting started VS Code is easier. The installation process is well-documented.

The main extension I'd suggest is Scheme Language Support for Visual Studio Code by Allen Huang which will set-up colours for keywords. Even more importantly it uses colours for brackets which makes it easier to see when they're balancing.

Screenshot of a video rebuilding the Guix Cbonsai package

Figure 3 (click to enlarge!): set the default scheme

There is also a scheme-lsp-server by rgherdt. To use this there's two parts, the vscode-scheme-lsp client and the lsp-server. There's two ways to install it:

  • install the VSCode Scheme LSP extension from within VSCode. This extension also offers to install a local copy of the scheme-lsp-server. I couldn't get this to work (see Figure 2) and instead did the second step.
  • Separately, install scheme-lsp-server. I had to do this as I couldn't get the integrated option to work.

Here's how to install scheme-lsp-server:

$ cd workspace
$ git clone https://codeberg.org/rgherdt/scheme-lsp-server
$ cd scheme-lsp-server

$ guix package -f guix.scm
$ GUIX_PROFILE="/home/steve/.guix-profile"
$ source "$GUIX_PROFILE/etc/profile"

$ guile-lsp-server --help

When we start VSCode and open our .scm file we'll get a message that the LSP server has been started. Make sure that it's configured for Guile Scheme (see Figure 3) with schemeLsp.schemeImplementation.

There are completions and snippets - for example if you see some code and do <Ctrl><Space> it will show you the completion options

Guix inherit resources

Here are some good resources that explore using inheritance in Guix packages:

  • A deep dive into Guix records
    Julien Lepiller's very well written coverage of Records and Guix's specific use. Read the Guile manual's coverage at the same time!
  • Guile Records
    Guile Records (SRFI-9 records) are built into the language. Note that Guix's implementation uses elements of Guile's SRFI-9 records with some syntactic additions (which come from SRFI-35). This is a long-winded way of saying read the default section, then read Julien's post.
  • inherit vs package/inherit
    There are two ways to inherit, plain inherit and package/inherit. The latter is better as it works with Grafts, but it's much more limited.

That's all folks!

We've covered the basics of creating package variants through inheritance in this post. Next time we'll look at the overall structure of recipes and delve into how we get a package's source.

I've noticed that some of these posts are becoming very long! In some cases they feel less 'how-to' and more 'introductory text book on X' - hopefully, this post was not too shallow, and not too deep - but just right! I'm interested in your thoughts, or if there are topics you'd like me to cover, reach out on email or @futurile on Mastodon.


Posted in Tech Friday 12 January 2024
Tagged with tech ubuntu guix