depot/nixos/modules/services/misc/guix/default.nix
Luke Granger-Brown 57725ef3ec Squashed 'third_party/nixpkgs/' content from commit 76612b17c0ce
git-subtree-dir: third_party/nixpkgs
git-subtree-split: 76612b17c0ce71689921ca12d9ffdc9c23ce40b2
2024-11-10 23:59:47 +00:00

466 lines
15 KiB
Nix

{ 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";
};
# Currently, this is just done the lazy way with the official Guix script. A
# more "formal" way would be creating our own Guix script to handle and
# generate the ACL file ourselves.
aclFile = pkgs.runCommandLocal "guix-acl" { } ''
export GUIX_CONFIGURATION_DIRECTORY=./
for official_server_keys in ${lib.concatStringsSep " " cfg.substituters.authorizedKeys}; do
${lib.getExe' cfg.package "guix"} archive --authorize < "$official_server_keys"
done
install -Dm0600 ./acl "$out"
'';
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";
};
substituters = {
urls = lib.mkOption {
type = with lib.types; listOf str;
default = [
"https://ci.guix.gnu.org"
"https://bordeaux.guix.gnu.org"
"https://berlin.guix.gnu.org"
];
example = lib.literalExpression ''
options.services.guix.substituters.urls.default ++ [
"https://guix.example.com"
"https://guix.example.org"
]
'';
description = ''
A list of substitute servers' URLs for the Guix daemon to download
substitutes from.
'';
};
authorizedKeys = lib.mkOption {
type = with lib.types; listOf path;
default = [
"${cfg.package}/share/guix/ci.guix.gnu.org.pub"
"${cfg.package}/share/guix/bordeaux.guix.gnu.org.pub"
"${cfg.package}/share/guix/berlin.guix.gnu.org.pub"
];
defaultText = ''
The packaged signing keys from {option}`services.guix.package`.
'';
example = lib.literalExpression ''
options.services.guix.substituters.authorizedKeys.default ++ [
(builtins.fetchurl {
url = "https://guix.example.com/signing-key.pub";
})
(builtins.fetchurl {
url = "https://guix.example.org/static/signing-key.pub";
})
]
'';
description = ''
A list of signing keys for each substitute server to be authorized as
a source of substitutes. Without this, the listed substitute servers
from {option}`services.guix.substituters.urls` would be ignored [with
some
exceptions](https://guix.gnu.org/manual/en/html_node/Substitute-Authentication.html).
'';
};
};
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.optionalString (cfg.substituters.urls != [ ])
"--substitute-urls='${lib.concatStringsSep " " cfg.substituters.urls}'"} \
${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.
environment.etc."guix/acl".source = aclFile;
# 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:
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;
PrivateNetwork = true;
ProtectControlGroups = true;
ProtectHostname = true;
ProtectKernelTunables = true;
SystemCallFilter = [
"@default"
"@file-system"
"@basic-io"
"@system-service"
];
};
};
systemd.timers.guix-gc.timerConfig.Persistent = true;
})
]);
}