about summary refs log tree commit diff
path: root/nixpkgs/nixos/modules/services/misc/guix/default.nix
blob: 7174ff36b7090827e34aadf1d86d6005217c089d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
{ config, pkgs, lib, ... }:

let
  cfg = config.services.guix;

  package = cfg.package.override { inherit (cfg) stateDir storeDir; };

  guixBuildUser = id: {
    name = "guixbuilder${toString id}";
    group = cfg.group;
    extraGroups = [ cfg.group ];
    createHome = false;
    description = "Guix build user ${toString id}";
    isSystemUser = true;
  };

  guixBuildUsers = numberOfUsers:
    builtins.listToAttrs (map
      (user: {
        name = user.name;
        value = user;
      })
      (builtins.genList guixBuildUser numberOfUsers));

  # A set of Guix user profiles to be linked at activation. All of these should
  # be default profiles managed by Guix CLI and the profiles are located in
  # `${cfg.stateDir}/profiles/per-user/$USER/$PROFILE`.
  guixUserProfiles = {
    # The default Guix profile managed by `guix pull`. Take note this should be
    # the profile with the most precedence in `PATH` env to let users use their
    # updated versions of `guix` CLI.
    "current-guix" = "\${XDG_CONFIG_HOME}/guix/current";

    # The default Guix home profile. This profile contains more than exports
    # such as an activation script at `$GUIX_HOME_PROFILE/activate`.
    "guix-home" = "$HOME/.guix-home/profile";

    # The default Guix profile similar to $HOME/.nix-profile from Nix.
    "guix-profile" = "$HOME/.guix-profile";
  };

  # All of the Guix profiles to be used.
  guixProfiles = lib.attrValues guixUserProfiles;

  serviceEnv = {
    GUIX_LOCPATH = "${cfg.stateDir}/guix/profiles/per-user/root/guix-profile/lib/locale";
    LC_ALL = "C.UTF-8";
  };
