Files
nix-config-ryan4yin/hosts/idols-ai/preservation.nix
T
Ryan Yin 35ae8732c3 fix(preservation): keep EasyEffects state across reboots
EasyEffects 8 keeps its KConfig settings database plus user presets, autoload
profiles and imported models under $HOME, which the tmpfs root drops on
reboot; bind-mount both directories from /persistent.

Assisted-by: DeepSeek Harness 0.2.1-rc.2 (deepseek-flash)
2026-10-18 21:42:52 +08:00

499 lines
15 KiB
Nix

{
preservation,
lib,
pkgs,
myvars,
...
}:
let
inherit (myvars) username;
in
{
imports = [
preservation.nixosModules.default
];
preservation.enable = true;
# pverservation required initrd using systemd.
boot.initrd.systemd.enable = true;
environment.systemPackages = [
# Whole-filesystem disk usage (`-x` stays on one filesystem):
# sudo gdu -n -x / # non-interactive, plain text
# sudo gdu -o /tmp/gdu.json / # JSON export
pkgs.gdu
];
# There are two ways to clear the root filesystem on every boot:
## 1. use tmpfs for /
## 2. (btrfs/zfs only)take a blank snapshot of the root filesystem and revert to it on every boot via:
## boot.initrd.postDeviceCommands = ''
## mkdir -p /run/mymount
## mount -o subvol=/ /dev/disk/by-uuid/UUID /run/mymount
## btrfs subvolume delete /run/mymount
## btrfs subvolume snapshot / /run/mymount
## '';
#
# See also https://grahamc.com/blog/erase-your-darlings/
# NOTE: preservation only mounts the directory/file list below to /persistent
# If the directory/file already exists in the root filesystem you should
# move those files/directories to /persistent first!
# Removing an entry stops its mount on the next activation, but leaves the empty
# root-owned mountpoint behind on the tmpfs root, so a Home Manager link into that
# path fails with "Permission denied" until it is removed once (`sudo rmdir
# <path>`); nothing recreates it afterwards.
preservation.preserveAt."/persistent" = {
directories = [
"/etc/NetworkManager/system-connections"
"/etc/ssh"
"/etc/nix/inputs"
"/etc/secureboot" # lanzaboote - secure boot
# my secrets
"/etc/agenix/"
"/var/log"
# system caches (e.g. restic, plocate; slow to rebuild)
"/var/cache"
# General scratch space. It is re-creatable and can grow large, so keep it
# off the root tmpfs and on the persistent volume. Contents only
# accumulate between reboots: the tmpfiles rule below wipes it at boot.
# World-writable scratch, so harden the bind mount like /tmp; the mode
# must stay 1777 to match that `D!` rule.
{
directory = "/var/tmp";
mode = "1777";
mountOptions = [
"nosuid"
"nodev"
];
}
# btrbk state/home (SSH keys, cache). The owner must match the btrbk
# module's `d /var/lib/btrbk 0750 btrbk btrbk` rule, which preservation
# would otherwise override with the default root:root.
{
directory = "/var/lib/btrbk";
user = "btrbk";
group = "btrbk";
mode = "0750";
}
# CUPS state (the print spool lives in /var/spool/cups, not here)
"/var/lib/cups"
# system-core
"/var/lib/nixos"
"/var/lib/systemd"
# upower device battery history (e.g. Magic Trackpad)
"/var/lib/upower"
{
directory = "/var/lib/private";
mode = "0700";
}
# containers
# "/var/lib/docker"
"/var/lib/cni"
"/var/lib/containers"
# virtualisation
"/var/lib/libvirt"
"/var/lib/qemu"
# "/var/lib/waydroid"
# network
"/var/lib/bluetooth"
"/var/lib/NetworkManager"
"/var/lib/iwd"
"/var/lib/tailscale"
# logrotate state. Persist the directory: a single-file bind mount cannot
# be replaced by logrotate's atomic rename() (EBUSY).
"/var/lib/logrotate"
];
files = [
# auto-generated machine ID
{
file = "/etc/machine-id";
inInitrd = true;
}
];
# the following directories will be passed to /persistent/home/$USER
users.${username} = {
commonMountOptions = [
"x-gvfs-hide"
];
directories = [
# ======================================
# XDG Directories
# ======================================
"Desktop"
"Downloads"
"Music"
"Pictures"
"Documents"
"Videos"
# Keep .cache off tmpfs to avoid high RAM usage; many apps use it and it is storage-heavy.
".cache"
# NOTE: do NOT persist ~/.local/share/Trash here. The trash crate (nushell
# `rm --trash`) picks the home trash only when the file's mount topdir equals
# the trash dir's topdir; a bind-mounted Trash dir would make files straight
# under $HOME fail with EACCES (it would try /.Trash-$uid). The home trash on
# tmpfs is lost on reboot, which is acceptable: scattered per-mount
# .Trash-$uid dirs on persistent volumes are covered by the trash-empty
# retention timer (modules/nixos/base/trash.nix).
# ======================================
# Codes / Work / Playground
# ======================================
"codes" # for personal code
"work" # work code tree
"src" # third-party source checkouts
"nix-config"
"tmp"
# ======================================
# Nix / Home Manager Profiles
# ======================================
".local/state/home-manager"
".local/state/nix/profiles"
".local/state/noctalia" # shell settings, notification/clipboard history
".local/share/nix"
# ======================================
# IDE / Editors
# ======================================
# vscode
".vscode"
".config/Code"
# zed
".config/zed"
".local/share/zed"
# ai agents
".agents" # skills for all agents
".config/agents"
".codex"
".dsh" # DeepSeek Harness
".pi"
".config/opencode"
".local/share/opencode"
".local/state/opencode"
# ctx7 docs lookup login
{
directory = ".config/context7";
mode = "0700";
}
# nvim
".local/share/nvim"
".local/state/nvim"
# ======================================
# TUIOS (terminal window manager)
# ======================================
# Without .local/state/tuios the daemon has no saved session state to
# restore after a reboot, and every session is lost.
# session structure (resurrection), plus each pane's saved history
{
directory = ".local/state/tuios";
mode = "0700";
}
# project-tape trust store and tape recordings
{
directory = ".local/share/tuios";
mode = "0700";
}
# layout templates (config.toml is a Home Manager symlink into the repo)
".config/tuios"
# Joplin
".config/joplin" # tui client
".config/Joplin" # joplin-desktop
".local/share/jupyter"
".ipython"
# ======================================
# Cloud Native
# ======================================
{
directory = ".aws";
mode = "0700";
}
{
directory = ".aliyun";
mode = "0700";
}
{
directory = ".config/gcloud";
mode = "0700";
}
{
directory = ".config/gh";
mode = "0700";
}
{
directory = ".kube";
mode = "0700";
}
".terraform.d/plugin-cache" # terraform's plugin cache
# ======================================
# language package managers
# ======================================
".npm" # typsescript/javascript
"go"
".cargo" # rust
".m2" # java maven
".gradle" # java gradle
".local/bin"
# python uv
".local/share/uv"
# ======================================
# Security
# ======================================
{
directory = ".gnupg";
mode = "0700";
}
# hand-edited secret config, plus the env file derived from it; Nix deploys neither
{
directory = ".secrets";
mode = "0700";
}
{
directory = ".ssh";
mode = "0700";
}
{
directory = ".pki";
mode = "0700";
}
{
directory = ".local/share/password-store";
mode = "0700";
}
{
# gnmome keyrings
directory = ".local/share/keyrings";
mode = "0700";
}
# ======================================
# Games / Media
# ======================================
"Games"
# .desktop entries created by games/installers (including the umu
# launchers the nix-config-umu-game skill generates). Home Manager
# rewrites its mimeapps.list symlink into the bind mount at activation,
# so that survives too.
".local/share/applications"
".steam"
".config/MangoHud"
".config/blender"
".local/share/umu"
".local/share/Steam"
# WiVRn atomically replaces its settings and pairing files, so preserve
# the directory rather than individual files.
{
directory = ".config/wivrn";
mode = "0700";
}
{
directory = ".android"; # ADB host keys for Quest USB authorization
mode = "0700";
}
".local/state/Heroic"
".config/heroic"
".local/share/GOG.com"
".local/share/StardewValley"
".local/share/feral-interactive"
# ======================================
# Meeting / Remote Desktop / Recording
# ======================================
".zoom"
".config/obs-studio"
".config/Moonlight Game Streaming Project"
".config/sunshine"
".config/freerdp"
".config/remmina"
".local/share/remmina"
# ======================================
# browsers
# ======================================
".mozilla"
".config/google-chrome"
".config/chromium"
".config/BraveSoftware/Brave-Origin"
".config/microsoft-edge"
# ======================================
# CLI data
# ======================================
".local/share/atuin"
".local/share/zoxide"
".local/share/direnv"
".local/share/k9s"
# ======================================
# Containers
# ======================================
".local/share/containers"
# nixpak app's data
{
directory = ".var";
mode = "0700";
}
# ======================================
# Misc
# ======================================
# Mihomo (native core): sources spec, generator and config all live here
# (contains subscription secrets, hence 0700).
{
directory = ".config/mihomo";
mode = "0700";
}
# Orca Slicer - 3D Printer Slicer
".local/share/orca-slicer"
".config/OrcaSlicer"
# Bambu Studio - 3D Printer Slicer
".local/share/bambu-studio"
".config/BambuStudio"
# Audio
".config/pulse"
".local/state/wireplumber"
# EasyEffects 8: KConfig settings database plus user presets, autoload
# profiles and imported impulse responses/RNNoise models. The preset
# declared in home/linux/gui/base/media.nix is re-linked here by Home
# Manager activation; everything the GUI writes needs the persistent
# mount or it is gone after a reboot.
".config/easyeffects"
".local/share/easyeffects"
# go-musicfox - TUI NetEase Cloud Music client
# ~/.config/go-musicfox/config.toml is force-managed by Home Manager
# (see home/linux/gui/base/go-musicfox/default.nix) and reset on each
# rebuild; this keeps the runtime themes/ directory and the app's
# between-rebuild config edits.
".config/go-musicfox"
".local/share/go-musicfox" # login cookie + library db
".local/state/go-musicfox" # logs
# Digital Painting
".local/share/krita"
# Japanese IME
".config/mozc" # used by fcitx5-mozc
# Vinput voice input (fcitx5-vinput): the downloaded sherpa-onnx models
# (~/.local/share/vinput/models/). Without them the daemon starts with
# "Local ASR provider model is not configured" after every reboot. Its
# config needs no entry: it is an out-of-store link into the preserved
# nix-config checkout. Setup: home/linux/gui/base/vinput/README.md
".local/share/vinput"
".config/nushell"
];
files = [
{
file = ".config/zoomus.conf";
how = "symlink";
}
{
file = ".config/zoom.conf";
how = "symlink";
}
];
};
};
# Keep logrotate's state inside the directory preserved above instead of the
# default /var/lib/logrotate.status (a mount point `rename()` cannot replace).
services.logrotate.extraArgs = [
"--state"
"/var/lib/logrotate/status"
];
# 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 =
let
permission = {
user = username;
group = lib.mkForce username;
mode = lib.mkForce "0750";
};
in
{
"/home/${username}/.config".d = permission;
"/home/${username}/.local".d = permission;
"/home/${username}/.local/share".d = permission;
"/home/${username}/.local/state".d = permission;
"/home/${username}/.local/state/nix".d = permission;
"/home/${username}/.terraform.d".d = permission;
};
# /var/tmp is re-creatable scratch, but it must not consume RAM, so it is
# bind-mounted onto the persistent volume above. Wipe it at boot (mirrors
# nixpkgs `boot.tmp.cleanOnBoot` for /tmp) so contents only accumulate
# between reboots.
#
# Crash dumps live in the persisted /var/lib/systemd and can contain secrets
# copied out of process memory, so don't keep them across reboots either.
systemd.tmpfiles.rules = [
"D! /var/tmp 1777 root root"
"D! /var/lib/systemd/coredump 0755 root root"
];
# Mask the commit service: /etc/machine-id is persisted by preservation
# (see the `files` entry above), so there is no transient ID to commit.
systemd.suppressedSystemUnits = [ "systemd-machine-id-commit.service" ];
# Inert while the unit is masked above; kept as the alternative approach.
systemd.services.systemd-machine-id-commit = {
unitConfig.ConditionPathIsMountPoint = [
""
"/persistent/etc/machine-id"
];
serviceConfig.ExecStart = [
""
"systemd-machine-id-setup --commit --root /persistent"
];
};
}