diff --git a/lib/default.nix b/lib/default.nix index 9ec1de6..7e1b276 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -1,7 +1,33 @@ { lib }: lib.makeExtensible (self: - { - flattenListSet = imports: (lib.lists.flatten (builtins.concatLists (builtins.attrValues imports))); - flattenSetList = attrSet: (builtins.mapAttrs (name: value: lib.lists.flatten value) attrSet); + + with lib; + rec { + flattenListSet = imports: (flatten (concatLists (attrValues imports))); + flattenSetList = attrSet: (mapAttrs (name: value: flatten value) attrSet); + + + # ["/home/user/" "/.screenrc"] -> ["home" "user" ".screenrc"] + splitPath = paths: + (filter + (s: builtins.typeOf s == "string" && s != "") + (concatMap (builtins.split "/") paths) + ); + + # ["home" "user" ".screenrc"] -> "home/user/.screenrc" + dirListToPath = dirList: (concatStringsSep "/" dirList); + + # ["/home/user/" "/.screenrc"] -> "/home/user/.screenrc" + concatPaths = paths: + let + prefix = optionalString (hasPrefix "/" (head paths)) "/"; + path = dirListToPath (splitPath paths); + in + prefix + path; + + sanitizeName = name: + replaceStrings + [ "." ] [ "" ] + (strings.sanitizeDerivationName (removePrefix "/" name)); } ) diff --git a/users/modules/bindmounts.nix b/users/modules/bindmounts.nix new file mode 100644 index 0000000..7798f31 --- /dev/null +++ b/users/modules/bindmounts.nix @@ -0,0 +1,257 @@ +{ pkgs, config, lib, ... }: + +# Modified from https://github.com/nix-community/impermanence/blob/master/home-manager.nix + +with lib; +with lib.our; +let + cfg = config.home.bindmounts; + + persistentStoragePaths = attrNames cfg; +in +{ + options = { + + home.bindmounts = mkOption { + default = { }; + type = with types; attrsOf ( + submodule ({ name, ... }: { + options = + { + directories = mkOption { + type = with types; listOf (submodule { + options = { + source = mkOption { + type = with types; str; + }; + + target = mkOption { + type = with types; str; + }; + }; + }); + default = [ ]; + description = '' + A list of directories and target locations that you wish to bind-mount from the initial source. + ''; + }; + + allowOther = mkOption { + type = with types; nullOr bool; + default = null; + example = true; + apply = x: + if x == null then + warn '' + home.bindmounts."${name}".allowOther not set; assuming 'false'. + See https://github.com/nix-community/impermanence#home-manager for more info. + '' + false + else + x; + description = '' + Whether to allow other users, such as + root, access to files through the + bind mounted directories listed in + directories. Requires the NixOS + configuration parameter + programs.fuse.userAllowOther to + be true. + ''; + }; + }; + }) + ); + }; + + }; + + config = { + systemd.user.services = + let + mkBindMountService = persistentStoragePath: dir: + let + inherit (dir) source target; + targetDir = escapeShellArg (concatPaths [ persistentStoragePath source ]); + mountPoint = escapeShellArg (concatPaths [ config.home.homeDirectory target ]); + name = "bindMount-${sanitizeName targetDir}"; + bindfsOptions = concatStringsSep "," ( + optional (!cfg.${persistentStoragePath}.allowOther) "no-allow-other" + ++ optional (versionAtLeast pkgs.bindfs.version "1.14.9") "fsname=${targetDir}" + ); + bindfsOptionFlag = optionalString (bindfsOptions != "") (" -o " + bindfsOptions); + bindfs = "bindfs -f" + bindfsOptionFlag; + startScript = pkgs.writeShellScript name '' + set -eu + if ! mount | grep -F ${mountPoint}' ' && ! mount | grep -F ${mountPoint}/; then + mkdir -p ${mountPoint} + ${bindfs} ${targetDir} ${mountPoint} + else + echo "There is already an active mount at or below ${mountPoint}!" >&2 + exit 1 + fi + ''; + stopScript = pkgs.writeShellScript "unmount-${name}" '' + set -eu + triesLeft=6 + while (( triesLeft > 0 )); do + if fusermount -u ${mountPoint}; then + exit 0 + else + (( triesLeft-- )) + if (( triesLeft == 0 )); then + echo "Couldn't perform regular unmount of ${mountPoint}. Attempting lazy unmount." + fusermount -uz ${mountPoint} + else + sleep 5 + fi + fi + done + ''; + in + { + inherit name; + value = { + Unit = { + Description = "Bind mount ${targetDir} at ${mountPoint}"; + + # Don't restart the unit, it could corrupt data and + # crash programs currently reading from the mount. + X-RestartIfChanged = false; + }; + + Install.WantedBy = [ "default.target" ]; + + Service = { + ExecStart = "${startScript}"; + ExecStop = "${stopScript}"; + Environment = "PATH=${makeBinPath [ pkgs.coreutils pkgs.utillinux pkgs.gnugrep pkgs.bindfs ]}:/run/wrappers/bin"; + }; + }; + }; + + mkBindMountServicesForPath = persistentStoragePath: + listToAttrs (map + (mkBindMountService persistentStoragePath) + cfg.${persistentStoragePath}.directories + ); + in + builtins.foldl' + recursiveUpdate + { } + (map mkBindMountServicesForPath persistentStoragePaths); + + home.activation = + let + dag = config.lib.dag; + + # The name of the activation script entry responsible for + # reloading systemd user services. The name was initially + # `reloadSystemD` but has been changed to `reloadSystemd`. + reloadSystemd = + if config.home.activation ? reloadSystemD then + "reloadSystemD" + else + "reloadSystemd"; + + mkBindMount = persistentStoragePath: dir: + let + inherit (dir) source target; + targetDir = escapeShellArg (concatPaths [ persistentStoragePath source ]); + mountPoint = escapeShellArg (concatPaths [ config.home.homeDirectory target ]); + mount = "${pkgs.utillinux}/bin/mount"; + bindfsOptions = concatStringsSep "," ( + optional (!cfg.${persistentStoragePath}.allowOther) "no-allow-other" + ++ optional (versionAtLeast pkgs.bindfs.version "1.14.9") "fsname=${targetDir}" + ); + bindfsOptionFlag = optionalString (bindfsOptions != "") (" -o " + bindfsOptions); + bindfs = "${pkgs.bindfs}/bin/bindfs" + bindfsOptionFlag; + systemctl = "XDG_RUNTIME_DIR=\${XDG_RUNTIME_DIR:-/run/user/$(id -u)} ${config.systemd.user.systemctlPath}"; + in + '' + mkdir -p ${targetDir} + mkdir -p ${mountPoint} + if ${mount} | grep -F ${mountPoint}' ' >/dev/null; then + if ! ${mount} | grep -F ${mountPoint}' ' | grep -F bindfs; then + if ! ${mount} | grep -F ${mountPoint}' ' | grep -F ${targetDir}' ' >/dev/null; then + # The target directory changed, so we need to remount + echo "remounting ${mountPoint}" + ${systemctl} --user stop bindMount-${sanitizeName targetDir} + ${bindfs} ${targetDir} ${mountPoint} + mountedPaths[${mountPoint}]=1 + fi + fi + elif ${mount} | grep -F ${mountPoint}/ >/dev/null; then + echo "Something is mounted below ${mountPoint}, not creating bind mount to ${targetDir}" >&2 + else + ${bindfs} ${targetDir} ${mountPoint} + mountedPaths[${mountPoint}]=1 + fi + ''; + + mkBindMountsForPath = persistentStoragePath: + concatMapStrings + (mkBindMount persistentStoragePath) + cfg.${persistentStoragePath}.directories; + + mkUnmount = persistentStoragePath: dir: + let + inherit (dir) target; + mountPoint = escapeShellArg (concatPaths [ config.home.homeDirectory target ]); + in + '' + if [[ -n ''${mountedPaths[${mountPoint}]+x} ]]; then + triesLeft=3 + while (( triesLeft > 0 )); do + if fusermount -u ${mountPoint}; then + break + else + (( triesLeft-- )) + if (( triesLeft == 0 )); then + echo "Couldn't perform regular unmount of ${mountPoint}. Attempting lazy unmount." + fusermount -uz ${mountPoint} || true + else + sleep 1 + fi + fi + done + fi + ''; + + mkUnmountsForPath = persistentStoragePath: + concatMapStrings + (mkUnmount persistentStoragePath) + cfg.${persistentStoragePath}.directories; + + in + mkIf (any (path: cfg.${path}.directories != [ ]) persistentStoragePaths) { + createAndMountPersistentStoragePaths = + dag.entryBefore + [ "writeBoundary" ] + '' + declare -A mountedPaths + ${(concatMapStrings mkBindMountsForPath persistentStoragePaths)} + ''; + + unmountPersistentStoragePaths = + dag.entryBefore + [ "createAndMountPersistentStoragePaths" ] + '' + unmountBindMounts() { + ${concatMapStrings mkUnmountsForPath persistentStoragePaths} + } + # Run the unmount function on error to clean up stray + # bind mounts + trap "unmountBindMounts" ERR + ''; + + runUnmountPersistentStoragePaths = + dag.entryBefore + [ reloadSystemd ] + '' + unmountBindMounts + ''; + }; + }; + +}