1{
2 config,
3 pkgs,
4 lib,
5 ...
6}:
7
8let
9 cfg = config.services.guix;
10
11 package = cfg.package.override { inherit (cfg) stateDir storeDir; };
12
13 guixBuildUser = id: {
14 name = "guixbuilder${toString id}";
15 group = cfg.group;
16 extraGroups = [ cfg.group ];
17 createHome = false;
18 description = "Guix build user ${toString id}";
19 isSystemUser = true;
20 };
21
22 guixBuildUsers =
23 numberOfUsers:
24 builtins.listToAttrs (
25 map (user: {
26 name = user.name;
27 value = user;
28 }) (builtins.genList guixBuildUser numberOfUsers)
29 );
30
31 # A set of Guix user profiles to be linked at activation. All of these should
32 # be default profiles managed by Guix CLI and the profiles are located in
33 # `${cfg.stateDir}/profiles/per-user/$USER/$PROFILE`.
34 guixUserProfiles = {
35 # The default Guix profile managed by `guix pull`. Take note this should be
36 # the profile with the most precedence in `PATH` env to let users use their
37 # updated versions of `guix` CLI.
38 "current-guix" = "\${XDG_CONFIG_HOME}/guix/current";
39
40 # The default Guix home profile. This profile contains more than exports
41 # such as an activation script at `$GUIX_HOME_PROFILE/activate`.
42 "guix-home" = "$HOME/.guix-home/profile";
43
44 # The default Guix profile similar to $HOME/.nix-profile from Nix.
45 "guix-profile" = "$HOME/.guix-profile";
46 };
47
48 # All of the Guix profiles to be used.
49 guixProfiles = lib.attrValues guixUserProfiles;
50
51 serviceEnv = {
52 GUIX_LOCPATH = "${cfg.stateDir}/guix/profiles/per-user/root/guix-profile/lib/locale";
53 LC_ALL = "C.UTF-8";
54 };
55
56 # Currently, this is just done the lazy way with the official Guix script. A
57 # more "formal" way would be creating our own Guix script to handle and
58 # generate the ACL file ourselves.
59 aclFile = pkgs.runCommandLocal "guix-acl" { } ''
60 export GUIX_CONFIGURATION_DIRECTORY=./
61 for official_server_keys in ${lib.concatStringsSep " " cfg.substituters.authorizedKeys}; do
62 ${lib.getExe' cfg.package "guix"} archive --authorize < "$official_server_keys"
63 done
64 install -Dm0600 ./acl "$out"
65 '';
66in
67{
68 meta.maintainers = with lib.maintainers; [ foo-dogsquared ];
69
70 options.services.guix = with lib; {
71 enable = mkEnableOption "Guix build daemon service";
72
73 group = mkOption {
74 type = types.str;
75 default = "guixbuild";
76 example = "guixbuild";
77 description = ''
78 The group of the Guix build user pool.
79 '';
80 };
81
82 nrBuildUsers = mkOption {
83 type = types.ints.unsigned;
84 description = ''
85 Number of Guix build users to be used in the build pool.
86 '';
87 default = 10;
88 example = 20;
89 };
90
91 extraArgs = mkOption {
92 type = with types; listOf str;
93 default = [ ];
94 example = [
95 "--max-jobs=4"
96 "--debug"
97 ];
98 description = ''
99 Extra flags to pass to the Guix daemon service.
100 '';
101 };
102
103 package = mkPackageOption pkgs "guix" {
104 extraDescription = ''
105 It should contain {command}`guix-daemon` and {command}`guix`
106 executable.
107 '';
108 };
109
110 storeDir = mkOption {
111 type = types.path;
112 default = "/gnu/store";
113 description = ''
114 The store directory where the Guix service will serve to/from. Take
115 note Guix cannot take advantage of substitutes if you set it something
116 other than {file}`/gnu/store` since most of the cached builds are
117 assumed to be in there.
118
119 ::: {.warning}
120 This will also recompile all packages because the normal cache no
121 longer applies.
122 :::
123 '';
124 };
125
126 stateDir = mkOption {
127 type = types.path;
128 default = "/var";
129 description = ''
130 The state directory where Guix service will store its data such as its
131 user-specific profiles, cache, and state files.
132
133 ::: {.warning}
134 Changing it to something other than the default will rebuild the
135 package.
136 :::
137 '';
138 example = "/gnu/var";
139 };
140
141 substituters = {
142 urls = lib.mkOption {
143 type = with lib.types; listOf str;
144 default = [
145 "https://ci.guix.gnu.org"
146 "https://bordeaux.guix.gnu.org"
147 "https://berlin.guix.gnu.org"
148 ];
149 example = lib.literalExpression ''
150 options.services.guix.substituters.urls.default ++ [
151 "https://guix.example.com"
152 "https://guix.example.org"
153 ]
154 '';
155 description = ''
156 A list of substitute servers' URLs for the Guix daemon to download
157 substitutes from.
158 '';
159 };
160
161 authorizedKeys = lib.mkOption {
162 type = with lib.types; listOf path;
163 default = [
164 "${cfg.package}/share/guix/ci.guix.gnu.org.pub"
165 "${cfg.package}/share/guix/bordeaux.guix.gnu.org.pub"
166 "${cfg.package}/share/guix/berlin.guix.gnu.org.pub"
167 ];
168 defaultText = ''
169 The packaged signing keys from {option}`services.guix.package`.
170 '';
171 example = lib.literalExpression ''
172 options.services.guix.substituters.authorizedKeys.default ++ [
173 (builtins.fetchurl {
174 url = "https://guix.example.com/signing-key.pub";
175 })
176
177 (builtins.fetchurl {
178 url = "https://guix.example.org/static/signing-key.pub";
179 })
180 ]
181 '';
182 description = ''
183 A list of signing keys for each substitute server to be authorized as
184 a source of substitutes. Without this, the listed substitute servers
185 from {option}`services.guix.substituters.urls` would be ignored [with
186 some
187 exceptions](https://guix.gnu.org/manual/en/html_node/Substitute-Authentication.html).
188 '';
189 };
190 };
191
192 publish = {
193 enable = mkEnableOption "substitute server for your Guix store directory";
194
195 generateKeyPair = mkOption {
196 type = types.bool;
197 description = ''
198 Whether to generate signing keys in {file}`/etc/guix` which are
199 required to initialize a substitute server. Otherwise,
200 `--public-key=$FILE` and `--private-key=$FILE` can be passed in
201 {option}`services.guix.publish.extraArgs`.
202 '';
203 default = true;
204 example = false;
205 };
206
207 port = mkOption {
208 type = types.port;
209 default = 8181;
210 example = 8200;
211 description = ''
212 Port of the substitute server to listen on.
213 '';
214 };
215
216 user = mkOption {
217 type = types.str;
218 default = "guix-publish";
219 description = ''
220 Name of the user to change once the server is up.
221 '';
222 };
223
224 extraArgs = mkOption {
225 type = with types; listOf str;
226 description = ''
227 Extra flags to pass to the substitute server.
228 '';
229 default = [ ];
230 example = [
231 "--compression=zstd:6"
232 "--discover=no"
233 ];
234 };
235 };
236
237 gc = {
238 enable = mkEnableOption "automatic garbage collection service for Guix";
239
240 extraArgs = mkOption {
241 type = with types; listOf str;
242 default = [ ];
243 description = ''
244 List of arguments to be passed to {command}`guix gc`.
245
246 When given no option, it will try to collect all garbage which is
247 often inconvenient so it is recommended to set [some
248 options](https://guix.gnu.org/en/manual/en/html_node/Invoking-guix-gc.html).
249 '';
250 example = [
251 "--delete-generations=1m"
252 "--free-space=10G"
253 "--optimize"
254 ];
255 };
256
257 dates = lib.mkOption {
258 type = types.str;
259 default = "03:15";
260 example = "weekly";
261 description = ''
262 How often the garbage collection occurs. This takes the time format
263 from {manpage}`systemd.time(7)`.
264 '';
265 };
266 };
267 };
268
269 config = lib.mkIf cfg.enable (
270 lib.mkMerge [
271 {
272 environment.systemPackages = [ package ];
273
274 users.users = guixBuildUsers cfg.nrBuildUsers;
275 users.groups.${cfg.group} = { };
276
277 # Guix uses Avahi (through guile-avahi) both for the auto-discovering and
278 # advertising substitute servers in the local network.
279 services.avahi.enable = lib.mkDefault true;
280 services.avahi.publish.enable = lib.mkDefault true;
281 services.avahi.publish.userServices = lib.mkDefault true;
282
283 # It's similar to Nix daemon so there's no question whether or not this
284 # should be sandboxed.
285 systemd.services.guix-daemon = {
286 environment = serviceEnv // config.networking.proxy.envVars;
287 script = ''
288 exec ${lib.getExe' package "guix-daemon"} \
289 --build-users-group=${cfg.group} \
290 ${
291 lib.optionalString (
292 cfg.substituters.urls != [ ]
293 ) "--substitute-urls='${lib.concatStringsSep " " cfg.substituters.urls}'"
294 } \
295 ${lib.escapeShellArgs cfg.extraArgs}
296 '';
297 serviceConfig = {
298 OOMPolicy = "continue";
299 RemainAfterExit = "yes";
300 Restart = "always";
301 TasksMax = 8192;
302 };
303 unitConfig.RequiresMountsFor = [
304 cfg.storeDir
305 cfg.stateDir
306 ];
307 wantedBy = [ "multi-user.target" ];
308 };
309
310 # This is based from Nix daemon socket unit from upstream Nix package.
311 # Guix build daemon has support for systemd-style socket activation.
312 systemd.sockets.guix-daemon = {
313 description = "Guix daemon socket";
314 before = [ "multi-user.target" ];
315 listenStreams = [ "${cfg.stateDir}/guix/daemon-socket/socket" ];
316 unitConfig.RequiresMountsFor = [
317 cfg.storeDir
318 cfg.stateDir
319 ];
320 wantedBy = [ "sockets.target" ];
321 };
322
323 systemd.mounts = [
324 {
325 description = "Guix read-only store directory";
326 before = [ "guix-daemon.service" ];
327 what = cfg.storeDir;
328 where = cfg.storeDir;
329 type = "none";
330 options = "bind,ro";
331
332 unitConfig.DefaultDependencies = false;
333 wantedBy = [ "guix-daemon.service" ];
334 }
335 ];
336
337 # Make transferring files from one store to another easier with the usual
338 # case being of most substitutes from the official Guix CI instance.
339 environment.etc."guix/acl".source = aclFile;
340
341 # Link the usual Guix profiles to the home directory. This is useful in
342 # ephemeral setups where only certain part of the filesystem is
343 # persistent (e.g., "Erase my darlings"-type of setup).
344 system.userActivationScripts.guix-activate-user-profiles.text =
345 let
346 guixProfile = profile: "${cfg.stateDir}/guix/profiles/per-user/\${USER}/${profile}";
347 linkProfile =
348 profile: location:
349 let
350 userProfile = guixProfile profile;
351 in
352 ''
353 [ -d "${userProfile}" ] && ln -sfn "${userProfile}" "${location}"
354 '';
355 linkProfileToPath =
356 acc: profile: location:
357 acc + (linkProfile profile location);
358
359 # This should contain export-only Guix user profiles. The rest of it is
360 # handled manually in the activation script.
361 guixUserProfiles' = lib.attrsets.removeAttrs guixUserProfiles [ "guix-home" ];
362
363 linkExportsScript = lib.foldlAttrs linkProfileToPath "" guixUserProfiles';
364 in
365 ''
366 # Don't export this please! It is only expected to be used for this
367 # activation script and nothing else.
368 XDG_CONFIG_HOME=''${XDG_CONFIG_HOME:-$HOME/.config}
369
370 # Linking the usual Guix profiles into the home directory.
371 ${linkExportsScript}
372
373 # Activate all of the default Guix non-exports profiles manually.
374 ${linkProfile "guix-home" "$HOME/.guix-home"}
375 [ -L "$HOME/.guix-home" ] && "$HOME/.guix-home/activate"
376 '';
377
378 # GUIX_LOCPATH is basically LOCPATH but for Guix libc which in turn used by
379 # virtually every Guix-built packages. This is so that Guix-installed
380 # applications wouldn't use incompatible locale data and not touch its host
381 # system.
382 environment.sessionVariables.GUIX_LOCPATH = lib.makeSearchPath "lib/locale" guixProfiles;
383
384 # What Guix profiles export is very similar to Nix profiles so it is
385 # acceptable to list it here. Also, it is more likely that the user would
386 # want to use packages explicitly installed from Guix so we're putting it
387 # first.
388 environment.profiles = lib.mkBefore guixProfiles;
389 }
390
391 (lib.mkIf cfg.publish.enable {
392 systemd.services.guix-publish = {
393 description = "Guix remote store";
394 environment = serviceEnv;
395
396 # Mounts will be required by the daemon service anyways so there's no
397 # need add RequiresMountsFor= or something similar.
398 requires = [ "guix-daemon.service" ];
399 after = [ "guix-daemon.service" ];
400 partOf = [ "guix-daemon.service" ];
401
402 preStart = lib.mkIf cfg.publish.generateKeyPair ''
403 # Generate the keypair if it's missing.
404 [ -f "/etc/guix/signing-key.sec" ] && [ -f "/etc/guix/signing-key.pub" ] || \
405 ${lib.getExe' package "guix"} archive --generate-key || {
406 rm /etc/guix/signing-key.*;
407 ${lib.getExe' package "guix"} archive --generate-key;
408 }
409 '';
410 script = ''
411 exec ${lib.getExe' package "guix"} publish \
412 --user=${cfg.publish.user} --port=${builtins.toString cfg.publish.port} \
413 ${lib.escapeShellArgs cfg.publish.extraArgs}
414 '';
415
416 serviceConfig = {
417 Restart = "always";
418 RestartSec = 10;
419
420 ProtectClock = true;
421 ProtectHostname = true;
422 ProtectKernelTunables = true;
423 ProtectKernelModules = true;
424 ProtectControlGroups = true;
425 SystemCallFilter = [
426 "@system-service"
427 "@debug"
428 "@setuid"
429 ];
430
431 RestrictNamespaces = true;
432 RestrictAddressFamilies = [
433 "AF_UNIX"
434 "AF_INET"
435 "AF_INET6"
436 ];
437
438 # While the permissions can be set, it is assumed to be taken by Guix
439 # daemon service which it has already done the setup.
440 ConfigurationDirectory = "guix";
441
442 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
443 CapabilityBoundingSet = [
444 "CAP_NET_BIND_SERVICE"
445 "CAP_SETUID"
446 "CAP_SETGID"
447 ];
448 };
449 wantedBy = [ "multi-user.target" ];
450 };
451
452 users.users.guix-publish = lib.mkIf (cfg.publish.user == "guix-publish") {
453 description = "Guix publish user";
454 group = config.users.groups.guix-publish.name;
455 isSystemUser = true;
456 };
457 users.groups.guix-publish = { };
458 })
459
460 (lib.mkIf cfg.gc.enable {
461 # This service should be handled by root to collect all garbage by all
462 # users.
463 systemd.services.guix-gc = {
464 description = "Guix garbage collection";
465 startAt = cfg.gc.dates;
466 script = ''
467 exec ${lib.getExe' package "guix"} gc ${lib.escapeShellArgs cfg.gc.extraArgs}
468 '';
469 serviceConfig = {
470 Type = "oneshot";
471 PrivateDevices = true;
472 PrivateNetwork = true;
473 ProtectControlGroups = true;
474 ProtectHostname = true;
475 ProtectKernelTunables = true;
476 SystemCallFilter = [
477 "@default"
478 "@file-system"
479 "@basic-io"
480 "@system-service"
481 ];
482 };
483 };
484
485 systemd.timers.guix-gc.timerConfig.Persistent = true;
486 })
487 ]
488 );
489}