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
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.
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.
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.
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.
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.
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.
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.
A form is basically (space separated elements)
. How (and if/when!) this is evaluated depends on the context:
(use-modules (a b)
(a b)
is a list of names that identify a module.(display (a b)
(a b)
is a function call to a
with argument b
(define (a b) ...)
a
is the name of a new function and b
is its only argument(if #t (c d) (a b))
(c d)
is a function call but (a b)
isn’t evaluated at all (because only the first branch is taken).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:
"I'm a string"
32
#f
#t
#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.
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.
(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.
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.
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.
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.
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.
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)
(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.
Let’s start with an example.
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.
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.
Ex:
(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))
.
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.
(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)
.
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.
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
.
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:
/
filesystem is ignored. You still need to specify it though! IMO this is a bug.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
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:
(filesystems ...)
!initrd
is 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](https://www.gnu.org/software/guix/manual/en/html_node/Invoking-guix-system.html
The instructions here are great, so please refer to them.
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'), ('255.255.255.255', 12345))
except OSError:
traceback.print_exc()
print(message)
time.sleep(5)
saved in server.py
in the same directory as the system config.
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 server.py
with:
(local-file "server.py")
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 server.py
we’ll do (roughly):
(system*
(string-append #$python-3 "/bin/python3")
#$(local-file "server.py"))
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:
#~
= `
(quasiquote). You’ll frequently see this followed by (begin ...)
since a g-expression is typically a script with multiple commands.#$
= ,
(unquote).#$@
= ,@
(splice unquote - flatten a list into the quote)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.
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:
/etc
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" "192.168.1.33"
#:gateway "192.168.1.1"
#:name-servers '("192.168.1.1"))
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:
(extensions ...)
which is a list of pairs of 1. other service types and 2. functions to turn the main configuration into an extension configuration for the paired service type.(extend ...)
is the function that defines how to merge the extension configuration into the main configuration (it returns a new main configuration with changes incorporated). (extend (lambda (config jobs)
(mcron-configuration
(inherit config)
(jobs (append (mcron-configuration-jobs config)
jobs)))))
This example returns a new (mcron-configuration ...)
that first inherits all the values from the main config config
then overrides (jobs ...)
with the original jobs (retrieved with (mcron-configuration-jobs config)
) plus the new jobs (using (append ...)
).Some caveats to this style of describing services:
service-type
doesn’t provide a way to extend its shepherd ordering dependencies. So you can’t define an mcron job that requires the database to be running before the job runs.service-type
doesn’t expose something for configuration you’re out of luck.I couldn’t find a native package for Arch, but there’s a script for automating installation:
wget https://git.savannah.gnu.org/cgit/guix.git/plain/etc/guix-install.sh
sudo sh guix-install.sh
Then immediately:
guix pull
~/.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.
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 "server.py")
)
#: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 server.py
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 server.py
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)
.
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.
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.
Run:
qemu-system-x86_64 -m 4096 myimage
When it finishes booting, you’ll start to see
YO!!!!
YO!!!!
YO!!!!
, proliferating every 5s.
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.
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 "127.0.0.1")
(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 "server.py")
)
#: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:
login-service
- nobody will be logging inagetty
- we don’t really need a serial portfuse
, alsa
, crda
sh
link either probablyThat leaves us with udev-service
.
I also got rid of all the user CLI packages in %base-packages
by specifying (packages (list))
.
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.
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.
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
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:
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: https://framagit.org/tyreunom/guix-more/blob/master/more/packages/scala.scm
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.
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.
Look forward to the next entry in this series, “If you really want to Guix then just!”