How to Guix for those who don’t


This introduction to Guix-SD will start with high level concepts, break them down into tiny pieces, and use the pieces to build a minimal kiosk (single application) system. Hopefully once you’ve read this you’ll know enough to understand the rest of the Guix documentation and navigate the source code.

This guide assumes you’re familiar with old school Linux basics (command line usage, building packages, formatting filesystems, setting up locales, etc) and simple programming, but not Guile/Scheme/Lisp.

Guix-SD (which I’m just going to call Guix from now on) is a Linux distribution featuring a declarative config-based setup with everything from base services and system glue to the config written in GNU Guile.

I have a small review here.

Table of Contents

1A definition of operating system

In Guix you have a configuration file that defines your “operating system”. What does that mean exactly?

In Guix, an operating system is basically:

It doesn’t include:

You can also think of your operating system as a program, which is run when the computer boots. The source code for this program is your Guix configuration file.

In this line of thinking, Guix itself is a framework for running your program on physical hardware. A barebones config is a program that runs and does nothing (although by default it won’t just shut down, it’ll sit there doing nothing). Services are the “program”’s entry point: by configuring services your program can do things like serve files, show pretty pictures, so on and so forth.

2The body of Guix

21System configuration

This is a program (.scm Guile script) that returns an operating-system object defining what a system that uses this config should look like.

The guix system ... CLI commands run your script to create an operating system definition and then produce an operating system installation based on it.

22The store

This is where nearly everything Guix manages goes. It’s usually in /gnu/store and read-only. You shouldn’t modify anything in here directly.

The store contains “derivations”, which are essentially independent file trees (or sometimes single files). Derivations are managed atomically - derivations are never modified, and are created/deleted as a unit.

A derivation contains “store items”, or individual files and directories.


When you install a package, Guix creates a new “derivation” based on your installation parameters. If you install the same package with three different options, or three different versions, or for three different target platforms, or whatever, you’ll get a different derivation for each one.

Conversely, if multiple users install the same programs the same derivation will be used for both users.

All of the package’s files are contained within the derivation tree, so there’s no conflict between installed packages.

The Guix CLI is a derivation! So you can have multiple versions of Guix on a system! At the start, everyone will have the same guix, but if one user does guix pull then they alone will have a new version while everyone else continues to use the original version.


Profiles are additional configuration separate from the system config. Typically there’s a root profile and per-user profiles. Profiles are managed by Guix commands - you shouldn’t need to modify them yourself.

A profile roots packages (see garbage collection below) and collects binaries/symlinks from all derivations to add to the profile in a single bin directory (within the profile) so you can easily reference it from PATH.

The root profile exists in /run/current-system/profile.

User profiles are located in ~/.config/guix/current.


Environments are “sub-profiles” - a user can have multiple profiles with separate programs/versions beyond just their default user profile.

Environments are typically ad-hoc - you set up an environment for a specific program you want to run (and it’s as simple as guix environment program) and discard it afterward, but the -r option can be used to name it and make it persistent.

26Garbage collection

All these derivations have dependencies and are depended on, creating a graph which is rooted in your system configuration (and profiles).

If you run guix gc it walks the graph, compares what’s needed (reachable from the roots) to what’s actually in /gnu/store and deletes everything that’s not needed… subject to some conditions. If you never run guix gc stuff will never be uninstalled.

You can additionally add extra roots with various commands. Ex: guix build and guix environment have -r parameters to generate a root, I’m not sure if there’s a general way to do this though.

27Init system, logging, monitoring

Illustrated with a short comparative history

Old systems used runlevels - a bunch of scripts that were run in order to start services when the computer booted. Processes logged to files or the syslog logger, which in turn logged to files.

Newer systems generally use systemd which defines a complex graph of services and their dependencies. Service output goes to the systemd logger which ccan be queried with journald - there are no human readable files. All service state events (starting, stopping, restarting) are sent via dbus so you can create services that listen to the events of other services.

Guix uses a combination of ordered boot scripts and GNU shepherd, which like systemd defines a graph but uses syslog for logging and doesn’t have service state change notifications.

3Guile primer

