···1415<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
160017- [maubot](https://github.com/maubot/maubot), a plugin-based Matrix bot framework. Available as [services.maubot](#opt-services.maubot.enable).
1819- [Anki Sync Server](https://docs.ankiweb.net/sync-server.html), the official sync server built into recent versions of Anki. Available as [services.anki-sync-server](#opt-services.anki-sync-server.enable).
···1415<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
1617+- [Guix](https://guix.gnu.org), a functional package manager inspired by Nix. Available as [services.guix](#opt-services.guix.enable).
18+19- [maubot](https://github.com/maubot/maubot), a plugin-based Matrix bot framework. Available as [services.maubot](#opt-services.maubot.enable).
2021- [Anki Sync Server](https://docs.ankiweb.net/sync-server.html), the official sync server built into recent versions of Anki. Available as [services.anki-sync-server](#opt-services.anki-sync-server.enable).
···1+{ config, pkgs, lib, ... }:
2+3+let
4+ cfg = config.services.guix;
5+6+ package = cfg.package.override { inherit (cfg) stateDir storeDir; };
7+8+ guixBuildUser = id: {
9+ name = "guixbuilder${toString id}";
10+ group = cfg.group;
11+ extraGroups = [ cfg.group ];
12+ createHome = false;
13+ description = "Guix build user ${toString id}";
14+ isSystemUser = true;
15+ };
16+17+ guixBuildUsers = numberOfUsers:
18+ builtins.listToAttrs (map
19+ (user: {
20+ name = user.name;
21+ value = user;
22+ })
23+ (builtins.genList guixBuildUser numberOfUsers));
24+25+ # A set of Guix user profiles to be linked at activation.
26+ guixUserProfiles = {
27+ # The current Guix profile that is created through `guix pull`.
28+ "current-guix" = "\${XDG_CONFIG_HOME}/guix/current";
29+30+ # The default Guix profile similar to $HOME/.nix-profile from Nix.
31+ "guix-profile" = "$HOME/.guix-profile";
32+ };
33+34+ # All of the Guix profiles to be used.
35+ guixProfiles = lib.attrValues guixUserProfiles;
36+37+ serviceEnv = {
38+ GUIX_LOCPATH = "${cfg.stateDir}/guix/profiles/per-user/root/guix-profile/lib/locale";
39+ LC_ALL = "C.UTF-8";
40+ };
41+in
42+{
43+ meta.maintainers = with lib.maintainers; [ foo-dogsquared ];
44+45+ options.services.guix = with lib; {
46+ enable = mkEnableOption "Guix build daemon service";
47+48+ group = mkOption {
49+ type = types.str;
50+ default = "guixbuild";
51+ example = "guixbuild";
52+ description = ''
53+ The group of the Guix build user pool.
54+ '';
55+ };
56+57+ nrBuildUsers = mkOption {
58+ type = types.ints.unsigned;
59+ description = ''
60+ Number of Guix build users to be used in the build pool.
61+ '';
62+ default = 10;
63+ example = 20;
64+ };
65+66+ extraArgs = mkOption {
67+ type = with types; listOf str;
68+ default = [ ];
69+ example = [ "--max-jobs=4" "--debug" ];
70+ description = ''
71+ Extra flags to pass to the Guix daemon service.
72+ '';
73+ };
74+75+ package = mkPackageOption pkgs "guix" {
76+ extraDescription = ''
77+ It should contain {command}`guix-daemon` and {command}`guix`
78+ executable.
79+ '';
80+ };
81+82+ storeDir = mkOption {
83+ type = types.path;
84+ default = "/gnu/store";
85+ description = ''
86+ The store directory where the Guix service will serve to/from. Take
87+ note Guix cannot take advantage of substitutes if you set it something
88+ other than {file}`/gnu/store` since most of the cached builds are
89+ assumed to be in there.
90+91+ ::: {.warning}
92+ This will also recompile all packages because the normal cache no
93+ longer applies.
94+ :::
95+ '';
96+ };
97+98+ stateDir = mkOption {
99+ type = types.path;
100+ default = "/var";
101+ description = ''
102+ The state directory where Guix service will store its data such as its
103+ user-specific profiles, cache, and state files.
104+105+ ::: {.warning}
106+ Changing it to something other than the default will rebuild the
107+ package.
108+ :::
109+ '';
110+ example = "/gnu/var";
111+ };
112+113+ publish = {
114+ enable = mkEnableOption "substitute server for your Guix store directory";
115+116+ generateKeyPair = mkOption {
117+ type = types.bool;
118+ description = ''
119+ Whether to generate signing keys in {file}`/etc/guix` which are
120+ required to initialize a substitute server. Otherwise,
121+ `--public-key=$FILE` and `--private-key=$FILE` can be passed in
122+ {option}`services.guix.publish.extraArgs`.
123+ '';
124+ default = true;
125+ example = false;
126+ };
127+128+ port = mkOption {
129+ type = types.port;
130+ default = 8181;
131+ example = 8200;
132+ description = ''
133+ Port of the substitute server to listen on.
134+ '';
135+ };
136+137+ user = mkOption {
138+ type = types.str;
139+ default = "guix-publish";
140+ description = ''
141+ Name of the user to change once the server is up.
142+ '';
143+ };
144+145+ extraArgs = mkOption {
146+ type = with types; listOf str;
147+ description = ''
148+ Extra flags to pass to the substitute server.
149+ '';
150+ default = [];
151+ example = [
152+ "--compression=zstd:6"
153+ "--discover=no"
154+ ];
155+ };
156+ };
157+158+ gc = {
159+ enable = mkEnableOption "automatic garbage collection service for Guix";
160+161+ extraArgs = mkOption {
162+ type = with types; listOf str;
163+ default = [ ];
164+ description = ''
165+ List of arguments to be passed to {command}`guix gc`.
166+167+ When given no option, it will try to collect all garbage which is
168+ often inconvenient so it is recommended to set [some
169+ options](https://guix.gnu.org/en/manual/en/html_node/Invoking-guix-gc.html).
170+ '';
171+ example = [
172+ "--delete-generations=1m"
173+ "--free-space=10G"
174+ "--optimize"
175+ ];
176+ };
177+178+ dates = lib.mkOption {
179+ type = types.str;
180+ default = "03:15";
181+ example = "weekly";
182+ description = ''
183+ How often the garbage collection occurs. This takes the time format
184+ from {manpage}`systemd.time(7)`.
185+ '';
186+ };
187+ };
188+ };
189+190+ config = lib.mkIf cfg.enable (lib.mkMerge [
191+ {
192+ environment.systemPackages = [ package ];
193+194+ users.users = guixBuildUsers cfg.nrBuildUsers;
195+ users.groups.${cfg.group} = { };
196+197+ # Guix uses Avahi (through guile-avahi) both for the auto-discovering and
198+ # advertising substitute servers in the local network.
199+ services.avahi.enable = lib.mkDefault true;
200+ services.avahi.publish.enable = lib.mkDefault true;
201+ services.avahi.publish.userServices = lib.mkDefault true;
202+203+ # It's similar to Nix daemon so there's no question whether or not this
204+ # should be sandboxed.
205+ systemd.services.guix-daemon = {
206+ environment = serviceEnv;
207+ script = ''
208+ ${lib.getExe' package "guix-daemon"} \
209+ --build-users-group=${cfg.group} \
210+ ${lib.escapeShellArgs cfg.extraArgs}
211+ '';
212+ serviceConfig = {
213+ OOMPolicy = "continue";
214+ RemainAfterExit = "yes";
215+ Restart = "always";
216+ TasksMax = 8192;
217+ };
218+ unitConfig.RequiresMountsFor = [
219+ cfg.storeDir
220+ cfg.stateDir
221+ ];
222+ wantedBy = [ "multi-user.target" ];
223+ };
224+225+ # This is based from Nix daemon socket unit from upstream Nix package.
226+ # Guix build daemon has support for systemd-style socket activation.
227+ systemd.sockets.guix-daemon = {
228+ description = "Guix daemon socket";
229+ before = [ "multi-user.target" ];
230+ listenStreams = [ "${cfg.stateDir}/guix/daemon-socket/socket" ];
231+ unitConfig = {
232+ RequiresMountsFor = [
233+ cfg.storeDir
234+ cfg.stateDir
235+ ];
236+ ConditionPathIsReadWrite = "${cfg.stateDir}/guix/daemon-socket";
237+ };
238+ wantedBy = [ "socket.target" ];
239+ };
240+241+ systemd.mounts = [{
242+ description = "Guix read-only store directory";
243+ before = [ "guix-daemon.service" ];
244+ what = cfg.storeDir;
245+ where = cfg.storeDir;
246+ type = "none";
247+ options = "bind,ro";
248+249+ unitConfig.DefaultDependencies = false;
250+ wantedBy = [ "guix-daemon.service" ];
251+ }];
252+253+ # Make transferring files from one store to another easier with the usual
254+ # case being of most substitutes from the official Guix CI instance.
255+ system.activationScripts.guix-authorize-keys = ''
256+ for official_server_keys in ${package}/share/guix/*.pub; do
257+ ${lib.getExe' package "guix"} archive --authorize < $official_server_keys
258+ done
259+ '';
260+261+ # Link the usual Guix profiles to the home directory. This is useful in
262+ # ephemeral setups where only certain part of the filesystem is
263+ # persistent (e.g., "Erase my darlings"-type of setup).
264+ system.userActivationScripts.guix-activate-user-profiles.text = let
265+ linkProfileToPath = acc: profile: location: let
266+ guixProfile = "${cfg.stateDir}/guix/profiles/per-user/\${USER}/${profile}";
267+ in acc + ''
268+ [ -d "${guixProfile}" ] && ln -sf "${guixProfile}" "${location}"
269+ '';
270+271+ activationScript = lib.foldlAttrs linkProfileToPath "" guixUserProfiles;
272+ in ''
273+ # Don't export this please! It is only expected to be used for this
274+ # activation script and nothing else.
275+ XDG_CONFIG_HOME=''${XDG_CONFIG_HOME:-$HOME/.config}
276+277+ # Linking the usual Guix profiles into the home directory.
278+ ${activationScript}
279+ '';
280+281+ # GUIX_LOCPATH is basically LOCPATH but for Guix libc which in turn used by
282+ # virtually every Guix-built packages. This is so that Guix-installed
283+ # applications wouldn't use incompatible locale data and not touch its host
284+ # system.
285+ environment.sessionVariables.GUIX_LOCPATH = lib.makeSearchPath "lib/locale" guixProfiles;
286+287+ # What Guix profiles export is very similar to Nix profiles so it is
288+ # acceptable to list it here. Also, it is more likely that the user would
289+ # want to use packages explicitly installed from Guix so we're putting it
290+ # first.
291+ environment.profiles = lib.mkBefore guixProfiles;
292+ }
293+294+ (lib.mkIf cfg.publish.enable {
295+ systemd.services.guix-publish = {
296+ description = "Guix remote store";
297+ environment = serviceEnv;
298+299+ # Mounts will be required by the daemon service anyways so there's no
300+ # need add RequiresMountsFor= or something similar.
301+ requires = [ "guix-daemon.service" ];
302+ after = [ "guix-daemon.service" ];
303+ partOf = [ "guix-daemon.service" ];
304+305+ preStart = lib.mkIf cfg.publish.generateKeyPair ''
306+ # Generate the keypair if it's missing.
307+ [ -f "/etc/guix/signing-key.sec" ] && [ -f "/etc/guix/signing-key.pub" ] || \
308+ ${lib.getExe' package "guix"} archive --generate-key || {
309+ rm /etc/guix/signing-key.*;
310+ ${lib.getExe' package "guix"} archive --generate-key;
311+ }
312+ '';
313+ script = ''
314+ ${lib.getExe' package "guix"} publish \
315+ --user=${cfg.publish.user} --port=${builtins.toString cfg.publish.port} \
316+ ${lib.escapeShellArgs cfg.publish.extraArgs}
317+ '';
318+319+ serviceConfig = {
320+ Restart = "always";
321+ RestartSec = 10;
322+323+ ProtectClock = true;
324+ ProtectHostname = true;
325+ ProtectKernelTunables = true;
326+ ProtectKernelModules = true;
327+ ProtectControlGroups = true;
328+ SystemCallFilter = [
329+ "@system-service"
330+ "@debug"
331+ "@setuid"
332+ ];
333+334+ RestrictNamespaces = true;
335+ RestrictAddressFamilies = [
336+ "AF_UNIX"
337+ "AF_INET"
338+ "AF_INET6"
339+ ];
340+341+ # While the permissions can be set, it is assumed to be taken by Guix
342+ # daemon service which it has already done the setup.
343+ ConfigurationDirectory = "guix";
344+345+ AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
346+ CapabilityBoundingSet = [
347+ "CAP_NET_BIND_SERVICE"
348+ "CAP_SETUID"
349+ "CAP_SETGID"
350+ ];
351+ };
352+ wantedBy = [ "multi-user.target" ];
353+ };
354+355+ users.users.guix-publish = lib.mkIf (cfg.publish.user == "guix-publish") {
356+ description = "Guix publish user";
357+ group = config.users.groups.guix-publish.name;
358+ isSystemUser = true;
359+ };
360+ users.groups.guix-publish = {};
361+ })
362+363+ (lib.mkIf cfg.gc.enable {
364+ # This service should be handled by root to collect all garbage by all
365+ # users.
366+ systemd.services.guix-gc = {
367+ description = "Guix garbage collection";
368+ startAt = cfg.gc.dates;
369+ script = ''
370+ ${lib.getExe' package "guix"} gc ${lib.escapeShellArgs cfg.gc.extraArgs}
371+ '';
372+373+ serviceConfig = {
374+ Type = "oneshot";
375+376+ MemoryDenyWriteExecute = true;
377+ PrivateDevices = true;
378+ PrivateNetworks = true;
379+ ProtectControlGroups = true;
380+ ProtectHostname = true;
381+ ProtectKernelTunables = true;
382+ SystemCallFilter = [
383+ "@default"
384+ "@file-system"
385+ "@basic-io"
386+ "@system-service"
387+ ];
388+ };
389+ };
390+391+ systemd.timers.guix-gc.timerConfig.Persistent = true;
392+ })
393+ ]);
394+}
···1+# Take note the Guix store directory is empty. Also, we're trying to prevent
2+# Guix from trying to downloading substitutes because of the restricted
3+# access (assuming it's in a sandboxed environment).
4+#
5+# So this test is what it is: a basic test while trying to use Guix as much as
6+# we possibly can (including the API) without triggering its download alarm.
7+8+import ../make-test-python.nix ({ lib, pkgs, ... }: {
9+ name = "guix-basic";
10+ meta.maintainers = with lib.maintainers; [ foo-dogsquared ];
11+12+ nodes.machine = { config, ... }: {
13+ environment.etc."guix/scripts".source = ./scripts;
14+ services.guix.enable = true;
15+ };
16+17+ testScript = ''
18+ import pathlib
19+20+ machine.wait_for_unit("multi-user.target")
21+ machine.wait_for_unit("guix-daemon.service")
22+23+ # Can't do much here since the environment has restricted network access.
24+ with subtest("Guix basic package management"):
25+ machine.succeed("guix build --dry-run --verbosity=0 hello")
26+ machine.succeed("guix show hello")
27+28+ # This is to see if the Guix API is usable and mostly working.
29+ with subtest("Guix API scripting"):
30+ scripts_dir = pathlib.Path("/etc/guix/scripts")
31+32+ text_msg = "Hello there, NixOS!"
33+ text_store_file = machine.succeed(f"guix repl -- {scripts_dir}/create-file-to-store.scm '{text_msg}'")
34+ assert machine.succeed(f"cat {text_store_file}") == text_msg
35+36+ machine.succeed(f"guix repl -- {scripts_dir}/add-existing-files-to-store.scm {scripts_dir}")
37+ '';
38+})
···1+# Testing out the substitute server with two machines in a local network. As a
2+# bonus, we'll also test a feature of the substitute server being able to
3+# advertise its service to the local network with Avahi.
4+5+import ../make-test-python.nix ({ pkgs, lib, ... }: let
6+ publishPort = 8181;
7+ inherit (builtins) toString;
8+in {
9+ name = "guix-publish";
10+11+ meta.maintainers = with lib.maintainers; [ foo-dogsquared ];
12+13+ nodes = let
14+ commonConfig = { config, ... }: {
15+ # We'll be using '--advertise' flag with the
16+ # substitute server which requires Avahi.
17+ services.avahi = {
18+ enable = true;
19+ nssmdns = true;
20+ publish = {
21+ enable = true;
22+ userServices = true;
23+ };
24+ };
25+ };
26+ in {
27+ server = { config, lib, pkgs, ... }: {
28+ imports = [ commonConfig ];
29+30+ services.guix = {
31+ enable = true;
32+ publish = {
33+ enable = true;
34+ port = publishPort;
35+36+ generateKeyPair = true;
37+ extraArgs = [ "--advertise" ];
38+ };
39+ };
40+41+ networking.firewall.allowedTCPPorts = [ publishPort ];
42+ };
43+44+ client = { config, lib, pkgs, ... }: {
45+ imports = [ commonConfig ];
46+47+ services.guix = {
48+ enable = true;
49+50+ extraArgs = [
51+ # Force to only get all substitutes from the local server. We don't
52+ # have anything in the Guix store directory and we cannot get
53+ # anything from the official substitute servers anyways.
54+ "--substitute-urls='http://server.local:${toString publishPort}'"
55+56+ # Enable autodiscovery of the substitute servers in the local
57+ # network. This machine shouldn't need to import the signing key from
58+ # the substitute server since it is automatically done anyways.
59+ "--discover=yes"
60+ ];
61+ };
62+ };
63+ };
64+65+ testScript = ''
66+ import pathlib
67+68+ start_all()
69+70+ scripts_dir = pathlib.Path("/etc/guix/scripts")
71+72+ for machine in machines:
73+ machine.wait_for_unit("multi-user.target")
74+ machine.wait_for_unit("guix-daemon.service")
75+ machine.wait_for_unit("avahi-daemon.service")
76+77+ server.wait_for_unit("guix-publish.service")
78+ server.wait_for_open_port(${toString publishPort})
79+ server.succeed("curl http://localhost:${toString publishPort}/")
80+81+ # Now it's the client turn to make use of it.
82+ substitute_server = "http://server.local:${toString publishPort}"
83+ client.wait_for_unit("network-online.target")
84+ response = client.succeed(f"curl {substitute_server}")
85+ assert "Guix Substitute Server" in response
86+87+ # Authorizing the server to be used as a substitute server.
88+ client.succeed(f"curl -O {substitute_server}/signing-key.pub")
89+ client.succeed("guix archive --authorize < ./signing-key.pub")
90+91+ # Since we're using the substitute server with the `--advertise` flag, we
92+ # might as well check it.
93+ client.succeed("avahi-browse --resolve --terminate _guix_publish._tcp | grep '_guix_publish._tcp'")
94+ '';
95+})
···1+;; A simple script that adds each file given from the command-line into the
2+;; store and checks them if it's the same.
3+(use-modules (guix)
4+ (srfi srfi-1)
5+ (ice-9 ftw)
6+ (rnrs io ports))
7+8+;; This is based from tests/derivations.scm from Guix source code.
9+(define* (directory-contents dir #:optional (slurp get-bytevector-all))
10+ "Return an alist representing the contents of DIR"
11+ (define prefix-len (string-length dir))
12+ (sort (file-system-fold (const #t)
13+ (lambda (path stat result)
14+ (alist-cons (string-drop path prefix-len)
15+ (call-with-input-file path slurp)
16+ result))
17+ (lambda (path stat result) result)
18+ (lambda (path stat result) result)
19+ (lambda (path stat result) result)
20+ (lambda (path stat errno result) result)
21+ '()
22+ dir)
23+ (lambda (e1 e2)
24+ (string<? (car e1) (car e2)))))
25+26+(define* (check-if-same store drv path)
27+ "Check if the given path and its store item are the same"
28+ (let* ((filetype (stat:type (stat drv))))
29+ (case filetype
30+ ((regular)
31+ (and (valid-path? store drv)
32+ (equal? (call-with-input-file path get-bytevector-all)
33+ (call-with-input-file drv get-bytevector-all))))
34+ ((directory)
35+ (and (valid-path? store drv)
36+ (equal? (directory-contents path)
37+ (directory-contents drv))))
38+ (else #f))))
39+40+(define* (add-and-check-item-to-store store path)
41+ "Add PATH to STORE and check if the contents are the same"
42+ (let* ((store-item (add-to-store store
43+ (basename path)
44+ #t "sha256" path))
45+ (is-same (check-if-same store store-item path)))
46+ (if (not is-same)
47+ (exit 1))))
48+49+(with-store store
50+ (map (lambda (path)
51+ (add-and-check-item-to-store store (readlink* path)))
52+ (cdr (command-line))))
+8
nixos/tests/guix/scripts/create-file-to-store.scm
···00000000
···1+;; A script that creates a store item with the given text and prints the
2+;; resulting store item path.
3+(use-modules (guix))
4+5+(with-store store
6+ (display (add-text-to-store store "guix-basic-test-text"
7+ (string-join
8+ (cdr (command-line))))))