From 1c1e415b4b0ef738d34a3a93b9d84dca50d2cb75 Mon Sep 17 00:00:00 2001
From: WilliButz <willibutz@posteo.de>
Date: Mon, 2 Sep 2024 15:06:39 +0200
Subject: [PATCH] init docs

---
 .github/workflows/render-options.yml |  27 ++
 .gitignore                           |   1 +
 README.md                            |  38 +--
 docs/.gitignore                      |   1 +
 docs/book.toml                       |  10 +
 docs/custom.css                      |   4 +
 docs/flake.lock                      |  27 ++
 docs/flake.nix                       |  60 ++++
 docs/src/SUMMARY.md                  |   9 +
 docs/src/examples.md                 | 131 ++++++++
 docs/src/impermanence-comparison.md  |  35 ++
 docs/src/impermanence-migration.md   | 111 ++++++
 docs/src/impermanence.md             |   7 +
 docs/src/library-and-testing.md      |  14 +
 flake.nix                            |   2 +-
 module.nix                           | 456 +------------------------
 options.nix                          | 482 +++++++++++++++++++++++++++
 17 files changed, 930 insertions(+), 485 deletions(-)
 create mode 100644 .github/workflows/render-options.yml
 create mode 100644 docs/.gitignore
 create mode 100644 docs/book.toml
 create mode 100644 docs/custom.css
 create mode 100644 docs/flake.lock
 create mode 100644 docs/flake.nix
 create mode 100644 docs/src/SUMMARY.md
 create mode 100644 docs/src/examples.md
 create mode 100644 docs/src/impermanence-comparison.md
 create mode 100644 docs/src/impermanence-migration.md
 create mode 100644 docs/src/impermanence.md
 create mode 100644 docs/src/library-and-testing.md
 create mode 100644 options.nix

diff --git a/.github/workflows/render-options.yml b/.github/workflows/render-options.yml
new file mode 100644
index 0000000..db61f85
--- /dev/null
+++ b/.github/workflows/render-options.yml
@@ -0,0 +1,27 @@
+name: Docs
+on:
+  push:
+    branches:
+      - main
+jobs:
+  render:
+    name: Render and Deploy
+
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v4
+      - uses: DeterminateSystems/nix-installer-action@ab6bcb2d5af0e904d04aea750e2089e9dc4cbfdd
+        with:
+          diagnostic-endpoint: ""
+
+      - uses: DeterminateSystems/magic-nix-cache-action@b46e247b898aa56e6d2d2e728dc6df6c84fdb738
+        with:
+          diagnostic-endpoint: ""
+
+      - run: nix build -L ./docs#packages.x86_64-linux.docs
+
+      - uses: peaceiris/actions-gh-pages@v4
+        with:
+          github_token: ${{ secrets.GITHUB_TOKEN }}
+          publish_dir: ./result
diff --git a/.gitignore b/.gitignore
index 96dec6d..95162d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@
 result*
 /.direnv
 /tests/.nixos-test-history
