nixos/guix: init

This commit is contained in:
Gabriel Arazas 2023-10-29 14:44:03 +08:00 committed by Weijia Wang
parent 092aaf8418
commit ad277ea47e
9 changed files with 599 additions and 0 deletions

View File

@ -14,6 +14,8 @@ In addition to numerous new and upgraded packages, this release has the followin
<!-- 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).

View File

@ -683,6 +683,7 @@
./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

View File

@ -0,0 +1,394 @@
{ 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;
})
]);
}

View File

@ -357,6 +357,7 @@ in {
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; };

View File

@ -0,0 +1,38 @@
# 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}")
'';
})

View File

@ -0,0 +1,8 @@
{ system ? builtins.currentSystem
, pkgs ? import ../../.. { inherit system; }
}:
{
basic = import ./basic.nix { inherit system pkgs; };
publish = import ./publish.nix { inherit system pkgs; };
}

View File

@ -0,0 +1,95 @@
# 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'")
'';
})

View File

@ -0,0 +1,52 @@
;; 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))))

View File

@ -0,0 +1,8 @@
;; 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))))))