nixos/guix: init

Changed files
+599
nixos
+2
nixos/doc/manual/release-notes/rl-2405.section.md
···
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
+
- [Guix](https://guix.gnu.org), a functional package manager inspired by Nix. Available as [services.guix](#opt-services.guix.enable).
+
- [maubot](https://github.com/maubot/maubot), a plugin-based Matrix bot framework. Available as [services.maubot](#opt-services.maubot.enable).
- [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
···
./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
+394
nixos/modules/services/misc/guix/default.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.
+
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;
+
})
+
]);
+
}
+1
nixos/tests/all-tests.nix
···
grow-partition = runTest ./grow-partition.nix;
grub = handleTest ./grub.nix {};
guacamole-server = handleTest ./guacamole-server.nix {};
+
guix = handleTest ./guix {};
gvisor = handleTest ./gvisor.nix {};
hadoop = import ./hadoop { inherit handleTestOn; package=pkgs.hadoop; };
hadoop_3_2 = import ./hadoop { inherit handleTestOn; package=pkgs.hadoop_3_2; };
+38
nixos/tests/guix/basic.nix
···
+
# Take note the Guix store directory is empty. Also, we're trying to prevent
+
# Guix from trying to downloading substitutes because of the restricted
+
# access (assuming it's in a sandboxed environment).
+
#
+
# So this test is what it is: a basic test while trying to use Guix as much as
+
# we possibly can (including the API) without triggering its download alarm.
+
+
import ../make-test-python.nix ({ lib, pkgs, ... }: {
+
name = "guix-basic";
+
meta.maintainers = with lib.maintainers; [ foo-dogsquared ];
+
+
nodes.machine = { config, ... }: {
+
environment.etc."guix/scripts".source = ./scripts;
+
services.guix.enable = true;
+
};
+
+
testScript = ''
+
import pathlib
+
+
machine.wait_for_unit("multi-user.target")
+
machine.wait_for_unit("guix-daemon.service")
+
+
# Can't do much here since the environment has restricted network access.
+
with subtest("Guix basic package management"):
+
machine.succeed("guix build --dry-run --verbosity=0 hello")
+
machine.succeed("guix show hello")
+
+
# This is to see if the Guix API is usable and mostly working.
+
with subtest("Guix API scripting"):
+
scripts_dir = pathlib.Path("/etc/guix/scripts")
+
+
text_msg = "Hello there, NixOS!"
+
text_store_file = machine.succeed(f"guix repl -- {scripts_dir}/create-file-to-store.scm '{text_msg}'")
+
assert machine.succeed(f"cat {text_store_file}") == text_msg
+
+
machine.succeed(f"guix repl -- {scripts_dir}/add-existing-files-to-store.scm {scripts_dir}")
+
'';
+
})
+8
nixos/tests/guix/default.nix
···
+
{ system ? builtins.currentSystem
+
, pkgs ? import ../../.. { inherit system; }
+
}:
+
+
{
+
basic = import ./basic.nix { inherit system pkgs; };
+
publish = import ./publish.nix { inherit system pkgs; };
+
}
+95
nixos/tests/guix/publish.nix
···
+
# Testing out the substitute server with two machines in a local network. As a
+
# bonus, we'll also test a feature of the substitute server being able to
+
# advertise its service to the local network with Avahi.
+
+
import ../make-test-python.nix ({ pkgs, lib, ... }: let
+
publishPort = 8181;
+
inherit (builtins) toString;
+
in {
+
name = "guix-publish";
+
+
meta.maintainers = with lib.maintainers; [ foo-dogsquared ];
+
+
nodes = let
+
commonConfig = { config, ... }: {
+
# We'll be using '--advertise' flag with the
+
# substitute server which requires Avahi.
+
services.avahi = {
+
enable = true;
+
nssmdns = true;
+
publish = {
+
enable = true;
+
userServices = true;
+
};
+
};
+
};
+
in {
+
server = { config, lib, pkgs, ... }: {
+
imports = [ commonConfig ];
+
+
services.guix = {
+
enable = true;
+
publish = {
+
enable = true;
+
port = publishPort;
+
+
generateKeyPair = true;
+
extraArgs = [ "--advertise" ];
+
};
+
};
+
+
networking.firewall.allowedTCPPorts = [ publishPort ];
+
};
+
+
client = { config, lib, pkgs, ... }: {
+
imports = [ commonConfig ];
+
+
services.guix = {
+
enable = true;
+
+
extraArgs = [
+
# Force to only get all substitutes from the local server. We don't
+
# have anything in the Guix store directory and we cannot get
+
# anything from the official substitute servers anyways.
+
"--substitute-urls='http://server.local:${toString publishPort}'"
+
+
# Enable autodiscovery of the substitute servers in the local
+
# network. This machine shouldn't need to import the signing key from
+
# the substitute server since it is automatically done anyways.
+
"--discover=yes"
+
];
+
};
+
};
+
};
+
+
testScript = ''
+
import pathlib
+
+
start_all()
+
+
scripts_dir = pathlib.Path("/etc/guix/scripts")
+
+
for machine in machines:
+
machine.wait_for_unit("multi-user.target")
+
machine.wait_for_unit("guix-daemon.service")
+
machine.wait_for_unit("avahi-daemon.service")
+
+
server.wait_for_unit("guix-publish.service")
+
server.wait_for_open_port(${toString publishPort})
+
server.succeed("curl http://localhost:${toString publishPort}/")
+
+
# Now it's the client turn to make use of it.
+
substitute_server = "http://server.local:${toString publishPort}"
+
client.wait_for_unit("network-online.target")
+
response = client.succeed(f"curl {substitute_server}")
+
assert "Guix Substitute Server" in response
+
+
# Authorizing the server to be used as a substitute server.
+
client.succeed(f"curl -O {substitute_server}/signing-key.pub")
+
client.succeed("guix archive --authorize < ./signing-key.pub")
+
+
# Since we're using the substitute server with the `--advertise` flag, we
+
# might as well check it.
+
client.succeed("avahi-browse --resolve --terminate _guix_publish._tcp | grep '_guix_publish._tcp'")
+
'';
+
})
+52
nixos/tests/guix/scripts/add-existing-files-to-store.scm
···
+
;; A simple script that adds each file given from the command-line into the
+
;; store and checks them if it's the same.
+
(use-modules (guix)
+
(srfi srfi-1)
+
(ice-9 ftw)
+
(rnrs io ports))
+
+
;; This is based from tests/derivations.scm from Guix source code.
+
(define* (directory-contents dir #:optional (slurp get-bytevector-all))
+
"Return an alist representing the contents of DIR"
+
(define prefix-len (string-length dir))
+
(sort (file-system-fold (const #t)
+
(lambda (path stat result)
+
(alist-cons (string-drop path prefix-len)
+
(call-with-input-file path slurp)
+
result))
+
(lambda (path stat result) result)
+
(lambda (path stat result) result)
+
(lambda (path stat result) result)
+
(lambda (path stat errno result) result)
+
'()
+
dir)
+
(lambda (e1 e2)
+
(string<? (car e1) (car e2)))))
+
+
(define* (check-if-same store drv path)
+
"Check if the given path and its store item are the same"
+
(let* ((filetype (stat:type (stat drv))))
+
(case filetype
+
((regular)
+
(and (valid-path? store drv)
+
(equal? (call-with-input-file path get-bytevector-all)
+
(call-with-input-file drv get-bytevector-all))))
+
((directory)
+
(and (valid-path? store drv)
+
(equal? (directory-contents path)
+
(directory-contents drv))))
+
(else #f))))
+
+
(define* (add-and-check-item-to-store store path)
+
"Add PATH to STORE and check if the contents are the same"
+
(let* ((store-item (add-to-store store
+
(basename path)
+
#t "sha256" path))
+
(is-same (check-if-same store store-item path)))
+
(if (not is-same)
+
(exit 1))))
+
+
(with-store store
+
(map (lambda (path)
+
(add-and-check-item-to-store store (readlink* path)))
+
(cdr (command-line))))
+8
nixos/tests/guix/scripts/create-file-to-store.scm
···
+
;; A script that creates a store item with the given text and prints the
+
;; resulting store item path.
+
(use-modules (guix))
+
+
(with-store store
+
(display (add-text-to-store store "guix-basic-test-text"
+
(string-join
+
(cdr (command-line))))))