in
{
  meta.maintainers = with lib.maintainers; [ foo-dogsquared ];

  options.services.guix = with lib; {
    enable = mkEnableOption "Guix build daemon service";

    group = mkOption {
      type = types.str;
      default = "guixbuild";
      example = "guixbuild";
      description = ''
        The group of the Guix build user pool.
      '';
    };

    nrBuildUsers = mkOption {
      type = types.ints.unsigned;
      description = ''
        Number of Guix build users to be used in the build pool.
      '';
      default = 10;
      example = 20;
    };

    extraArgs = mkOption {
      type = with types; listOf str;
      default = [ ];
      example = [ "--max-jobs=4" "--debug" ];
      description = ''
        Extra flags to pass to the Guix daemon service.
      '';
    };

    package = mkPackageOption pkgs "guix" {
      extraDescription = ''
        It should contain {command}`guix-daemon` and {command}`guix`
        executable.
      '';
    };

    storeDir = mkOption {
      type = types.path;
      default = "/gnu/store";
      description = ''
        The store directory where the Guix service will serve to/from. Take
        note Guix cannot take advantage of substitutes if you set it something
        other than {file}`/gnu/store` since most of the cached builds are
        assumed to be in there.

        ::: {.warning}
        This will also recompile all packages because the normal cache no
        longer applies.
        :::
      '';
    };

    stateDir = mkOption {
      type = types.path;
      default = "/var";
      description = ''
        The state directory where Guix service will store its data such as its
        user-specific profiles, cache, and state files.

        ::: {.warning}
        Changing it to something other than the default will rebuild the
        package.
        :::
      '';
      example = "/gnu/var";
    };

    publish = {
      enable = mkEnableOption "substitute server for your Guix store directory";

      generateKeyPair = mkOption {
        type = types.bool;
        description = ''
          Whether to generate signing keys in {file}`/etc/guix` which are
          required to initialize a substitute server. Otherwise,
          `--public-key=$FILE` and `--private-key=$FILE` can be passed in
          {option}`services.guix.publish.extraArgs`.
        '';
        default = true;
        example = false;
      };

      port = mkOption {
        type = types.port;
        default = 8181;
        example = 8200;
        description = ''
          Port of the substitute server to listen on.
        '';
      };

      user = mkOption {
        type = types.str;
        default = "guix-publish";
        description = ''
          Name of the user to change once the server is up.
        '';
      };

      extraArgs = mkOption {
        type = with types; listOf str;
        description = ''
          Extra flags to pass to the substitute server.
        '';
        default = [];
        example = [
          "--compression=zstd:6"
          "--discover=no"
        ];
      };
    };

    gc = {
      enable = mkEnableOption "automatic garbage collection service for Guix";

      extraArgs = mkOption {
        type = with types; listOf str;
        default = [ ];
        description = ''
          List of arguments to be passed to {command}`guix gc`.

          When given no option, it will try to collect all garbage which is
          often inconvenient so it is recommended to set [some
          options](https://guix.gnu.org/en/manual/en/html_node/Invoking-guix-gc.html).
        '';
        example = [
          "--delete-generations=1m"
          "--free-space=10G"
          "--optimize"
        ];
      };

      dates = lib.mkOption {
        type = types.str;
        default = "03:15";
        example = "weekly";
        description = ''
          How often the garbage collection occurs. This takes the time format
          from {manpage}`systemd.time(7)`.
        '';
      };
    };
  };

  config = lib.mkIf cfg.enable (lib.mkMerge [
    {
      environment.systemPackages = [ package ];

      users.users = guixBuildUsers cfg.nrBuildUsers;
      users.groups.${cfg.group} = { };

      # Guix uses Avahi (through guile-avahi) both for the auto-discovering and
      # advertising substitute servers in the local network.
      services.avahi.enable = lib.mkDefault true;
      services.avahi.publish.enable = lib.mkDefault true;
      services.avahi.publish.userServices = lib.mkDefault true;

      # It's similar to Nix daemon so there's no question whether or not this
      # should be sandboxed.
      systemd.services.guix-daemon = {
        environment = serviceEnv;
        script = ''
          ${lib.getExe' package "guix-daemon"} \
            --build-users-group=${cfg.group} \
            ${lib.escapeShellArgs cfg.extraArgs}
        '';
        serviceConfig = {
          OOMPolicy = "continue";
          RemainAfterExit = "yes";
          Restart = "always";
          TasksMax = 8192;
        };
        unitConfig.RequiresMountsFor = [
          cfg.storeDir
          cfg.stateDir
        ];
        wantedBy = [ "multi-user.target" ];
      };

      # This is based from Nix daemon socket unit from upstream Nix package.
      # Guix build daemon has support for systemd-style socket activation.
      systemd.sockets.guix-daemon = {
        description = "Guix daemon socket";
        before = [ "multi-user.target" ];
        listenStreams = [ "${cfg.stateDir}/guix/daemon-socket/socket" ];
        unitConfig.RequiresMountsFor = [ cfg.storeDir cfg.stateDir ];
        wantedBy = [ "sockets.target" ];
      };

      systemd.mounts = [{
        description = "Guix read-only store directory";
        before = [ "guix-daemon.service" ];
        what = cfg.storeDir;
        where = cfg.storeDir;
        type = "none";
        options = "bind,ro";

        unitConfig.DefaultDependencies = false;
        wantedBy = [ "guix-daemon.service" ];
      }];

      # Make transferring files from one store to another easier with the usual
      # case being of most substitutes from the official Guix CI instance.
      system.activationScripts.guix-authorize-keys = ''
        for official_server_keys in ${package}/share/guix/*.pub; do
          ${lib.getExe' package "guix"} archive --authorize < $official_server_keys
        done
      '';

      # Link the usual Guix profiles to the home directory. This is useful in
      # ephemeral setups where only certain part of the filesystem is
      # persistent (e.g., "Erase my darlings"-type of setup).
      system.userActivationScripts.guix-activate-user-profiles.text = let
        guixProfile = profile: "${cfg.stateDir}/guix/profiles/per-user/\${USER}/${profile}";
        linkProfile = profile: location: let
          userProfile = guixProfile profile;
        in ''
          [ -d "${userProfile}" ] && ln -sfn "${userProfile}" "${location}"
        '';
        linkProfileToPath = acc: profile: location: let
          in acc + (linkProfile profile location);

        # This should contain export-only Guix user profiles. The rest of it is
        # handled manually in the activation script.
        guixUserProfiles' = lib.attrsets.removeAttrs guixUserProfiles [ "guix-home" ];

        linkExportsScript = lib.foldlAttrs linkProfileToPath "" guixUserProfiles';
      in ''
        # Don't export this please! It is only expected to be used for this
        # activation script and nothing else.
        XDG_CONFIG_HOME=''${XDG_CONFIG_HOME:-$HOME/.config}

        # Linking the usual Guix profiles into the home directory.
        ${linkExportsScript}

        # Activate all of the default Guix non-exports profiles manually.
        ${linkProfile "guix-home" "$HOME/.guix-home"}
        [ -L "$HOME/.guix-home" ] && "$HOME/.guix-home/activate"
      '';

      # GUIX_LOCPATH is basically LOCPATH but for Guix libc which in turn used by
      # virtually every Guix-built packages. This is so that Guix-installed
      # applications wouldn't use incompatible locale data and not touch its host
      # system.
      environment.sessionVariables.GUIX_LOCPATH = lib.makeSearchPath "lib/locale" guixProfiles;

      # What Guix profiles export is very similar to Nix profiles so it is
      # acceptable to list it here. Also, it is more likely that the user would
      # want to use packages explicitly installed from Guix so we're putting it
      # first.
      environment.profiles = lib.mkBefore guixProfiles;
    }

    (lib.mkIf cfg.publish.enable {
      systemd.services.guix-publish = {
        description = "Guix remote store";
        environment = serviceEnv;

        # Mounts will be required by the daemon service anyways so there's no
        # need add RequiresMountsFor= or something similar.
        requires = [ "guix-daemon.service" ];
        after = [ "guix-daemon.service" ];
        partOf = [ "guix-daemon.service" ];

        preStart = lib.mkIf cfg.publish.generateKeyPair ''
          # Generate the keypair if it's missing.
          [ -f "/etc/guix/signing-key.sec" ] && [ -f "/etc/guix/signing-key.pub" ] || \
            ${lib.getExe' package "guix"} archive --generate-key || {
              rm /etc/guix/signing-key.*;
              ${lib.getExe' package "guix"} archive --generate-key;
            }
        '';
        script = ''
          ${lib.getExe' package "guix"} publish \
            --user=${cfg.publish.user} --port=${builtins.toString cfg.publish.port} \
            ${lib.escapeShellArgs cfg.publish.extraArgs}
        '';

        serviceConfig = {
          Restart = "always";
          RestartSec = 10;

          ProtectClock = true;
          ProtectHostname = true;
          ProtectKernelTunables = true;
          ProtectKernelModules = true;
          ProtectControlGroups = true;
          SystemCallFilter = [
            "@system-service"
            "@debug"
            "@setuid"
          ];

          RestrictNamespaces = true;
          RestrictAddressFamilies = [
            "AF_UNIX"
            "AF_INET"
            "AF_INET6"
          ];

          # While the permissions can be set, it is assumed to be taken by Guix
          # daemon service which it has already done the setup.
          ConfigurationDirectory = "guix";

          AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
          CapabilityBoundingSet = [
            "CAP_NET_BIND_SERVICE"
            "CAP_SETUID"
            "CAP_SETGID"
          ];
        };
        wantedBy = [ "multi-user.target" ];
      };

      users.users.guix-publish = lib.mkIf (cfg.publish.user == "guix-publish") {
        description = "Guix publish user";
        group = config.users.groups.guix-publish.name;
        isSystemUser = true;
      };
      users.groups.guix-publish = {};
    })

    (lib.mkIf cfg.gc.enable {
      # This service should be handled by root to collect all garbage by all
      # users.
      systemd.services.guix-gc = {
        description = "Guix garbage collection";
        startAt = cfg.gc.dates;
        script = ''
          ${lib.getExe' package "guix"} gc ${lib.escapeShellArgs cfg.gc.extraArgs}
        '';

        serviceConfig = {
          Type = "oneshot";

          PrivateDevices = true;
          PrivateNetworks = true;
          ProtectControlGroups = true;
          ProtectHostname = true;
          ProtectKernelTunables = true;
          SystemCallFilter = [
            "@default"
            "@file-system"
            "@basic-io"
            "@system-service"
          ];
        };
      };

      systemd.timers.guix-gc.timerConfig.Persistent = true;
    })
  ]);
}