at 25.11-pre 16 kB view raw
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}