about summary refs log tree commit diff
path: root/nixos/modules
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules')
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/programs/tsm-client.nix299
-rw-r--r--nixos/modules/services/backup/tsm.nix11
-rw-r--r--nixos/modules/services/hardware/thinkfan.nix2
-rw-r--r--nixos/modules/services/misc/ankisyncd.nix6
-rw-r--r--nixos/modules/services/misc/guix/default.nix394
6 files changed, 566 insertions, 147 deletions
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index b505a294c8b9..fee7c35ed8f4 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -684,6 +684,7 @@
   ./services/misc/gollum.nix
   ./services/misc/gpsd.nix
   ./services/misc/greenclip.nix
+  ./services/misc/guix
   ./services/misc/headphones.nix
   ./services/misc/heisenbridge.nix
   ./services/misc/homepage-dashboard.nix
diff --git a/nixos/modules/programs/tsm-client.nix b/nixos/modules/programs/tsm-client.nix
index 6cb225d102de..45d436221ee3 100644
--- a/nixos/modules/programs/tsm-client.nix
+++ b/nixos/modules/programs/tsm-client.nix
@@ -1,193 +1,144 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:  # XXX migration code for freeform settings: `options` can be removed in 2025
+let optionsGlobal = options; in
 
 let
 
-  inherit (builtins) length map;
-  inherit (lib.attrsets) attrNames filterAttrs hasAttr mapAttrs mapAttrsToList optionalAttrs;
+  inherit (lib.attrsets) attrNames attrValues mapAttrsToList removeAttrs;
+  inherit (lib.lists) all allUnique concatLists elem isList map;
   inherit (lib.modules) mkDefault mkIf;
-  inherit (lib.options) literalExpression mkEnableOption mkOption mkPackageOption;
-  inherit (lib.strings) concatLines optionalString toLower;
-  inherit (lib.types) addCheck attrsOf lines nonEmptyStr nullOr package path port str strMatching submodule;
+  inherit (lib.options) mkEnableOption mkOption mkPackageOption;
+  inherit (lib.strings) concatLines match optionalString toLower;
+  inherit (lib.trivial) isInt;
+  inherit (lib.types) addCheck attrsOf coercedTo either enum int lines listOf nonEmptyStr nullOr oneOf path port singleLineStr strMatching submodule;
 
-  # Checks if given list of strings contains unique
-  # elements when compared without considering case.
-  # Type: checkIUnique :: [string] -> bool
-  # Example: checkIUnique ["foo" "Foo"] => false
-  checkIUnique = lst:
-    let
-      lenUniq = l: length (lib.lists.unique l);
-    in
-      lenUniq lst == lenUniq (map toLower lst);
+  scalarType =
+    # see the option's description below for the
+    # handling/transformation of each possible type
+    oneOf [ (enum [ true null ]) int path singleLineStr ];
 
   # TSM rejects servername strings longer than 64 chars.