I’ll be honest, the only Guile/Scheme/Lisp I know is what I picked up to work with Guix, but here’s what I found out after X hours of googling distilled into a 15m primer. This should cover enough to read and use most of the Guix source.

311. Data

All data (at least in this primer) is immutable. Any operations that modify a list or value return a new value and the input is unmodified.

322. Forms and values

A form is basically (space separated elements). How (and if/when!) this is evaluated depends on the context:

In the vast majority of the code a form evaluates as a function call.

(dog apple bat "peanut")

This calls function dog with arguments apple, bat (these two are “names”), and "peanut" (a string literal).

Values are pretty straight forward:

#f is also used to represent “no value specified”/null/nil/none.

You can concatenate strings with (string-append "string1" "string2") and do more complex formatting with:

(use-modules (ice-9 format)) (format #t "format string ~a ~a bye\n" "string1" 17) -> format string string1 17 bye

An expression can be either a form or a value literal.

333. Modules

Sometimes code is separated into different modules. I think this helps with preventing circular imports, namespacing, etc. You can separate code into different files without using modules but you’ll see a lot of modules in the Guix source.

A module is referred to by a series of names. If you look at the module’s source code, you’ll see something like:

(define-module (module name one) ...

You can import it with:

(use-modules (module name one) ...)

All the “exported” (#:export (func1 func2 func3)) names from the module are added to the local namespace.

You can also do (@ (another module name) func1) to import just the exported func1 (it’s not added to the namespace, just returned so you can use it in expressions) or (@@ (another module name) secret-thing) to import just the not-exported secret-thing. You aren’t supposed to use the 2nd version too much.

344. Begin

(begin do various things) allows you to write a bunch of statements, and the value of the last statement becomes the expression’s result. This example would run do various and then things and whatever things evaluates to becomes the result of the begin form.

355. Names

There are two ways to bind a value to a name:

(define name1 value1) (define name2 value2)

Which defines name1 and name2 for the entire scope the defines are in. And

(let ((name1 value2) (name2 value2)) do various things)

Which defines name1 and name2 for the scope of the (let …).

You can combine the two if you’re feeling wild:

(let () (define name2 value2) do various things)

In this case any (define ...) forms need to come before any non-(define ...) forms in the let block.

(let ...) is similar to (begin ...) in that it takes multiple statements after the variable declarations and the value of the last one is the result.

366. Functions

A function is defined as:

(lambda (argument1 argument2) do various things)

Like with begin, the result of calling the function will be whatever the last element, things, evaluates to.

You’ll notice the function doesn’t have a name! But you can pass it to another form, like define or let which let you name it:

(define my-function (lambda (argument1 argument2) various things))

There’s a sugar to shorten this to:

(define (my-function argument1 argument2) various things)

This is differentiated from the non-sugar one because the first parameter of define - the name - has parentheses around it.

377. Printing

Okay, your stuff’s broken. Time for debugging tool number one: the print statement. Just do:

(display x)

Sometimes your terminal will get messed up and lines will truncate instead of wrapping. You can work around this by wrapping yourself:

(use-modules (texinfo string-utils)) (display (fill-string (format #f "~a" x) #:line-width 150))

texinfo is probably installed because it’s a GNU thing and so it’s a requirement of all sorts of GNU software.

388. Listing and unlisting

Create a list with:

(list dog bat cat) -> (dog bat cat)

Get the length of the list:

(length (list 1 "hi" 2)) -> 3

You can iterate the list with:

(for-each (lambda (element) do something) (list dog bat cat))

You can run a function on each element to create a new list:

(map (lambda (element) create a new value) (list dog bat cat))

You can create a new list including only elements matching a condition:

(filter (lambda (element) (> element 0)) (list -1 0 1 2) -> (1 2)

You can combine lists (returns a new list):

(append (list "germany" "straw") (list 0 1 2)) -> ("germany" "straw" 0 1 2)

You can add one element:

(cons "germany" (list 0 1 2)) -> ("germany" 0 1 2)

or several elements (one or more):

(cons* "germany" "straw" (list 0 1 2)) -> ("germany" "straw" 0 1 2)

And you can convert a list into function arguments (you can mix it with non-list arguments like "arg1", "arg2" too):

(apply my-function "arg1" "arg2" (list "arg3" "arg4"))

For the following you’ll need (use-modules (srfi srfi-1)).

Get the first element:

(first (list 0 1 2)) -> 0

Get the last element:

(last (list 0 1 2)) -> 2

Get the Nth element (0 indexed):

(list-ref (list 0 "hi" 2) 1) -> "hi"

FYI there are a lot of other convenient list methods in srfi-1.

Lists are also used for associative maps and other data structures.

399. Quoting and unquoting

Quotes are basically values that contain Guile code. Like a string, but it’s always syntactically valid code and… not a string. One specific use case is to pass names that haven’t been defined yet to functions (like (create-something 'myname (lambda (x, y, z) ...))). I’m not sure exactly why these are useful otherwise, but Guix uses them a bunch so here’s how they work.

(quote + 1 2) -> (+ 1 2) '(+ 1 2) -> (+ 1 2)

Quotes are also used to make lists… that aren’t lists. Like: '(1 2 3)

You can turn quoted code back into real code (that is - evaluate the contained code) with:

(eval '(+ 1 2) interaction-environment) -> 3

Any names included in the quoted code will be resolved from the scope the eval is written in, so you’ll sometimes see quoted code that contains names that haven’t been defined passed to a function (the names will have been defined by the time the function evaluates it).

If you need to programmatically generate a quote, you can use quasiquotes which allow you to evaluate subtrees and add the results to the quote:

(quasiquote 1 2 (unquote + 3 4)) -> (1 2 7) `(1 2 ,(+ 3 4)) -> (1 2 7)

You can also flatten a list into a quote with the list unquote syntax:

`(1 2 ,@(list 3 4 5))` -> (1 2 3 4 5)

31010. Branching

(if cond val1 val2) returns val1 if cond evaluates to #t, otherwise val2.

Note that val1 and val2 aren’t evaluated unless the appropriate branch path is taken.

4The Guix configuration

Let’s start with an example.

41A simple operating system

Refer to the sample configuration copied from the top of this:

;; This is an operating system configuration template ;; for a "bare bones" setup, with no X11 display server. (use-modules (gnu)) (use-service-modules networking ssh) (use-package-modules screen) (operating-system (host-name "komputilo") (timezone "Europe/Berlin") (locale "en_US.utf8") ;; Boot in "legacy" BIOS mode, assuming /dev/sdX is the ;; target hard disk, and "my-root" is the label of the target ;; root file system. (bootloader (bootloader-configuration (bootloader grub-bootloader) (target "/dev/sdX"))) (file-systems (cons (file-system (device (file-system-label "my-root")) (mount-point "/") (type "ext4")) %base-file-systems)) ;; This is where user accounts are specified. The "root" ;; account is implicit, and is initially created with the ;; empty password. (users (cons (user-account (name "alice") (comment "Bob's sister") (group "users") ;; Adding the account to the "wheel" group ;; makes it a sudoer. Adding it to "audio" ;; and "video" allows the user to play sound ;; and access the webcam. (supplementary-groups '("wheel" "audio" "video")) (home-directory "/home/alice")) %base-user-accounts)) ;; Globally-installed packages. (packages (cons screen %base-packages)) ;; Add services to the baseline: a DHCP client and ;; an SSH server. (services (cons* (service dhcp-client-service-type) (service openssh-service-type (openssh-configuration (port-number 2222))) %base-services)))

As you can see, (operating-system ...), (host-name ...), (users ...), (service ...) etc. are all function calls. For the most part these just return a dictionary/hash-map type object containing the parameters passed in and maybe some defaults, some values set by other functions, etc. This script, when run, returns the result of (operating-system ...) and uses that to actually build the system.

%base-services, %base-user-accounts, and %base-file-systems are normal values (lists) - the % doesn’t mean anything in particular.

Most of this (timezone, locale, users) should be fairly straightforward but a few elements need elaboration.

Reference here


This section defines mounts - Guix for the most part won’t format or partition a drive for you. The definitions here are used 1. to set up Shepherd services for mounts that are activated at boot and 2. to create an fstab for use with the mount command.

The Shepherd services do not use fstab, or the mount command at all - they use the the mount syscall described in man 2 mount. Some standard mount flags are not available. For instance, for noauto you should specify (mount? #f) instead. Others, like rw, are synthetic options that are provided by mount here to help you prevent mistakes (ex: accidentally specifying ro) and can be omitted.

(device ...) can take one of three parameters - a block node (such as /dev/sda) as a string, a (uuid "...") with the partition/drive’s UUID, or (file-system-label "...") which is the target file system’s label.

Swap partitions are set up separately, in operating-system’s (swap-devices ...) argument.


(swap-devices (list "/dev/disk/by-uuid/907ff1c6-5fd2-4e1d-b412-2ef815a20dc0"))

You can do bind mounts by setting (type "none") and (flags '(bind-mount)).

Reference here


The bootloader defined here will be installed on the target device/filesystem, so that needs to be available (in your current /dev tree). Note that if you’re using a VM or disk-image this behaves a bit differently.

Reference here


(packages ...) is where you install software for all human users of the system - %base-packages is a list with a recommended baseline but you don’t need them for the system to boot. Packages listed here are added to the global profile. Packages required by other parts of the config (services, file-systems, etc) will be automatically pulled in regardless of whether they’re specified in (packages ...).

The elements of (packages ...) are names bound to the output of calls to (package ...). In order to use these names you need to import the appropriate package module with (use-package-modules ...).

(use-package-modules ...) is a wrapper around (use-modules ...) that does some undocumented stuff, but the biggest difference is (use-package-modules ssh vim ntp) automatically prefixes the names with gnu packages to form full module names: (gnu packages ssh), (gnu packages vim), and so on.


Services are basically what you’re familiar with: things like ssh, ngninx, docker, etc. Here you’re also see mcron services, which are like crontab entries. A bit later in the guide I’ll go into more detail on what services are and how to create your own.

Like packages, services have an import helper: (use-service-modules ...): it prefixes the module names with (gnu services NAME) instead of (gnu packages NAME).

42Deployment targets

The configuration can be used to create systems of several target types. Each target type has some additional behavioral differences.

Here’s some information on three of them, but there are actually several more targets if you look at the reference.

421Bare metal

This is when you’re running Guix on the system you’re installing to/modifying.

The options above will mostly work as described.

Invoke with guix system reconfigure my-configuration.scm.

Any boot filesystem in (filesystems ...) needs to be available (in /dev) when configuring.

You can optionally make your system root “volatile”, or essentially read-only (I would have called this “transient”). If you’ve used Docker it’s similar to how containers drop modifications. In this mode when the system is rebooted all changes (installed packages, configuration changes) on the root partition are lost. Guix does this by 1. mounting root read-only; 2. creating a writable tempfs filesystem; 3. using overlayfs to turn the tempfs filesystem into a writable layer on the read-only root. The reason root still needs writability is some core service need to create/modify files to operate properly (ex: resolv.conf).

To set up a volatile root, you need to set (initrd ...) as in this example. Replace #:qemu-networking #t with #:volatile-root #t.

Reference here

422Disk image

This target creates a file that can be flashed onto a usb drive/cd/dvd which can then be booted from.

Invoke with guix system disk-image my-configuration.scm. It outputs (on stdout) the path to the created disk image file. To delete it run guix gc.

There are some tricks to it:

  1. Your / filesystem is ignored. You still need to specify it though! IMO this is a bug.
  2. Your initrd is ignored. The disk-image operating system is always volatile (see above).

Guix creates a root filesystem of the type specified (ext4 for usb or iso9660 for cd/dvd - note if you specify an unsupported type here Guix silently uses ext4) and if you configured a UEFI bootloader it will also create an ESP partition. It will silently ignore the bootloader (target ...) if specified.

A minimum-size image is always created, but you can make it larger via command line flags.

There are also some caveats:

You can test your disk image using qemu. First copy the image somewhere and make it user writable (even though it shouldn’t be possible to modify it). Then run:

qemu-system-x86_64 -m 1024 my-image-name

Reference here

423Docker image

Invoke with guix system docker-image my-configuration.scm to get the image file name and docker load < /gnu/store/name-of-my-docker-image.tar.gz to load the image. The latter will print the docker-usable tag of the imported image.

There are some tricks to it:

  1. All your filesystems are ignored, but you still need to specify (filesystems ...)!
  2. Your initrd is ignored.
  3. The locale and hostname are ignored (but hostname and timezone must still be specified)
  4. Services are ignored

The image doesn’t do anything - you need to specify the entrypoint and command line arguments to run a process from the image. It’s basically just a bag containing all the packages you requested with the standard Guix filesystem tree.

To make sure whatever you want to run has a deterministic path add it to (packages ...) in (operating-system ...), or if you don’t have a package you might be able to symlink your executable to /usr/bin or somewhere convenient using special-files-service-type (more information on using services below). Otherwise you’ll have to use the derivation path which may change when you update.

As a tip, do:

$ docker run -it system:vxpaflq62g3ji95h6vnavh4c6rlv0m5i whatever docker: Error response from daemon: OCI runtime create failed: container_linux.go:345: starting container process caused "exec: \"whatever\": executable file not found in $PATH": unknown. $ docker export $(docker ps -lq) | tar tf - | less

to see the generated image filesystem tree. Be sure to replace the image id with the one you got from docker load.

[Reference here](

5Installing on bare metal

The instructions here are great, so please refer to them.

6Creating a read only bootable kiosk USB image

As a tangible example, let’s create a bootable USB image system. The system is a kiosk - it just runs a single piece of software and is otherwise entirely locked down. The program is a simple Python udp noise maker, which we’ll run as a service.

The server is basically:

import socket import time import traceback out = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) out.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) out.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) while True: message = 'HELLO!!!!' try: out.sendto(message.encode('utf-8'), ('', 12345)) except OSError: traceback.print_exc() print(message) time.sleep(5)

saved in in the same directory as the system config.

61Installing individual files

This server script isn’t included in any packages, so how can we include it? Guix has a couple functions that will create a file (immutable, managed by the garbage collector) on the installed system and give you its path.

(local-file path-to-file) copies the file specified by path-to-file in the current system into the system you’re building.

(plain-file string) creates a file with the contents string in the system you’re building.

Both of these return a file object which when lowered (we’ll get to that in the g-expression section) become the path to the file as a string.

We’ll include with:

(local-file "")

Reference here

611Running executables

So now that we have our script, how can we run it? This is canonically done with the function (system* command arg1 arg2 ...). In our case the command is python3, and the script will be the first argument.

Due to derivation sandboxing you can’t assume the command is in the executable search path, but there’s a pattern for specifying the absolute location of a command:

(system* (string-append #$openssl "/bin/openssl") "genrsa" "-out" private-key "2048"))

openssl in this case is a package defined in tls.scm. The way to read this is #$ “lowers” (package ...), which turns it into a path to the root of the derivation of the package, which is suffixed with /bin/openssl to get the absolute path to the executable within the derivation. #$ is one of the g-expression operators we’ll get to in a minute.

Some commands have been wrapped up into convenient functions like (mkdir-p path), (dirname path), (chdir path), (copy-recursively source dest), etc. You can find them in module (guix build utils).

To run we’ll do (roughly):

(system* (string-append #$python-3 "/bin/python3") #$(local-file ""))

Reference here


We don’t want to run it when the configuration file is evaluated though, we want to run it after the built system boots. A lot of Guix uses “g-expressions” to define code that will be run at boot time.

A g-expression is like a quote in that it is a value that contains code, but g-expressions can be written to a file, read back in somewhere else, then executed. It’s basically a movable “sub-program”.

You need to have

(use-modules (guix gexp))

to use g-expressions, otherwise you’ll get very opaque errors when building.

The g-expression operators parallel the quote operators:

Unquoting has the additional feature of “lowering” whatever’s being unquoted. In practice, if you have a package ((package ...)) like python-3 and lower it you get a string representing the absolute path to the package’s derivation, and if you have a file object ((local-file ...)) and lower it you get a string representing the absolute path of the file in the store.

Lowering does one other important thing - it roots whatever’s lowered to the system, which means 1. it will be installed and 2. it won’t be garbage collected as long as the current config continues to lower it.

There’s an additional helper, (file-append ...) which produces a string like (string-append ...) but it automatically lowers all lowerable objects in its argument list.

Reference here


To run our server, we’ll wrap this all up into a service.

Guix has a unique concept of services.

A service is basically a function that injects additional configuration into one or more other services. It sounds like a really abstract way to define it, but that’s really all it is: for example, the ssh service adds configuration to the activation service to set up various directories, and to shepherd to start the ssh process. The shepherd and activation services in turn add configuration to other services. In Guix this method of injecting config is called “extension”.

Some of the core services are:

The boot service is the root of the service tree (actually there’s one more, but it doesn’t do anything or have any configuration) - everything that’s run on a system is indirectly run by the boot service. The boot service’s collected expressions are written to the filesystem when building and initrd runs them when the system boots. This all happens externally to the code for the boot service itself.

A service type is defined by a function that takes a “service configuration” and outputs “extension configurations” for each service it extends (actually one function per extension), and additionally a function that takes an “extension configuration” and merges it with the “service configuration”.

The (services ...) section in (operating-system ...) is a list of pairs of “service-type” and “service configuration” (prepared via (service ...)). When building the system, the configurations are assembled into a tree based on what extends whatelse. Starting from the leaves, the configurations are sent to their respective service functions. The service functions return extension configurations that are grouped by service type and passed into the next services, and the process is repeated until the configuration for the boot service is assembled and processed.

Many service packages provide a function in the form (my-service config) that outputs the correct pair. Ex:

(static-networking-service "enp1s0" "" #:gateway "" #:name-servers '(""))

Some services require you to specify the pair elements yourself using (service some-service-type config). Ex:

(service openssh-service-type (openssh-configuration (port-number 2222)))

If you’d like to make your own service that’s an extension of multiple other services, you can create the service-type using (service-type ...) (reference).

(service-type ...) has two key sections for the two functions described above:

Some caveats to this style of describing services:

614Installing the Guix CLI

I couldn’t find a native package for Arch, but there’s a script for automating installation:

wget sudo sh

Then immediately:

  1. Run guix pull
  2. Add ~/.config/guix/current/bin to your PATH.

This is important! The Guix version distributed on the website is old, and the update process doesn’t swap the guix binary on your default path – the new binary is only available in your current profile. If you don’t modify PATH you’ll continue to use the old version of guix after updating.

615Defining our kiosk system

To keep things simple, we’ll make our server a shepherd process.

Name the config system.scm:

(use-modules (gnu) (gnu services) (guix gexp)) (use-service-modules networking shepherd) (use-package-modules python) (operating-system (host-name "kiosk") (timezone "UTC") (locale "en_US.utf8") (bootloader (bootloader-configuration (bootloader (if is-vm? grub-bootloader grub-efi-bootloader)) (timeout 0) )) (file-systems (cons* (file-system (device (file-system-label "NOT USED")) (mount-point "/") (type "ext4")) %base-file-systems)) (users (cons* ; defining root prevents creation of the default root account... (user-account (name "root") ; ... so that we can disable login by setting an invalid crypt (password "x") (uid 0) (group "root") (home-directory "/root")) %base-user-accounts)) (services (cons* (service dhcp-client-service-type) (let ((name 'myserver)) (service (shepherd-service-type ; name is needed once for build errors name (lambda (config) (shepherd-service ; name is needed twice for shepherd cli control (provision `(,name)) (start #~(make-forkexec-constructor ; This is a list of command arguments (similar to what system* takes) (list #$(file-append python-3 "/bin/python3") #$(local-file "") ) #:user "nobody" #:group "nogroup" )) (stop #~(make-kill-destructor)) )) ; The default config - since we don't specify a config argument ; as the 3rd (service ...) parameter and you get an error ; if it's entirely unspecified. #f ))) %base-services)) )

In the shepherd service, I didn’t add (requirement '(networking)) to make it start after the network is up. This is because even if the network is up, the link may not have been established so network errors are still possible. In this is paved over by catching the OSError that’s thrown when the network is down (“Network is unreachable” or similar). When the network eventually comes up will naturally start working as expected. This is generally a good practice when writing network software.

As a note, if you don’t want to implement start or stop in a shepherd service just set them to #~(const #f).

616Building the system

Build the system with

guix system disk-image system.scm --on-error backtrace -v 10

If you don’t add the --on-error you get thrown zero bones when something goes wrong. Sometimes you get thrown zero bones anyway, but when it helps it helps. -v 10 verbosity is also useful.

guix system writes the resulting path to the image in the store to stdout.

617Testing the system

Copy the image (final output from guix system) to some writable directory and run chmod u+x myimage. Qemu requires that the root filesystem is writable even if it’s mounted read only.


qemu-system-x86_64 -m 4096 myimage

When it finishes booting, you’ll start to see

YO!!!! YO!!!! YO!!!!

, proliferating every 5s.

618Copying to USB

You can copy it to the USB drive with something like:

#!/usr/bin/bash set -ex sudo bash -c 'pv /gnu/store/my-image-file-name > /dev/disk/by-id/my-drive-id' sudo sync /dev/disk/by-id/my-drive-id

Be careful to copy it to the block device node and not a filesystem node within the block device.

Most people will tell you to use dd, but cp works just as well, as will pv (which shows a progress bar). Unfortunately since most of the writing happens in the disk sync process and not the code that these three run, a progress bar isn’t super informative.

Yank the drive, plug it in somewhere, press the power button, and you should have your working kiosk!

To be extra thorough, you can run

tcpdump -i eth0 port 12345 -XX

on another computer in the same subnet and you should start to see YO!!!! messages roll in.

7Taking things a step further than you really want to

So the kiosk example was nice, but how far can we simplify things? We only want to run one program, who cares about things like logging in and TTYs right?

Looking at the Guix source, gnu/services/base.scm, you’ll find the definition of %base-services:

(define %base-services ;; Convenience variable holding the basic services. (list (login-service) (service virtual-terminal-service-type) (service console-font-service-type (map (lambda (tty) (cons tty %default-console-font)) '("tty1" "tty2" "tty3" "tty4" "tty5" "tty6"))) (agetty-service (agetty-configuration (extra-options '("-L")) ; no carrier detect (term "vt100") (tty #f))) ; automatic (mingetty-service (mingetty-configuration (tty "tty1"))) (mingetty-service (mingetty-configuration (tty "tty2"))) (mingetty-service (mingetty-configuration (tty "tty3"))) (mingetty-service (mingetty-configuration (tty "tty4"))) (mingetty-service (mingetty-configuration (tty "tty5"))) (mingetty-service (mingetty-configuration (tty "tty6"))) (service static-networking-service-type (list (static-networking (interface "lo") (ip "") (requirement '()) (provision '(loopback))))) (syslog-service) (service urandom-seed-service-type) (guix-service) (nscd-service) ;; The LVM2 rules are needed as soon as LVM2 or the device-mapper is ;; used, so enable them by default. The FUSE and ALSA rules are ;; less critical, but handy. (udev-service #:rules (list lvm2 fuse alsa-utils crda)) (service special-files-service-type `(("/bin/sh" ,(file-append (canonical-package bash) "/bin/sh"))))))

It’s pretty short, so let’s see how much we can get rid of.

(use-modules (gnu) (gnu services) (guix gexp)) (use-service-modules networking shepherd) (use-package-modules python linux) (operating-system (host-name "kiosk") (timezone "UTC") (locale "en_US.utf8") (bootloader (bootloader-configuration (bootloader (if is-vm? grub-bootloader grub-efi-bootloader)) (timeout 0) )) (file-systems (cons* (file-system (device (file-system-label "my-root")) (mount-point "/") (type "ext4")) %base-file-systems)) (users (cons* ; defining root prevents creation of the default root account... (user-account (name "root") ; ... so that we can disable login by setting an invalid crypt (password "x") (uid 0) (group "root") (home-directory "/root")) %base-user-accounts)) (services (list (service dhcp-client-service-type) (let ((name 'myserver)) (service (shepherd-service-type ; name is needed once for build errors name (lambda (config) (shepherd-service ; name is needed twice for shepherd cli control (provision `(,name)) (start #~(make-forkexec-constructor ; This is a list of command arguments (similar to what system* takes) (list #$(file-append python-3 "/bin/python3") #$(local-file "") ) #:user "nobody" #:group "nogroup" )) (stop #~(make-kill-destructor)) )) ; The default config - since we don't specify a config argument ; as the 3rd (service ...) parameter and you get an error ; if it's entirely unspecified. #f ))) (udev-service #:rules (list lvm2)) )) )

I inlined %base-services then removed:

  1. login-service - nobody will be logging in
  2. agetty - we don’t really need a serial port
  3. Actually we don’t need any ttys at all
  4. Since we have no ttys, get rid of the tty font service
  5. According to the comments, the virtual terminal service isn’t really needed
  6. Don’t need loopback TBH
  7. We’re not installing software, get rid of the guix service
  8. We’re just broadcasting on the network, don’t need domain names
  9. Not using any crypto, so let’s get rid of the urandom seed
  10. Syslog, not like we can ssh in to check it anyway
  11. fuse, alsa, crda
  12. Don’t need the sh link either probably

That leaves us with udev-service.

I also got rid of all the user CLI packages in %base-packages by specifying (packages (list)).

8Final notes

81Parameterizing the build

You can wrap (operating-system ...) in a function like (lambda (arg1 arg2) (operating-system ...)) and pass parameters to the build when starting it with:

guix system disk-image -e '((load "system.scm") "arg1" 3)'

(replacing the config file with -e ...).

-e ... specifies a Guile expression to run (instead of a script). (load ...) runs a script and returns the result of the final value (the lambda) - and then we call it with the parameters to get our (operating-system ...).

There are libraries in other languages to escape/format Lisp-y strings so you can generate the -e value programmatically. In Python I recommend sexpdata.


821Unrecognized mount option “rw”

Guix uses the mount system call rather than the mount executable on filesystems. Check man 2 mount for available options and how they work. Sometimes man 8 mount also has hints about individual options. In this case option rw is just a helper flag to prevent you from accidentally specifying ro and is not required.

822Debugging runtime scripts

If you get an error such as:

In execvp of /gnu/store/yys15k0yrmm.../bin/some-package-binary No such file or directory

The fact the expanded derivation path (yys15k...) is shown in the error indicates the files are included in the image/on the system. Confirm that the path after the derivation directory (/bin/some-package-binary) is correct, maybe the file’s in sbin not bin for instance.

You can view the contents of a disk image by running

sudo losetup -fP /gnu/store/abcd1234...-disk-image losetup -j /gnu/store/abcd1234...-disk-image

The second command will show where the disk image is mounted. Partition 0 is the root partition.

mkdir mnt sudo mount /dev/loopNp0 mnt find mnt | less

83Creating packages

You can create a package with another function call, (package ...). The key parameters are (source ...) which specifies where to get the package binaries/source code, and (build-system ...) which tells Guix what to do with it.

There are a bunch of official build system wrappers: Ant, Maven, NPM, Python, Autotools, …, and the “trivial build system”. The latter is the base build system and is entirely undocumented - I believe you don’t need the trivial build system though (for the most part) for good reason:

  1. As demonstrated with our kiosk service, if you don’t intend to distribute software it’s easy to include it in the system and run it without putting it in a package.
  2. Guix separates packages into subtrees with an expected structure. The existing build systems translate from the tools’ default output directories to the correct installation paths, which trivial build system won’t do.

Therefore as much as possible you should package your code using standard build tools and create the Guix package using the Guix build system for that tool.

By luck though, here’s an example of the trivial build system that @roptat posted on irc while I was writing this:

84Further reading

I recommend you get a copy of the Guix source code. It has 9k (?) packages and a bunch of services which are pretty good reference. While writing this, whenever something wasn’t explained in the various references, blog posts, mailing list threads, etc. I’d search through the source and usually have an answer within 30m.

85What’s missing

Actually this is shaping up to be a pretty good guide. If you see some conceptual void here let me know and I’ll add a note to this section.

86Coming next

Look forward to the next entry in this series, “If you really want to Guix then just!”