{ 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; }) ]); }