-  servernameType = strMatching ".{1,64}";
+  servernameType = strMatching "[^[:space:]]{1,64}";
 
   serverOptions = { name, config, ... }: {
-    options.name = mkOption {
+    freeformType = attrsOf (either scalarType (listOf scalarType));
+    # Client system-options file directives are explained here:
+    # https://www.ibm.com/docs/en/storage-protect/8.1.20?topic=commands-processing-options
+    options.servername = mkOption {
       type = servernameType;
+      default = name;
       example = "mainTsmServer";
       description = lib.mdDoc ''
         Local name of the IBM TSM server,
-        must be uncapitalized and no longer than 64 chars.
-        The value will be used for the
-        `server`
-        directive in {file}`dsm.sys`.
+        must not contain space or more than 64 chars.
       '';
     };
-    options.server = mkOption {
+    options.tcpserveraddress = mkOption {
       type = nonEmptyStr;
       example = "tsmserver.company.com";
       description = lib.mdDoc ''
         Host/domain name or IP address of the IBM TSM server.
-        The value will be used for the
-        `tcpserveraddress`
-        directive in {file}`dsm.sys`.
       '';
     };
-    options.port = mkOption {
+    options.tcpport = mkOption {
       type = addCheck port (p: p<=32767);
       default = 1500;  # official default
       description = lib.mdDoc ''
         TCP port of the IBM TSM server.
-        The value will be used for the
-        `tcpport`
-        directive in {file}`dsm.sys`.
         TSM does not support ports above 32767.
       '';
     };
-    options.node = mkOption {
+    options.nodename = mkOption {
       type = nonEmptyStr;
       example = "MY-TSM-NODE";
       description = lib.mdDoc ''
         Target node name on the IBM TSM server.
-        The value will be used for the
-        `nodename`
-        directive in {file}`dsm.sys`.
       '';
     };
     options.genPasswd = mkEnableOption (lib.mdDoc ''
       automatic client password generation.
-      This option influences the
-      `passwordaccess`
-      directive in {file}`dsm.sys`.
+      This option does *not* cause a line in
+      {file}`dsm.sys` by itself, but generates a
+      corresponding `passwordaccess` directive.
       The password will be stored in the directory
-      given by the option {option}`passwdDir`.
+      given by the option {option}`passworddir`.
       *Caution*:
       If this option is enabled and the server forces
       to renew the password (e.g. on first connection),
       a random password will be generated and stored
     '');
-    options.passwdDir = mkOption {
-      type = path;
+    options.passwordaccess = mkOption {
+      type = enum [ "generate" "prompt" ];
+      visible = false;
+    };
+    options.passworddir = mkOption {
+      type = nullOr path;
+      default = null;
       example = "/home/alice/tsm-password";
       description = lib.mdDoc ''
         Directory that holds the TSM
         node's password information.
-        The value will be used for the
-        `passworddir`
-        directive in {file}`dsm.sys`.
       '';
     };
-    options.includeExclude = mkOption {
-      type = lines;
-      default = "";
+    options.inclexcl = mkOption {
+      type = coercedTo lines
+        (pkgs.writeText "inclexcl.dsm.sys")
+        (nullOr path);
+      default = null;
       example = ''
         exclude.dir     /nix/store
         include.encrypt /home/.../*
       '';
       description = lib.mdDoc ''
-        `include.*` and
-        `exclude.*` directives to be
-        used when sending files to the IBM TSM server.
-        The lines will be written into a file that the
-        `inclexcl`
-        directive in {file}`dsm.sys` points to.
-      '';
-    };
-    options.extraConfig = mkOption {
-      # TSM option keys are case insensitive;
-      # we have to ensure there are no keys that
-      # differ only by upper and lower case.
-      type = addCheck
-        (attrsOf (nullOr str))
-        (attrs: checkIUnique (attrNames attrs));
-      default = {};
-      example.compression = "yes";
-      example.passwordaccess = null;
-      description = lib.mdDoc ''
-        Additional key-value pairs for the server stanza.
-        Values must be strings, or `null`
-        for the key not to be used in the stanza
-        (e.g. to overrule values generated by other options).
-      '';
-    };
-    options.text = mkOption {
-      type = lines;
-      example = literalExpression
-        ''lib.modules.mkAfter "compression no"'';
-      description = lib.mdDoc ''
-        Additional text lines for the server stanza.
-        This option can be used if certion configuration keys
-        must be used multiple times or ordered in a certain way
-        as the {option}`extraConfig` option can't
-        control the order of lines in the resulting stanza.
-        Note that the `server`
-        line at the beginning of the stanza is
-        not part of this option's value.
+        Text lines with `include.*` and `exclude.*` directives
+        to be used when sending files to the IBM TSM server,
+        or an absolute path pointing to a file with such lines.
       '';
     };
-    options.stanza = mkOption {
-      type = str;
-      internal = true;
-      visible = false;
-      description = lib.mdDoc "Server stanza text generated from the options.";
-    };
-    config.name = mkDefault name;
-    # Client system-options file directives are explained here:
-    # https://www.ibm.com/docs/en/spectrum-protect/8.1.13?topic=commands-processing-options
-    config.extraConfig =
-      mapAttrs (lib.trivial.const mkDefault) (
-        {
-          commmethod = "v6tcpip";  # uses v4 or v6, based on dns lookup result
-          tcpserveraddress = config.server;
-          tcpport = builtins.toString config.port;
-          nodename = config.node;
-          passwordaccess = if config.genPasswd then "generate" else "prompt";
-          passworddir = ''"${config.passwdDir}"'';
-        } // optionalAttrs (config.includeExclude!="") {
-          inclexcl = ''"${pkgs.writeText "inclexcl.dsm.sys" config.includeExclude}"'';
-        }
-      );
-    config.text =
-      let
-        attrset = filterAttrs (k: v: v!=null) config.extraConfig;
-        mkLine = k: v: k + optionalString (v!="") "  ${v}";
-        lines = mapAttrsToList mkLine attrset;
-      in
-        concatLines lines;
-    config.stanza = ''
-      server  ${config.name}
-      ${config.text}
-    '';
+    config.commmethod = mkDefault "v6tcpip";  # uses v4 or v6, based on dns lookup result
+    config.passwordaccess = if config.genPasswd then "generate" else "prompt";
+    # XXX migration code for freeform settings, these can be removed in 2025:
+    options.warnings = optionsGlobal.warnings;
+    options.assertions = optionsGlobal.assertions;
+    imports = let inherit (lib.modules) mkRemovedOptionModule mkRenamedOptionModule; in [
+      (mkRemovedOptionModule [ "extraConfig" ] "Please just add options directly to the server attribute set, cf. the description of `programs.tsmClient.servers`.")
+      (mkRemovedOptionModule [ "text" ] "Please just add options directly to the server attribute set, cf. the description of `programs.tsmClient.servers`.")
+      (mkRenamedOptionModule [ "name" ] [ "servername" ])
+      (mkRenamedOptionModule [ "server" ] [ "tcpserveraddress" ])
+      (mkRenamedOptionModule [ "port" ] [ "tcpport" ])
+      (mkRenamedOptionModule [ "node" ] [ "nodename" ])
+      (mkRenamedOptionModule [ "passwdDir" ] [ "passworddir" ])
+      (mkRenamedOptionModule [ "includeExclude" ] [ "inclexcl" ])
+    ];
   };
 
   options.programs.tsmClient = {
     enable = mkEnableOption (lib.mdDoc ''
-      IBM Spectrum Protect (Tivoli Storage Manager, TSM)
+      IBM Storage Protect (Tivoli Storage Manager, TSM)
       client command line applications with a
       client system-options file "dsm.sys"
     '');
     servers = mkOption {
-      type = attrsOf (submodule [ serverOptions ]);
+      type = attrsOf (submodule serverOptions);
       default = {};
       example.mainTsmServer = {
-        server = "tsmserver.company.com";
-        node = "MY-TSM-NODE";
-        extraConfig.compression = "yes";
+        tcpserveraddress = "tsmserver.company.com";
+        nodename = "MY-TSM-NODE";
+        compression = "yes";
       };
       description = lib.mdDoc ''
         Server definitions ("stanzas")
         for the client system-options file.
+        The name of each entry will be used for
+        the internal `servername` by default.
+        Each attribute will be transformed into a line
+        with a key-value pair within the server's stanza.
+        Integers as values will be
+        canonically turned into strings.
+        The boolean value `true` will be turned
+        into a line with just the attribute's name.
+        The value `null` will not generate a line.
+        A list as values generates an entry for
+        each value, according to the rules above.
       '';
     };
     defaultServername = mkOption {
@@ -222,45 +173,107 @@ let
         to add paths to the client system-options file.
       '';
     };
-    wrappedPackage = mkOption {
-      type = package;
-      readOnly = true;
-      description = lib.mdDoc ''
-        The TSM client derivation, wrapped with the path
-        to the client system-options file "dsm.sys".
-        This option is to provide the effective derivation
+    wrappedPackage = mkPackageOption pkgs "tsm-client" {
+      default = null;
+      extraDescription = ''
+        This option is to provide the effective derivation,
+        wrapped with the path to the
+        client system-options file "dsm.sys".
+        It should not be changed, but exists
         for other modules that want to call TSM executables.
       '';
-    };
+    } // { readOnly = true; };
   };
 
   cfg = config.programs.tsmClient;
+  servernames = map (s: s.servername) (attrValues cfg.servers);
 
-  assertions = [
-    {
-      assertion = checkIUnique (mapAttrsToList (k: v: v.name) cfg.servers);
+  assertions =
+    [
+      {
+        assertion = allUnique (map toLower servernames);
+        message = ''
+          TSM server names
+          (option `programs.tsmClient.servers`)
+          contain duplicate name
+          (note that server names are case insensitive).
+        '';
+      }
+      {
+        assertion = (cfg.defaultServername!=null)->(elem cfg.defaultServername servernames);
+        message = ''
+          TSM default server name
+          `programs.tsmClient.defaultServername="${cfg.defaultServername}"`
+          not found in server names in
+          `programs.tsmClient.servers`.
+        '';
+      }
+    ] ++ (mapAttrsToList (name: serverCfg: {
+      assertion = all (key: null != match "[^[:space:]]+" key) (attrNames serverCfg);
       message = ''
-        TSM servernames contain duplicate name
-        (note that case doesn't matter!)
+        TSM server setting names in
+        `programs.tsmClient.servers.${name}.*`
+        contain spaces, but that's not allowed.
+      '';
+    }) cfg.servers) ++ (mapAttrsToList (name: serverCfg: {
+      assertion = allUnique (map toLower (attrNames serverCfg));
+      message = ''
+        TSM server setting names in
+        `programs.tsmClient.servers.${name}.*`
+        contain duplicate names
+        (note that setting names are case insensitive).
+      '';
+    }) cfg.servers)
+    # XXX migration code for freeform settings, this can be removed in 2025:
+    ++ (enrichMigrationInfos "assertions" (addText: { assertion, message }: { inherit assertion; message = addText message; }));
+
+  makeDsmSysLines = key: value:
+    # Turn a key-value pair from the server options attrset
+    # into zero (value==null), one (scalar value) or
+    # more (value is list) configuration stanza lines.
+    if isList value then map (makeDsmSysLines key) value else  # recurse into list
+    if value == null then [ ] else  # skip `null` value
+    [ ("  ${key}${
+      if value == true then "" else  # just output key if value is `true`
+      if isInt value then "  ${builtins.toString value}" else
+      if path.check value then "  \"${value}\"" else  # enclose path in ".."
+      if singleLineStr.check value then "  ${value}" else
+      throw "assertion failed: cannot convert type"  # should never happen
+    }") ];
+
+  makeDsmSysStanza = {servername, ... }@serverCfg:
+    let
+      # drop special values that should not go into server config block
+      attrs = removeAttrs serverCfg [ "servername" "genPasswd"
+        # XXX migration code for freeform settings, these can be removed in 2025:
+        "assertions" "warnings"
+        "extraConfig" "text"
+        "name" "server" "port" "node" "passwdDir" "includeExclude"
+      ];
+    in
+      ''
+        servername  ${servername}
+        ${concatLines (concatLists (mapAttrsToList makeDsmSysLines attrs))}
       '';
-    }
-    {
-      assertion = (cfg.defaultServername!=null)->(hasAttr cfg.defaultServername cfg.servers);
-      message = "TSM defaultServername not found in list of servers";
-    }
-  ];
 
   dsmSysText = ''
-    ****  IBM Spectrum Protect (Tivoli Storage Manager)
+    ****  IBM Storage Protect (Tivoli Storage Manager)
     ****  client system-options file "dsm.sys".
     ****  Do not edit!
     ****  This file is generated by NixOS configuration.
 
     ${optionalString (cfg.defaultServername!=null) "defaultserver  ${cfg.defaultServername}"}
 
-    ${concatLines (mapAttrsToList (k: v: v.stanza) cfg.servers)}
+    ${concatLines (map makeDsmSysStanza (attrValues cfg.servers))}
   '';
 
+  # XXX migration code for freeform settings, this can be removed in 2025:
+  enrichMigrationInfos = what: how: concatLists (
+    mapAttrsToList
+    (name: serverCfg: map (how (text: "In `programs.tsmClient.servers.${name}`: ${text}")) serverCfg."${what}")
+    cfg.servers
+  );
+
 in
 
 {
@@ -275,6 +288,8 @@ in
       dsmSysApi = dsmSysCli;
     };
     environment.systemPackages = [ cfg.wrappedPackage ];
+    # XXX migration code for freeform settings, this can be removed in 2025:
+    warnings = enrichMigrationInfos "warnings" (addText: addText);
   };
 
   meta.maintainers = [ lib.maintainers.yarny ];
diff --git a/nixos/modules/services/backup/tsm.nix b/nixos/modules/services/backup/tsm.nix
index c4de0b16d47d..6798b18b3af7 100644
--- a/nixos/modules/services/backup/tsm.nix
+++ b/nixos/modules/services/backup/tsm.nix
@@ -3,6 +3,7 @@
 let
 
   inherit (lib.attrsets) hasAttr;
+  inherit (lib.meta) getExe';
   inherit (lib.modules) mkDefault mkIf;
   inherit (lib.options) mkEnableOption mkOption;
   inherit (lib.types) nonEmptyStr nullOr;
@@ -10,7 +11,7 @@ let
   options.services.tsmBackup = {
     enable = mkEnableOption (lib.mdDoc ''
       automatic backups with the
-      IBM Spectrum Protect (Tivoli Storage Manager, TSM) client.
+      IBM Storage Protect (Tivoli Storage Manager, TSM) client.
       This also enables
       {option}`programs.tsmClient.enable`
     '');
@@ -78,10 +79,10 @@ in
   config = mkIf cfg.enable {
     inherit assertions;
     programs.tsmClient.enable = true;
-    programs.tsmClient.servers.${cfg.servername}.passwdDir =
+    programs.tsmClient.servers.${cfg.servername}.passworddir =
       mkDefault "/var/lib/tsm-backup/password";
     systemd.services.tsm-backup = {
-      description = "IBM Spectrum Protect (Tivoli Storage Manager) Backup";
+      description = "IBM Storage Protect (Tivoli Storage Manager) Backup";
       # DSM_LOG needs a trailing slash to have it treated as a directory.
       # `/var/log` would be littered with TSM log files otherwise.
       environment.DSM_LOG = "/var/log/tsm-backup/";
@@ -89,12 +90,12 @@ in
       environment.HOME = "/var/lib/tsm-backup";
       serviceConfig = {
         # for exit status description see
-        # https://www.ibm.com/docs/en/spectrum-protect/8.1.13?topic=clients-client-return-codes
+        # https://www.ibm.com/docs/en/storage-protect/8.1.20?topic=clients-client-return-codes
         SuccessExitStatus = "4 8";
         # The `-se` option must come after the command.
         # The `-optfile` option suppresses a `dsm.opt`-not-found warning.
         ExecStart =
-          "${cfgPrg.wrappedPackage}/bin/dsmc ${cfg.command} -se='${cfg.servername}' -optfile=/dev/null";
+          "${getExe' cfgPrg.wrappedPackage "dsmc"} ${cfg.command} -se='${cfg.servername}' -optfile=/dev/null";
         LogsDirectory = "tsm-backup";
         StateDirectory = "tsm-backup";
         StateDirectoryMode = "0750";
diff --git a/nixos/modules/services/hardware/thinkfan.nix b/nixos/modules/services/hardware/thinkfan.nix
index 8fa7b456f20e..cca35f492b8e 100644
--- a/nixos/modules/services/hardware/thinkfan.nix
+++ b/nixos/modules/services/hardware/thinkfan.nix
@@ -217,6 +217,8 @@ in {
 
     systemd.services = {
       thinkfan.environment.THINKFAN_ARGS = escapeShellArgs ([ "-c" configFile ] ++ cfg.extraArgs);
+      thinkfan.serviceConfig.Restart = "on-failure";
+      thinkfan.serviceConfig.RestartSec = "30s";
 
       # must be added manually, see issue #81138
       thinkfan.wantedBy = [ "multi-user.target" ];
diff --git a/nixos/modules/services/misc/ankisyncd.nix b/nixos/modules/services/misc/ankisyncd.nix
index e4de46e19a8f..f5acfbb0ee96 100644
--- a/nixos/modules/services/misc/ankisyncd.nix
+++ b/nixos/modules/services/misc/ankisyncd.nix
@@ -46,6 +46,12 @@ in
     };
 
     config = mkIf cfg.enable {
+      warnings = [
+        ''
+        `services.ankisyncd` has been replaced by `services.anki-sync-server` and will be removed after
+        24.05 because anki-sync-server(-rs and python) are not maintained.
+        ''
+      ];
       networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
 
       systemd.services.ankisyncd = {
diff --git a/nixos/modules/services/misc/guix/default.nix b/nixos/modules/services/misc/guix/default.nix
new file mode 100644
index 000000000000..00e84dc74554
--- /dev/null
+++ b/nixos/modules/services/misc/guix/default.nix
@@ -0,0 +1,394 @@
+{ 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.
+  guixUserProfiles = {
+    # The current Guix profile that is created through `guix pull`.
+    "current-guix" = "\${XDG_CONFIG_HOME}/guix/current";
+
+    # 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
+          ];
+          ConditionPathIsReadWrite = "${cfg.stateDir}/guix/daemon-socket";
+        };
+        wantedBy = [ "socket.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
+        linkProfileToPath = acc: profile: location: let
+          guixProfile = "${cfg.stateDir}/guix/profiles/per-user/\${USER}/${profile}";
+          in acc + ''
+            [ -d "${guixProfile}" ] && ln -sf "${guixProfile}" "${location}"
+          '';
+
+        activationScript = 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.
+        ${activationScript}
+      '';
+
+      # 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";
+
+          MemoryDenyWriteExecute = true;
+          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;
+    })
+  ]);
+}