nixos/guix: init

authored by Gabriel Arazas and committed by Weijia Wang ad277ea4 092aaf84

+599
+2
nixos/doc/manual/release-notes/rl-2405.section.md
··· 14 14 15 15 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. --> 16 16 17 + - [Guix](https://guix.gnu.org), a functional package manager inspired by Nix. Available as [services.guix](#opt-services.guix.enable). 18 + 17 19 - [maubot](https://github.com/maubot/maubot), a plugin-based Matrix bot framework. Available as [services.maubot](#opt-services.maubot.enable). 18 20 19 21 - [Anki Sync Server](https://docs.ankiweb.net/sync-server.html), the official sync server built into recent versions of Anki. Available as [services.anki-sync-server](#opt-services.anki-sync-server.enable).
+1
nixos/modules/module-list.nix
··· 683 683 ./services/misc/gollum.nix 684 684 ./services/misc/gpsd.nix 685 685 ./services/misc/greenclip.nix 686 + ./services/misc/guix 686 687 ./services/misc/headphones.nix 687 688 ./services/misc/heisenbridge.nix 688 689 ./services/misc/homepage-dashboard.nix
+394
nixos/modules/services/misc/guix/default.nix
··· 1 + { config, pkgs, lib, ... }: 2 + 3 + let 4 + cfg = config.services.guix; 5 + 6 + package = cfg.package.override { inherit (cfg) stateDir storeDir; }; 7 + 8 + guixBuildUser = id: { 9 + name = "guixbuilder${toString id}"; 10 + group = cfg.group; 11 + extraGroups = [ cfg.group ]; 12 + createHome = false; 13 + description = "Guix build user ${toString id}"; 14 + isSystemUser = true; 15 + }; 16 + 17 + guixBuildUsers = numberOfUsers: 18 + builtins.listToAttrs (map 19 + (user: { 20 + name = user.name; 21 + value = user; 22 + }) 23 + (builtins.genList guixBuildUser numberOfUsers)); 24 + 25 + # A set of Guix user profiles to be linked at activation. 26 + guixUserProfiles = { 27 + # The current Guix profile that is created through `guix pull`. 28 + "current-guix" = "\${XDG_CONFIG_HOME}/guix/current"; 29 + 30 + # The default Guix profile similar to $HOME/.nix-profile from Nix. 31 + "guix-profile" = "$HOME/.guix-profile"; 32 + }; 33 + 34 + # All of the Guix profiles to be used. 35 + guixProfiles = lib.attrValues guixUserProfiles; 36 + 37 + serviceEnv = { 38 + GUIX_LOCPATH = "${cfg.stateDir}/guix/profiles/per-user/root/guix-profile/lib/locale"; 39 + LC_ALL = "C.UTF-8"; 40 + }; 41 + in 42 + { 43 + meta.maintainers = with lib.maintainers; [ foo-dogsquared ]; 44 + 45 + options.services.guix = with lib; { 46 + enable = mkEnableOption "Guix build daemon service"; 47 + 48 + group = mkOption { 49 + type = types.str; 50 + default = "guixbuild"; 51 + example = "guixbuild"; 52 + description = '' 53 + The group of the Guix build user pool. 54 + ''; 55 + }; 56 + 57 + nrBuildUsers = mkOption { 58 + type = types.ints.unsigned; 59 + description = '' 60 + Number of Guix build users to be used in the build pool. 61 + ''; 62 + default = 10; 63 + example = 20; 64 + }; 65 + 66 + extraArgs = mkOption { 67 + type = with types; listOf str; 68 + default = [ ]; 69 + example = [ "--max-jobs=4" "--debug" ]; 70 + description = '' 71 + Extra flags to pass to the Guix daemon service. 72 + ''; 73 + }; 74 + 75 + package = mkPackageOption pkgs "guix" { 76 + extraDescription = '' 77 + It should contain {command}`guix-daemon` and {command}`guix` 78 + executable. 79 + ''; 80 + }; 81 + 82 + storeDir = mkOption { 83 + type = types.path; 84 + default = "/gnu/store"; 85 + description = '' 86 + The store directory where the Guix service will serve to/from. Take 87 + note Guix cannot take advantage of substitutes if you set it something 88 + other than {file}`/gnu/store` since most of the cached builds are 89 + assumed to be in there. 90 + 91 + ::: {.warning} 92 + This will also recompile all packages because the normal cache no 93 + longer applies. 94 + ::: 95 + ''; 96 + }; 97 + 98 + stateDir = mkOption { 99 + type = types.path; 100 + default = "/var"; 101 + description = '' 102 + The state directory where Guix service will store its data such as its 103 + user-specific profiles, cache, and state files. 104 + 105 + ::: {.warning} 106 + Changing it to something other than the default will rebuild the 107 + package. 108 + ::: 109 + ''; 110 + example = "/gnu/var"; 111 + }; 112 + 113 + publish = { 114 + enable = mkEnableOption "substitute server for your Guix store directory"; 115 + 116 + generateKeyPair = mkOption { 117 + type = types.bool; 118 + description = '' 119 + Whether to generate signing keys in {file}`/etc/guix` which are 120 + required to initialize a substitute server. Otherwise, 121 + `--public-key=$FILE` and `--private-key=$FILE` can be passed in 122 + {option}`services.guix.publish.extraArgs`. 123 + ''; 124 + default = true; 125 + example = false; 126 + }; 127 + 128 + port = mkOption { 129 + type = types.port; 130 + default = 8181; 131 + example = 8200; 132 + description = '' 133 + Port of the substitute server to listen on. 134 + ''; 135 + }; 136 + 137 + user = mkOption { 138 + type = types.str; 139 + default = "guix-publish"; 140 + description = '' 141 + Name of the user to change once the server is up. 142 + ''; 143 + }; 144 + 145 + extraArgs = mkOption { 146 + type = with types; listOf str; 147 + description = '' 148 + Extra flags to pass to the substitute server. 149 + ''; 150 + default = []; 151 + example = [ 152 + "--compression=zstd:6" 153 + "--discover=no" 154 + ]; 155 + }; 156 + }; 157 + 158 + gc = { 159 + enable = mkEnableOption "automatic garbage collection service for Guix"; 160 + 161 + extraArgs = mkOption { 162 + type = with types; listOf str; 163 + default = [ ]; 164 + description = '' 165 + List of arguments to be passed to {command}`guix gc`. 166 + 167 + When given no option, it will try to collect all garbage which is 168 + often inconvenient so it is recommended to set [some 169 + options](https://guix.gnu.org/en/manual/en/html_node/Invoking-guix-gc.html). 170 + ''; 171 + example = [ 172 + "--delete-generations=1m" 173 + "--free-space=10G" 174 + "--optimize" 175 + ]; 176 + }; 177 + 178 + dates = lib.mkOption { 179 + type = types.str; 180 + default = "03:15"; 181 + example = "weekly"; 182 + description = '' 183 + How often the garbage collection occurs. This takes the time format 184 + from {manpage}`systemd.time(7)`. 185 + ''; 186 + }; 187 + }; 188 + }; 189 + 190 + config = lib.mkIf cfg.enable (lib.mkMerge [ 191 + { 192 + environment.systemPackages = [ package ]; 193 + 194 + users.users = guixBuildUsers cfg.nrBuildUsers; 195 + users.groups.${cfg.group} = { }; 196 + 197 + # Guix uses Avahi (through guile-avahi) both for the auto-discovering and 198 + # advertising substitute servers in the local network. 199 + services.avahi.enable = lib.mkDefault true; 200 + services.avahi.publish.enable = lib.mkDefault true; 201 + services.avahi.publish.userServices = lib.mkDefault true; 202 + 203 + # It's similar to Nix daemon so there's no question whether or not this 204 + # should be sandboxed. 205 + systemd.services.guix-daemon = { 206 + environment = serviceEnv; 207 + script = '' 208 + ${lib.getExe' package "guix-daemon"} \ 209 + --build-users-group=${cfg.group} \ 210 + ${lib.escapeShellArgs cfg.extraArgs} 211 + ''; 212 + serviceConfig = { 213 + OOMPolicy = "continue"; 214 + RemainAfterExit = "yes"; 215 + Restart = "always"; 216 + TasksMax = 8192; 217 + }; 218 + unitConfig.RequiresMountsFor = [ 219 + cfg.storeDir 220 + cfg.stateDir 221 + ]; 222 + wantedBy = [ "multi-user.target" ]; 223 + }; 224 + 225 + # This is based from Nix daemon socket unit from upstream Nix package. 226 + # Guix build daemon has support for systemd-style socket activation. 227 + systemd.sockets.guix-daemon = { 228 + description = "Guix daemon socket"; 229 + before = [ "multi-user.target" ]; 230 + listenStreams = [ "${cfg.stateDir}/guix/daemon-socket/socket" ]; 231 + unitConfig = { 232 + RequiresMountsFor = [ 233 + cfg.storeDir 234 + cfg.stateDir 235 + ]; 236 + ConditionPathIsReadWrite = "${cfg.stateDir}/guix/daemon-socket"; 237 + }; 238 + wantedBy = [ "socket.target" ]; 239 + }; 240 + 241 + systemd.mounts = [{ 242 + description = "Guix read-only store directory"; 243 + before = [ "guix-daemon.service" ]; 244 + what = cfg.storeDir; 245 + where = cfg.storeDir; 246 + type = "none"; 247 + options = "bind,ro"; 248 + 249 + unitConfig.DefaultDependencies = false; 250 + wantedBy = [ "guix-daemon.service" ]; 251 + }]; 252 + 253 + # Make transferring files from one store to another easier with the usual 254 + # case being of most substitutes from the official Guix CI instance. 255 + system.activationScripts.guix-authorize-keys = '' 256 + for official_server_keys in ${package}/share/guix/*.pub; do 257 + ${lib.getExe' package "guix"} archive --authorize < $official_server_keys 258 + done 259 + ''; 260 + 261 + # Link the usual Guix profiles to the home directory. This is useful in 262 + # ephemeral setups where only certain part of the filesystem is 263 + # persistent (e.g., "Erase my darlings"-type of setup). 264 + system.userActivationScripts.guix-activate-user-profiles.text = let 265 + linkProfileToPath = acc: profile: location: let 266 + guixProfile = "${cfg.stateDir}/guix/profiles/per-user/\${USER}/${profile}"; 267 + in acc + '' 268 + [ -d "${guixProfile}" ] && ln -sf "${guixProfile}" "${location}" 269 + ''; 270 + 271 + activationScript = lib.foldlAttrs linkProfileToPath "" guixUserProfiles; 272 + in '' 273 + # Don't export this please! It is only expected to be used for this 274 + # activation script and nothing else. 275 + XDG_CONFIG_HOME=''${XDG_CONFIG_HOME:-$HOME/.config} 276 + 277 + # Linking the usual Guix profiles into the home directory. 278 + ${activationScript} 279 + ''; 280 + 281 + # GUIX_LOCPATH is basically LOCPATH but for Guix libc which in turn used by 282 + # virtually every Guix-built packages. This is so that Guix-installed 283 + # applications wouldn't use incompatible locale data and not touch its host 284 + # system. 285 + environment.sessionVariables.GUIX_LOCPATH = lib.makeSearchPath "lib/locale" guixProfiles; 286 + 287 + # What Guix profiles export is very similar to Nix profiles so it is 288 + # acceptable to list it here. Also, it is more likely that the user would 289 + # want to use packages explicitly installed from Guix so we're putting it 290 + # first. 291 + environment.profiles = lib.mkBefore guixProfiles; 292 + } 293 + 294 + (lib.mkIf cfg.publish.enable { 295 + systemd.services.guix-publish = { 296 + description = "Guix remote store"; 297 + environment = serviceEnv; 298 + 299 + # Mounts will be required by the daemon service anyways so there's no 300 + # need add RequiresMountsFor= or something similar. 301 + requires = [ "guix-daemon.service" ]; 302 + after = [ "guix-daemon.service" ]; 303 + partOf = [ "guix-daemon.service" ]; 304 + 305 + preStart = lib.mkIf cfg.publish.generateKeyPair '' 306 + # Generate the keypair if it's missing. 307 + [ -f "/etc/guix/signing-key.sec" ] && [ -f "/etc/guix/signing-key.pub" ] || \ 308 + ${lib.getExe' package "guix"} archive --generate-key || { 309 + rm /etc/guix/signing-key.*; 310 + ${lib.getExe' package "guix"} archive --generate-key; 311 + } 312 + ''; 313 + script = '' 314 + ${lib.getExe' package "guix"} publish \ 315 + --user=${cfg.publish.user} --port=${builtins.toString cfg.publish.port} \ 316 + ${lib.escapeShellArgs cfg.publish.extraArgs} 317 + ''; 318 + 319 + serviceConfig = { 320 + Restart = "always"; 321 + RestartSec = 10; 322 + 323 + ProtectClock = true; 324 + ProtectHostname = true; 325 + ProtectKernelTunables = true; 326 + ProtectKernelModules = true; 327 + ProtectControlGroups = true; 328 + SystemCallFilter = [ 329 + "@system-service" 330 + "@debug" 331 + "@setuid" 332 + ]; 333 + 334 + RestrictNamespaces = true; 335 + RestrictAddressFamilies = [ 336 + "AF_UNIX" 337 + "AF_INET" 338 + "AF_INET6" 339 + ]; 340 + 341 + # While the permissions can be set, it is assumed to be taken by Guix 342 + # daemon service which it has already done the setup. 343 + ConfigurationDirectory = "guix"; 344 + 345 + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; 346 + CapabilityBoundingSet = [ 347 + "CAP_NET_BIND_SERVICE" 348 + "CAP_SETUID" 349 + "CAP_SETGID" 350 + ]; 351 + }; 352 + wantedBy = [ "multi-user.target" ]; 353 + }; 354 + 355 + users.users.guix-publish = lib.mkIf (cfg.publish.user == "guix-publish") { 356 + description = "Guix publish user"; 357 + group = config.users.groups.guix-publish.name; 358 + isSystemUser = true; 359 + }; 360 + users.groups.guix-publish = {}; 361 + }) 362 + 363 + (lib.mkIf cfg.gc.enable { 364 + # This service should be handled by root to collect all garbage by all 365 + # users. 366 + systemd.services.guix-gc = { 367 + description = "Guix garbage collection"; 368 + startAt = cfg.gc.dates; 369 + script = '' 370 + ${lib.getExe' package "guix"} gc ${lib.escapeShellArgs cfg.gc.extraArgs} 371 + ''; 372 + 373 + serviceConfig = { 374 + Type = "oneshot"; 375 + 376 + MemoryDenyWriteExecute = true; 377 + PrivateDevices = true; 378 + PrivateNetworks = true; 379 + ProtectControlGroups = true; 380 + ProtectHostname = true; 381 + ProtectKernelTunables = true; 382 + SystemCallFilter = [ 383 + "@default" 384 + "@file-system" 385 + "@basic-io" 386 + "@system-service" 387 + ]; 388 + }; 389 + }; 390 + 391 + systemd.timers.guix-gc.timerConfig.Persistent = true; 392 + }) 393 + ]); 394 + }
+1
nixos/tests/all-tests.nix
··· 357 357 grow-partition = runTest ./grow-partition.nix; 358 358 grub = handleTest ./grub.nix {}; 359 359 guacamole-server = handleTest ./guacamole-server.nix {}; 360 + guix = handleTest ./guix {}; 360 361 gvisor = handleTest ./gvisor.nix {}; 361 362 hadoop = import ./hadoop { inherit handleTestOn; package=pkgs.hadoop; }; 362 363 hadoop_3_2 = import ./hadoop { inherit handleTestOn; package=pkgs.hadoop_3_2; };
+38
nixos/tests/guix/basic.nix
··· 1 + # Take note the Guix store directory is empty. Also, we're trying to prevent 2 + # Guix from trying to downloading substitutes because of the restricted 3 + # access (assuming it's in a sandboxed environment). 4 + # 5 + # So this test is what it is: a basic test while trying to use Guix as much as 6 + # we possibly can (including the API) without triggering its download alarm. 7 + 8 + import ../make-test-python.nix ({ lib, pkgs, ... }: { 9 + name = "guix-basic"; 10 + meta.maintainers = with lib.maintainers; [ foo-dogsquared ]; 11 + 12 + nodes.machine = { config, ... }: { 13 + environment.etc."guix/scripts".source = ./scripts; 14 + services.guix.enable = true; 15 + }; 16 + 17 + testScript = '' 18 + import pathlib 19 + 20 + machine.wait_for_unit("multi-user.target") 21 + machine.wait_for_unit("guix-daemon.service") 22 + 23 + # Can't do much here since the environment has restricted network access. 24 + with subtest("Guix basic package management"): 25 + machine.succeed("guix build --dry-run --verbosity=0 hello") 26 + machine.succeed("guix show hello") 27 + 28 + # This is to see if the Guix API is usable and mostly working. 29 + with subtest("Guix API scripting"): 30 + scripts_dir = pathlib.Path("/etc/guix/scripts") 31 + 32 + text_msg = "Hello there, NixOS!" 33 + text_store_file = machine.succeed(f"guix repl -- {scripts_dir}/create-file-to-store.scm '{text_msg}'") 34 + assert machine.succeed(f"cat {text_store_file}") == text_msg 35 + 36 + machine.succeed(f"guix repl -- {scripts_dir}/add-existing-files-to-store.scm {scripts_dir}") 37 + ''; 38 + })
+8
nixos/tests/guix/default.nix
··· 1 + { system ? builtins.currentSystem 2 + , pkgs ? import ../../.. { inherit system; } 3 + }: 4 + 5 + { 6 + basic = import ./basic.nix { inherit system pkgs; }; 7 + publish = import ./publish.nix { inherit system pkgs; }; 8 + }
+95
nixos/tests/guix/publish.nix
··· 1 + # Testing out the substitute server with two machines in a local network. As a 2 + # bonus, we'll also test a feature of the substitute server being able to 3 + # advertise its service to the local network with Avahi. 4 + 5 + import ../make-test-python.nix ({ pkgs, lib, ... }: let 6 + publishPort = 8181; 7 + inherit (builtins) toString; 8 + in { 9 + name = "guix-publish"; 10 + 11 + meta.maintainers = with lib.maintainers; [ foo-dogsquared ]; 12 + 13 + nodes = let 14 + commonConfig = { config, ... }: { 15 + # We'll be using '--advertise' flag with the 16 + # substitute server which requires Avahi. 17 + services.avahi = { 18 + enable = true; 19 + nssmdns = true; 20 + publish = { 21 + enable = true; 22 + userServices = true; 23 + }; 24 + }; 25 + }; 26 + in { 27 + server = { config, lib, pkgs, ... }: { 28 + imports = [ commonConfig ]; 29 + 30 + services.guix = { 31 + enable = true; 32 + publish = { 33 + enable = true; 34 + port = publishPort; 35 + 36 + generateKeyPair = true; 37 + extraArgs = [ "--advertise" ]; 38 + }; 39 + }; 40 + 41 + networking.firewall.allowedTCPPorts = [ publishPort ]; 42 + }; 43 + 44 + client = { config, lib, pkgs, ... }: { 45 + imports = [ commonConfig ]; 46 + 47 + services.guix = { 48 + enable = true; 49 + 50 + extraArgs = [ 51 + # Force to only get all substitutes from the local server. We don't 52 + # have anything in the Guix store directory and we cannot get 53 + # anything from the official substitute servers anyways. 54 + "--substitute-urls='http://server.local:${toString publishPort}'" 55 + 56 + # Enable autodiscovery of the substitute servers in the local 57 + # network. This machine shouldn't need to import the signing key from 58 + # the substitute server since it is automatically done anyways. 59 + "--discover=yes" 60 + ]; 61 + }; 62 + }; 63 + }; 64 + 65 + testScript = '' 66 + import pathlib 67 + 68 + start_all() 69 + 70 + scripts_dir = pathlib.Path("/etc/guix/scripts") 71 + 72 + for machine in machines: 73 + machine.wait_for_unit("multi-user.target") 74 + machine.wait_for_unit("guix-daemon.service") 75 + machine.wait_for_unit("avahi-daemon.service") 76 + 77 + server.wait_for_unit("guix-publish.service") 78 + server.wait_for_open_port(${toString publishPort}) 79 + server.succeed("curl http://localhost:${toString publishPort}/") 80 + 81 + # Now it's the client turn to make use of it. 82 + substitute_server = "http://server.local:${toString publishPort}" 83 + client.wait_for_unit("network-online.target") 84 + response = client.succeed(f"curl {substitute_server}") 85 + assert "Guix Substitute Server" in response 86 + 87 + # Authorizing the server to be used as a substitute server. 88 + client.succeed(f"curl -O {substitute_server}/signing-key.pub") 89 + client.succeed("guix archive --authorize < ./signing-key.pub") 90 + 91 + # Since we're using the substitute server with the `--advertise` flag, we 92 + # might as well check it. 93 + client.succeed("avahi-browse --resolve --terminate _guix_publish._tcp | grep '_guix_publish._tcp'") 94 + ''; 95 + })
+52
nixos/tests/guix/scripts/add-existing-files-to-store.scm
··· 1 + ;; A simple script that adds each file given from the command-line into the 2 + ;; store and checks them if it's the same. 3 + (use-modules (guix) 4 + (srfi srfi-1) 5 + (ice-9 ftw) 6 + (rnrs io ports)) 7 + 8 + ;; This is based from tests/derivations.scm from Guix source code. 9 + (define* (directory-contents dir #:optional (slurp get-bytevector-all)) 10 + "Return an alist representing the contents of DIR" 11 + (define prefix-len (string-length dir)) 12 + (sort (file-system-fold (const #t) 13 + (lambda (path stat result) 14 + (alist-cons (string-drop path prefix-len) 15 + (call-with-input-file path slurp) 16 + result)) 17 + (lambda (path stat result) result) 18 + (lambda (path stat result) result) 19 + (lambda (path stat result) result) 20 + (lambda (path stat errno result) result) 21 + '() 22 + dir) 23 + (lambda (e1 e2) 24 + (string<? (car e1) (car e2))))) 25 + 26 + (define* (check-if-same store drv path) 27 + "Check if the given path and its store item are the same" 28 + (let* ((filetype (stat:type (stat drv)))) 29 + (case filetype 30 + ((regular) 31 + (and (valid-path? store drv) 32 + (equal? (call-with-input-file path get-bytevector-all) 33 + (call-with-input-file drv get-bytevector-all)))) 34 + ((directory) 35 + (and (valid-path? store drv) 36 + (equal? (directory-contents path) 37 + (directory-contents drv)))) 38 + (else #f)))) 39 + 40 + (define* (add-and-check-item-to-store store path) 41 + "Add PATH to STORE and check if the contents are the same" 42 + (let* ((store-item (add-to-store store 43 + (basename path) 44 + #t "sha256" path)) 45 + (is-same (check-if-same store store-item path))) 46 + (if (not is-same) 47 + (exit 1)))) 48 + 49 + (with-store store 50 + (map (lambda (path) 51 + (add-and-check-item-to-store store (readlink* path))) 52 + (cdr (command-line))))
+8
nixos/tests/guix/scripts/create-file-to-store.scm
··· 1 + ;; A script that creates a store item with the given text and prints the 2 + ;; resulting store item path. 3 + (use-modules (guix)) 4 + 5 + (with-store store 6 + (display (add-text-to-store store "guix-basic-test-text" 7 + (string-join 8 + (cdr (command-line))))))