Tiny Build Farm for Guix, part 1
Setting up scores of services
One of the oft-cited reasons people give for not switching to Guix is that their favourite software is too outdated, and a look at Repology shows that they are not wrong. Now the number of active committers in the Guix project is amazingly small, and even counting all contributors I am impressed by what these few people actually achieve. Nevertheless I wondered how I could improve the situation at least a little bit for packages I am interested in, that is, for the science team; and the first step is to get an account of what actually builds and what does not. So I decided to set up my own little build farm, limited to the packages in the scope of the science team, using the same technology that powers the bordeaux build farm. I call it the Tiny Build Farm for Guix, or TBFG for short, and this post is the first one in (hopefully) a series of blog posts about the topic; at the time of starting this series, the TBFG does not actually exist yet, so wish me luck.
Motivation
Before trying to solve a technical problem, let me digress a little bit:
Is there actually a problem? And if yes, why?
As has become my conviction over the years, the really difficult and major
problems in a project such as Guix are actually social and not technical.
They are rooted in the structure of Guix as a loosely coupled group of
volunteers who work on a common goal, mostly in their spare time; but when
I speak about a common goal, every volunteer has in fact their own goals,
and arriving at a coherent whole is partially due to the internal
structuring of the social project, and partially an emerging property
of a complex system.
Concretely, it happens often that contributors propose a package for a
software project they like; if it concerns free software and follows our
packaging
guidelines, it usually ends up being committed to the software
distribution. The original contributor may leave, the package may bitrot
and stop being buildable due to changes made in other parts of the
distribution. Sometimes people submit a bug report, sometimes committers
without interest in the actual software provide a fix, but maybe nobody
uses the software anymore, and it happens that over several years nobody
notices it is broken. This is problematic since even broken packages use
resources on the build farms. And when introducing, say, an update to a
library package, it becomes difficult to say whether the failure of a
dependency is a new phenomenon due to the change or whether it was already
present. So there are good reasons to strive for a distribution that is
100% buildable at all times.
And while I alone certainly cannot reach this for the currently more than
28000 packages in
Guix, doing it only for the science team, or maybe only the
algebra
and maths
modules, in which I am
particularly interested, appears to be a reachable goal.
But is a tiny build farm really needed? The honest answer is “no”, since the information is already out there in the big build farms. For historical reasons, Guix has two of them. One is CI, also called berlin for the location of most of its build machines; it relies on Cuirass, a continuous integration system written purposefully for Guix. It shows the state of the master branch and provides a dashboard from which the desired information could certainly be extracted automatically. On the other hand there is the bordeaux build farm, named after the location of its head node; it runs a suite of continuous integration tools written purposefully for Guix by Christopher Baines. One of its parts is the Guix Data Service, and its REST API with a JSON frontend makes it again possible to extract the information I am interested in – a system that knows about all packages in Guix by definition knows about the packages in the realm of the science team.
So setting up my TBFG is mainly an educational project – I would like to learn how our technology works. But the TBFG can also be used to obtain information about a collection of packages that is not part of Guix proper, or to look at the impact of local changes. Many people and projects have successfully set up Cuirass to manage their local package collection; this is, for instance, the case for the Guix Science and Guix HPC projects through the build server at INRIA Bordeaux. The software behind the bordeaux build farm is more complex and consists of several interconnected Shepherd services, so that it may in fact be less suited for a personal project. However, not least because I host part of that build farm at home, I am more interested in understanding this software stack, which is also less documented. So I am going to build the TBFG on top of this technology. Before diving in, I take the opportunity to thank Christopher Baines for his precious help during a Guix/Nix hackers' meeting; without him, I would not have been able to launch myself into this endeavour.
Build coordinator and agent
As a first step, we need to install the Guix Build Coordinator and one or more build agents. For a really tiny TBFG, I will keep everything on only one machine set aside for the purpose; it is called bedok and is one of the Lenovo Thinkpad X1 Gen9 with a four core 11th Gen Intel Core i7-1165G7 processor running at 2.80GHz graciously donated by Tweag. For the build coordinator, this is trivial; simply add the two lines
(service guix-build-coordinator-service-type (guix-build-coordinator-configuration))
to the services configuration of the Guix system declaration and reconfigure
the machine. For a start, the default configuration options explained in
more detail in the
manual
are appropriate (things will become more complicated if anything is to be
done with the build results, which would require to set the
hooks
field of the
configuration
record).
See also the documentation in the
git repository
of the project.
Next we need the guix-build-coordinator
package, which provides
a command line interface to the coordinator. It could be installed into an
arbitrary profile since the security model of the build coordinator is very
basic: It is assumed that the coordinator runs on a server of its own,
and anybody with access to the machine has control over the service.
⚠ However installing the package into a user profile currently has a big
drawback: It propagates the guix package, and the corresponding guix takes
precedence over the one in
$HOME/.config/guix/current/bin
.
The latter one is updated by guix pull
, but the former one
is not; so without a special approach, this prevents the user from
updating Guix.
Instead one can start a Guix shell as follows:
guix shell guix-build-coordinator
Running
$ guix-build-coordinator agent list
shows nothing, so the next step is to set up a build agent, which requires some preparations on the machine where the build server is running. Executing
$ guix-build-coordinator agent new e092df28-3418-4f94-b7f0-a214b03291ee
creates and prints a new random version 4 UUID and stores it in the agents table in its internal database. The next step is to set up authentication; for a small number of build agents, a password file is a suitable approach, otherwise an authentication token, which may be shared among several agents, can be used. So we create a password for the agent by running
$ guix-build-coordinator agent e092df28-3418-4f94-b7f0-a214b03291ee password new new password: IUwGrsEklfu0kVf_QquCOdzfa6-P52qPlcwBd5YB
which again prints the password and saves it into the coordinator database.
To simplify things for the human brain, we can give the agent a name; since
there is not yet a command line argument for this, we take it as a pretense
to look more closely at the SQLite database structure. So install the
sqlite
package into the root
profile, launch
sqlite3
on the coordinator database, and run the following
commands:
# sqlite3 /var/lib/guix-build-coordinator/guix_build_coordinator.db sqlite> .tables agent_passwords agent_tags agents … builds … tags … sqlite> select * from agent_passwords; 1|e092df28-3418-4f94-b7f0-a214b03291ee|IUwGrsEklfu0kVf_QquCOdzfa6-P52qPlcwBd5YB|2025-08-13 00:00:00 sqlite> .schema agents CREATE TABLE agents ( id TEXT PRIMARY KEY, description TEXT , name TEXT, active NOT NULL DEFAULT 1);
There are quite a few tables, but for now we are mainly interested in
those related to the agents. We can recover the password in case we forgot
to write it down (alternatively we could run
guix-build-coordinator agent e092df28-3418-4f94-b7f0-a214b03291ee password
),
and we see that agents can have a name and a description, and be active
or not.
So still in SQLite run
sqlite> update agents set name='bedok', description='TBFG agent' where id='e092df28-3418-4f94-b7f0-a214b03291ee'; sqlite> select * from agents; e092df28-3418-4f94-b7f0-a214b03291ee|TBFG agent|bedok|1
Alternatively, to check that everything has gone well, run (not necessarily as root anymore):
$ guix-build-coordinator agent list e092df28-3418-4f94-b7f0-a214b03291ee: bedok description: TBFG agent active?: true 0 allocated builds: requested systems: tags:
Now it is finally time to really set up the build agent! On the machine
where it is supposed to run (in our case, this is bedok again, but
it could be an arbitrary machine somewhere on the Internet, since all
communication would take place over https), we need to handle the password.
As usual in Guix, secrets are not saved in configuration that is publicly
visible in the store, but rather as separate state; so as root
create a file /etc/guix-build-coordinator/agent-bedok-passwd
containing the password
IUwGrsEklfu0kVf_QquCOdzfa6-P52qPlcwBd5YB
created above by the coordinator.
Then add the following snippet to the server part of the operating system
configuration:
(service guix-build-coordinator-agent-service-type (guix-build-coordinator-agent-configuration (authentication (guix-build-coordinator-agent-password-file-auth (uuid "e092df28-3418-4f94-b7f0-a214b03291ee") (password-file "/etc/guix-build-coordinator/agent-bedok-passwd"))) (derivation-substitute-urls '("https://data.guix.gnu.org")) (non-derivation-substitute-urls '("https://bordeaux.guix.gnu.org")) (systems '("x86_64-linux" "i686-linux")) (max-parallel-builds 4) (max-parallel-uploads 2) (max-1min-load-average 6)))
and reconfigure the machine.
Concerning the different parameters, see the
documentation.
For authentication, we need to provide our uuid
and the
location of the password-file
.
Since the agent runs on the same machine as the coordinator, I kept the
coordinator
field at its default
"http://localhost:8745"
; otherwise localhost
needs
to be replaced by the host name of the coordinator, and the protocol should
be set to https
.
The derivation-substitute-urls
field has no default; we will
discuss it in the next section.
The non-derivation-substitute-urls
field also needs to be set
to avoid compiling each and every package input locally; here I chose to
only use the bordeaux build farm, but one could add
"https://ci.guix.gnu.org"
to also fetch packages from berlin.
If the system
field is not set, then only packages for the
system on which the agent is running (most likely x86_64-linux
)
are handled; here it is useful to add i686-linux
.
Or on an ARM machine, the combo
'("aarch64-linux" "armhf-linux")
makes sense.
The numerical parameters can be left at their defaults; here I am trying
to limit the load on my four core processor.
If all goes well, we should see lines in the agent logfile
/var/log/guix-build-coordinator/agent.log
looking like
2025-08-13 00:00:00 (INFO ): starting agent e092df28-3418-4f94-b7f0-a214b03291ee 2025-08-13 00:00:00 (INFO ): connecting to coordinator http://localhost:8745 2025-08-13 00:00:00 (INFO ): running 0 threads, currently allocated 0 builds 2025-08-13 00:00:00 (INFO ): starting 0 new builds
and running
$ guix-build-coordinator agent list
on the coordinator machine again should now print two entries
beneath requested systems
.
As a last step, get back to the
/etc/guix-build-coordinator/agent-bedok-passwd
file,
which is probably world-readable. Starting the build agent has created
a user guix-build-coordinator-agent
, and I would recommend
to have the file be owned by that user, and remove permissions from all
other users.
Data service
This is the elephant in the room, almost literally. When starting my project of the TBFG, I had intended to set up the full stack of software needed to run the bordeaux build farm, and the Guix Data Service is a very important part of it. But the code base is massive (more than 30000 lines of Scheme code at the time of writing), and also the required ressources are massive: The service spends its time polling the git repository of Guix, compiling the sources and computing all the derivations for all the packages, which are then stored in a database. Rinse and repeat for the next commit. (In reality, the data service does even more, in particular it also queries build servers and stores information about builds; but the above functionality is everything we need for the TBFG.) As can be seen, not even the official data service, running continuously on a powerful server, manages to do that for all commits: Some of them are marked as green, others, the grey ones, are skipped, in particular at times of high commit activity.
So I have decided to rely on the central Guix data service by configuring
the corresponding derivation-substitute-urls
field of the
build coordinator as '(https://data.guix.gnu.org)
.
All information obtained by clicking through the data service website
can also be obtained as JSON through its REST API, which we will use in our
scripts to determine the derivations of science team packages to be built.
For this to work, we also need the Guix daemon to accept the signing key
of the data service; so the services
field of the operating
system declaration should look like this:
(services (append (modify-services %base-services (guix-service-type config => (guix-configuration (substitute-urls '("https://bordeaux.guix.gnu.org")) (authorized-keys (list (local-file "keys/guix/bordeaux.guix.gnu.org-export.pub") (local-file "keys/guix/data.guix.gnu.org.pub"))) (max-silent-time (* 24 3600)) (timeout (* 48 3600))))) (list (service guix-build-coordinator-service-type (guix-build-coordinator-configuration)) …)))
where the key files are copy-pasted into the local
keys/guix
subdirectory from the
corresponding
place
in the
guix/maintenance
git repository.
BFFE – Build Farm Front End
We could start submitting build jobs now, but to visualise what is
happening, we need another service, bffe
. The
build farm frontend actually serves two purposes:
On one hand on the bordeaux build farm, it submits build jobs for the
master branch to ensure continuous substitute availability (and a
different service, qa-frontpage
, submits build jobs for
testing branches and pull requests to the same build coordinator instance).
On the other hand, it provides a web server that connects to the
build coordinator and shows information about its status.
We will only need the second functionality. For this, add the following
snippet to the service configuration of the TBFG machine:
(service bffe-service-type (bffe-configuration (arguments #~(list #:web-server-args '(#:event-source "http://localhost:8746" #:controller-args (#:title "Science team build farm"))))))
We use the #:web-server-args
argument and provide as
event-source
the local build coordinator instance, which
communicates with clients on port 8746 (while communication with the
agents runs on port 8745 as seen above).
For more details on the optional #:build
argument, see
the
documentation
in the Guix manual or the
source code.
Reconfigure the system and open the BFFE website, either at
http://localhost:8767
locally on the TBFG machine, or at
http://192.168.1.80:8767
from another machine in your local network, where you have to adapt the
IP address to your situation.
The result is a rather empty web page, but it should at least show the title
we have chosen.
For our purposes, we are interested in the page obtained by appending
/activity
to the URL.
This shows a box Recent activity, which is rightfully empty;
and a list of agents per architecture and their current occupation,
which should also be void, except possibly for the percentage giving the
CPU load in case the agent also has other business.
Clicking on the name of an agent sends us to yet another page with
more details (among which the description we provided previously) .
Submitting a build
After all these preparations, we can finally submit our first build job!
For this, keep the /activity
page open, and run
$ DRV=`guix build hello --derivations` $ guix-build-coordinator build --derivation-substitute-urls=https://data.guix.gnu.org $DRV build submitted as 7bdd2249-1214-431a-aa61-de4d907f1b32
Providing a URL from which to fetch derivations is necessary because the
build coordinator receives only the store path of the derivation and not
the actual file; the same then recursively holds for inputs referenced
in the submitted derivation. This could be a global parameter of the
guix-build-coordinator-configuration
, but currently needs
to be specified by hand and can thus vary over time or depending on the
build.
If all goes well, a UUID for the build is printed in the terminal, and
three lines appear on the BFFE web page in the Recent activity box:
Build submitted, Build started and Build succeeded.
The /var/log/guix-build-coordinator/coordinator.log
and
/var/log/guix-build-coordinator/agent.log
files should also
contain matching information.
And you can go to the
http://192.168.1.80:8767/build/7bdd2249-1214-431a-aa61-de4d907f1b32
URL to see more detailed information on the build, with potentially a link
to the build log.
This link unfortunately does not work out of the box, and some more configuration is required to make the build logs available.
Nginx for the build logs
One possibility for accessing the build logs is by directly looking them
up in the place where they are stored by the build coordinator, the
directory /var/lib/guix-build-coordinator/build-logs/
.
Each build corresponds to a subdirectory named after its UUID and containing
the file log.gz
.
Alternatively, we can imitate the behaviour of the bordeaux build farm and set up a separate nginx web server using the following service snippet:
(service nginx-service-type (nginx-configuration (server-blocks (list (nginx-server-configuration (listen '("80" "[::]:80")) (locations (list (nginx-location-configuration (uri "~ \"\\/build\\/([a-z0-9-]{36})/log$\"") (body '("alias /var/lib/guix-build-coordinator/build-logs/$1/log;" "add_header Content-Type 'text/plain; charset=UTF-8';" "gzip_static always;" "gunzip on;"))) (nginx-location-configuration (uri "/") (body '("proxy_pass http://localhost:8767;" "proxy_http_version 1.1;" "proxy_set_header Connection \"\";"))) (nginx-location-configuration (uri "/events") (body '("proxy_pass http://localhost:8767;" "proxy_http_version 1.1;" "proxy_buffering off;" "proxy_set_header Connection \"\";"))))))))))
The first
nginx-location-configuration
serves the build logs, while the other two are reverse proxies towards
the pages provided by BFFE at port 8767
.
If the activity page is now accessed through the nginx web server at the
standard port through the URL
http://192.168.1.80/activity
,
it presents links to builds and their log files, which can be clicked on,
and the uncompressed log files are shown directly in the browser.
Outlook
This was a lot of work for setting up the necessary services! But hopefully it was also a good occasion to understand how the different components interact. In the next installment we will start writing our own scripts to communicate with these services; in particular we will work with the data service to retrieve information about the packages we are interested in.