{ config, lib, utils, ... }: let cfg = config.environment.persistence; # ["/home/user/" "/.screenrc"] -> ["home" "user" ".screenrc"] splitPath = paths: (builtins.filter (s: builtins.typeOf s == "string" && s != "") ( builtins.concatMap (builtins.split "/") paths )); # ["/home/user/" "/.screenrc"] -> "/home/user/.screenrc" mergePaths = paths: let prefix = lib.strings.optionalString (lib.strings.hasPrefix "/" (builtins.head paths)) "/"; path = lib.strings.concatStringsSep "/" (splitPath paths); in prefix + path; # "/home/user/.screenrc" -> ["/home", "/home/user"] parentsOf = path: let prefix = lib.strings.optionalString (lib.strings.hasPrefix "/" path) "/"; split = splitPath [ path ]; parents = lib.lists.take ((lib.lists.length split) - 1) split; in lib.lists.foldl' ( state: item: state ++ [ (mergePaths [ (if state != [ ] then lib.lists.last state else prefix) item ]) ] ) [ ] parents; in { options.environment = with lib; with types; { impermanence.device = mkOption { type = str; default = config.disko.devices.disk.main.content.partitions.root.content.content.device; description = '' LUKS BTRFS partition to wipe on boot. ''; }; persistence = let isPathLike = strings.hasPrefix "/"; in mkOption { type = ( addCheck (attrsOf ( attrsOf ( submodule ( { name, config, ... }: { options = { enable = mkOption { type = bool; default = true; description = "Whether to enable the item."; }; service = mkOption { type = str; readOnly = true; description = '' Systemd service that prepares and syncs the item. Can be used as a dependency in other units. ''; }; mount = mkOption { type = str; readOnly = true; description = '' Systemd mount that binds the item. Can be used as a dependency in other units. ''; }; path = mkOption { type = str; readOnly = true; default = name; }; _sourceRoot = mkOption { type = str; internal = true; }; source = mkOption { type = str; readOnly = true; }; _targetRoot = mkOption { type = str; internal = true; }; target = mkOption { type = str; readOnly = true; }; }; } ) ) )) (attrs: lists.all isPathLike (builtins.attrNames attrs)) ); apply = ps: builtins.mapAttrs ( persistence: items: builtins.mapAttrs ( _: config: let path = config.path; _sourceRoot = persistence; source = mergePaths [ _sourceRoot path ]; _targetRoot = let parents = lists.reverseList (parentsOf path); in lists.foldl' ( acc: parent: if acc == "/" then lists.findFirst ( otherPersistence: lists.any (other: parent == other) (builtins.attrNames ps.${otherPersistence}) ) "/" (builtins.attrNames ps) else acc ) "/" parents; target = mergePaths [ _targetRoot path ]; in config // { inherit _sourceRoot source _targetRoot target ; service = "${utils.escapeSystemdPath target}.service"; mount = "${utils.escapeSystemdPath target}.mount"; } ) items ) ps; default = { }; description = "Persistence config."; }; }; config = let all = lib.lists.flatten (builtins.concatMap builtins.attrValues (builtins.attrValues cfg)); in { fileSystems = builtins.mapAttrs (_: _: { neededForBoot = true; }) cfg; systemd = { mounts = builtins.map (c: { description = c.path; requiredBy = [ "local-fs.target" ]; requires = [ c.service ]; bindsTo = [ c.service ]; after = [ c.service ]; unitConfig.ConditionPathExists = [ (lib.strings.escape [ " " ] c.source) ]; what = c.source; where = c.target; options = lib.strings.concatStringsSep "," ([ "bind" "X-fstrim.notrim" "x-gvfs-hide" ]); }) all; services = builtins.listToAttrs ( builtins.map (c: { name = utils.escapeSystemdPath c.target; value = { description = c.path; after = [ "local-fs-pre.target" ]; requiredBy = [ "local-fs.target" c.mount ]; before = [ "local-fs.target" c.mount "umount.target" ]; conflicts = [ "umount.target" ]; unitConfig = { DefaultDependencies = false; RefuseManualStart = true; RefuseManualStop = true; }; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; TimeoutStopSec = "1800"; }; script = '' source=${lib.strings.escapeShellArg c._sourceRoot} target=${lib.strings.escapeShellArg c._targetRoot} path=${lib.strings.escapeShellArg c.path} ${builtins.readFile ./scripts/start.sh} ''; preStop = '' source=${lib.strings.escapeShellArg c._sourceRoot} target=${lib.strings.escapeShellArg c._targetRoot} path=${lib.strings.escapeShellArg c.path} ${builtins.readFile ./scripts/stop.sh} ''; }; }) all ); }; assertions = let paths = builtins.map (c: c.path) all; duplicates = lib.lists.filter (t: lib.lists.count (o: o == t) paths > 1) (lib.lists.unique paths); in [ { assertion = lib.lists.length duplicates == 0; message = "Each target must be defined under a single persistence. Duplicate targets found: ${lib.concatStringsSep ", " duplicates}"; } ]; }; }