mirror of
https://github.com/ryan4yin/nix-config.git
synced 2026-11-13 12:13:36 +01:00
* feat(nixpak): sandbox Steam and Heroic in ~/Gaming idols-ai stops preserving ~/.local/share/Steam, ~/.steam, ~/.config/heroic and their game data, which must be moved into ~/Gaming before deploying. Assisted-by: DeepSeek Harness (deepseek-flash) * refactor(nixpak): replace steam-fix.nu with sandboxed protontricks Assisted-by: DeepSeek Harness (claude-opus-5-5) * fix(nixpak): hide host documents and GTK config from game sandboxes Assisted-by: DeepSeek Harness (claude-opus-5-5) * docs(ai): locate the Steam library under ~/Gaming Assisted-by: DeepSeek Harness (claude-opus-5-5) * fix(umu-game): confine writes to ~/Gaming and keep foreign entries Assisted-by: DeepSeek Harness (claude-opus-5-5) * fix(nixpak): keep Steam and Heroic in the application launcher The wrappers only carried bin/, so neither launcher had a desktop entry after replacing programs.steam. Assisted-by: DeepSeek Harness (deepseek-flash) * fix(nixpak): give the game sandboxes a fontconfig that sees CJK fonts Steam rendered Chinese as tofu: the FHS layers build their own /etc/fonts, whose config lists only DejaVu and the XDG font directory, so neither the host fonts nor the FHS /usr/share/fonts were ever scanned. Point FONTCONFIG_FILE at a small config that includes /usr/share/fonts, and ship WenQuanYi Zen Hei and Noto Sans CJK in all three FHS layers. Assisted-by: DeepSeek Harness (deepseek-flash) * fix(nixpak): grant the game launchers a StatusNotifier tray icon Steam and Heroic register a tray icon over D-Bus, and the game sandboxes granted no StatusNotifierWatcher access, so the desktop shell had nothing to show for them. Enable desktopIntegration.tray for all three launchers and teach the policy suites to expect it. Assisted-by: DeepSeek Harness (deepseek-flash) * fix(nixpak): drop the duplicate host CJK font and tidy the font overrides Steam reads the fonts inside its own FHS layer, so the system-wide wqy_zenhei added for it no longer serves any sandbox. Give gaming-policy its own comment for FONTCONFIG_FILE instead of leaving it under the compat-tool one, make the desktop-base value a default, and note the SNI tray grant in the README table. Assisted-by: DeepSeek Harness (deepseek-flash)
501 lines
16 KiB
Nix
501 lines
16 KiB
Nix
{
|
|
preservation,
|
|
lib,
|
|
pkgs,
|
|
myvars,
|
|
...
|
|
}:
|
|
let
|
|
inherit (myvars) username;
|
|
in
|
|
{
|
|
imports = [
|
|
preservation.nixosModules.default
|
|
];
|
|
|
|
preservation.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"
|
|
# OpenRGB SDK server settings and profiles
|
|
"/var/lib/OpenRGB"
|
|
|
|
# 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"
|
|
"models"
|
|
|
|
# ======================================
|
|
# 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
|
|
# ======================================
|
|
|
|
# zed
|
|
".config/zed"
|
|
".local/share/zed"
|
|
|
|
# ai agents
|
|
".agents" # skills for all agents
|
|
".config/agents"
|
|
".codex"
|
|
".dsh" # DeepSeek Harness
|
|
".pi"
|
|
# Qwen Code keeps settings, credentials, history, sessions, and logs
|
|
# under ~/.qwen; keep the whole directory private and persistent.
|
|
{
|
|
directory = ".qwen";
|
|
mode = "0700";
|
|
}
|
|
".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"
|
|
# Deliberately not persisted: IPython keeps its history database in
|
|
# memory (HistoryManager.hist_file = ":memory:") and the profile config
|
|
# and startup helpers are recreated from the Nix store on every
|
|
# activation, so this directory holds no state worth preserving.
|
|
|
|
# ======================================
|
|
# 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
|
|
# ======================================
|
|
|
|
# Home of the game sandboxes (hardening/nixpaks/modules/gaming-policy.nix).
|
|
# Steam, Heroic, umu and the games they launch all see it as $HOME, so
|
|
# their libraries, prefixes, saves and Proton builds live inside it:
|
|
# ~/Gaming/.local/share/Steam, ~/Gaming/.config/heroic,
|
|
# ~/Gaming/.local/share/umu, ~/Gaming/Games/... .
|
|
"Gaming"
|
|
# .desktop entries created on the host for sandboxed launchers. Home
|
|
# Manager rewrites its mimeapps.list symlink into the bind mount at
|
|
# activation, so that survives too.
|
|
".local/share/applications"
|
|
".config/MangoHud"
|
|
".config/blender"
|
|
|
|
# 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";
|
|
}
|
|
|
|
# ======================================
|
|
# 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"
|
|
|
|
# OpenRGB GUI settings, profiles and plugins
|
|
".config/OpenRGB"
|
|
|
|
# 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"
|
|
];
|
|
};
|
|
}
|