What goes into Guix-shaped hole?

2024-09-04

Did you ever try to customize system-wide Guix? I tried. It did not go well.

Why is it useful? It is not, on my laptop. However on servers it allows access (e.g. guix shell) to up-to-date packages and extra channels without having to do guix pull. That saves time, a lot of time. I like saving time.

Now, how to actually do it? guix-configuration does have guix field that can be used for this. Well, this was short blog post, problem solved, we can go home (I work from home, so I am staying in my chair, but you can go away).

Just kidding. You see, when you just naively follow the documentation above and use guix-for-channels, it does not cache the result. What does that mean? Build the following code. And do it twice.

(use-modules (guix channels)
             (gnu packages package-management))

(guix-for-channels
 (list (channel
        (name 'guix)
        (url "https://git.wolfsden.cz/.git/guix")
        (commit
         "b03eddc326ee4eb26b25743faee2080de6aded7e")
        (introduction
         (make-channel-introduction
          "028e445a2028068e3c83996daa281057f19141a0"
          (openpgp-fingerprint
           "B783 49B3 8C14 7D36 2988  68A4 2FBF EE7D B67F C1A9"))))))

I spare you the copy pasting. Here is the result:

$ guix build -f y.scm
Updating channel 'guix' from Git repository at 'https://git.wolfsden.cz/.git/guix'...
Computing Guix derivation for 'x86_64-linux'... /
/gnu/store/98xr1qahb7z60l50l7mfd54zvll9b01r-profile
$ guix build -f y.scm
Updating channel 'guix' from Git repository at 'https://git.wolfsden.cz/.git/guix'...
Computing Guix derivation for 'x86_64-linux'... \
/gnu/store/98xr1qahb7z60l50l7mfd54zvll9b01r-profile

Despite the channel having commit specified, it pulled every time, Guix derivation was computed every time, all just to produce the exact same profile. If I was deploying just to a single machine, maybe this would fly. But no, I am not willing to spent so much time (and electricity) on every deploy. There has to be a better way.

Let us check guix-for-channels, maybe we can make it cache?

(define-public (guix-for-channels channels)
  "Return a package corresponding to CHANNELS."
  (package
    (inherit guix)
    (source (find guix-channel? channels))
    (build-system channel-build-system)
    (arguments
     `(#:channels ,(remove guix-channel? channels)))
    (inputs '())
    (native-inputs '())
    (propagated-inputs '())))

Hm. There is not much to work with here. We can see that it accepts multiple channels, but we already set the commit, so not much more we can do. Onward to channel-build-system we go. Interesting bits are:

(instances
 (cond ((channel-instance? source)
        (return (list source)))
       ((channel? source)
        (latest-channel-instances*
         (cons source channels)
         #:authenticate? authenticate?))
       ((string? source)
        ;; If SOURCE is a store file name, as is the
        ;; case when called from (gnu ci), return it as
        ;; is.
        (return
         (list (checkout->channel-instance
                source #:commit commit))))
       (else
        (mlet %store-monad ((source
                             (lower-object source)))
          (return
           (list (checkout->channel-instance
                  source #:commit commit)))))))

One thing to notice that the extra channels (passed as #:channels argument, here available as channels) are processed only if source is a channel. Otherwise they are silently ignored. Ugh.

So, either I can pass channel type and have extra channels (but we already know that does not cache) or I can (maybe) get it to cache, but without the extra channels. Not exactly what I need.

I got bit desperate, I have even considered stealing (*cough* borrowing) code from guix time-machine. But then I had a brilliant realization. I already have a Guix profile I want to deploy. The very one that is running the deploy.

Lo and behold, here is my masterpiece. Try not to get eye cancer.

(use-modules (guix build-system trivial)
             (guix gexp)
             (guix packages))

(define (%current-guix)
  (let ((guix-bin (car (command-line))))
    (unless (string-suffix? "/bin/guix" guix-bin)
      (error "Does not look like guix binary" guix-bin))
    (let* ((guix-profile (string-drop-right guix-bin 9))
           (guix-profile (canonicalize-path guix-profile)))
      (package
        (name "custom-guix")
        (version "1")
        (source #f)
        (build-system trivial-build-system)
        (arguments
         (list
          #:builder
          #~(symlink #$guix-profile #$output)))
        (home-page #f)
        (synopsis #f)
        (description #f)
        (license #f)))))

It works for simple deploys of my main system(s). It even works for slightly more complex deploy involving time-machine and shell:

export GUILE_LOAD_PATH=.
guix shell -m manifest.scm \
     -- guix time-machine -C channels.scm \
     -- deploy "$@"

The order matters. Swap guix shell and guix time-machine and you will deploy wrong system-wide Guix.

Yes, this is completely insane. Yes, this works. I am sure it will blow up into my face later. ¯\_(ツ)_/¯

So, the answer to the original question is symbolic link. Apparently symbolic link goes into Guix-shaped hole.

Please please please tell me there is a better way.

(눈_눈)

Update 2024-12-08

I switched the definition above to be a procedure instead of a variable. That allows loading the file from Guile REPL.

Additionally I have observed (and reported) this bug, which was pretty annoying to debug. I still install the (%current-guix) on remote machines where I do not ever invoke guix pull, but I stopped on my laptop. Hopefully the bug will get fixed one day.