···14141515<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
16161717+- [Guix](https://guix.gnu.org), a functional package manager inspired by Nix. Available as [services.guix](#opt-services.guix.enable).
1818+1719- [maubot](https://github.com/maubot/maubot), a plugin-based Matrix bot framework. Available as [services.maubot](#opt-services.maubot.enable).
18201921- [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).
···11+{ config, pkgs, lib, ... }:
22+33+let
44+ cfg = config.services.guix;
55+66+ package = cfg.package.override { inherit (cfg) stateDir storeDir; };
77+88+ guixBuildUser = id: {
99+ name = "guixbuilder${toString id}";
1010+ group = cfg.group;
1111+ extraGroups = [ cfg.group ];
1212+ createHome = false;
1313+ description = "Guix build user ${toString id}";
1414+ isSystemUser = true;
1515+ };
1616+1717+ guixBuildUsers = numberOfUsers:
1818+ builtins.listToAttrs (map
1919+ (user: {
2020+ name = user.name;
2121+ value = user;
2222+ })
2323+ (builtins.genList guixBuildUser numberOfUsers));
2424+2525+ # A set of Guix user profiles to be linked at activation.
2626+ guixUserProfiles = {
2727+ # The current Guix profile that is created through `guix pull`.
2828+ "current-guix" = "\${XDG_CONFIG_HOME}/guix/current";
2929+3030+ # The default Guix profile similar to $HOME/.nix-profile from Nix.
3131+ "guix-profile" = "$HOME/.guix-profile";
3232+ };
3333+3434+ # All of the Guix profiles to be used.
3535+ guixProfiles = lib.attrValues guixUserProfiles;
3636+3737+ serviceEnv = {
3838+ GUIX_LOCPATH = "${cfg.stateDir}/guix/profiles/per-user/root/guix-profile/lib/locale";
3939+ LC_ALL = "C.UTF-8";
4040+ };
4141+in
4242+{
4343+ meta.maintainers = with lib.maintainers; [ foo-dogsquared ];
4444+4545+ options.services.guix = with lib; {
4646+ enable = mkEnableOption "Guix build daemon service";
4747+4848+ group = mkOption {
4949+ type = types.str;
5050+ default = "guixbuild";
5151+ example = "guixbuild";
5252+ description = ''
5353+ The group of the Guix build user pool.
5454+ '';
5555+ };
5656+5757+ nrBuildUsers = mkOption {
5858+ type = types.ints.unsigned;
5959+ description = ''
6060+ Number of Guix build users to be used in the build pool.
6161+ '';
6262+ default = 10;
6363+ example = 20;
6464+ };
6565+6666+ extraArgs = mkOption {
6767+ type = with types; listOf str;
6868+ default = [ ];
6969+ example = [ "--max-jobs=4" "--debug" ];
7070+ description = ''
7171+ Extra flags to pass to the Guix daemon service.
7272+ '';
7373+ };
7474+7575+ package = mkPackageOption pkgs "guix" {
7676+ extraDescription = ''
7777+ It should contain {command}`guix-daemon` and {command}`guix`
7878+ executable.
7979+ '';
8080+ };
8181+8282+ storeDir = mkOption {
8383+ type = types.path;
8484+ default = "/gnu/store";
8585+ description = ''
8686+ The store directory where the Guix service will serve to/from. Take
8787+ note Guix cannot take advantage of substitutes if you set it something
8888+ other than {file}`/gnu/store` since most of the cached builds are
8989+ assumed to be in there.
9090+9191+ ::: {.warning}
9292+ This will also recompile all packages because the normal cache no
9393+ longer applies.
9494+ :::
9595+ '';
9696+ };
9797+9898+ stateDir = mkOption {
9999+ type = types.path;
100100+ default = "/var";
101101+ description = ''
102102+ The state directory where Guix service will store its data such as its
103103+ user-specific profiles, cache, and state files.
104104+105105+ ::: {.warning}
106106+ Changing it to something other than the default will rebuild the
107107+ package.
108108+ :::
109109+ '';
110110+ example = "/gnu/var";
111111+ };
112112+113113+ publish = {
114114+ enable = mkEnableOption "substitute server for your Guix store directory";
115115+116116+ generateKeyPair = mkOption {
117117+ type = types.bool;
118118+ description = ''
119119+ Whether to generate signing keys in {file}`/etc/guix` which are
120120+ required to initialize a substitute server. Otherwise,
121121+ `--public-key=$FILE` and `--private-key=$FILE` can be passed in
122122+ {option}`services.guix.publish.extraArgs`.
123123+ '';
124124+ default = true;
125125+ example = false;
126126+ };
127127+128128+ port = mkOption {
129129+ type = types.port;
130130+ default = 8181;
131131+ example = 8200;
132132+ description = ''
133133+ Port of the substitute server to listen on.
134134+ '';
135135+ };
136136+137137+ user = mkOption {
138138+ type = types.str;
139139+ default = "guix-publish";
140140+ description = ''
141141+ Name of the user to change once the server is up.
142142+ '';
143143+ };
144144+145145+ extraArgs = mkOption {
146146+ type = with types; listOf str;
147147+ description = ''
148148+ Extra flags to pass to the substitute server.
149149+ '';
150150+ default = [];
151151+ example = [
152152+ "--compression=zstd:6"
153153+ "--discover=no"
154154+ ];
155155+ };
156156+ };
157157+158158+ gc = {
159159+ enable = mkEnableOption "automatic garbage collection service for Guix";
160160+161161+ extraArgs = mkOption {
162162+ type = with types; listOf str;
163163+ default = [ ];
164164+ description = ''
165165+ List of arguments to be passed to {command}`guix gc`.
166166+167167+ When given no option, it will try to collect all garbage which is
168168+ often inconvenient so it is recommended to set [some
169169+ options](https://guix.gnu.org/en/manual/en/html_node/Invoking-guix-gc.html).
170170+ '';
171171+ example = [
172172+ "--delete-generations=1m"
173173+ "--free-space=10G"
174174+ "--optimize"
175175+ ];
176176+ };
177177+178178+ dates = lib.mkOption {
179179+ type = types.str;
180180+ default = "03:15";
181181+ example = "weekly";
182182+ description = ''
183183+ How often the garbage collection occurs. This takes the time format
184184+ from {manpage}`systemd.time(7)`.
185185+ '';
186186+ };
187187+ };
188188+ };
189189+190190+ config = lib.mkIf cfg.enable (lib.mkMerge [
191191+ {
192192+ environment.systemPackages = [ package ];
193193+194194+ users.users = guixBuildUsers cfg.nrBuildUsers;
195195+ users.groups.${cfg.group} = { };
196196+197197+ # Guix uses Avahi (through guile-avahi) both for the auto-discovering and
198198+ # advertising substitute servers in the local network.
199199+ services.avahi.enable = lib.mkDefault true;
200200+ services.avahi.publish.enable = lib.mkDefault true;
201201+ services.avahi.publish.userServices = lib.mkDefault true;
202202+203203+ # It's similar to Nix daemon so there's no question whether or not this
204204+ # should be sandboxed.
205205+ systemd.services.guix-daemon = {
206206+ environment = serviceEnv;
207207+ script = ''
208208+ ${lib.getExe' package "guix-daemon"} \
209209+ --build-users-group=${cfg.group} \
210210+ ${lib.escapeShellArgs cfg.extraArgs}
211211+ '';
212212+ serviceConfig = {
213213+ OOMPolicy = "continue";
214214+ RemainAfterExit = "yes";
215215+ Restart = "always";
216216+ TasksMax = 8192;
217217+ };
218218+ unitConfig.RequiresMountsFor = [
219219+ cfg.storeDir
220220+ cfg.stateDir
221221+ ];
222222+ wantedBy = [ "multi-user.target" ];
223223+ };
224224+225225+ # This is based from Nix daemon socket unit from upstream Nix package.
226226+ # Guix build daemon has support for systemd-style socket activation.
227227+ systemd.sockets.guix-daemon = {
228228+ description = "Guix daemon socket";
229229+ before = [ "multi-user.target" ];
230230+ listenStreams = [ "${cfg.stateDir}/guix/daemon-socket/socket" ];
231231+ unitConfig = {
232232+ RequiresMountsFor = [
233233+ cfg.storeDir
234234+ cfg.stateDir
235235+ ];
236236+ ConditionPathIsReadWrite = "${cfg.stateDir}/guix/daemon-socket";
237237+ };
238238+ wantedBy = [ "socket.target" ];
239239+ };
240240+241241+ systemd.mounts = [{
242242+ description = "Guix read-only store directory";
243243+ before = [ "guix-daemon.service" ];
244244+ what = cfg.storeDir;
245245+ where = cfg.storeDir;
246246+ type = "none";
247247+ options = "bind,ro";
248248+249249+ unitConfig.DefaultDependencies = false;
250250+ wantedBy = [ "guix-daemon.service" ];
251251+ }];
252252+253253+ # Make transferring files from one store to another easier with the usual
254254+ # case being of most substitutes from the official Guix CI instance.
255255+ system.activationScripts.guix-authorize-keys = ''
256256+ for official_server_keys in ${package}/share/guix/*.pub; do
257257+ ${lib.getExe' package "guix"} archive --authorize < $official_server_keys
258258+ done
259259+ '';
260260+261261+ # Link the usual Guix profiles to the home directory. This is useful in
262262+ # ephemeral setups where only certain part of the filesystem is
263263+ # persistent (e.g., "Erase my darlings"-type of setup).
264264+ system.userActivationScripts.guix-activate-user-profiles.text = let
265265+ linkProfileToPath = acc: profile: location: let
266266+ guixProfile = "${cfg.stateDir}/guix/profiles/per-user/\${USER}/${profile}";
267267+ in acc + ''
268268+ [ -d "${guixProfile}" ] && ln -sf "${guixProfile}" "${location}"
269269+ '';
270270+271271+ activationScript = lib.foldlAttrs linkProfileToPath "" guixUserProfiles;
272272+ in ''
273273+ # Don't export this please! It is only expected to be used for this
274274+ # activation script and nothing else.
275275+ XDG_CONFIG_HOME=''${XDG_CONFIG_HOME:-$HOME/.config}
276276+277277+ # Linking the usual Guix profiles into the home directory.
278278+ ${activationScript}
279279+ '';
280280+281281+ # GUIX_LOCPATH is basically LOCPATH but for Guix libc which in turn used by
282282+ # virtually every Guix-built packages. This is so that Guix-installed
283283+ # applications wouldn't use incompatible locale data and not touch its host
284284+ # system.
285285+ environment.sessionVariables.GUIX_LOCPATH = lib.makeSearchPath "lib/locale" guixProfiles;
286286+287287+ # What Guix profiles export is very similar to Nix profiles so it is
288288+ # acceptable to list it here. Also, it is more likely that the user would
289289+ # want to use packages explicitly installed from Guix so we're putting it
290290+ # first.
291291+ environment.profiles = lib.mkBefore guixProfiles;
292292+ }
293293+294294+ (lib.mkIf cfg.publish.enable {
295295+ systemd.services.guix-publish = {
296296+ description = "Guix remote store";
297297+ environment = serviceEnv;
298298+299299+ # Mounts will be required by the daemon service anyways so there's no
300300+ # need add RequiresMountsFor= or something similar.
301301+ requires = [ "guix-daemon.service" ];
302302+ after = [ "guix-daemon.service" ];
303303+ partOf = [ "guix-daemon.service" ];
304304+305305+ preStart = lib.mkIf cfg.publish.generateKeyPair ''
306306+ # Generate the keypair if it's missing.
307307+ [ -f "/etc/guix/signing-key.sec" ] && [ -f "/etc/guix/signing-key.pub" ] || \
308308+ ${lib.getExe' package "guix"} archive --generate-key || {
309309+ rm /etc/guix/signing-key.*;
310310+ ${lib.getExe' package "guix"} archive --generate-key;
311311+ }
312312+ '';
313313+ script = ''
314314+ ${lib.getExe' package "guix"} publish \
315315+ --user=${cfg.publish.user} --port=${builtins.toString cfg.publish.port} \
316316+ ${lib.escapeShellArgs cfg.publish.extraArgs}
317317+ '';
318318+319319+ serviceConfig = {
320320+ Restart = "always";
321321+ RestartSec = 10;
322322+323323+ ProtectClock = true;
324324+ ProtectHostname = true;
325325+ ProtectKernelTunables = true;
326326+ ProtectKernelModules = true;
327327+ ProtectControlGroups = true;
328328+ SystemCallFilter = [
329329+ "@system-service"
330330+ "@debug"
331331+ "@setuid"
332332+ ];
333333+334334+ RestrictNamespaces = true;
335335+ RestrictAddressFamilies = [
336336+ "AF_UNIX"
337337+ "AF_INET"
338338+ "AF_INET6"
339339+ ];
340340+341341+ # While the permissions can be set, it is assumed to be taken by Guix
342342+ # daemon service which it has already done the setup.
343343+ ConfigurationDirectory = "guix";
344344+345345+ AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
346346+ CapabilityBoundingSet = [
347347+ "CAP_NET_BIND_SERVICE"
348348+ "CAP_SETUID"
349349+ "CAP_SETGID"
350350+ ];
351351+ };
352352+ wantedBy = [ "multi-user.target" ];
353353+ };
354354+355355+ users.users.guix-publish = lib.mkIf (cfg.publish.user == "guix-publish") {
356356+ description = "Guix publish user";
357357+ group = config.users.groups.guix-publish.name;
358358+ isSystemUser = true;
359359+ };
360360+ users.groups.guix-publish = {};
361361+ })
362362+363363+ (lib.mkIf cfg.gc.enable {
364364+ # This service should be handled by root to collect all garbage by all
365365+ # users.
366366+ systemd.services.guix-gc = {
367367+ description = "Guix garbage collection";
368368+ startAt = cfg.gc.dates;
369369+ script = ''
370370+ ${lib.getExe' package "guix"} gc ${lib.escapeShellArgs cfg.gc.extraArgs}
371371+ '';
372372+373373+ serviceConfig = {
374374+ Type = "oneshot";
375375+376376+ MemoryDenyWriteExecute = true;
377377+ PrivateDevices = true;
378378+ PrivateNetworks = true;
379379+ ProtectControlGroups = true;
380380+ ProtectHostname = true;
381381+ ProtectKernelTunables = true;
382382+ SystemCallFilter = [
383383+ "@default"
384384+ "@file-system"
385385+ "@basic-io"
386386+ "@system-service"
387387+ ];
388388+ };
389389+ };
390390+391391+ systemd.timers.guix-gc.timerConfig.Persistent = true;
392392+ })
393393+ ]);
394394+}
···11+# Take note the Guix store directory is empty. Also, we're trying to prevent
22+# Guix from trying to downloading substitutes because of the restricted
33+# access (assuming it's in a sandboxed environment).
44+#
55+# So this test is what it is: a basic test while trying to use Guix as much as
66+# we possibly can (including the API) without triggering its download alarm.
77+88+import ../make-test-python.nix ({ lib, pkgs, ... }: {
99+ name = "guix-basic";
1010+ meta.maintainers = with lib.maintainers; [ foo-dogsquared ];
1111+1212+ nodes.machine = { config, ... }: {
1313+ environment.etc."guix/scripts".source = ./scripts;
1414+ services.guix.enable = true;
1515+ };
1616+1717+ testScript = ''
1818+ import pathlib
1919+2020+ machine.wait_for_unit("multi-user.target")
2121+ machine.wait_for_unit("guix-daemon.service")
2222+2323+ # Can't do much here since the environment has restricted network access.
2424+ with subtest("Guix basic package management"):
2525+ machine.succeed("guix build --dry-run --verbosity=0 hello")
2626+ machine.succeed("guix show hello")
2727+2828+ # This is to see if the Guix API is usable and mostly working.
2929+ with subtest("Guix API scripting"):
3030+ scripts_dir = pathlib.Path("/etc/guix/scripts")
3131+3232+ text_msg = "Hello there, NixOS!"
3333+ text_store_file = machine.succeed(f"guix repl -- {scripts_dir}/create-file-to-store.scm '{text_msg}'")
3434+ assert machine.succeed(f"cat {text_store_file}") == text_msg
3535+3636+ machine.succeed(f"guix repl -- {scripts_dir}/add-existing-files-to-store.scm {scripts_dir}")
3737+ '';
3838+})
···11+# Testing out the substitute server with two machines in a local network. As a
22+# bonus, we'll also test a feature of the substitute server being able to
33+# advertise its service to the local network with Avahi.
44+55+import ../make-test-python.nix ({ pkgs, lib, ... }: let
66+ publishPort = 8181;
77+ inherit (builtins) toString;
88+in {
99+ name = "guix-publish";
1010+1111+ meta.maintainers = with lib.maintainers; [ foo-dogsquared ];
1212+1313+ nodes = let
1414+ commonConfig = { config, ... }: {
1515+ # We'll be using '--advertise' flag with the
1616+ # substitute server which requires Avahi.
1717+ services.avahi = {
1818+ enable = true;
1919+ nssmdns = true;
2020+ publish = {
2121+ enable = true;
2222+ userServices = true;
2323+ };
2424+ };
2525+ };
2626+ in {
2727+ server = { config, lib, pkgs, ... }: {
2828+ imports = [ commonConfig ];
2929+3030+ services.guix = {
3131+ enable = true;
3232+ publish = {
3333+ enable = true;
3434+ port = publishPort;
3535+3636+ generateKeyPair = true;
3737+ extraArgs = [ "--advertise" ];
3838+ };
3939+ };
4040+4141+ networking.firewall.allowedTCPPorts = [ publishPort ];
4242+ };
4343+4444+ client = { config, lib, pkgs, ... }: {
4545+ imports = [ commonConfig ];
4646+4747+ services.guix = {
4848+ enable = true;
4949+5050+ extraArgs = [
5151+ # Force to only get all substitutes from the local server. We don't
5252+ # have anything in the Guix store directory and we cannot get
5353+ # anything from the official substitute servers anyways.
5454+ "--substitute-urls='http://server.local:${toString publishPort}'"
5555+5656+ # Enable autodiscovery of the substitute servers in the local
5757+ # network. This machine shouldn't need to import the signing key from
5858+ # the substitute server since it is automatically done anyways.
5959+ "--discover=yes"
6060+ ];
6161+ };
6262+ };
6363+ };
6464+6565+ testScript = ''
6666+ import pathlib
6767+6868+ start_all()
6969+7070+ scripts_dir = pathlib.Path("/etc/guix/scripts")
7171+7272+ for machine in machines:
7373+ machine.wait_for_unit("multi-user.target")
7474+ machine.wait_for_unit("guix-daemon.service")
7575+ machine.wait_for_unit("avahi-daemon.service")
7676+7777+ server.wait_for_unit("guix-publish.service")
7878+ server.wait_for_open_port(${toString publishPort})
7979+ server.succeed("curl http://localhost:${toString publishPort}/")
8080+8181+ # Now it's the client turn to make use of it.
8282+ substitute_server = "http://server.local:${toString publishPort}"
8383+ client.wait_for_unit("network-online.target")
8484+ response = client.succeed(f"curl {substitute_server}")
8585+ assert "Guix Substitute Server" in response
8686+8787+ # Authorizing the server to be used as a substitute server.
8888+ client.succeed(f"curl -O {substitute_server}/signing-key.pub")
8989+ client.succeed("guix archive --authorize < ./signing-key.pub")
9090+9191+ # Since we're using the substitute server with the `--advertise` flag, we
9292+ # might as well check it.
9393+ client.succeed("avahi-browse --resolve --terminate _guix_publish._tcp | grep '_guix_publish._tcp'")
9494+ '';
9595+})
···11+;; A simple script that adds each file given from the command-line into the
22+;; store and checks them if it's the same.
33+(use-modules (guix)
44+ (srfi srfi-1)
55+ (ice-9 ftw)
66+ (rnrs io ports))
77+88+;; This is based from tests/derivations.scm from Guix source code.
99+(define* (directory-contents dir #:optional (slurp get-bytevector-all))
1010+ "Return an alist representing the contents of DIR"
1111+ (define prefix-len (string-length dir))
1212+ (sort (file-system-fold (const #t)
1313+ (lambda (path stat result)
1414+ (alist-cons (string-drop path prefix-len)
1515+ (call-with-input-file path slurp)
1616+ result))
1717+ (lambda (path stat result) result)
1818+ (lambda (path stat result) result)
1919+ (lambda (path stat result) result)
2020+ (lambda (path stat errno result) result)
2121+ '()
2222+ dir)
2323+ (lambda (e1 e2)
2424+ (string<? (car e1) (car e2)))))
2525+2626+(define* (check-if-same store drv path)
2727+ "Check if the given path and its store item are the same"
2828+ (let* ((filetype (stat:type (stat drv))))
2929+ (case filetype
3030+ ((regular)
3131+ (and (valid-path? store drv)
3232+ (equal? (call-with-input-file path get-bytevector-all)
3333+ (call-with-input-file drv get-bytevector-all))))
3434+ ((directory)
3535+ (and (valid-path? store drv)
3636+ (equal? (directory-contents path)
3737+ (directory-contents drv))))
3838+ (else #f))))
3939+4040+(define* (add-and-check-item-to-store store path)
4141+ "Add PATH to STORE and check if the contents are the same"
4242+ (let* ((store-item (add-to-store store
4343+ (basename path)
4444+ #t "sha256" path))
4545+ (is-same (check-if-same store store-item path)))
4646+ (if (not is-same)
4747+ (exit 1))))
4848+4949+(with-store store
5050+ (map (lambda (path)
5151+ (add-and-check-item-to-store store (readlink* path)))
5252+ (cdr (command-line))))
+8
nixos/tests/guix/scripts/create-file-to-store.scm
···11+;; A script that creates a store item with the given text and prints the
22+;; resulting store item path.
33+(use-modules (guix))
44+55+(with-store store
66+ (display (add-text-to-store store "guix-basic-test-text"
77+ (string-join
88+ (cdr (command-line))))))