💾 Archived View for michal_atlas.srht.site › posts › g-exp.gmi captured on 2024-05-10 at 10:34:18. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2024-02-05)

-=-=-=-=-=-=-

G-Expressions are one of the more difficult edges one needs to understand when learning Guix more into depth, especially because their errors can be slightly cryptic and as it may be, usually the more different explanations the merrier for difficult subjects, since maybe the next one is the one for you that’ll make it click.

G-Exps are described in the manual as a separation of “host” code and “build” code, which serves as a good idea / definition, but helped me less with understanding how to actually use them.

Quick prelude

You should basically never ever need to issue these commands but they help with exploration and understanding:

(import (guix store) (guix gexp) (guix derivations))

Returns a derivation which if printed includes a file with some interesting paths to look at: scheme (with-store store (run-with-store store (gexp->derivation "somename" <GEXP>))

Builds the G-Exp: scheme (with-store store (build-derivations store (list (run-with-store store (gexp->derivation "somename" <GEXP>)))))

Shows you the output paths if, you don’t want to scavenge through the .drv: scheme (with-store store (derivation->output-paths (run-with-store store (gexp->derivation "somename" <GEXP>))))

Why do we need to separate that at all?

So, consider you have a package, of any sort, and all it does is to provide a file, with the words “Hello, G-Exp”.

So, how would I do that?

You probably want a universal way to state a build process, and express writing text to a file in that. Then just pass that to the builder.

What might be useful is to just have a lambda, something like:

(lambda (output)
  (with-output-to-file output
    (lambda () (display "Hello, G-Exp"))))

and if we save that to builder.scm then running:

((load "builder.scm") "/gnu/store/...-output.txt")

will perfectly yield what we want.

Success!!! Yes, for now.

So it’s a pretty good idea to just have a build script, that the daemon can leverage, it also makes other things such as isolation simpler, since you don’t have to interact with the user process at all, when building, just your own little script and nothing else.

It also prevents the user from causing strange shenanigans, by having some module loaded, which then doesn’t get picked up by the build daemon.

Why do I need anything else?

Say you’d like to include some variable in your build script,

(let ((myvar 42))
  (lambda (output)
    (with-output-to-file output
      (lambda () (display myvar)))))

Well, if we just write the lambda to the file, then the lambda won’t have any knowledge about myvar.

But we can’t just include the entire file, because what if myvar is from a module, or something, and how would we know which part to run anyways…

Here comes the G-Exp

(import (guix gexp))

(let ((myvar 42))
  #~(with-output-to-file #$output
      (lambda () (display #$myvar))))

Okay, so, the new syntax shouldn’t be too strange, you may consider it quoting for the builder, and unquoting for you to process.

Interestingly, unquoting output here causes you to implicitly get an output path you should occupy, with something to even be considered a successful build.

You’ll get a failed to produce output path otherwise.

This acts exactly as the output procedure argument we had above.

How is this really different though? Well if you’d look at the emitted build script, the file contains the following:

(with-output-to-file ((@ (guile) getenv) "out")
  (lambda () (display 42)))

myvar has been completely substituted into our expression, so there’s no need to capture the environment, since the value is verbatim written out in the file.

Remember that this is a simple write of the scheme value, there’s not that much extra trickery so far. This tripped me up a lot before I realized this:

If I only have scheme (let ((myvar '(1 2 3))) #~(display #$myvar)) it does a simple substitution and ends up with running: scheme (display (1 2 3)) which is obviously no good, and errors out.

What you’d have to do is scheme (let ((myvar '(1 2 3))) #~(display '#$myvar)) this should become quite straightforward to you though, after a while if you know what G-Exps actually do with your stuff.

If you ever want to combine multiple G-Exps into one, you can simply unquote a G-Exp in another G-Exp, imma tell you right now why that’s more awesome than you think.

But what about packages!!!

Oh, of course, a Guix user must of course want to interact with his packages.

What we saw above unquoted is officially called a G-Expression input. These may take many forms, but we started with the general case, that basically just pastes the value, happens to also be the most boring version.

What if I happen to unquote a package?

#~(with-output-to-file #$output
    (lambda () (display #$guile-3.0)))

the unprintable version of the G-Exp might show up something like this:

#<gexp (with-output-to-file #<gexp-output out> (lambda () (display #<gexp-input #<package guile@3.0.9 gnu/packages/guile.scm:326 7f8805826580>:out>))) 7f8802564db0>

Here you see that it keeps some info about the expression you put in, but then there’s a gexp-input with the contents of package guile. This is awesome, because this G-Exp just recorded guile as a dependency.

Dependency???

YES.

If you check the built file it contains a path: /gnu/store/...-guile-3.0.9, and you actually have reign over that path, it is guaranteed that this package got built before your G-Exp evaluates, and you may access any files therein.

I mentioned composing G-Exps, how would that work?

If I have scheme (define foo #~(list #$guile-3.0 #$sbcl)) and then look at the G-Exp scheme #~(list #$foo) I get this representation: scheme #<gexp (list #<gexp-input #<gexp (list #<gexp-input #<package guile@3.0.9 gnu/packages/guile.scm:326 7f8805826580>:out> #<gexp-input #<package sbcl@2.3.5 gnu/packages/lisp.scm:433 7f88054c2d10>:out>) 7f88049de8a0>:out>) 7f8803891b40>

You might notice that all the packages get picked up, and I’m safe knowing that when the G-Exp evaluates, Guix made it so that the unquotes are replaced with strings containing existing store paths, that are built and fetched accordingly.

Where can I use this?

These things are quite ubiquitous throughout Guix. Build phases, mcron jobs, shepherd services, and other config files all often use G-Exps for their definitions and configuration.

You have file-like objects (such as plain-file, local-file, …), which unquote to store paths created from local expressions and files of your choosing.

It’s also useful for ad-hoc scripts in your configs, for example my Sway config is a fest of G-Exps and packages, scheme ,@(sway-exec-bindings `(("u" (,(file-append foot "/bin/foot") ,(program-file "unison-wait" #~(begin (system* #$(file-append unison "/bin/unison")) (sleep 5)))))) ...)

This without the need for anything to be installed, ensures that when $mod+u is pressed foot opens and synchronizes my files with unison, then waits a bit so I can read through the output.

Guess how the config file is specified? Yet more G-Exps.

The added advantage is that once emitted, this config always works, these paths shouldn’t be garbage collected without the config files, the config files will always call the exact binary they were created with, and there’s no chance you’ll override or uninstall something accidentally.

Huzzah.