Logo for my website.

Nemin's Blog

Guix System - One Month Later

2026-03-02

Tags: programming, guix

Table of Contents

It's been about a month since I installed Guix System on my PC and now that I had some time to play around with it more seriously, I figured I'd record my additional thoughts, positive and negative, and why I ultimately chose not to stick with it.

1. Community

1.1. IRC

I am happy to report, that the IRC chat of Guix is both surprisingly active and very welcoming. I was able to join in and participate in discussions as a complete newcomer and I never once felt like people were ignoring me or valuing my opinion less due to my newness. This is excellent and a high standard for any other community to strive for.

My general impression was that there is plenty of good-hearted banter, people helping each other figure stuff out, people discussing their configs and such. Hell, I even got a few shout outs for my previous post, which was super encouraging as a new author.

1.2. Mailing lists

I feel like it's much harder to engage with Guix discussions on a longer-term scale. By this I mean that there are a few mailing lists, which seem quite active, but as someone who grew up after mailing lists were cool, they're a bit intimidating to me. This doesn't mean I cannot find my way around (with some effort), but it's definitely a barrier compared to having a conventional forum.

However, I recognise that this is a me / my generation problem and that likely for many others forums would prove just as cumbersome. Mailing lists are a very common thing in GNU/FSF related projects and, without even looking at the larger GNU projects and just by perusing the Guix archives, it's very clear that they're an effective way of communication, that requires no account and is compatible with a ton of configurations.

I just wonder if there could be a better way of handling them, that doesn't feel as daunting to people who didn't grow up with this form of communication. I think even something as simple as a bit of CSS applied to the online interface of the mailing list could make it immediately a lot more appealing, but perhaps I'm simply being shallow.

1.3. Subreddit

r/GUIX is not an official community, but it doesn't seem bad. People generally seem to only post questions, with the exception of the occasional screenshot of their config. It is a little barren, with only a couple posts a month, but Guix System is a niche distro and most communication happens on either the mailing list or IRC (or on the Codeberg issue tracker), which leaves only a small subset of people who are both interested enough in Guix too want to talk about it, but are also willing to put up with a closed and not exactly sqeaky-clean platform like Reddit.

1.4. Fediverse

Guix has a presence on Hachyderm, which is one of the more known Mastodon servers out there. I have little experience with the Fediverse (I've never even used Twitter much, which is the mainstream equivalent to Mastodon), but I'm glad that it exists. The account seems quite active too, the team running it is diligently sharing Guix-related posts and even makes some posts of their own.

They actually shared my own article on their page, which I'm super happy about. If you're reading this, thank you very much! Interactions like this make me want to look deeper into this corner of the internet.

2. Packaging wows and woes

After my success with packaging WezTerm, I decided to jump into upgrading some other packages too. These upgrades were mostly minor stuff, not really worth their own posts, but they did come with recurring issues and notes, that I figured would be worth collecting here.

2.1. I really love the go-getter attitude of Guix

You want to upgrade a package? Go ahead, do it. With the exception of trying to do something so big that it requires a team consensus or is so huge that it requires a dedicated team (think GNOME or KDE), nobody will say "That's not your authority!". You can just open a PR, get some eyes on your change and that's it.

At the time I was writing this, I was packaging the Odin language for Guix. Apparently nobody did it before me, their compilation guide seemed simple enough, so I went and did it myself. As someone who works in a corporate environment, this sort of freedom is exhilarating, especially since you're not just simply helping yourself, but all the other people who might want to try this language in the future and happen to be using Guix too.

This is very much unlike Nix, where every package has one or more so-called maintainers, who have first say in how a package is developed and upgraded. On one hand, this does provide a bit of accountability. People will feel stronger about keeping stuff up to date and tidy if it's at least superficially "theirs". On the other hand, it's yet another hurdle for those who would like to help out, but aren't yet in the maintainers list and it makes Nix feel more private than it really is.

2.1.1. A side-note about speed

The only real negative part of this whole contributing experience is the speed of reviews. There are PRs open from eight months ago. While such is not unusual in corporate environments (hell, make it years), I believe it's not healthy for a rolling release Linux distro, except in very specific cases (projects of such magnitude that you really need a year-ish time to develop them).

By the time people actually take a look at the PR, the project had moved on and is substantially different from when the PR was opened. At best this will result in headache-inducing merge conflicts, at worst the PR is no longer relevant, because some other development had obsoleted it. While I haven't experienced such myself yet, I bet it'd be a pretty disheartening feeling.

Now, I'll freely admit, my PRs so far had generally speedy review processes, so I personally don't have anything to complain about. My point more is that this is not a given thing, but rather something that comes down to the maintainers free time and available effort. And long delays can result from something as mild as some irritation to actual breakages (see the section about Thunderbird of this article).

This is nothing new though. The Guix team itself identified the slowness of reviews as one of the main things people find cumbersome about the project. This presentation is from a year, so hopefully the gears are turning in the background as I'm writing this and soon enough this problem will be a thing of the past. Until then, any prospective packagers should be aware that they are more than welcome, but they should also come prepared for long waits of up to a week or more, before their package is considered.

2.2. Debugging is an unpleasant experience

In my experience, errors messages in Guix are not very helpful. This is not super surprising seeing Guix is a massive build system built on a completely dynamic language, but regardless it makes debugging a real pain in the neck. Which is a shame, because one of the main selling points of Lisp(likes) is the fact that their REPL experience is unparalleled and that all other languages lag behind with their tacked-on / simplistic debuggers.

In Guix' case, while it is possible to use the Guile REPL to interface with the build system, so far I didn't find it useful enough to replace the basic "Edit in editor -> Compile in terminal -> Try to figure out errors" loop.

The three most confusing issues I've experienced so far were:

2.2.1. Missing parentheses

For this first example, I'll include the entire stack trace. In the other two, it will be truncated, but be aware that with each error we get this much on our screen.

  /home/nemin/programming/projects/guix/gnu/packages/guile-xyz.scm:7973:1: unexpected end of input while searching for: )