+*.html
diff --git a/README.md b/README.md
index 9f5867a..2ec734d 100644
--- a/README.md
+++ b/README.md
@@ -5,47 +5,23 @@ Nix tooling to enable declarative management of non-volatile system state.
 Inspired and heavily influenced by [impermanence](https://github.com/nix-community/impermanence) but not
 meant to be a drop-in replacement.
 
-## Work in Progress
+## Documentation
 
-🚧 still under construction 🚧
+Docs are available at <https://willibutz.github.io/preservation>
 
-Check out [the test](tests/basic.nix) for a usage example 👀
+## Prerequisites
 
-Depends on https://github.com/NixOS/nixpkgs/pull/307528 (merged)
-
-## How does it compare to impermanence
-
-* Preservation does not attempt to be a very generic solution, it tries to fill a specific niche.
-  Specifically Preservation does not support non-NixOS systems via home-manager, which is supported
-  by impermanence.
-
-* Preservation only creates static configuration for
-  [systemd-tmpfiles](https://www.freedesktop.org/software/systemd/man/latest/systemd-tmpfiles.html)
-  and systemd [mount units](https://www.freedesktop.org/software/systemd/man/latest/systemd.mount.html).
-  This makes Preservation a potential candidate for state management on interpreter-less systems.
-
-  Impermanence makes use of NixOS activation scripts and custom systemd services with bash (at the point
-  of writing this), to create and configure files and directories.
-
-* Preservation must be precisely configured, there is no [special runtime logic](https://github.com/nix-community/impermanence/blob/23c1f06316b67cb5dabdfe2973da3785cfe9c34a/mount-file.bash#L31-L42)
-  in place. This means that the user must define:
-  * when the preservation should be set up: either in the initrd, or after (the default)
-  * how the preservation should be set up: either by symlink, or bindmount (the default)
-  * whether or not parent directories of the persisted files require special permissions
-
-* Preservation's configuration is based on, and very similar to that of impermanence.
-
-* Preservation uses a global `enable` option, impermanence does not (see https://github.com/nix-community/impermanence/pull/171)
+Depends on <https://github.com/NixOS/nixpkgs/pull/307528> (merged, available on nixos-unstable).
 
 ## Why?
 
 This aims to provide a declarative state management solution for NixOS systems without resorting to
 interpreters to do the heavy lifting. This should enable impermanence-like state management on
-an "interpreter-less" system.
+an "interpreter-less" NixOS system.
 
 Related:
-- https://github.com/NixOS/nixpkgs/issues/265640
-- https://github.com/nix-community/projects/blob/main/proposals/nixpkgs-security-phase2.md#boot-chain-security
+- <https://github.com/NixOS/nixpkgs/issues/265640>
+- <https://github.com/nix-community/projects/blob/main/proposals/nixpkgs-security-phase2.md#boot-chain-security>
 
 ## License
 
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 0000000..7585238
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1 @@
+book
diff --git a/docs/book.toml b/docs/book.toml
new file mode 100644
index 0000000..323666f
--- /dev/null
+++ b/docs/book.toml
@@ -0,0 +1,10 @@
+[book]
+title = "Preservation"
+authors = ["Willi Butz"]
+language = "en"
+src = "src"
+
+[output.html]
+additional-css = ["custom.css"]
+git-repository-url = "https://github.com/willibutz/preservation"
+edit-url-template = "https://github.com/willibutz/preservation/edit/main/docs/{path}"
diff --git a/docs/custom.css b/docs/custom.css
new file mode 100644
index 0000000..738eee0
--- /dev/null
+++ b/docs/custom.css
@@ -0,0 +1,4 @@
+:root {
+  /* accommodate long option paths on big screens */
+  --content-max-width: 1000px;
+}
diff --git a/docs/flake.lock b/docs/flake.lock
new file mode 100644
index 0000000..27fefb3
--- /dev/null
+++ b/docs/flake.lock
@@ -0,0 +1,27 @@
+{
+  "nodes": {
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1724819573,
+        "narHash": "sha256-GnR7/ibgIH1vhoy8cYdmXE6iyZqKqFxQSVkFgosBh6w=",
+        "owner": "nixos",
+        "repo": "nixpkgs",
+        "rev": "71e91c409d1e654808b2621f28a327acfdad8dc2",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nixos",
+        "ref": "nixos-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "nixpkgs": "nixpkgs"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
diff --git a/docs/flake.nix b/docs/flake.nix
new file mode 100644
index 0000000..0f9bd0e
--- /dev/null
+++ b/docs/flake.nix
@@ -0,0 +1,60 @@
+{
+  inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
+
+  outputs = inputs: {
+    packages.x86_64-linux =
+      let
+        pkgs = import inputs.nixpkgs { system = "x86_64-linux"; };
+      in
+      {
+        optionsManualMD =
+          let
+            eval = pkgs.lib.evalModules {
+              modules = [
+                ../options.nix
+                (args: {
+                  options._module.args = args.lib.mkOption { internal = true; };
+                })
+              ];
+            };
+            optionsDoc = pkgs.nixosOptionsDoc {
+              inherit (eval) options;
+              transformOptions =
+                o:
+                o
+                // {
+                  declarations = map (
+                    declaration:
+                    let
+                      flakeOutPath = inputs.self.sourceInfo.outPath;
+                      name = pkgs.lib.removePrefix "${flakeOutPath}/" declaration;
+                    in
+                    if pkgs.lib.hasPrefix "${flakeOutPath}/" declaration then
+                      {
+                        inherit name;
+                        url = "https://github.com/willibutz/preservation/blob/main/${name}";
+                      }
+                    else
+                      declaration
+                  ) o.declarations;
+                };
+            };
+          in
+          optionsDoc.optionsCommonMark;
+
+        docs = pkgs.stdenv.mkDerivation {
+          name = "preservation-docs";
+          src = pkgs.lib.cleanSource ../.;
+          nativeBuildInputs = [ pkgs.mdbook ];
+          patchPhase = ''
+            cat ${inputs.self.packages.x86_64-linux.optionsManualMD} > docs/src/configuration-options.md
+          '';
+          buildPhase = ''
+            cd docs
+            mdbook build
+          '';
+          installPhase = "cp -vr book $out";
+        };
+      };
+  };
+}
diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md
new file mode 100644
index 0000000..d5ca1c8
--- /dev/null
+++ b/docs/src/SUMMARY.md
@@ -0,0 +1,9 @@
+# Summary
+
+[Preservation](../../README.md)
+- [Configuration Options](./configuration-options.md)
+- [Examples](./examples.md)
+- [Impermanence](./impermanence.md)
+  - [Comparison](./impermanence-comparison.md)
+  - [Migration](./impermanence-migration.md)
+- [Library and Testing](./library-and-testing.md)
diff --git a/docs/src/examples.md b/docs/src/examples.md
new file mode 100644
index 0000000..1159725
--- /dev/null
+++ b/docs/src/examples.md
@@ -0,0 +1,131 @@
+# Examples
+
+See [Configuration Options](./configuration-options.md) for all available options.
+
+## Simple
+
+```nix
+# configuration.nix
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+{
+  preservation = {
+    enable = true;
+    preserveAt."/persistent" = {
+      files = [
+        # auto-generated machine ID
+        { file = "/etc/machine-id"; inInitrd = true; }
+      ];
+      directories = [
+        "/var/lib/systemd/timers"
+        # NixOS user state
+        "/var/lib/nixos"
+        # preparing /var/log early (inInitrd) avoids a dependency cycle (see TODO.md)
+        { directory = "/var/log"; inInitrd = true; }
+      ];
+    };
+  };
+}
+```
+
+## Complex
+
+```nix
+# configuration.nix
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+{
+  preservation = {
+    # the module doesn't do anything unless it is enabled
+    enable = true;
+
+    preserveAt."/persistent" = {
+
+      # preserve system directories
+      directories = [
+        "/etc/secureboot"
+        "/var/lib/bluetooth"
+        "/var/lib/fprint"
+        "/var/lib/fwupd"
+        "/var/lib/libvirt"
+        "/var/lib/power-profiles-daemon"
+        "/var/lib/systemd/coredump"
+        "/var/lib/systemd/rfkill"
+        "/var/lib/systemd/timers"
+        { directory = "/var/lib/nixos"; inInitrd = true; }
+
+        # preparing /var/log early (inInitrd) avoids a dependency cycle (see TODO.md)
+        { directory =  "/var/log"; inInitrd = true; }
+      ];
+
+      # preserve system files
+      files = [
+        { file = "/etc/machine-id"; inInitrd = true; }
+        "/etc/ssh/ssh_host_ed25519_key"
+        "/etc/ssh/ssh_host_rsa_key"
+        "/var/lib/usbguard/rules.conf"
+
+        # creates a symlink on the volatile root
+        # creates an empty directory on the persistent volume, i.e. /persistent/var/lib/systemd
+        # does not create an empty file at the symlink's target (would require `createLinkTarget = true`)
+        { file = "/var/lib/systemd/random-seed"; how = "symlink"; inInitrd = true; configureParent = true; }
+      ];
+
+      # preserve user-specific files, implies ownership
+      users = {
+        butz = {
+          directories = [
+            { directory = ".ssh"; mode = "0700"; }
+            ".config/syncthing"
+            ".config/Element"
+            ".local/state/nvim"
+            ".local/state/wireplumber"
+            ".local/share/direnv"
+            ".local/state/nix"
+            ".mozilla"
+          ];
+          files = [
+            ".histfile"
+          ];
+        };
+        users.root = {
+          # specify user home when it is not `/home/${user}`
+          home = "/root";
+          directories = [
+            { directory = ".ssh"; mode = "0700"; }
+          ];
+        };
+      };
+    };
+  };
+
+  # Create some directories with custom permissions.
+  #
+  # In this configuration the path `/home/butz/.local` is not an immediate parent
+  # of any persisted file, so it would be created with the systemd-tmpfiles default
+  # ownership `root:root` and mode `0755`. This would mean that the user `butz`
+  # could not create other files or directories inside `/home/butz/.local`.
+  #
+  # Therefore systemd-tmpfiles is used to prepare such directories with
+  # appropriate permissions.
+  #
+  # Note that immediate parent directories of persisted files can also be
+  # configured with ownership and permissions from the `parent` settings if
+  # `configureParent = true` is set for the file.
+  systemd.tmpfiles.settings.preservation = {
+    "/home/butz/.config".d = { user = "butz"; group = "users"; mode = "0755"; };
+    "/home/butz/.local".d = { user = "butz"; group = "users"; mode = "0755"; };
+    "/home/butz/.local/share".d = { user = "butz"; group = "users"; mode = "0755"; };
+    "/home/butz/.local/state".d = { user = "butz"; group = "users"; mode = "0755"; };
+  };
+
+}
+```
diff --git a/docs/src/impermanence-comparison.md b/docs/src/impermanence-comparison.md
new file mode 100644
index 0000000..18bb09f
--- /dev/null
+++ b/docs/src/impermanence-comparison.md
@@ -0,0 +1,35 @@
+# How does Preservation compare to [impermanence](https://github.com/nix-community/impermanence)
+
+### Preservation does not attempt to be a very generic solution
+
+Preservation tries to fill a specific niche.
+For instance, Preservation does not support non-NixOS systems via home-manager, which is supported
+by impermanence. See [Migration](./impermanence-migration.md) for more technical details.
+
+### Preservation only generates static configuration
+
+That is configuration for [systemd-tmpfiles](https://www.freedesktop.org/software/systemd/man/latest/systemd-tmpfiles.html)
+and systemd [mount units](https://www.freedesktop.org/software/systemd/man/latest/systemd.mount.html).
+This makes Preservation a potential candidate for state management on interpreter-less NixOS systems.
+
+Impermanence makes use of NixOS activation scripts and custom systemd services with bash (at the point
+of writing this), to create files and directories, setup mounts and configure ownership and permissions (see next point).
+
+### Preservation must be precisely configured
+There is no [special runtime logic](https://github.com/nix-community/impermanence/blob/23c1f06316b67cb5dabdfe2973da3785cfe9c34a/mount-file.bash#L31-L42)
+  in place. This means that the user must define:
+  * when the preservation should be set up: either in the initrd, or after (the default)
+  * how the preservation should be set up: either by symlink, or bindmount (the default)
+  * whether or not parent directories of the persisted files require special permissions
+
+See [Migration](./impermanence-migration.md) for specifics that need to be considered when coming from an impermanence setup.
+
+### Similar configuration
+
+Preservation's configuration is based on, and very similar to that of impermanence. See [Migration](./impermanence-migration.md) for technical details.
+
+### Global `enable` option
+
+Preservation uses a global `enable` option, impermanence does not.
+
+For thoughts on the `enable` option, see the discussion at <https://github.com/nix-community/impermanence/pull/171> and for available configuration options see [Configuration Options](./configuration-options.md).
diff --git a/docs/src/impermanence-migration.md b/docs/src/impermanence-migration.md
new file mode 100644
index 0000000..d1d2b71
--- /dev/null
+++ b/docs/src/impermanence-migration.md
@@ -0,0 +1,111 @@
+# Migration from impermanence to Preservation
+
+This section lists individual differences between impermanence and
+Preservation, to better understand them in context of a complete configuration
+[Examples](./examples.md) may be helpful.
+
+The following points need to be considered when migrating an existing
+impermanence configuration to Preservation:
+
+### Global `enable` switch
+
+The module must be explicitly enabled by setting `preservation.enable` to `true`.
+
+### When to persist
+
+Files and directories that need to be persisted early, must be explicitly configured. For example `/etc/machine-id`:
+
+This file needs to be persisted very early, by explicitly setting `inInitrd` to `true`:
+```nix
+preservation.preserveAt."/persistent".files = [
+  { file = "/etc/machine-id"; inInitrd = true; }
+];
+```
+
+### How to persist
+
+The mode of preservation must be set explicitly for some files and directories.
+This can be done by setting `how` to either `symlink` or `bindmount` (default).
+For most cases the default is sufficient but sometimes a symlink may be needed,
+for example `/var/lib/systemd/random-seed`.
+
+This file is expected to not exist before it is initialized. A symlink can be
+used to cause its creation to happen on the persistent volume:
+
+```nix
+preservation.preserveAt."/persistent".files = [
+  {
+    file = "/var/lib/systemd/random-seed";
+    # create a symlink on the volatile volume
+    how = "symlink";
+    # prepare the preservation early during startup
+    inInitrd = true;
+  }
+];
+```
+
+Note that no file is created at the symlink's target, unless `createLinkTarget` is set to `true`.
+
+### Configuration of intermediate path components
+
+Preservation does not handle any files or directories other than those specifically configured
+to be preserved, and optionally their immediate parent directories (via `configureParent` and
+the `parent` options).
+
+All missing components of a preserved path that do not already exist, are created by
+systemd-tmpfiles with default ownership `root:root` and mode `0755`.
+
+Should such directories require different ownership or mode, the intended way to provision them
+is directly via systemd-tmpfiles.
+
+**Example**
+
+Consider a preserved file `/foo/bar/baz`:
+
+```nix
+preservation.preserveAt."/persistent".files = [
+  { file = "/foo/bar/baz"; user = "baz"; group = "baz"; };
+];
+```
+
+This would create the file with desired ownership on both the volatile and persistent volumes.
+However, the parent directories that did not exist before, i.e. `/foo` and `/foo/bar`, are
+created with ownership `root:root` and mode `0755`.
+
+Preservations allows the configuration of immediate parents, so the permissions for `/foo/bar`
+can be configured:
+```nix
+preservation.preserveAt."/persistent".files = [
+  {
+    file = "/foo/bar/baz"; user = "baz"; group = "baz";
+    configureParent = true;
+    parent.user = "baz";
+    parent.group = "bar";
+  };
+];
+```
+Now the parent directory `/foo/bar` is configured with ownership `baz:bar`. But the first
+path component `/foo` still has systemd-tmpfiles' default ownership and the configuration
+becomes quite convoluted.
+
+**Solution**
+
+To create or configure intermediate path components of a persisted path, systemd-tmpfiles
+may be used directly:
+
+```nix
+# configure preservation of single file
+preservation.preserveAt."/persistent".files = [
+  { file = "/foo/bar/baz"; user = "baz"; group = "bar"; };
+];
+
+# create and configure parents of preserved file on the volatile volume with custom permissions
+# The Preservation module also uses `settings.preservation` here.
+systemd.tmpfiles.settings.preservation = {
+  "/foo".d = { user = "foo"; group = "bar"; mode = "0775"; };
+  "/foo/bar".d = { user = "bar"; group = "bar"; mode = "0755"; };
+};
+```
+
+See [tmpfiles.d(5)](https://www.freedesktop.org/software/systemd/man/latest/tmpfiles.d.html)
+for available configuration options.
diff --git a/docs/src/impermanence.md b/docs/src/impermanence.md
new file mode 100644
index 0000000..da5d181
--- /dev/null
+++ b/docs/src/impermanence.md
@@ -0,0 +1,7 @@
+# Impermanence
+
+[Impermanence](https://github.com/nix-community/impermanence) is the established solution for managing persistent state on NixOS systems.
+Preservation is inspired and heavily influenced by impermanence.
+
+See [Comparison](./impermanence-comparison.md) for a high-level overview of their differences.
+
diff --git a/docs/src/library-and-testing.md b/docs/src/library-and-testing.md
new file mode 100644
index 0000000..5139c87
--- /dev/null
+++ b/docs/src/library-and-testing.md
@@ -0,0 +1,14 @@
+# Library and Testing
+
+## Library
+
+The functionality that is used in the module to discover the files and
+directories that are persisted and to generate the corresponding tmpfiles
+config and mount units is available from [`lib.nix`](../../lib.nix).
+It is also available from the flake `lib` output.
+
+In both cases it needs to be instantiated with the nixpkgs `lib`.
+
+## Testing
+
+The integration test(s) can be found in [/tests](../../tests).
diff --git a/flake.nix b/flake.nix
index fc1b3e0..3655a26 100644
--- a/flake.nix
+++ b/flake.nix
@@ -1,6 +1,6 @@
 {
   outputs = inputs: {
-    lib = import ./lib.nix; # need to pass '{ lib, ... }'
+    lib = import ./lib.nix; # need to pass nixpkgs-lib
     nixosModules = {
       default = inputs.self.nixosModules.preservation;
       preservation = import ./module.nix;
diff --git a/module.nix b/module.nix
index 473e949..9daaa2a 100644
--- a/module.nix
+++ b/module.nix
@@ -9,461 +9,11 @@ let
     mkRegularTmpfilesRules
     mkInitrdTmpfilesRules
     ;
-
-  mountOption = lib.types.submodule {
-    options = {
-      name = lib.mkOption {
-        type = lib.types.str;
-        description = ''
-          Specify the name of the mount option.
-        '';
-        example = "bind";
-      };
-      value = lib.mkOption {
-        type = with lib.types; nullOr str;
-        default = null;
-        description = ''
-          Optionally specify a value for the mount option.
-        '';
-      };
-    };
-  };
-
-  directoryPath =
-    attrs@{ defaultOwner, ... }:
-    {
-      options = {
-        directory = lib.mkOption {
-          type = lib.types.str;
-          description = ''
-            Specify the path to the directory that should be preserved.
-          '';
-        };
-        how = lib.mkOption {
-          type = lib.types.enum [
-            "bindmount"
-            "symlink"
-          ];
-          default = "bindmount";
-          description = ''
-            Specify how this directory should be preserved.
-          '';
-        };
-        user = lib.mkOption {
-          type = lib.types.str;
-          default = defaultOwner;
-          description = ''
-            Specify the user that owns the directory.
-          '';
-        };
-        group = lib.mkOption {
-          type = lib.types.str;
-          default = config.users.users.${defaultOwner}.group;
-          description = ''
-            Specify the group that owns the directory.
-          '';
-        };
-        mode = lib.mkOption {
-          type = lib.types.str;
-          default = "0755";
-          description = ''
-            Specify the access mode of the directory.
-            See the section `Mode` in {manpage}`tmpfiles.d(5)` for more information.
-          '';
-        };
-        configureParent = lib.mkOption {
-          type = lib.types.bool;
-          default = attrs.config.how == "symlink" && attrs.config.user != "root";
-          description = ''
-            Specify whether the parent directory of this directory shall be configured with
-            custom ownership and permissions.
-
-            By default, missing parent directories are always created with ownership
-            `root:root` and mode `0755`, as described in {manpage}`tmpfiles.d(5)`.
-
-            Ownership and mode may be configured through the options
-            {option}`parent.user`,
-            {option}`parent.group`,
-            {option}`parent.mode`.
-
-            Defaults to `true` when {option}`how` is set to `symlink` and
-            {option}`user` is not `root`.
-          '';
-        };
-        parent.user = lib.mkOption {
-          type = lib.types.str;
-          default = defaultOwner;
-          description = ''
-            Specify the user that owns the parent directory of this file.
-          '';
-        };
-        parent.group = lib.mkOption {
-          type = lib.types.str;
-          default = config.users.users.${defaultOwner}.group;
-          description = ''
-            Specify the group that owns the parent directory of this file.
-          '';
-        };
-        parent.mode = lib.mkOption {
-          type = lib.types.str;
-          default = "0755";
-          description = ''
-            Specify the access mode of the parent directory of this file.
-            See the section `Mode` in {manpage}`tmpfiles.d(5)` for more information.
-          '';
-        };
-        mountOptions = lib.mkOption {
-          type = with lib.types; listOf (coercedTo str (n: { name = n; }) mountOption);
-          default = [
-            "bind"
-            "X-fstrim.notrim" # see fstrim(8)
-          ];
-          description = ''
-            Specify a list of mount options that should be used for this directory.
-            These options are only used when {option}`how` is set to `bindmount`.
-          '';
-        };
-        createLinkTarget = lib.mkOption {
-          type = lib.types.bool;
-          default = false;
-          description = ''
-            Only used when {option}`how` is set to `symlink`.
-
-            Specify whether to create an empty directory with the specified ownership
-            and permissions as target of the symlink.
-          '';
-        };
-        inInitrd = lib.mkOption {
-          type = lib.types.bool;
-          default = false;
-          description = ''
-            Whether to prepare preservation of this directory in initrd.
-
-            ::: {.note}
-            For most directories there is no need to enable this option.
-            :::
-
-            ::: {.important}
-            Note that both owner and group for this directory need to be
-            available in the initrd for permissions to be set correctly.
-            :::
-          '';
-        };
-      };
-    };
-
-  filePath =
-    attrs@{ defaultOwner, ... }:
-    {
-      options = {
-        file = lib.mkOption {
-          type = lib.types.str;
-          description = ''
-            Specify the path to the file that should be preserved.
-          '';
-        };
-        how = lib.mkOption {
-          type = lib.types.enum [
-            "bindmount"
-            "symlink"
-          ];
-          default = "bindmount";
-          description = ''
-            Specify how this file should be preserved:
-
-            1. Either a file is placed both on the volatile and on the
-            persistent volume, with a bind mount from the former to the
-            latter.
-
-            2. Or a symlink is created on the volatile volume, pointing
-            to the corresponding location on the persistent volume.
-          '';
-        };
-        user = lib.mkOption {
-          type = lib.types.str;
-          default = defaultOwner;
-          description = ''
-            Specify the user that owns the file.
-          '';
-        };
-        group = lib.mkOption {
-          type = lib.types.str;
-          default = config.users.users.${defaultOwner}.group;
-          description = ''
-            Specify the group that owns the file.
-          '';
-        };
-        mode = lib.mkOption {
-          type = lib.types.str;
-          default = "0644";
-          description = ''
-            Specify the access mode of the file.
-            See the section `Mode` in {manpage}`tmpfiles.d(5)` for more information.
-          '';
-        };
-        configureParent = lib.mkOption {
-          type = lib.types.bool;
-          default = attrs.config.how == "symlink" && attrs.config.user != "root";
-          description = ''
-            Specify whether the parent directory of this file shall be configured with
-            custom ownership and permissions.
-
-            By default, missing parent directories are always created with ownership
-            `root:root` and mode `0755`, as described in {manpage}`tmpfiles.d(5)`.
-
-            Ownership and mode may be configured through the options
-            {option}`parent.user`,
-            {option}`parent.group`,
-            {option}`parent.mode`.
-
-            Defaults to `true` when {option}`how` is set to `symlink` and
-            {option}`user` is not `root`.
-          '';
-        };
-        parent.user = lib.mkOption {
-          type = lib.types.str;
-          default = defaultOwner;
-          description = ''
-            Specify the user that owns the parent directory of this file.
-          '';
-        };
-        parent.group = lib.mkOption {
-          type = lib.types.str;
-          default = config.users.users.${attrs.defaultOwner}.group;
-          description = ''
-            Specify the group that owns the parent directory of this file.
-          '';
-        };
-        parent.mode = lib.mkOption {
-          type = lib.types.str;
-          default = "0755";
-          description = ''
-            Specify the access mode of the parent directory of this file.
-            See the section `Mode` in {manpage}`tmpfiles.d(5)` for more information.
-          '';
-        };
-        mountOptions = lib.mkOption {
-          type = with lib.types; listOf (coercedTo str (o: { name = o; }) mountOption);
-          default = [ "bind" ];
-          description = ''
-            Specify a list of mount options that should be used for this file.
-            These options are only used when {option}`how` is set to `bindmount`.
-          '';
-        };
-        createLinkTarget = lib.mkOption {
-          type = lib.types.bool;
-          default = false;
-          description = ''
-            Only used when {option}`how` is set to `symlink`.
-
-            Specify whether to create an empty file with the specified ownership
-            and permissions as target of the symlink.
-          '';
-        };
-        inInitrd = lib.mkOption {
-          type = lib.types.bool;
-          default = false;
-          description = ''
-            Whether to prepare preservation of this file in the initrd.
-
-            ::: {.note}
-            For most files there is no need to enable this option.
-
-            {file}`/etc/machine-id` is an exception because it needs to
-            be populated/read very early.
-            :::
-
-            ::: {.important}
-            Note that both owner and group for this file need to be
-            available in the initrd for permissions to be set correctly.
-            :::
-          '';
-        };
-      };
-    };
-
-  userModule =
-    attrs@{ name, ... }:
-    {
-      options = {
-        username = lib.mkOption {
-          type = with lib.types; passwdEntry str;
-          default = name;
-          description = ''
-            Specify the user for which the {option}`directories` and {option}`files`
-            should be persisted. Defaults to the name of the parent attribute set.
-          '';
-        };
-        home = lib.mkOption {
-          type = with lib.types; passwdEntry path;
-          default = config.users.users.${name}.home;
-          description = ''
-            Specify the path to the user's home directory.
-          '';
-        };
-        directories = lib.mkOption {
-          type =
-            with lib.types;
-            listOf (
-              coercedTo str (d: { directory = d; }) (submodule [
-                { _module.args.defaultOwner = attrs.config.username; }
-                directoryPath
-              ])
-            );
-          default = [ ];
-          apply = map (d: d // { directory = "${attrs.config.home}/${d.directory}"; });
-          description = ''
-            Specify a list of directories that should be preserved for this user.
-            The paths are interpreted relative to {option}`home`.
-          '';
-          example = [ ".rabbit_hole" ];
-        };
-        files = lib.mkOption {
-          type =
-            with lib.types;
-            listOf (
-              coercedTo str (f: { file = f; }) (submodule [
-                { _module.args.defaultOwner = attrs.config.username; }
-                filePath
-              ])
-            );
-          default = [ ];
-          apply = map (f: f // { file = "${attrs.config.home}/${f.file}"; });
-          description = ''
-            Specify a list of files that should be preserved for this user.
-            The paths are interpreted relative to {option}`home`.
-          '';
-          example = [
-            {
-              file = ".config/foo";
-              mode = "0600";
-            }
-            "bar"
-          ];
-        };
-      };
-    };
-
-  preserveAtSubmodule =
-    attrs@{ name, ... }:
-    {
-      options = {
-        persistentStoragePath = lib.mkOption {
-          type = lib.types.path;
-          default = name;
-          description = ''
-            Specify the location at which the {option}`directories`, {option}`files`,
-            {option}`users.directories` and {option}`users.files` should be preserved.
-            Defaults to the name of the parent attribute set.
-          '';
-        };
-        directories = lib.mkOption {
-          type =
-            with lib.types;
-            listOf (
-              coercedTo str (d: { directory = d; }) (submodule [
-                { _module.args.defaultOwner = "root"; }
-                directoryPath
-              ])
-            );
-          default = [ ];
-          description = ''
-            Specify a list of directories that should be preserved.
-            The paths are interpreted as absolute paths.
-          '';
-          example = [ "/var/lib/someservice" ];
-        };
-        files = lib.mkOption {
-          type =
-            with lib.types;
-            listOf (
-              coercedTo str (f: { file = f; }) (submodule [
-                { _module.args.defaultOwner = "root"; }
-                filePath
-              ])
-            );
-          default = [ ];
-          description = ''
-            Specify a list of files that should be preserved.
-            The paths are interpreted as absolute paths.
-          '';
-          example = [
-            {
-              file = "/etc/wpa_supplicant.conf";
-              how = "symlink";
-            }
-            {
-              file = "/etc/machine-id";
-              inInitrd = true;
-            }
-          ];
-        };
-        users = lib.mkOption {
-          type = with lib.types; attrsOf (submodule userModule);
-          default = { };
-          description = ''
-            Specify a set of users with corresponding files and directories that
-            should be preserved.
-          '';
-          example = {
-            alice.directories = [ ".rabbit_hole" ];
-            butz = {
-              files = [
-                {
-                  file = ".config/foo";
-                  mode = "0600";
-                }
-                "bar"
-              ];
-              directories = [ "unshaved_yaks" ];
-            };
-          };
-        };
-      };
-    };
-
 in
 {
-  options.preservation = {
-    enable = lib.mkEnableOption "the preservation module";
-
-    preserveAt = lib.mkOption {
-      type = with lib.types; attrsOf (submodule preserveAtSubmodule);
-      description = ''
-        Specify a set of locations and the corresponding state that
-        should be preserved there.
-      '';
-      default = { };
-      example = {
-        "/state" = {
-          directories = [ "/var/lib/someservice" ];
-          files = [
-            {
-              file = "/etc/wpa_supplicant.conf";
-              how = "symlink";
-            }
-            {
-              file = "/etc/machine-id";
-              inInitrd = true;
-            }
-          ];
-          users = {
-            alice.directories = [ ".rabbit_hole" ];
-            butz = {
-              files = [
-                {
-                  file = ".config/foo";
-                  mode = "0600";
-                }
-                "bar"
-              ];
-              directories = [ "unshaved_yaks" ];
-            };
-          };
-        };
-      };
-    };
-  };
+  imports = [
+    ./options.nix
+  ];
 
   config = lib.mkIf cfg.enable {
     assertions = [
diff --git a/options.nix b/options.nix
new file mode 100644
index 0000000..00e3c33
--- /dev/null
+++ b/options.nix
@@ -0,0 +1,482 @@
+{ config, lib, ... }:
+
+let
+  mountOption = lib.types.submodule {
+    options = {
+      name = lib.mkOption {
+        type = lib.types.str;
+        description = ''
+          Specify the name of the mount option.
+        '';
+        example = "bind";
+      };
+      value = lib.mkOption {
+        type = with lib.types; nullOr str;
+        default = null;
+        description = ''
+          Optionally specify a value for the mount option.
+        '';
+      };
+    };
+  };
+
+  directoryPath =
+    attrs@{ defaultOwner, ... }:
+    {
+      options = {
+        directory = lib.mkOption {
+          type = lib.types.str;
+          description = ''
+            Specify the path to the directory that should be preserved.
+          '';
+        };
+        how = lib.mkOption {
+          type = lib.types.enum [
+            "bindmount"
+            "symlink"
+          ];
+          default = "bindmount";
+          description = ''
+            Specify how this directory should be preserved.
+          '';
+        };
+        user = lib.mkOption {
+          type = lib.types.str;
+          default = defaultOwner;
+          description = ''
+            Specify the user that owns the directory.
+          '';
+        };
+        group = lib.mkOption {
+          type = lib.types.str;
+          default = config.users.users.${defaultOwner}.group;
+          defaultText = "config.users.users.\${defaultOwner}.group";
+          description = ''
+            Specify the group that owns the directory.
+          '';
+        };
+        mode = lib.mkOption {
+          type = lib.types.str;
+          default = "0755";
+          description = ''
+            Specify the access mode of the directory.
+            See the section `Mode` in {manpage}`tmpfiles.d(5)` for more information.
+          '';
+        };
+        configureParent = lib.mkOption {
+          type = lib.types.bool;
+          default = attrs.config.how == "symlink" && attrs.config.user != "root";
+          description = ''
+            Specify whether the parent directory of this directory shall be configured with
+            custom ownership and permissions.
+
+            By default, missing parent directories are always created with ownership
+            `root:root` and mode `0755`, as described in {manpage}`tmpfiles.d(5)`.
+
+            Ownership and mode may be configured through the options
+            {option}`parent.user`,
+            {option}`parent.group`,
+            {option}`parent.mode`.
+
+            Defaults to `true` when {option}`how` is set to `symlink` and
+            {option}`user` is not `root`.
+          '';
+        };
+        parent.user = lib.mkOption {
+          type = lib.types.str;
+          default = defaultOwner;
+          description = ''
+            Specify the user that owns the parent directory of this file.
+          '';
+        };
+        parent.group = lib.mkOption {
+          type = lib.types.str;
+          default = config.users.users.${defaultOwner}.group;
+          defaultText = "config.users.users.\${defaultOwner}.group";
+          description = ''
+            Specify the group that owns the parent directory of this file.
+          '';
+        };
+        parent.mode = lib.mkOption {
+          type = lib.types.str;
+          default = "0755";
+          description = ''
+            Specify the access mode of the parent directory of this file.
+            See the section `Mode` in {manpage}`tmpfiles.d(5)` for more information.
+          '';
+        };
+        mountOptions = lib.mkOption {
+          type = with lib.types; listOf (coercedTo str (n: { name = n; }) mountOption);
+          default = [
+            "bind"
+            "X-fstrim.notrim" # see fstrim(8)
+          ];
+          description = ''
+            Specify a list of mount options that should be used for this directory.
+            These options are only used when {option}`how` is set to `bindmount`.
+          '';
+        };
+        createLinkTarget = lib.mkOption {
+          type = lib.types.bool;
+          default = false;
+          description = ''
+            Only used when {option}`how` is set to `symlink`.
+
+            Specify whether to create an empty directory with the specified ownership
+            and permissions as target of the symlink.
+          '';
+        };
+        inInitrd = lib.mkOption {
+          type = lib.types.bool;
+          default = false;
+          description = ''
+            Whether to prepare preservation of this directory in initrd.
+
+            ::: {.note}
+            For most directories there is no need to enable this option.
+            :::
+
+            ::: {.important}
+            Note that both owner and group for this directory need to be
+            available in the initrd for permissions to be set correctly.
+            :::
+          '';
+        };
+      };
+    };
+
+  filePath =
+    attrs@{ defaultOwner, ... }:
+    {
+      options = {
+        file = lib.mkOption {
+          type = lib.types.str;
+          description = ''
+            Specify the path to the file that should be preserved.
+          '';
+        };
+        how = lib.mkOption {
+          type = lib.types.enum [
+            "bindmount"
+            "symlink"
+          ];
+          default = "bindmount";
+          description = ''
+            Specify how this file should be preserved:
+
+            1. Either a file is placed both on the volatile and on the
+            persistent volume, with a bind mount from the former to the
+            latter.
+
+            2. Or a symlink is created on the volatile volume, pointing
+            to the corresponding location on the persistent volume.
+          '';
+        };
+        user = lib.mkOption {
+          type = lib.types.str;
+          default = defaultOwner;
+          description = ''
+            Specify the user that owns the file.
+          '';
+        };
+        group = lib.mkOption {
+          type = lib.types.str;
+          default = config.users.users.${defaultOwner}.group;
+          defaultText = "config.users.users.\${defaultOwner}.group";
+          description = ''
+            Specify the group that owns the file.
+          '';
+        };
+        mode = lib.mkOption {
+          type = lib.types.str;
+          default = "0644";
+          description = ''
+            Specify the access mode of the file.
+            See the section `Mode` in {manpage}`tmpfiles.d(5)` for more information.
+          '';
+        };
+        configureParent = lib.mkOption {
+          type = lib.types.bool;
+          default = attrs.config.how == "symlink" && attrs.config.user != "root";
+          description = ''
+            Specify whether the parent directory of this file shall be configured with
+            custom ownership and permissions.
+
+            By default, missing parent directories are always created with ownership
+            `root:root` and mode `0755`, as described in {manpage}`tmpfiles.d(5)`.
+
+            Ownership and mode may be configured through the options
+            {option}`parent.user`,
+            {option}`parent.group`,
+            {option}`parent.mode`.
+
+            Defaults to `true` when {option}`how` is set to `symlink` and
+            {option}`user` is not `root`.
+          '';
+        };
+        parent.user = lib.mkOption {
+          type = lib.types.str;
+          default = defaultOwner;
+          description = ''
+            Specify the user that owns the parent directory of this file.
+          '';
+        };
+        parent.group = lib.mkOption {
+          type = lib.types.str;
+          default = config.users.users.${attrs.defaultOwner}.group;
+          defaultText = "config.users.users.\${defaultOwner}.group";
+          description = ''
+            Specify the group that owns the parent directory of this file.
+          '';
+        };
+        parent.mode = lib.mkOption {
+          type = lib.types.str;
+          default = "0755";
+          description = ''
+            Specify the access mode of the parent directory of this file.
+            See the section `Mode` in {manpage}`tmpfiles.d(5)` for more information.
+          '';
+        };
+        mountOptions = lib.mkOption {
+          type = with lib.types; listOf (coercedTo str (o: { name = o; }) mountOption);
+          default = [ "bind" ];
+          description = ''
+            Specify a list of mount options that should be used for this file.
+            These options are only used when {option}`how` is set to `bindmount`.
+          '';
+        };
+        createLinkTarget = lib.mkOption {
+          type = lib.types.bool;
+          default = false;
+          description = ''
+            Only used when {option}`how` is set to `symlink`.
+
+            Specify whether to create an empty file with the specified ownership
+            and permissions as target of the symlink.
+          '';
+        };
+        inInitrd = lib.mkOption {
+          type = lib.types.bool;
+          default = false;
+          description = ''
+            Whether to prepare preservation of this file in the initrd.
+
+            ::: {.note}
+            For most files there is no need to enable this option.
+
+            {file}`/etc/machine-id` is an exception because it needs to
+            be populated/read very early.
+            :::
+
+            ::: {.important}
+            Note that both owner and group for this file need to be
+            available in the initrd for permissions to be set correctly.
+            :::
+          '';
+        };
+      };
+    };
+
+  userModule =
+    attrs@{ name, ... }:
+    {
+      options = {
+        username = lib.mkOption {
+          type = with lib.types; passwdEntry str;
+          default = name;
+          description = ''
+            Specify the user for which the {option}`directories` and {option}`files`
+            should be persisted. Defaults to the name of the parent attribute set.
+          '';
+        };
+        home = lib.mkOption {
+          type = with lib.types; passwdEntry path;
+          default = config.users.users.${name}.home;
+          defaultText = "config.users.users.\${name}.home";
+          description = ''
+            Specify the path to the user's home directory.
+          '';
+        };
+        directories = lib.mkOption {
+          type =
+            with lib.types;
+            listOf (
+              coercedTo str (d: { directory = d; }) (submodule [
+                { _module.args.defaultOwner = attrs.config.username; }
+                directoryPath
+              ])
+            );
+          default = [ ];
+          apply = map (d: d // { directory = "${attrs.config.home}/${d.directory}"; });
+          description = ''
+            Specify a list of directories that should be preserved for this user.
+            The paths are interpreted relative to {option}`home`.
+          '';
+          example = [ ".rabbit_hole" ];
+        };
+        files = lib.mkOption {
+          type =
+            with lib.types;
+            listOf (
+              coercedTo str (f: { file = f; }) (submodule [
+                { _module.args.defaultOwner = attrs.config.username; }
+                filePath
+              ])
+            );
+          default = [ ];
+          apply = map (f: f // { file = "${attrs.config.home}/${f.file}"; });
+          description = ''
+            Specify a list of files that should be preserved for this user.
+            The paths are interpreted relative to {option}`home`.
+          '';
+          example = lib.literalMD ''
+            ```nix
+            [
+              {
+                file = ".config/foo";
+                mode = "0600";
+              }
+              "bar"
+            ]
+            ```
+          '';
+        };
+      };
+      config = {
+        _module.args.name = lib.mkOverride 1499 "‹user›";
+      };
+    };
+
+  preserveAtSubmodule =
+    attrs@{ name, ... }:
+    {
+      options = {
+        persistentStoragePath = lib.mkOption {
+          type = lib.types.path;
+          default = name;
+          description = ''
+            Specify the location at which the {option}`directories`, {option}`files`,
+            {option}`users.directories` and {option}`users.files` should be preserved.
+            Defaults to the name of the parent attribute set.
+          '';
+        };
+        directories = lib.mkOption {
+          type =
+            with lib.types;
+            listOf (
+              coercedTo str (d: { directory = d; }) (submodule [
+                { _module.args.defaultOwner = "root"; }
+                directoryPath
+              ])
+            );
+          default = [ ];
+          description = ''
+            Specify a list of directories that should be preserved.
+            The paths are interpreted as absolute paths.
+          '';
+          example = [ "/var/lib/someservice" ];
+        };
+        files = lib.mkOption {
+          type =
+            with lib.types;
+            listOf (
+              coercedTo str (f: { file = f; }) (submodule [
+                { _module.args.defaultOwner = "root"; }
+                filePath
+              ])
+            );
+          default = [ ];
+          description = ''
+            Specify a list of files that should be preserved.
+            The paths are interpreted as absolute paths.
+          '';
+          example = lib.literalMD ''
+            ```nix
+            [
+              {
+                file = "/etc/wpa_supplicant.conf";
+                how = "symlink";
+              }
+              {
+                file = "/etc/machine-id";
+                inInitrd = true;
+              }
+            ]
+            ```
+          '';
+        };
+        users = lib.mkOption {
+          type = with lib.types; attrsOf (submodule userModule);
+          default = { };
+          description = ''
+            Specify a set of users with corresponding files and directories that
+            should be preserved.
+          '';
+          example = lib.literalMD ''
+            ```nix
+            {
+              alice.directories = [ ".rabbit_hole" ];
+              butz = {
+                files = [
+                  {
+                    file = ".config/foo";
+                    mode = "0600";
+                  }
+                  "bar"
+                ];
+                directories = [ "unshaved_yaks" ];
+              };
+            }
+            ```
+          '';
+        };
+      };
+    };
+
+in
+{
+  options.preservation = {
+    enable = lib.mkEnableOption "the preservation module";
+
+    preserveAt = lib.mkOption {
+      type = with lib.types; attrsOf (submodule preserveAtSubmodule);
+      description = ''
+        Specify a set of locations and the corresponding state that
+        should be preserved there.
+      '';
+      default = { };
+      example = lib.literalMD ''
+        ```nix
+        {
+          "/state" = {
+            directories = [ "/var/lib/someservice" ];
+            files = [
+              {
+                file = "/etc/wpa_supplicant.conf";
+                how = "symlink";
+              }
+              {
+                file = "/etc/machine-id";
+                inInitrd = true;
+              }
+            ];
+            users = {
+              alice.directories = [ ".rabbit_hole" ];
+              butz = {
+                files = [
+                  {
+                    file = ".config/foo";
+                    mode = "0600";
+                  }
+                  "bar"
+                ];
+                directories = [ "unshaved_yaks" ];
+              };
+            };
+          };
+        }
+        ```
+      '';
+    };
+  };
+}