error: googletest: unbound variable
hint: Did you forget a `use-modules' form?

error: bzip2: unbound variable
hint: Did you forget a `use-modules' form?

error: libusb: unbound variable
hint: Did you forget a `use-modules' form?

error: make-lld-wrapper: unbound variable
hint: Did you forget `(use-modules (gnu packages llvm))' or `#:use-module (gnu packages llvm)'?

error: tcc: unbound variable
hint: Did you forget a `use-modules' form?

error: cross-gcc-toolchain: unbound variable
hint: Did you forget `(use-modules (gnu packages cross-base))' or `#:use-module (gnu packages
cross-base)'?

error: gnu-make: unbound variable
hint: Did you forget a `use-modules' form?

error: tar: unbound variable
hint: Did you forget a `use-modules' form?

error: gcc-toolchain: unbound variable
hint: Did you forget a `use-modules' form?

error: xdgpp: unbound variable
hint: Did you forget a `use-modules' form?

error: cross-binutils: unbound variable
hint: Did you forget `(use-modules (gnu packages cross-base))' or `#:use-module (gnu packages
cross-base)'?

Throw to key `unbound-variable' with args `("resolve-interface" "no binding `~A' in module ~A" (shared-mime-info (gnu packages freedesktop)) #f)'.
Backtrace:
In guix/status.scm:
    842:4 19 (call-with-status-report _ _)
In ice-9/boot-9.scm:
  1752:10 18 (with-exception-handler _ _ #:unwind? _ # _)
In guix/store.scm:
   504:37 17 (thunk)
   1119:8 16 (call-with-build-handler #<procedure 7f6d9e2ab390 at g???> ???)
In guix/scripts/build.scm:
    646:2 15 (_)
   695:42 14 (loop _ _ ())
In gnu/packages.scm:
    512:2 13 (%find-package "haunt-next" "haunt-next" #f)
    392:6 12 (find-best-packages-by-name _ _)
   322:56 11 (_ "haunt-next" _)
In unknown file:
          10 (force #<promise #<procedure 7f6d9e306700 at gnu/packag???>)
In gnu/packages.scm:
   244:33  9 (fold-packages #<procedure 7f6d9d90b778 at gnu/package???> ???)
In guix/discovery.scm:
   158:11  8 (all-modules _ #:warn _)
In srfi/srfi-1.scm:
   460:18  7 (fold #<procedure 7f6d9e3437e0 at guix/discovery.scm:1???> ???)
In guix/discovery.scm:
   148:19  6 (_ _ ())
    115:5  5 (scheme-modules _ _ #:warn _)
In srfi/srfi-1.scm:
   691:23  4 (filter-map #<procedure 7f6d9e343680 at guix/discove???> . #)
In guix/discovery.scm:
   123:24  3 (_ . _)
In guix/ui.scm:
    365:2  2 (report-unbound-variable-error _ #:frame _)
In ice-9/boot-9.scm:
  1685:16  1 (raise-exception _ #:continuable? _)
  1685:16  0 (raise-exception _ #:continuable? _)

ice-9/boot-9.scm:1685:16: In procedure raise-exception:
Throw to key `match-error' with args `("match" "no matching pattern" (unbound-variable "resolve-interface" "no binding `~A' in module ~A" (shared-mime-info (gnu packages freedesktop)) #f))'.

Admittedly in this case it's not too bad, because Guile does actually inform you that a closing parenthesis is missing, but the error is still buried under a load of completely irrelevant error messages.

Additionally, if this happens, guix edit no longer works and you need to manually open the file that contains the issue and find the package. Not a big deal, but a bummer.

2.2.2. Non-existent variables

error: reversion: unbound variable
hint: Did you forget a `use-modules' form?

error: googletest: unbound variable
hint: Did you forget a `use-modules' form?

error: bzip2: unbound variable
hint: Did you forget a `use-modules' form?

<... a lot of other stuff ...>

It happened to me once or twice that I was either still referencing a variable that I previously cut or I made an accidental typo.1

Just like with the missing parentheses, you technically get an error, but it's buried and blends in with all the other completely irrelevant errors (which are actually not even legitimate errors, they're just caused by the process buckling on itself).

Of course, it's hard to blame Guix here. There is no programmatic way of telling if the user was just silly or if there really is a variable somewhere in some yet unused module. But it'd still be so much better if the error stopped right after the first (and only legitimate) missing variable.

Also guix edit breaks as before.

2.2.3. Circular dependencies

This one in my opinion is the most problematic common issue a package author might face. Chiefly because the error message you get in this case is not even useful:

error: gcc-toolchain: unbound variable
hint: Did you forget a `use-modules' form?

error: xdgpp: unbound variable
hint: Did you forget a `use-modules' form?

error: cross-binutils: unbound variable
hint: Did you forget `(use-modules (gnu packages cross-base))' or `#:use-module (gnu packages
cross-base)'?

Throw to key `unbound-variable' with args `("resolve-interface" "no binding `~A' in module ~A" (shared-mime-info (gnu packages freedesktop)) #f)'.

<... a lot of other irrelevant junk ...>

ice-9/boot-9.scm:1685:16: In procedure raise-exception:
Throw to key `match-error' with args `("match" "no matching pattern" (unbound-variable "resolve-interface" "no binding `~A' in module ~A" (shared-mime-info (gnu packages freedesktop)) #f))'.

The untrained packager might start pulling in stuff like gcc-toolchain or xdgpp, not understanding why they even need these or start poking around (gnu packages freedesktop) to no avail. As far as I know, there is no good method for figuring out what is causing a circular dependency beyond good intuition and bisecting your code until you find the culprit.

I've struggled quite a bit with this while packaging Hare 0.26.0 and, while I did manage to solve the issue I faced there, I wasn't extremely happy with the solution:

(build-system hare-build-system)
(native-inputs
   (list scdoc
         (module-ref (resolve-interface '(gnu packages hare)) 'harec)
         qbe))

In this instance, as you may have guessed, harec is the culprit. I'm still not 100% on all the details, but I believe the problem is caused by hare-build-system pulling in the hare module (which harec belongs to) and so when we're trying to import it in the package definition, it can't work out the right order.

Whatever's the reason, the solution isn't super difficult, but it is also not exactly obvious. Firstly, we need to get the module "behind Guix's back":

-- Scheme Procedure: resolve-interface name [#:select=#f] [#:hide='()]
        [#:prefix=#f] [#:renamer=#f] [#:version=#f]
   Find the module named NAME as with ???resolve-module??? and return its
   interface.  The interface of a module is also a module object, but
   it contains only the exported bindings.

Next, we need to extract harec from it:

-- Scheme Procedure: module-ref module name
   Look up the value bound to NAME in MODULE.  Like ???module-variable???,
   but also does a ???variable-ref??? on the resulting variable, raising
   an error if NAME is unbound.

What we're ultimately left with is the same as if we simply imported the module normally and then put harec into the list. The trick here is that the various inputs Guix takes (including native-inputs) are thunked. This means the code inside these forms isn't immediately run, but is instead delayed to a later moment, unlike the build system which is already resolved during the start of evaluation.

In effect, by only ever referring to harec in a thunked environment, we've completely side-stepped the whole "where should we import the module" problem by forcing the package definition to wait.

I'll freely admit that I would have not figured this out had there not been many other packages whose authors have faced this exact problem before and before the kind help of tinystar, who guided me towards targeting the native-inputs instead of my original idea.2

2.3. A cornucopia of styles

Guix is a very flexible system. This is usually a great thing, because the last thing you want is your build system to constrain you from including a package that otherwise fits into the repo. However, this also bring with itself the fact that (beyond what the linting script offers) there is little guidance on how your package should be organised.

This shows up both in small and big things. A good example is that the order of fields in the package definition is arbitrary. People generally put name first, but there is nothing actually stopping you from putting it last. Sure, this probably wouldn't pass peer-review, but with other fields, the situation is far less obvious.

For instance, should you introduce all static metadata first or should you go by the usual order and put arguments before the description and license? How do you break lists aesthetically? Do you use (arguments (list stuff)) or (arguments `(stuff))?

And then there's the even larger differences:

  • Path construction: When it comes to constructing paths,

    (format #f "~a/path/to/bin" #$output)
    
    (string-append #$output "/path/to/bin")
    
    (in-vicinity #$output "path/to/bin")
    
  • Code repetition: When installing completions,

    (with-output-to-file bash-file
      (lambda _ (invoke "binary" "output-bash")))
    (with-output-to-file zsh-file
      (lambda _ (invoke "binary" "output-zsh")))
    (with-output-to-file fish-file
      (lambda _ (invoke "binary" "output-fish")))
    
    (for-each
     (match-lambda
       ((file-name . command)
        (with-output-to-file file-name
          (lambda _ (invoke "binary" command))))
       '((bash-file . "output-bash")
         (zsh-file . "output-zsh")
         (fish-file . "output-fish"))))
    
  • Environment variables: If you need to set environment variables,

    (modify-phases %standard-phases
      (add-before 'build 'set-environment
        (lambda _
          (setenv "VAR1" "VALUE1")
          (setenv "VAR2" "VALUE2")))
      (replace 'build
        (lambda _ ...)))
    
    (modify-phases %standard-phases
      (replace 'build
        (lambda _
          (setenv "VAR1" "VALUE1")
          (setenv "VAR2" "VALUE2")
          ...)))
    
  • Post-processing: If you need your built binary for post-processing (e.g. installing completions, generating extra files, etc.),

    (let ((bin "./build/folder/your-binary"))
      (invoke bin "postprocess"))
    
    ;; Also, do note that the whole previous debacle of which
    ;; path joining method to use still applies!
    (let ((bin (string-append #$output "/bin/your-binary")))
      (invoke bin "postprocess"))
    
    ;; Taken from gnu/packages/rust-apps.scm, "just" package
    (let ((just (if ,(%current-target-system)
                    (search-input-file native-inputs "/bin/just")
                    (string-append out "/bin/just"))))
      (with-output-to-file
                 (string-append bash-completions-dir "/just")
                 (lambda _ (invoke just "--completions" "bash"))))
    

I'm sure there's a lot more, but these are the stuff that I personally experienced. The issue is that for most of these you might feel like one or the other is the obvious answer, but if you scroll through the package definitions, I'd bet money on it that you'd find multiple examples of all of them.

Now, don't get me wrong, I'm not pointing fingers at anybody. Guix is a volunteer project to which hundreds, if not thousands of people contribute to. You cannot expect perfect coordination from so many people, especially volunteers who might just want to see X or Y package in the registry and don't necessarily care about digging super deep into how things work. Not to mention, having ten different similarly good ways of doing something is a known Lisp curse. When you have barely any syntax and very convenient tools to mess with ASTs, all hell breaks loose.

But I think it'd still make sense to codify some basics at least. Have a given order where package fields should go. If someone strays from this, have them justify it with a solid reason. Give pointers on divisive styles like the examples above, providing reasons why Guix recommends one over the other.3

This would serve two important purposes:

  • Firstly, with less noise in the code, it'd be much easier for everyone to study the code, make changes, and review others' changes. Mundane code beats smart code when it comes to working together and with Lisp(likes) one already needs discipline to write "standardised" code.
  • Secondly, with these things codified, newcomers would have one less thing to be stumped by. I was so surprised when during my first PR my usage of string-append was replaced with in-vicinity, not only because I had no idea that this function existed in the first place, but because I was working based on another package where string-append was used and didn't even consider it's not the only option.

2.4. ChangeLog is confusing

I briefly touched on ChangeLog in my WezTerm post, but I wanted to bring it up again, because despite my best efforts at trying to build "muscle memory" for it, I still feel nearly as lost as I did at the start.

Okay, that's not entirely fair. I was able to get the provided Emacs's yasnippet templates loaded, which automate some of the tedium that comes with the ChangeLog commit message style. I've also been made aware that there is a file under etc called commiter.scm, which practically writes all the boilerplate for you.

However, it's not really the tedium that bothers me. If ChangeLog were really just a very rigid and verbose system where each possibility is set in stone and obvious from a glance, I probably wouldn't mention it or, hell, maybe even would have put it in as a positive.

But that isn't the case, because where these templates cannot help is the nuanced parts. It is not entirely obvious how involved you need to make your commit messages and, even from my very limited experience, it differs greatly between maintainers who accepts what.

For instance, with Rust applications you are expected to import all dependencies into the registry. So far so good. But how does one reflect this in their commits?

For one, you must include your note about the crate import in the same commit as the one that introduces your package. This goes against the usual convention where each change gets their own commit, but (while I don't remember it mentioned in the guide) it's obvious enough to figure out based on a quick git log.

What's far less obvious, however, is how to phrase this import. To keep everyone in the loop, an entry in a ChangeLog-formatted commit message looks like the following:

  • An asterisk to denote a new entry,
  • The name of the file that was edited,
  • Then the name of the variable / function that was edited in parentheses,
  • Then, only if what you were editing is a struct with multiple fields, the name of the field in square brackets,
  • Then, if said field has its own sections, the name of the section in curly brackets,
  • Finally, after a set of colons, you add a short textual description of what you've done.

For example:

* foo/bar/baz.scm (example-struct)[section]{subsection}: Frobnicated some walruses.

Now that we're on the same page, let's try to construct a line like this for our Rust imports. The start of the line is easy: * gnu/packages/rust-crates.scm. But what comes right afterwards and especially in the description field is nontrivial and wildly varies between authors.

From what I can see, there are two bigger schools of thought:

* gnu/packages/rust-apps.scm ($PACKAGE): Init at $VERSION.
* gnu/packages/rust-crates.scm (lookup-cargo-inputs): Added imports.

This variant only records the essentials. "We're handling $PACKAGE, which is a Rust application" and "We've imported a given number of crates". The pros of this is that it's extremely short and simple, and it conveys intent without going into the details. The con is that it doesn't mention anything about which crates exactly were imported.

And that is exactly what the second variant focuses on:

* gnu/packages/rust-apps.scm ($PACKAGE): Init at $VERSION.
* gnu/packages/rust-crates.scm ($CRATE1, $CRATE2, $CRATE3, ...): New variables.

This is the polar opposite of the first version. Very specific, clearly spells out what crates were imported into the registry and at what version, and fits more into the usual commit message schema.

On the other hand, it occasionally produces monsters like this:

cargo_imports.avif

Figure 1: This goes on for four more pages, by the way.

In my personal and, indeed, just as subjective opinion, Variant 1 is the clear winner. The point of a commit message is to convey intent or, as the ChangeLog manual puts it:

ChangeLog Concepts: People can see the current version; they don???t need the change log to tell them what is in it. What they want from a change log is a clear explanation of how the earlier version differed.

Yes, spelling out the whole list is "clear" in a certain sense, but since these crates were imported by a script and (beyond a couple of cases) aren't really touched by human hands, I believe the only relevant thing worth recording is that the importing process happened.

But, while I have laser-focused on a single pain area, it's not the only place where disagreements like this happen. I've been given recommendations to rephrase several of my commit messages and it's not that I'm lazy to do this (in fact I've followed this advice most of the time), I just feel like the system is both rigid enough not to be very pleasant to use, but also loose to be truly automated.

Again, Guix is a very flexible system, so maybe this "looseness" is actually a sheep in wolf's clothing. Better this than constant overrides in case the rigid system doesn't quite cover all the needs of maintainers.

3. System ups and downs

While the previous section was about my experience with packaging stuff, this will be about using others' packages.

3.1. Stability of my config

One thing I found interesting is how stable my config files ended up being while using Guix. Beyond the occasional addition to my home configuration, I found that I didn't have to touch my system config file at all.

While this is obviously partly because I already experimented a bunch on NixOS before finding the right subset of applications that I need to be productive, I also feel like Guix is generally the less fiddly system of the two (in this particular aspect).

Even more notably, I haven't had to touch Shepherd, Guix System's service manager once yet. Everything Just Works???, including changing over to PipeWire (my preferred audio server), which is both very well documented and only required a couple of lines in my home config:

(service home-dbus-service-type)
(service home-pipewire-service-type)

If everything was this simple in the Guix world, I'd have nothing to complain about. This is a gold standard.

3.2. Interfacing with my phone

I wanted to move some files from my phone to my PC. On every other distro I've tried before, all this took was plugging it in and clicking on "Mount filesystem". Or, perhaps if it was a more frugal distro, I had to issue lsblk, find the right disk drive, and manually mount it at a folder of my liking.

android.avif

With Guix System, I was unable to do this. I plugged my phone in, KDE Plasma offered to mount it. I clicked on it and all I got was a really confusing error. Finally, after quite a bit of digging on Google, I found a topic on the Arch Linux forums where someone mentioned kio-extras and kio-fuse. By adding these to my profile, the phone was finally able to connect.

This particular instance wasn't a huge ordeal, but it is an annoying paper-cut, that makes Guix System feel slightly less polished. I believe the existence of a "Guix Wiki" (in the spirit of the Gentoo and Arch wikis) would help a lot in situations like this.4

3.3. Blunderbird

I generally use Thunderbird (or, since Guix doesn't provide Mozilla-branded binaries, Icedove) to handle my mail. It's a far more pleasant process in my opinion than constantly having a tab open in your browser.

Well, as it turns out an update recently broke Icedove, because it implicitly relies on the other Mozilla-packages being the same version. Because of this, none of the build servers were able to build the package. This ultimately resulted in me being unable to update my system (without removing Icedove), because the moment I tried, Guix would notice that there is no pre-built package and began building it on my PC.

This would be fine in most other cases. I do have a 12 core Ryzen CPU and 32 GBs of RAM,5 so it's not like I don't have the hardware usually. However, in this particular instance, even such mighty specs failed to cope with all of Icedove's dependencies (I believe it's webkit-gtk that fails specifically, but I'm not 100% sure), so after my PC crashed twice trying to update, I decided to try to pursue different avenues.

My first attempt was using GNOME's Evolution mail client. No luck. No matter what I tried, it couldn't connect GMail using OAuth2 and all the errors I've got were very cryptic.

Fine, whatever, I use KDE Plasma anyway, so why not give KMail a shot? I installed KMail and?

kmail.avif

Well, that's annoying. Neither the link, nor the button worked. I tried looking up what's going on and was led to believe the issue is caused by another dependency named akonadi missing. It's packaged in Guix, so I added it to my profile, reconfigured the environment, restarted KMail and? Nothing. Still the same error.

A bit more investigation later, I found that running akonadictl status should give me a bit more insight:

akonadi.avif

So it's unable to connect to MariaDB (which I've also added to my profile off-screen). At this point I was kind of stuck and abandoned this experiment.

I don't know, maybe there was an easy solution and I was merely a step away from making KMail work, but manually pulling in 5-6 dependencies for an application on a declarative distro is kind of above my tolerance. I get that not everything can work out of the box, but this style of debugging fits Slackware6 a lot more than it does a modern Linux distro.

Well, with the obvious alternatives ruled out, I was left with figuring out what to do with the Icedove situation. At the time I started writing this article, the fix was yet unmerged7 so there were only a couple of options someone who uses the package could take:

  1. Not update:
    • Pros: Requires no actions.
    • Cons: No upgrades until the situation is resolved.
  2. Remove icedove from your profile:
    • Pros: Very simple to do. You get other upgrades for everything else.
    • Cons: You have to use another method to manage your mail. As seen above, if you don't want to use the browser route, this isn't necessarily an easy process.
  3. Use an Inferior: (a.k.a. instruct Guix to pull in an older channel's package)
    • Pros: You still have Icedove (even if it's an older version) and upgrades for everything else.
    • Cons: Requires tracking down the last commit that still worked, setting up the inferior, and then eventually removing it once the upgrade is done.

Ultimately I chose option #3 and made an inferior for Icedove. In this instance, it was thankfully super easy to find the last good commit, because someone else already did the dirty work. Despite the warning in the manual, it didn't even take particularly long to compute the new Guix derivation.

On one hand, I suppose it's another praise towards Guix's flexibility to be able to "patch" the package lookup with an arbitrary older (or even completely separate) channel. Doing so on a traditional, imperative package manager would be a giant pain.

On the other hand, it's an unfortunate situation to see a mainstream application breaking people's systems and no urgent action being taken for nearly a week, when a PR is already ready and waiting.

This is, of course, the blessing and curse of a volunteer project. Just as you can jump into any task that you fancy, nobody is obligated to check out your work, which can (in unfortunate cases, such as this) cause disruptions.

For some more positive news, this seems to be going to be at least partly addressed soon as Guix is considering a grant system, which would financially support certain developers working in key areas.

3.4. NVIDIA drivers and non-LTS kernels

Massive disclaimer here: This is a Nonguix "issue". And in-fact it's not even an issue, it's more just a case of "things could be explained much better". Still, it was something that caused me quite a bit of pain and if I can spare only one other person from experiencing it, it was already a win in my book.

After one of the updates, my system started being unable to properly start up my desktop environment. I was able to get to GDM, enter my login info, press login, and then it'd either hang there or I'd get my windows stuck in the top left corner, without any of the usual Plasma UX decorations.

Thankfully, as Guix is a generation-based declarative distro, I was able to roll back to an older version and have a working PC, but I found it really annoying that no matter how much I upgraded my system, it never quite seemed to help.

I was quite close to giving up and going back to Nix or some other distro, when I finally found a lead. By reading the dmesg logs, I found out that the problem was the kernel module not finding certain functions:

[   95.122493] nvidia: loading out-of-tree module taints kernel.
[   95.122503] nvidia: module license 'NVIDIA' taints kernel.
[   95.122507] nvidia: module verification failed: signature and/or required key missing - tainting kernel
[   95.122508] nvidia: module license taints kernel.
[   95.595636] nvidia-nvlink: Nvlink Core is being initialized, major device number 508
[   95.596880] nvidia 0000:01:00.0: enabling device (0406 -> 0407)
[  101.074782] nvidia_drm: Unknown symbol nvKmsKapiGetFunctionsTable (err -2)
[12407.716914] nvidia_drm: Unknown symbol nvKmsKapiGetFunctionsTable (err -2)

While this didn't quite solve the issue yet, it gave me a vital pointer, leading back to the Nonguix README. I gave the NVIDIA section a closer look and lo, the culprit was found:

nonguix-transformation-nvidia [#:driver nvda]
                              [#:open-source-kernel-module? #f]
                              [#:s0ix-power-management? #f]
                              [#:kernel-mode-setting? #t]
                              [#:configure-xorg? #f]

Return a procedure that transforms an operating system, setting up
DRIVER (default: nvda) for NVIDIA graphics card.

OPEN-SOURCE-KERNEL-MODULE? (default: #f) only supports Turing and later
architectures and is expected to work with 'linux-lts'.

S0IX-POWER-MANAGEMENT? (default: #f) improves suspend and hibernate on systems
with supported graphics cards.

KERNEL-MODE-SETTING? (default: #t) is required for Wayland and rootless Xorg
support.

CONFIGURE-XORG? (default: #f) is required for Xorg display managers.  Currently
this argument configures the one used by '%desktop-services', GDM or SDDM.

Use 'replace-mesa', for application setup out of the operating system
declaration.

Can you see it? If not, here's the line of note: "and is expected to work with 'linux-lts'". Yup, that's all the warning you get that "if you use the usual linux package as your kernel, it can crash on you at any time."

Obviously, this doesn't mean that the Nonguix team hasn't done their due-diligence. After all, the solution is there. But I definitely think it'd be worth a stronger emphasis on the fact that you can and will break your install if you don't use the LTS kernel with a newer card / open-source driver.

I'm also certain I'm not the only one who fell for this as there is already an issue on Nonguix' Gitlab, where the ticket opener's phrasing strongly implies they had to figure out the linux-lts solution themselves as well.

3.5. No easy way of seeing what is updated

One thing I sorely feel is missing (though Nix is no better in this regard) is the fact that there is no easy built in way to see what upgrading will do to your system.

There are approximations, sure. You can either pass --dry-run to Guix and get something along the lines of work items. There is also a new project by LiterateLisp's author Wilko, which allows you to diff two derivations, which is super cool and I hope one day it'll be upstreamed into Guix proper, but (unless I massively misunderstood things) it's a tool for after the fact. That is to say this only tells you what changed once the update is done.

Now, of course, there is an argument to be made that, due to the way declarative distros work, you don't really have to care what's what version and it's not a huge deal to just roll back if you're not happy with the changes. But every imperative distro manages to give you a list of "here's what's gonna happen" and give you a choice if you consent to the upgrade or not, and I really miss it from here.

3.6. Sleep causes crashes

I don't know if it's me doing things wrong or the driver not being good enough or something completely different causing it, but I am unable to use the Sleep function of the PC. The moment I press it, the PC freezes and requires a hard restart.

What this means is that, if I know I need to go away for some time, I either leave things on (which wastes a lot of electricity) or I turn my PC off (not a huge deal, but a bit more inconvenient than just continuing from where you've left off).

It's not exactly a vital feature, but all previous distros I've used had this working out of the box, so Guix System struggling with it is really unfortunate.

3.7. Odd freezes and crashes

I cannot effectively quantify this, but Guix System doesn't really feel stable in my experience. In this ~1 month of use, I've experienced at least 4-5 spontaneous freezes and choppy performance while not doing anything particularly strenuous on the PC, which does not inspire confidence when it comes to working uninterrupted.

I had freezes during listening to Youtube, browsing files in Dolphin, writing text in Emacs. Usually these issues went away on their own after a while, but not only does it make me worry that something (physical or digital) will end up damaged, I also get completely thrown out of my flow state.

I definitely can't rule it out that some (perhaps even all) of these were caused by the notoriously finicky NVIDIA drivers. But it is the unfortunate reality, that I cannot swap GPUs for the sake of a distro, no matter how much I like it and none of the other distros I've tried had produced instability this bad before.

4. Conclusion

On a conceptual level, I love Guix and Guix System. I much prefer writing Scheme over Nix's language and I really like that there is far lesser of a divide between the builders and the package definitions.8

I also love the low barrier of entry when it comes to contributing to the ecosystem. I'd wager Guix might be one of the easiest distros to have packages accepted for, if you have a bit of Scheme experience and are willing to oblige the maintainers.

But, and I'm sad to say this 'but', both still feel unpolished to me. Ever since I've grown accustomed to Linux in a way that no longer makes me want to distro-hop, my journey was always about finding a distro that maximises stability and package freshness, and settling for it.

Guix System sadly neither fulfilled the stability (due to all the crashes), nor the freshness (due to many of the packages lagging far behind upstream) in the way I was hoping for and I am unlikely to stick with it much longer. I will likely still have Guix present on whichever distro I go back to, but for now I'm buying out of the GNU dream.

Now, I don't want to sound entitled and talk down a project I'm otherwise very much aligned with. I recognise the gargantuan effort that went into getting things this far, especially considering that Guix isn't just aiming towards x86 and ARM, but also to be a host and accelerator for HURD. I also recognise and respect that most of this effort was accomplished with no monetary gain expected and on what is considered (at least in corporate terms) a shoestring-budget.

Even more importantly, things aren't happening in a vacuum and, while Guix's current state didn't prove to be quite what I was looking for, it doesn't mean it's not going to continue developing. Not only are they pulling in fresh funds to fund infrastructure and projects, there's also a lot of ideas floating around how to make things better.

I will only bring one example, but it will be comprehensive: The Shared Cryptpad of Guix Days 2026. This is the loose log of a couple of brainstorming sessions of Guix veterans and core developers, which touches on a massive amount of potential ways to make Guix more approachable including:

  • How to manage secrets declaratively (this is not simple even on Nix),
  • How to communicate between the teams better,
  • How to introduce breaking changes in a way that won't cause pain for everyone,
  • Bringing a new GC to Guile, that should speed everything up (especially multi-threaded code),
  • What they could learn from Nix,
  • How to make the substitution situation better,
  • How to automate PRs,
  • How to make package breaks rarer and how to respond to them quicker,
  • And (this was the one I found most interesting) thoughts about breaking with the GNU hardline: The possibility of enabling installing firmware in the installer, breaking the self-imposed wall between Guix and Nonguix, and even vaguely alluding towards the option of dropping the GNU label as a whole, while still retaining their best qualities and the commitment to free software.

The list goes on. Obviously, all of these exist only as ideas and plans so far, but the important part is that the maintainers aren't sitting on their laurels and doing nothing. They're smart people trying their best to make Guix an even better experience than it already is.

I wanted to end my post on a positive note, especially since much of my post has been anything but. Even though Guix System as it is today didn't quite live up to my expectations, I have no doubts that in a couple years time, if the project continues with this fervour and dedication, it will be a very serious contender. Perhaps still a niche one, because its commitments to Scheme and only free software severely limits its audience, but a well-polished and incredibly powerful system for those that can live within these bounds.

In fact, I've read some comments of people who have been using Guix System for over a decade. You cannot get that amount of dedication, if you yourselves (i.e. the maintainers) aren't dedicated yourself, and Guix's clearly are. I wish them nothing but the best and it's not like I'll completely remove myself from the project's orbit, as I still have a couple packages I'd love to see merged and I'm still interested in the project's future developments.

Thanks for reading!

Footnotes:

1

At work I happen to use Django, which has an extension called "reversion". It allows reverting objects in your database to a previous revision. Pretty clever name, but it burnt into my mind so much, I accidentally mixed up the two.

2

I immediately went gung-ho and put both harec and qbe (which doesn't even cause a dependency issue) into hare-build-system.

In hindsight, while I can justify my actions (my reasoning was that harec and qbe are used by the compiler, therefore it makes sense to have them present in the build system), it wasn't the right course of action.

Especially because this issue only really came up with a single package. Just like with any other project a big blast radius has to make sense. In this case, it didn't.

3

Obviously the best would be if the guix lint script was able to identify and fix these, but let's be real, making a script to rewrite an arbitrary input to something that conforms to a given output is a fool's errand.

4

In the strictest sense there is already one or rather two. There is Libreplanet, which has a Guix page, but it's pretty much only used by direct maintainers for storing information that doesn't directly belong in the Guix Manual. For the sake of completeness, there is also the System Crafters Wiki, which at the time of writing hasn't seen any contributions in two years.

5

Before you ask, my toilet isn't made from gold. I bought my sticks before the price increase for a third of what they cost now? I'm not rich, just lucky.

6

No disrespect towards Slackware or its users. It's a very cool distribution and the fact that it is still going strong is nothing short of impressive. But it's "do it yourself" approach is exactly the opposite of what I'm looking for in a declarative distribution. In an ideal world, your entire setup would be a couple files of Scheme code and you'd practically never have to "figure things out".

7

The fix was ultimately merged after six days and I have since removed the inferior from my install.

8

In Nix, you're expected to write Bash snippets in your build stages with some rudimentary substitution of variables provided by the system. This works, but is (in my opinion) far less ergonomic or elegant than Guix's all or nothing approach to Scheme.