diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c30de3..e7c893c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,16 +4,16 @@ jobs: nix_parsing: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: cachix/install-nix-action@v12 + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@v31 - name: Check Nix parsing run: | find . -name "*.nix" -exec nix-instantiate --parse --quiet {} >/dev/null + nix_formatting: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: cachix/install-nix-action@v12 + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - name: Check Nix formatting @@ -22,8 +22,8 @@ jobs: shell_formatting: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: cachix/install-nix-action@v12 + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - name: Check shell script formatting @@ -32,10 +32,22 @@ jobs: shell_error_checking: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: cachix/install-nix-action@v12 + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - name: Check for shell script errors run: | find . -name "*.*sh" -exec nix-shell -p shellcheck --run "shellcheck {}" \; + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@v31 + with: + enable_kvm: true + extra_nix_config: "system-features = nixos-test benchmark big-parallel kvm" + github_access_token: ${{ secrets.GITHUB_TOKEN }} + - name: Flake checks, NixOS tests + run: | + nix flake check -L diff --git a/README.org b/README.org index dd0ad56..e2d3253 100644 --- a/README.org +++ b/README.org @@ -158,7 +158,7 @@ * Module usage - There are currently two modules: one for ~NixOS~ and one for ~home-manager~. + There are currently two modules: one for NixOS and one for Home Manager. *** NixOS @@ -252,6 +252,7 @@ manager. If enabled, it sets the mount option ~x-gvfs-hide~ on all the bind mounts. + #+NAME: directories - ~directories~ are all directories you want to bind mount to persistent storage. A directory can be represented either as a string, simply denoting its path, or as a submodule. The @@ -289,6 +290,7 @@ permissions. Changing this once the directory has been created has no effect. + #+NAME: files - ~files~ are all files you want to link or bind to persistent storage. A file can be represented either as a string, simply denoting its path, or as a submodule. The submodule @@ -311,8 +313,11 @@ exist. Available options are ~user~, ~group~ and ~mode~. See their definition in ~directories~ above. - If the file exists in persistent storage, it will be bind - mounted to the target path; otherwise it will be symlinked. + - ~method~, the method used to link the file to persistent + storage. ~"auto"~, the default, uses a bind mount when the file + exists in persistent storage and a symlink + otherwise. ~"symlink"~ uses a symlink regardless. Only change + this to ~"symlink"~ if ~"auto"~ is giving you issues. - ~users.talyz~ handles files and directories in ~talyz~'s home directory @@ -322,34 +327,27 @@ submodule work like their root counterparts, but the paths are automatically prefixed with with the user's home directory. - If the user has a non-standard home directory (i.e. not - ~/home/~), the ~users..home~ option has to be - set to this path - it can't currently be automatically deduced - due to a limitation in ~nixpkgs~. - /Important note:/ Make sure your persistent volumes are marked with ~neededForBoot~, otherwise you will run into problems. -*** home-manager +*** Home Manager - Usage of the ~home-manager~ module is very similar to the one of the - ~NixOS~ module - the key differences are that the ~persistence~ option - is now under ~home~, rather than ~environment~, and the addition of - the submodule option ~removePrefixDirectory~. + The usage of the Home Manager module is very similar to the one of + the NixOS module - the key difference is that the ~persistence~ + option is now under ~home~, rather than ~environment~. - /Important note:/ You have to use the ~home-manager~ ~NixOS~ module (in - the ~nixos~ directory of ~home-manager~'s repo) in order for this - module to work as intended. + /Important note:/ You have to use the Home Manager NixOS module (in + the ~nixos~ directory of Home Manager's repo) *and* the NixOS + ~persistence~ module in order for this module to work as intended. - To use the module, import it into your configuration with + The module will be loaded automatically when you [[*NixOS][import the NixOS + persistence module]] and the Home Manager NixOS module. - #+begin_src nix - { - imports = [ /path/to/impermanence/home-manager.nix ]; - } - #+end_src + It adds the ~home.persistence~ option, which is an attribute set + of submodules, where the attribute name is the path to persistent + storage. - or use the provided ~homeManagerModules.impermanence~ flake output: + Usage is shown best with an example: #+begin_src nix { @@ -370,16 +368,32 @@ system = "x86_64-linux"; modules = [ { - imports = [ home-manager.nixosModules.home-manager ]; + imports = [ + impermanence.nixosModules.impermanence + home-manager.nixosModules.home-manager + ]; - home-manager.users.username = - { ... }: + home-manager.users.bird = { - imports = [ - impermanence.homeManagerModules.impermanence - ./home/impermanence.nix # Your home-manager impermanence-configuration - ]; - }; + home.persistence."/persistent" = { + directories = [ + "Downloads" + "Music" + "Pictures" + "Documents" + "Videos" + "VirtualBox VMs" + { directory = ".gnupg"; mode = "0700"; } + { directory = ".ssh"; mode = "0700"; } + { directory = ".nixops"; mode = "0700"; } + { directory = ".local/share/keyrings"; mode = "0700"; } + ".local/share/direnv" + ]; + files = [ + ".screenrc" + ]; + }; + }; } ]; }; @@ -387,78 +401,12 @@ } #+end_src - This adds the ~home.persistence~ option, which is an attribute set - of submodules, where the attribute name is the path to persistent - storage. - - Usage is shown best with an example: - - #+begin_src nix - { - home.persistence."/persistent/home/talyz" = { - directories = [ - "Downloads" - "Music" - "Pictures" - "Documents" - "Videos" - "VirtualBox VMs" - ".gnupg" - ".ssh" - ".nixops" - ".local/share/keyrings" - ".local/share/direnv" - { - directory = ".local/share/Steam"; - method = "symlink"; - } - ]; - files = [ - ".screenrc" - ]; - allowOther = true; - }; - } - #+end_src - - - ~"/persistent/home/talyz"~ is the path to your persistent storage location - - ~directories~ are all directories you want to link to persistent storage - - It is possible to switch the linking ~method~ between bindfs (the - default) and symbolic links. - - ~files~ are all files you want to link to persistent storage. These are - symbolic links to their target location. - - ~allowOther~ allows other users, such as ~root~, to access files - through the bind mounted directories listed in - ~directories~. Useful for ~sudo~ operations, Docker, etc. Requires - the NixOS configuration ~programs.fuse.userAllowOther = true~. - - Additionally, the ~home-manager~ module allows for compatibility - with ~dotfiles~ repos structured for use with [[https://www.gnu.org/software/stow/][GNU Stow]], where the - files linked to are one level deeper than where they should end - up. This can be achieved by setting ~removePrefixDirectory~ to ~true~: - - #+begin_src nix - { - home.persistence."/etc/nixos/home-talyz-nixpkgs/dotfiles" = { - removePrefixDirectory = true; - files = [ - "screen/.screenrc" - ]; - directories = [ - "fish/.config/fish" - ]; - }; - } - #+end_src - - In the example, the ~.screenrc~ file and ~.config/fish~ directory - should be linked to from the home directory; ~removePrefixDirectory~ - removes the first part of the path when deciding where to put the - links. - - /Note:/ When using ~bindfs~ fuse filesystem for directories, the names of - the directories you add will be visible in the ~/etc/mtab~ file and in the - output of ~mount~ to all users. + - ~"/persistent"~ is the path to your persistent storage location. + - ~directories~ are all directories you want to bind mount to + persistent storage. See [[directories][the NixOS module directories section]] for + more details. + - ~files~ are all files you want to link or bind to persistent + storage. See [[files][the NixOS module files section]] for more details. ** Further reading The following blog posts provide more information on the concept of ephemeral diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..1323c93 --- /dev/null +++ b/flake.lock @@ -0,0 +1,48 @@ +{ + "nodes": { + "home-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1747978958, + "narHash": "sha256-pQQnbxWpY3IiZqgelXHIe/OAE/Yv4NSQq7fch7M6nXQ=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "7419250703fd5eb50e99bdfb07a86671939103ea", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1748026106, + "narHash": "sha256-6m1Y3/4pVw1RWTsrkAK2VMYSzG4MMIj7sqUy7o8th1o=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "063f43f2dbdef86376cc29ad646c45c46e93234c", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "home-manager": "home-manager", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix index 281ae4c..3af25a1 100644 --- a/flake.nix +++ b/flake.nix @@ -1,13 +1,237 @@ { - outputs = { self }: { - nixosModules.default = self.nixosModules.impermanence; - nixosModules.impermanence = import ./nixos.nix; + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + home-manager.url = "github:nix-community/home-manager"; + home-manager.inputs.nixpkgs.follows = "nixpkgs"; + }; - homeManagerModules.default = self.homeManagerModules.impermanence; - homeManagerModules.impermanence = import ./home-manager.nix; + outputs = { self, nixpkgs, home-manager }: + let + inherit (nixpkgs) lib; + forAllSystems = lib.genAttrs lib.systems.flakeExposed; + in + { + lib = import ./lib.nix { inherit lib; }; - # Deprecated - nixosModule = self.nixosModules.impermanence; - nixosModules.home-manager.impermanence = self.homeManagerModules.impermanence; - }; + nixosModules.default = self.nixosModules.impermanence; + nixosModules.impermanence = import ./nixos.nix; + + # Deprecated + homeManagerModules.default = self.homeManagerModules.impermanence; + homeManagerModules.impermanence = { + assertions = [ + { + assertion = false; + message = '' + home.persistence: The Home Manager flake outputs are deprecated! + + The Home Manager module will be automatically imported by the NixOS + module. Please remove any manual imports. + + See https://github.com/nix-community/impermanence?tab=readme-ov-file#home-manager + for updated usage instructions. + ''; + } + ]; + }; + nixosModule = self.nixosModules.impermanence; + nixosModules.home-manager.impermanence = self.homeManagerModules.impermanence; + + devShells = forAllSystems + (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + default = + pkgs.mkShell { + packages = [ + pkgs.nixpkgs-fmt + ]; + }; + } + ); + + checks = forAllSystems + (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + mkTest = { name, configuration }: + pkgs.testers.runNixOSTest { + inherit name; + nodes = { + persistence = + { config, ... }: + { + virtualisation.diskImage = "./persistent.qcow2"; + virtualisation.graphics = false; + + boot.initrd.verbose = true; + + imports = [ + self.nixosModule + configuration + ]; + + services.openssh.enable = true; + + users.users.bird = { + isNormalUser = true; + uid = 1000; + }; + + users.users.fish = { + isNormalUser = true; + uid = 1001; + }; + + virtualisation.fileSystems = { + "/" = { + fsType = lib.mkForce "tmpfs"; + device = lib.mkForce "none"; + neededForBoot = true; + }; + "/persistent" = { + device = "/dev/vda"; + fsType = "ext4"; + neededForBoot = true; + }; + }; + + environment.persistence.main = { + persistentStoragePath = "/persistent"; + enableDebugging = true; + files = [ + "/etc/machine-id" + "/etc/ssh/ssh_host_ed25519_key" + { file = "/etc/ssh/ssh_host_ed25519_key.pub"; method = "symlink"; } + "/etc/ssh/ssh_host_rsa_key" + "/etc/ssh/ssh_host_rsa_key.pub" + ]; + directories = [ + { directory = "/etc/nixos"; mode = "0700"; user = "root"; group = "root"; } + "/var/log" + "/var/lib/bluetooth" + "/var/lib/nixos" + "/var/lib/systemd/coredump" + "/etc/NetworkManager/system-connections" + ]; + }; + }; + }; + + testScript = { nodes, ... }: + let + nixos = nodes.persistence.environment.persistence.main; + nixos-users = nodes.persistence.environment.persistence.main.users.bird or { }; + home-manager = nodes.persistence.home-manager.users.bird.home.persistence.main or { }; + main = lib.zipAttrsWith (_name: lib.flatten) [ nixos nixos-users home-manager ]; + in + '' + persistence.start(allow_reboot=True) + + persistence.wait_for_unit("sshd.service") + + persistence.succeed("echo potato > '/home/bird/.config/persistence_test'") + + ${lib.concatMapStrings (file: + let + targetFile = self.lib.concatPaths [ file.persistentStoragePath file.filePath ]; + in '' + persistence.wait_for_file("${targetFile}", 1) + persistence.succeed("diff ${targetFile} ${file.filePath}") + '') + main.files} + + ${lib.concatMapStrings (dir: + let + targetDir = self.lib.concatPaths [ dir.persistentStoragePath dir.dirPath ]; + in '' + persistence.wait_for_file("${targetDir}", 1) + persistence.succeed("diff <(stat -c '%Hd %Ld %i' ${targetDir}) <(stat -c '%Hd %Ld %i' ${dir.dirPath})") + persistence.succeed("test ${dir.user} = $(stat -c %U ${targetDir})") + persistence.succeed("test ${dir.mode} = $(stat -c %#01a ${targetDir})") + '') + main.directories} + + persistence.reboot() + + persistence.wait_for_console_text("reviving user .* with UID") + + ${lib.concatMapStrings (file: + let + targetFile = self.lib.concatPaths [ file.persistentStoragePath file.filePath ]; + in '' + persistence.wait_for_file("${targetFile}", 1) + ${if file.method == "auto" then '' + persistence.succeed("diff <(stat -c '%Hd %Ld %i' ${targetFile}) <(stat -c '%Hd %Ld %i' ${file.filePath})") + '' else '' + persistence.succeed("test ${targetFile} = $(readlink -f ${file.filePath})") + ''} + persistence.succeed("diff ${targetFile} ${file.filePath}") + '') + main.files} + ''; + }; + in + { + nixos = mkTest { + name = "nixos-persistence"; + configuration = { + boot.initrd.systemd.enable = true; + + environment.persistence.main.users.bird = { + directories = [ + "Downloads" + "Music" + "Pictures" + "Documents" + "Videos" + ]; + files = [ + ".config/persistence_test" + ]; + }; + }; + }; + home-manager = mkTest { + name = "hm-persistence"; + configuration = { config, ... }: + { + imports = [ + home-manager.nixosModules.home-manager + ]; + + home-manager.sharedModules = [{ home.stateVersion = config.system.stateVersion; }]; + + home-manager.users.bird = + { + home.persistence.main = { + persistentStoragePath = "/persistent"; + directories = [ + "Downloads" + "Music" + "Pictures" + "Documents" + "Videos" + ]; + files = [ + ".config/persistence_test" + ]; + }; + }; + + home-manager.users.fish = + { + home.file = { + "useless".text = '' + a useless file + ''; + }; + }; + }; + }; + } + ); + }; } diff --git a/home-manager.nix b/home-manager.nix index ab44147..59d09c9 100644 --- a/home-manager.nix +++ b/home-manager.nix @@ -1,486 +1,86 @@ -{ pkgs, config, lib, ... }: +{ pkgs +, config +, lib +, ... +}: -with lib; let - cfg = config.home.persistence; - - persistentStorageNames = (filter (path: cfg.${path}.enable) (attrNames cfg)); - - inherit (pkgs.callPackage ./lib.nix { }) - splitPath - dirListToPath - concatPaths - sanitizeName + inherit (lib) + mkOption + types + catAttrs + any + hasInfix + attrValues ; - mount = "${pkgs.util-linux}/bin/mount"; - unmountScript = mountPoint: tries: sleep: '' - triesLeft=${toString tries} - if ${mount} | grep -F ${mountPoint}' ' >/dev/null; then - 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} - else - sleep ${toString sleep} - fi - fi - done - fi - ''; -in -{ - options = { - - home.persistence = mkOption { - default = { }; - type = with types; attrsOf ( - submodule ({ name, config, ... }: { - options = - { - persistentStoragePath = mkOption { - type = path; - default = name; - description = '' - The path to persistent storage where the real - files and directories should be stored. - ''; - }; - - enable = mkOption { - type = bool; - default = true; - description = "Whether to enable this persistent storage location."; - }; - - defaultDirectoryMethod = mkOption { - type = types.enum [ "bindfs" "symlink" ]; - default = "bindfs"; - description = '' - The linking method that should be used for directories. - - - bindfs is very transparent, and thus used as a safe - default. It has, however, a significant performance impact in - IO-heavy situations. - - - symlinks have great performance but may be treated - specially by some programs that may e.g. generate - errors/warnings, or replace them. - - This can be overridden on a per entry basis. - ''; - }; - - directories = mkOption { - type = types.listOf ( - types.coercedTo types.str (directory: { inherit directory; }) (submodule { - options = { - directory = mkOption { - type = str; - description = "The directory path to be linked."; - }; - method = mkOption { - type = types.enum [ "bindfs" "symlink" ]; - default = config.defaultDirectoryMethod; - description = '' - The linking method to be used for this specific - directory entry. See - defaultDirectoryMethod for more - information on the tradeoffs. - ''; - }; - }; - }) - ); - default = [ ]; - example = [ - "Downloads" - "Music" - "Pictures" - "Documents" - "Videos" - "VirtualBox VMs" - ".gnupg" - ".ssh" - ".local/share/keyrings" - ".local/share/direnv" - { - directory = ".local/share/Steam"; - method = "symlink"; - } - ]; - description = '' - A list of directories in your home directory that - you want to link to persistent storage. You may optionally - specify the linking method each directory should use. - ''; - }; - - files = mkOption { - type = with types; listOf str; - default = [ ]; - example = [ - ".screenrc" - ]; - description = '' - A list of files in your home directory you want to - link to persistent storage. - ''; - }; - - allowOther = mkOption { - type = with types; nullOr bool; - default = null; - example = true; - apply = x: - if x == null then - warn '' - home.persistence."${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. - ''; - }; - - removePrefixDirectory = mkOption { - type = types.bool; - default = false; - example = true; - description = '' - Note: This is mainly useful if you have a dotfiles - repo structured for use with GNU Stow; if you don't, - you can likely ignore it. + inherit (types) + attrsOf + submodule + bool + ; - Whether to remove the first directory when linking - or mounting; e.g. for the path - "screen/.screenrc", the - screen/ is ignored for the path - linked to in your home directory. - ''; - }; - }; - }) - ); - description = '' - A set of persistent storage location submodules listing the - files and directories to link to their respective persistent - storage location. + inherit (config) home; - Each attribute name should be the path relative to the user's - home directory. + cfg = config.home.persistence; - For detailed usage, check the documentation. - ''; - example = literalExpression '' - { - "/persistent/home/talyz" = { - directories = [ - "Downloads" - "Music" - "Pictures" - "Documents" - "Videos" - "VirtualBox VMs" - ".gnupg" - ".ssh" - ".nixops" - ".local/share/keyrings" - ".local/share/direnv" - { - directory = ".local/share/Steam"; - method = "symlink"; - } - ]; - files = [ - ".screenrc" - ]; - allowOther = true; - }; - } - ''; + persistentStoragePaths = catAttrs "persistentStoragePath" (attrValues cfg); +in +{ + options = + { + home.persistence = mkOption { + default = { }; + type = attrsOf ( + submodule ( + { name, config, ... }: + import ./submodule-options.nix { + inherit pkgs lib name config; + + user = home.username; + homeDir = home.homeDirectory; + + # Home Manager doesn't seem to know about the user's group, + # so we default it to null here and fill it in in the NixOS + # module instead + group = null; + } + )); + }; + home._nixosModuleImported = mkOption { + default = false; + type = bool; + internal = true; + description = '' + Internal option to signal whether the NixOS persistence + module was properly imported. Do not set this! + ''; + }; }; - - }; - config = { - home.file = - let - link = file: - pkgs.runCommand - "${sanitizeName file}" - { } - "ln -s '${file}' $out"; - - mkLinkNameValuePair = persistentStorageName: fileOrDir: { - name = - if cfg.${persistentStorageName}.removePrefixDirectory then - dirListToPath (tail (splitPath [ fileOrDir ])) - else - fileOrDir; - value = { source = link (concatPaths [ cfg.${persistentStorageName}.persistentStoragePath fileOrDir ]); }; - }; - - mkLinksToPersistentStorage = persistentStorageName: - listToAttrs (map - (mkLinkNameValuePair persistentStorageName) - (cfg.${persistentStorageName}.files ++ (map (v: v.directory) - (filter (v: v.method == "symlink") cfg.${persistentStorageName}.directories))) - ); - in - foldl' recursiveUpdate { } (map mkLinksToPersistentStorage persistentStorageNames); - - systemd.user.services = - let - mkBindMountService = persistentStorageName: dir: - let - mountDir = - if cfg.${persistentStorageName}.removePrefixDirectory then - dirListToPath (tail (splitPath [ dir ])) - else - dir; - targetDir = escapeShellArg (concatPaths [ cfg.${persistentStorageName}.persistentStoragePath dir ]); - mountPoint = escapeShellArg (concatPaths [ config.home.homeDirectory mountDir ]); - name = "bindMount-${sanitizeName targetDir}"; - bindfsOptions = concatStringsSep "," ( - optional (!cfg.${persistentStorageName}.allowOther) "no-allow-other" - ++ optional (versionAtLeast pkgs.bindfs.version "1.14.9") "fsname=${targetDir}" - ); - bindfsOptionFlag = optionalString (bindfsOptions != "") (" -o " + bindfsOptions); - bindfs = "bindfs" + bindfsOptionFlag; - startScript = pkgs.writeShellScript name '' - set -eu - if ! mount | grep -F ${mountPoint}' ' && ! mount | grep -F ${mountPoint}/; then - mkdir -p ${mountPoint} - exec ${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 - ${unmountScript mountPoint 6 5} - ''; - 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; - - # Don't add an implicit After=basic.target. - DefaultDependencies = false; - - Before = [ - "bluetooth.target" - "basic.target" - "default.target" - "paths.target" - "sockets.target" - "timers.target" - ]; - }; - - Install.WantedBy = [ "paths.target" ]; - - Service = { - Type = "forking"; - ExecStart = "${startScript}"; - ExecStop = "${stopScript}"; - Environment = "PATH=${makeBinPath [ pkgs.coreutils pkgs.util-linux pkgs.gnugrep pkgs.bindfs ]}:/run/wrappers/bin"; - }; - }; - }; - - mkBindMountServicesForPath = persistentStorageName: - listToAttrs (map - (mkBindMountService persistentStorageName) - (map (v: v.directory) (filter (v: v.method == "bindfs") cfg.${persistentStorageName}.directories)) - ); - in - builtins.foldl' - recursiveUpdate - { } - (map mkBindMountServicesForPath persistentStorageNames); - - home.activation = - let - dag = config.lib.dag; - mount = "${pkgs.util-linux}/bin/mount"; - - # 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 = persistentStorageName: dir: - let - mountDir = - if cfg.${persistentStorageName}.removePrefixDirectory then - dirListToPath (tail (splitPath [ dir ])) - else - dir; - targetDir = escapeShellArg (concatPaths [ cfg.${persistentStorageName}.persistentStoragePath dir ]); - mountPoint = escapeShellArg (concatPaths [ config.home.homeDirectory mountDir ]); - bindfsOptions = concatStringsSep "," ( - optional (!cfg.${persistentStorageName}.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 = persistentStorageName: - concatMapStrings - (mkBindMount persistentStorageName) - (map (v: v.directory) (filter (v: v.method == "bindfs") cfg.${persistentStorageName}.directories)); - - mkUnmount = persistentStorageName: dir: - let - mountDir = - if cfg.${persistentStorageName}.removePrefixDirectory then - dirListToPath (tail (splitPath [ dir ])) - else - dir; - mountPoint = escapeShellArg (concatPaths [ config.home.homeDirectory mountDir ]); - in - '' - if [[ -n ''${mountedPaths[${mountPoint}]+x} ]]; then - ${unmountScript mountPoint 3 1} - fi - ''; - - mkUnmountsForPath = persistentStorageName: - concatMapStrings - (mkUnmount persistentStorageName) - (map (v: v.directory) (filter (v: v.method == "bindfs") cfg.${persistentStorageName}.directories)); - - mkLinkCleanup = persistentStorageName: dir: - let - mountDir = - if cfg.${persistentStorageName}.removePrefixDirectory then - dirListToPath (tail (splitPath [ dir ])) - else - dir; - mountPoint = escapeShellArg (concatPaths [ config.home.homeDirectory mountDir ]); - in - '' - # Unmount if it's mounted. Ensures smooth transition: bindfs -> symlink - ${unmountScript mountPoint 3 1} - - # If it is a directory and it's empty - if [ -d ${mountPoint} ] && [ -z "$(ls -A ${mountPoint})" ]; then - echo "Removing empty directory ${mountPoint}" - rm -d ${mountPoint} - fi - ''; - - mkLinkCleanupForPath = persistentStorageName: - concatMapStrings - (mkLinkCleanup persistentStorageName) - (map (v: v.directory) (filter (v: v.method == "symlink") cfg.${persistentStorageName}.directories)); - - - in - mkMerge [ - (mkIf (any (path: (filter (v: v.method == "symlink") cfg.${path}.directories) != [ ]) persistentStorageNames) { - # Clean up existing empty directories in the way of links - cleanEmptyLinkTargets = - dag.entryBefore - [ "checkLinkTargets" ] - '' - ${concatMapStrings mkLinkCleanupForPath persistentStorageNames} - ''; - }) - (mkIf (any (path: (filter (v: v.method == "bindfs") cfg.${path}.directories) != [ ]) persistentStorageNames) { - createAndMountPersistentStoragePaths = - dag.entryBefore - [ "writeBoundary" ] - '' - declare -A mountedPaths - ${(concatMapStrings mkBindMountsForPath persistentStorageNames)} - ''; - - unmountPersistentStoragePaths = - dag.entryBefore - [ "createAndMountPersistentStoragePaths" ] - '' - PATH=$PATH:/run/wrappers/bin - unmountBindMounts() { - ${concatMapStrings mkUnmountsForPath persistentStorageNames} - } - - # Run the unmount function on error to clean up stray - # bind mounts - trap "unmountBindMounts" ERR - ''; - - runUnmountPersistentStoragePaths = - dag.entryBefore - [ reloadSystemd ] - '' - unmountBindMounts - ''; - }) - (mkIf (any (path: (cfg.${path}.files != [ ]) || ((filter (v: v.method == "symlink") cfg.${path}.directories) != [ ])) persistentStorageNames) { - createTargetFileDirectories = - dag.entryBefore - [ "writeBoundary" ] - (concatMapStrings - (persistentStorageName: - concatMapStrings - (targetFilePath: '' - mkdir -p ${escapeShellArg (concatPaths [ cfg.${persistentStorageName}.persistentStoragePath (dirOf targetFilePath) ])} - '') - (cfg.${persistentStorageName}.files ++ (map (v: v.directory) (filter (v: v.method == "symlink") cfg.${persistentStorageName}.directories)))) - persistentStorageNames); - }) - ]; + assertions = [ + { + assertion = config.home._nixosModuleImported; + message = '' + home.persistence: Module was imported manually! + + The Home Manager persistence module should not be imported + manually. It will be imported by the NixOS module + automatically. See + https://github.com/nix-community/impermanence?tab=readme-ov-file#home-manager + for instructions and examples. + ''; + } + { + assertion = !(any (hasInfix home.homeDirectory) persistentStoragePaths); + message = '' + home.persistence: persistentStoragePath contains home directory path! + + The API has changed - the persistent storage path should no longer + contain the path to the user's home directory, as it will be added + automatically. + ''; + } + ]; }; - } diff --git a/mount-file.bash b/mount-file.bash index 4d557ad..299c8d5 100755 --- a/mount-file.bash +++ b/mount-file.bash @@ -10,14 +10,15 @@ shopt -s inherit_errexit # Inherit the errexit option status in subshells. trap 'echo Error when executing ${BASH_COMMAND} at line ${LINENO}! >&2' ERR # Get inputs from command line arguments -if [[ $# != 3 ]]; then - echo "Error: 'mount-file.bash' requires *three* args." >&2 +if [[ $# != 4 ]]; then + echo "Error: 'mount-file.bash' requires *four* args." >&2 exit 1 fi mountPoint="$1" targetFile="$2" -debug="$3" +method="$3" +debug="$4" trace() { if (( debug )); then @@ -35,10 +36,10 @@ elif findmnt "$mountPoint" >/dev/null; then elif [[ -s $mountPoint ]]; then echo "A file already exists at $mountPoint!" >&2 exit 1 -elif [[ -e $targetFile ]]; then +elif [[ $method == "auto" && -e $targetFile ]]; then touch "$mountPoint" mount -o bind "$targetFile" "$mountPoint" -elif [[ $mountPoint == "/etc/machine-id" ]]; then +elif [[ $method == "auto" && $mountPoint == "/etc/machine-id" ]]; then # Work around an issue with persisting /etc/machine-id. For more # details, see https://github.com/nix-community/impermanence/pull/242 echo "Creating initial /etc/machine-id" diff --git a/nixos.nix b/nixos.nix index 347da3b..5f5beb6 100644 --- a/nixos.nix +++ b/nixos.nix @@ -1,23 +1,20 @@ -{ pkgs, config, lib, utils, ... }: +{ pkgs, config, options, lib, utils, ... }: let inherit (lib) attrNames attrValues + mapAttrsToList zipAttrsWith flatten mkAfter mkOption - mkDefault mkIf mkMerge - mapAttrsToList types foldl' unique - concatMap concatMapStrings - listToAttrs escapeShellArg escapeShellArgs recursiveUpdate @@ -25,434 +22,169 @@ let filter filterAttrs concatStringsSep - concatMapStringsSep catAttrs - optional + optionals optionalString literalExpression elem - mapAttrs intersectLists any id - head + ; + + inherit (types) + attrsOf + submodule + ; + + inherit (lib.modules) + importApply ; inherit (utils) escapeSystemdPath - fsNeededForBoot + pathsNeededForBoot ; inherit (pkgs.callPackage ./lib.nix { }) - splitPath concatPaths parentsOf duplicates ; + inherit (config.users) users; + cfg = config.environment.persistence; - users = config.users.users; - allPersistentStoragePaths = zipAttrsWith (_name: flatten) (filter (v: v.enable) (attrValues cfg)); + + # All persistent storage path submodule values zipped together into + # one set. This includes paths from the Home Manager persistence + # module and `users` submodules. + allPersistentStoragePaths = + let + # All enabled system paths + nixos = filter (v: v.enable) (attrValues cfg); + + # Get the files and directories from the `users` submodules of + # enabled system paths + nixosUsers = flatten (map attrValues (catAttrs "users" nixos)); + + # Fetch enabled paths from all Home Manager users who have the + # persistence module loaded + homeManager = + let + paths = flatten + (mapAttrsToList + (_name: value: + attrValues (value.home.persistence or { })) + config.home-manager.users or { }); + in + filter (v: v.enable) paths; + in + zipAttrsWith (_: flatten) (nixos ++ nixosUsers ++ homeManager); + inherit (allPersistentStoragePaths) files directories; - mountFile = pkgs.runCommand "impermanence-mount-file" { buildInputs = [ pkgs.bash ]; } '' + + mountFile = pkgs.runCommand "persistence-mount-file" { buildInputs = [ pkgs.bash ]; } '' cp ${./mount-file.bash} $out patchShebangs $out ''; + mkPersistFile = { filePath, persistentStoragePath, method, enableDebugging, ... }: + let + mountPoint = filePath; + targetFile = concatPaths [ persistentStoragePath filePath ]; + args = escapeShellArgs [ + mountPoint + targetFile + method + enableDebugging + ]; + in + '' + ${mountFile} ${args} + ''; + defaultPerms = { mode = "0755"; user = "root"; group = "root"; }; - - # Create fileSystems bind mount entry. - mkBindMountNameValuePair = { dirPath, persistentStoragePath, hideMount, ... }: { - name = concatPaths [ "/" dirPath ]; - value = { - device = concatPaths [ persistentStoragePath dirPath ]; - noCheck = true; - options = [ "bind" "X-fstrim.notrim" ] - ++ optional hideMount "x-gvfs-hide"; - depends = [ persistentStoragePath ]; - }; - }; - - # Create all fileSystems bind mount entries for a specific - # persistent storage path. - bindMounts = listToAttrs (map mkBindMountNameValuePair directories); in { options = { - environment.persistence = mkOption { default = { }; type = - let - inherit (types) - attrsOf - bool - listOf - submodule - nullOr - path - str - coercedTo - ; - in attrsOf ( - submodule ( - { name, config, ... }: - let - commonOpts = { - options = { - persistentStoragePath = mkOption { - type = path; - default = config.persistentStoragePath; - defaultText = "environment.persistence.‹name›.persistentStoragePath"; - description = '' - The path to persistent storage where the real - file or directory should be stored. - ''; - }; - home = mkOption { - type = nullOr path; - default = null; - internal = true; - description = '' - The path to the home directory the file is - placed within. - ''; - }; - enableDebugging = mkOption { - type = bool; - default = config.enableDebugging; - defaultText = "environment.persistence.‹name›.enableDebugging"; - internal = true; - description = '' - Enable debug trace output when running - scripts. You only need to enable this if asked - to. - ''; - }; - }; - }; - dirPermsOpts = { - user = mkOption { - type = str; - description = '' - If the directory doesn't exist in persistent - storage it will be created and owned by the user - specified by this option. - ''; - }; - group = mkOption { - type = str; - description = '' - If the directory doesn't exist in persistent - storage it will be created and owned by the - group specified by this option. - ''; - }; - mode = mkOption { - type = str; - example = "0700"; - description = '' - If the directory doesn't exist in persistent - storage it will be created with the mode - specified by this option. - ''; - }; - }; - fileOpts = { - options = { - file = mkOption { - type = str; - description = '' - The path to the file. - ''; - }; - parentDirectory = - commonOpts.options // - mapAttrs - (_: x: - if x._type or null == "option" then - x // { internal = true; } - else - x) - dirOpts.options; - filePath = mkOption { - type = path; - internal = true; - }; - }; - }; - dirOpts = { + submodule [ + ({ name, config, ... }: + (importApply ./submodule-options.nix { + inherit pkgs lib name config; + user = "root"; + group = "root"; + homeDir = null; + })) + ({ name, config, ... }: + { options = { - directory = mkOption { - type = str; - description = '' - The path to the directory. - ''; - }; - hideMount = mkOption { - type = bool; - default = config.hideMounts; - defaultText = "environment.persistence.‹name›.hideMounts"; - example = true; - description = '' - Whether to hide bind mounts from showing up as - mounted drives. - ''; - }; - # Save the default permissions at the level the - # directory resides. This used when creating its - # parent directories, giving them reasonable - # default permissions unaffected by the - # directory's own. - defaultPerms = mapAttrs (_: x: x // { internal = true; }) dirPermsOpts; - dirPath = mkOption { - type = path; - internal = true; - }; - } // dirPermsOpts; - }; - rootFile = submodule [ - commonOpts - fileOpts - ({ config, ... }: { - parentDirectory = mkDefault (defaultPerms // rec { - directory = dirOf config.file; - dirPath = directory; - inherit (config) persistentStoragePath; - inherit defaultPerms; - }); - filePath = mkDefault config.file; - }) - ]; - rootDir = submodule ([ - commonOpts - dirOpts - ({ config, ... }: { - defaultPerms = mkDefault defaultPerms; - dirPath = mkDefault config.directory; - }) - ] ++ (mapAttrsToList (n: v: { ${n} = mkDefault v; }) defaultPerms)); - in - { - options = - { - enable = mkOption { - type = bool; - default = true; - description = "Whether to enable this persistent storage location."; - }; - - persistentStoragePath = mkOption { - type = path; - default = name; - defaultText = "‹name›"; - description = '' - The path to persistent storage where the real - files and directories should be stored. - ''; - }; - - users = mkOption { - type = attrsOf ( - submodule ( - { name, config, ... }: - let - userDefaultPerms = { - inherit (defaultPerms) mode; + users = + let + outerName = name; + outerConfig = config; + in + mkOption { + type = attrsOf ( + submodule ( + { name, config, ... }: + importApply ./submodule-options.nix { + inherit pkgs lib; + config = outerConfig // config; + name = outerName; + usersOpts = true; user = name; - group = users.${userDefaultPerms.user}.group; - }; - fileConfig = - { config, ... }: - { - parentDirectory = rec { - directory = dirOf config.file; - dirPath = concatPaths [ config.home directory ]; - inherit (config) persistentStoragePath home; - defaultPerms = userDefaultPerms; - }; - filePath = concatPaths [ config.home config.file ]; - }; - userFile = submodule [ - commonOpts - fileOpts - { inherit (config) home; } - { - parentDirectory = mkDefault userDefaultPerms; - } - fileConfig - ]; - dirConfig = - { config, ... }: - { - defaultPerms = mkDefault userDefaultPerms; - dirPath = concatPaths [ config.home config.directory ]; - }; - userDir = submodule ([ - commonOpts - dirOpts - { inherit (config) home; } - dirConfig - ] ++ (mapAttrsToList (n: v: { ${n} = mkDefault v; }) userDefaultPerms)); - in + group = users.${name}.group; + homeDir = users.${name}.home; + } + ) + ); + default = { }; + description = '' + A set of user submodules listing the files and + directories to link to their respective user's + home directories. + + Each attribute name should be the name of the + user. + + For detailed usage, check the documentation. + ''; + example = literalExpression '' { - options = - { - # Needed because defining fileSystems - # based on values from users.users - # results in infinite recursion. - home = mkOption { - type = path; - default = "/home/${userDefaultPerms.user}"; - defaultText = "/home/"; - description = '' - The user's home directory. Only - useful for users with a custom home - directory path. - - Cannot currently be automatically - deduced due to a limitation in - nixpkgs. - ''; - }; - - files = mkOption { - type = listOf (coercedTo str (f: { file = f; }) userFile); - default = [ ]; - example = [ - ".screenrc" - ]; - description = '' - Files that should be stored in - persistent storage. - ''; - }; - - directories = mkOption { - type = listOf (coercedTo str (d: { directory = d; }) userDir); - default = [ ]; - example = [ - "Downloads" - "Music" - "Pictures" - "Documents" - "Videos" - ]; - description = '' - Directories to bind mount to - persistent storage. - ''; - }; - }; + talyz = { + directories = [ + "Downloads" + "Music" + "Pictures" + "Documents" + "Videos" + "VirtualBox VMs" + { directory = ".gnupg"; mode = "0700"; } + { directory = ".ssh"; mode = "0700"; } + { directory = ".nixops"; mode = "0700"; } + { directory = ".local/share/keyrings"; mode = "0700"; } + ".local/share/direnv" + ]; + files = [ + ".screenrc" + ]; + }; } - ) - ); - default = { }; - description = '' - A set of user submodules listing the files and - directories to link to their respective user's - home directories. - - Each attribute name should be the name of the - user. - - For detailed usage, check the documentation. - ''; - example = literalExpression '' - { - talyz = { - directories = [ - "Downloads" - "Music" - "Pictures" - "Documents" - "Videos" - "VirtualBox VMs" - { directory = ".gnupg"; mode = "0700"; } - { directory = ".ssh"; mode = "0700"; } - { directory = ".nixops"; mode = "0700"; } - { directory = ".local/share/keyrings"; mode = "0700"; } - ".local/share/direnv" - ]; - files = [ - ".screenrc" - ]; - }; - } - ''; - }; - - files = mkOption { - type = listOf (coercedTo str (f: { file = f; }) rootFile); - default = [ ]; - example = [ - "/etc/machine-id" - "/etc/nix/id_rsa" - ]; - description = '' - Files that should be stored in persistent storage. - ''; - }; - - directories = mkOption { - type = listOf (coercedTo str (d: { directory = d; }) rootDir); - default = [ ]; - example = [ - "/var/log" - "/var/lib/bluetooth" - "/var/lib/nixos" - "/var/lib/systemd/coredump" - "/etc/NetworkManager/system-connections" - ]; - description = '' - Directories to bind mount to persistent storage. - ''; - }; - - hideMounts = mkOption { - type = bool; - default = false; - example = true; - description = '' - Whether to hide bind mounts from showing up as mounted drives. - ''; - }; - - enableDebugging = mkOption { - type = bool; - default = false; - internal = true; - description = '' - Enable debug trace output when running - scripts. You only need to enable this if asked - to. - ''; - }; - - enableWarnings = mkOption { - type = bool; - default = true; - description = '' - Enable non-critical warnings. - ''; - }; - }; - config = - let - allUsers = zipAttrsWith (_name: flatten) (attrValues config.users); - in - { - files = allUsers.files or [ ]; - directories = allUsers.directories or [ ]; + ''; + }; }; - } - ) + }) + ] ); description = '' A set of persistent storage location submodules listing the @@ -485,454 +217,394 @@ in } ''; }; - - # Forward declare a dummy option for VM filesystems since the real one won't exist - # unless the VM module is actually imported. - virtualisation.fileSystems = mkOption { }; }; - config = mkIf (allPersistentStoragePaths != { }) - (mkMerge [ - { - systemd.services = - let - mkPersistFileService = { filePath, persistentStoragePath, enableDebugging, ... }: + config = + mkMerge [ + (lib.optionalAttrs (options ? home-manager.sharedModules) { + home-manager.sharedModules = [ + ./home-manager.nix + { + home._nixosModuleImported = true; + } + ]; + }) + (mkIf (allPersistentStoragePaths != { }) + (mkMerge [ + { + systemd.services = + let + mkPersistFileService = { filePath, persistentStoragePath, ... }@args: + let + targetFile = concatPaths [ persistentStoragePath filePath ]; + mountPoint = escapeShellArg filePath; + in + { + "persist-${escapeSystemdPath targetFile}" = { + description = "Bind mount or link ${targetFile} to ${mountPoint}"; + wantedBy = [ "local-fs.target" ]; + before = [ "local-fs.target" ]; + path = [ pkgs.util-linux ]; + unitConfig.DefaultDependencies = false; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = mkPersistFile args; + ExecStop = pkgs.writeShellScript "unbindOrUnlink-${escapeSystemdPath targetFile}" '' + set -eu + if [[ -L ${mountPoint} ]]; then + rm ${mountPoint} + else + umount ${mountPoint} + rm ${mountPoint} + fi + ''; + }; + }; + }; + in + foldl' recursiveUpdate { } (map mkPersistFileService files); + + boot.initrd.systemd.mounts = let - targetFile = escapeShellArg (concatPaths [ persistentStoragePath filePath ]); - mountPoint = escapeShellArg filePath; + mkBindMount = { dirPath, persistentStoragePath, hideMount, ... }: { + wantedBy = [ "initrd.target" ]; + before = [ "initrd-nixos-activation.service" ]; + where = concatPaths [ "/sysroot" dirPath ]; + what = concatPaths [ "/sysroot" persistentStoragePath dirPath ]; + unitConfig.DefaultDependencies = false; + type = "none"; + options = concatStringsSep "," ([ + "bind" + ] ++ optionals hideMount [ + "x-gvfs-hide" + ]); + }; + dirs = filter (d: elem d.dirPath pathsNeededForBoot) directories; in - { - "persist-${escapeSystemdPath targetFile}" = { - description = "Bind mount or link ${targetFile} to ${mountPoint}"; + map mkBindMount dirs; + + systemd.mounts = + let + mkBindMount = { dirPath, persistentStoragePath, hideMount, ... }: { wantedBy = [ "local-fs.target" ]; before = [ "local-fs.target" ]; - path = [ pkgs.util-linux ]; + where = concatPaths [ "/" dirPath ]; + what = concatPaths [ persistentStoragePath dirPath ]; unitConfig.DefaultDependencies = false; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - ExecStart = "${mountFile} ${mountPoint} ${targetFile} ${escapeShellArg enableDebugging}"; - ExecStop = pkgs.writeShellScript "unbindOrUnlink-${escapeSystemdPath targetFile}" '' - set -eu - if [[ -L ${mountPoint} ]]; then - rm ${mountPoint} - else - umount ${mountPoint} - rm ${mountPoint} - fi - ''; - }; + type = "none"; + options = concatStringsSep "," ([ + "bind" + ] ++ optionals hideMount [ + "x-gvfs-hide" + ]); }; - }; - in - foldl' recursiveUpdate { } (map mkPersistFileService files); - - fileSystems = mkIf (directories != [ ]) bindMounts; - # So the mounts still make it into a VM built from `system.build.vm` - virtualisation.fileSystems = mkIf (directories != [ ]) bindMounts; - - system.activationScripts = - let - # Script to create directories in persistent and ephemeral - # storage. The directory structure's mode and ownership mirror - # those of persistentStoragePath/dir. - createDirectories = pkgs.runCommand "impermanence-create-directories" { buildInputs = [ pkgs.bash ]; } '' - cp ${./create-directories.bash} $out - patchShebangs $out - ''; - - mkDirWithPerms = - { dirPath - , persistentStoragePath - , user - , group - , mode - , enableDebugging - , ... - }: - let - args = [ - persistentStoragePath - dirPath - user - group - mode - enableDebugging - ]; in - '' - ${createDirectories} ${escapeShellArgs args} - ''; + map mkBindMount directories; - # Build an activation script which creates all persistent - # storage directories we want to bind mount. - dirCreationScript = + system.activationScripts = let - # The parent directories of files. - fileDirs = unique (catAttrs "parentDirectory" files); - - # All the directories actually listed by the user and the - # parent directories of listed files. - explicitDirs = directories ++ fileDirs; - - # Home directories have to be handled specially, since - # they're at the permissions boundary where they - # themselves should be owned by the user and have stricter - # permissions than regular directories, whereas its parent - # should be owned by root and have regular permissions. - # - # This simply collects all the home directories and sets - # the appropriate permissions and ownership. - homeDirs = - foldl' - (state: dir: - let - homeDir = { - directory = dir.home; - dirPath = dir.home; - home = null; - mode = "0700"; - user = dir.user; - group = users.${dir.user}.group; - inherit defaultPerms; - inherit (dir) persistentStoragePath enableDebugging; - }; - in - if dir.home != null then - if !(elem homeDir state) then - state ++ [ homeDir ] - else - state - else - state - ) - [ ] - explicitDirs; - - # Persistent storage directories. These need to be created - # unless they're at the root of a filesystem. - persistentStorageDirs = - foldl' - (state: dir: + # Script to create directories in persistent and ephemeral + # storage. The directory structure's mode and ownership mirror + # those of persistentStoragePath/dir. + createDirectories = pkgs.runCommand "persistence-create-directories" { buildInputs = [ pkgs.bash ]; } '' + cp ${./create-directories.bash} $out + patchShebangs $out + ''; + + mkDirWithPerms = + { dirPath + , persistentStoragePath + , user + , group + , mode + , enableDebugging + , ... + }: + let + args = [ + persistentStoragePath + dirPath + user + # Home Manager doesn't seem to know about the user's group + (if group == null then users.${user}.group else group) + mode + enableDebugging + ]; + in + '' + ${createDirectories} ${escapeShellArgs args} + ''; + + # Build an activation script which creates all persistent + # storage directories we want to bind mount. + dirCreationScript = + let + # The parent directories of files. + fileDirs = unique (catAttrs "parentDirectory" files); + + # All the directories actually listed by the user and the + # parent directories of listed files. + explicitDirs = directories ++ fileDirs; + + # Home directories have to be handled specially, since + # they're at the permissions boundary where they + # themselves should be owned by the user and have stricter + # permissions than regular directories, whereas its parent + # should be owned by root and have regular permissions. + # + # This simply collects all the home directories and sets + # the appropriate permissions and ownership. + homeDirs = + foldl' + (state: dir: + let + homeDir = { + directory = dir.home; + dirPath = dir.home; + home = null; + mode = "0700"; + user = dir.user; + group = users.${dir.user}.group; + inherit defaultPerms; + inherit (dir) persistentStoragePath enableDebugging; + }; + in + if dir.home != null then + if !(elem homeDir state) then + state ++ [ homeDir ] + else + state + else + state + ) + [ ] + explicitDirs; + + # Persistent storage directories. These need to be created + # unless they're at the root of a filesystem. + persistentStorageDirs = + foldl' + (state: dir: + let + persistentStorageDir = { + directory = dir.persistentStoragePath; + dirPath = dir.persistentStoragePath; + persistentStoragePath = ""; + home = null; + inherit (dir) defaultPerms enableDebugging; + inherit (dir.defaultPerms) user group mode; + }; + in + if dir.home == null && !(elem persistentStorageDir state) then + state ++ [ persistentStorageDir ] + else + state + ) + [ ] + (explicitDirs ++ homeDirs); + + # Generate entries for all parent directories of the + # argument directories, listed in the order they need to + # be created. The parent directories are assigned default + # permissions. + mkParentDirs = dirs: let - persistentStorageDir = { - directory = dir.persistentStoragePath; - dirPath = dir.persistentStoragePath; - persistentStoragePath = ""; - home = null; - inherit (dir) defaultPerms enableDebugging; + # Create a new directory item from `dir`, the child + # directory item to inherit properties from and + # `path`, the parent directory path. + mkParent = dir: path: { + directory = path; + dirPath = + if dir.home != null then + concatPaths [ dir.home path ] + else + path; + inherit (dir) persistentStoragePath home enableDebugging; inherit (dir.defaultPerms) user group mode; }; + # Create new directory items for all parent + # directories of a directory. + mkParents = dir: + map (mkParent dir) (parentsOf dir.directory); in - if dir.home == null && !(elem persistentStorageDir state) then - state ++ [ persistentStorageDir ] - else - state - ) - [ ] - (explicitDirs ++ homeDirs); - - # Generate entries for all parent directories of the - # argument directories, listed in the order they need to - # be created. The parent directories are assigned default - # permissions. - mkParentDirs = dirs: - let - # Create a new directory item from `dir`, the child - # directory item to inherit properties from and - # `path`, the parent directory path. - mkParent = dir: path: { - directory = path; - dirPath = - if dir.home != null then - concatPaths [ dir.home path ] - else - path; - inherit (dir) persistentStoragePath home enableDebugging; - inherit (dir.defaultPerms) user group mode; - }; - # Create new directory items for all parent - # directories of a directory. - mkParents = dir: - map (mkParent dir) (parentsOf dir.directory); + unique (flatten (map mkParents dirs)); + + persistentStorageDirParents = mkParentDirs persistentStorageDirs; + + # Parent directories of home folders. This is usually only + # /home, unless the user's home is in a non-standard + # location. + homeDirParents = mkParentDirs homeDirs; + + # Parent directories of all explicitly listed directories. + parentDirs = mkParentDirs explicitDirs; + + # All directories in the order they should be created. + allDirs = + persistentStorageDirParents + ++ persistentStorageDirs + ++ homeDirParents + ++ homeDirs + ++ parentDirs + ++ explicitDirs; in - unique (flatten (map mkParents dirs)); - - persistentStorageDirParents = mkParentDirs persistentStorageDirs; - - # Parent directories of home folders. This is usually only - # /home, unless the user's home is in a non-standard - # location. - homeDirParents = mkParentDirs homeDirs; - - # Parent directories of all explicitly listed directories. - parentDirs = mkParentDirs explicitDirs; - - # All directories in the order they should be created. - allDirs = - persistentStorageDirParents - ++ persistentStorageDirs - ++ homeDirParents - ++ homeDirs - ++ parentDirs - ++ explicitDirs; + pkgs.writeShellScript "persistence-run-create-directories" '' + _status=0 + trap "_status=1" ERR + ${concatMapStrings mkDirWithPerms allDirs} + exit $_status + ''; + + persistFileScript = + pkgs.writeShellScript "persistence-persist-files" '' + _status=0 + trap "_status=1" ERR + ${concatMapStrings mkPersistFile files} + exit $_status + ''; in - pkgs.writeShellScript "impermanence-run-create-directories" '' - _status=0 - trap "_status=1" ERR - ${concatMapStrings mkDirWithPerms allDirs} - exit $_status - ''; - - mkPersistFile = { filePath, persistentStoragePath, enableDebugging, ... }: + { + "createPersistentStorageDirs" = { + deps = [ "users" "groups" ]; + text = "${dirCreationScript}"; + }; + "persist-files" = { + deps = [ "createPersistentStorageDirs" ]; + text = "${persistFileScript}"; + }; + }; + + boot.initrd.postMountCommands = let - mountPoint = filePath; - targetFile = concatPaths [ persistentStoragePath filePath ]; - args = escapeShellArgs [ - mountPoint - targetFile - enableDebugging - ]; + neededForBootDirs = filter (dir: elem dir.dirPath pathsNeededForBoot) directories; + mkBindMount = { persistentStoragePath, dirPath, ... }: + let + target = concatPaths [ "/mnt-root" persistentStoragePath dirPath ]; + in + '' + mkdir -p ${escapeShellArg target} + mountFS ${escapeShellArgs [ target dirPath ]} bind none + ''; in - '' - ${mountFile} ${args} - ''; - - persistFileScript = - pkgs.writeShellScript "impermanence-persist-files" '' - _status=0 - trap "_status=1" ERR - ${concatMapStrings mkPersistFile files} - exit $_status - ''; - in + mkIf (!config.boot.initrd.systemd.enable) + (mkAfter (concatMapStrings mkBindMount neededForBootDirs)); + } + + # Work around an issue with persisting /etc/machine-id where the + # systemd-machine-id-commit.service unit fails if the final + # /etc/machine-id is bind mounted from persistent storage. For + # more details, see + # https://github.com/nix-community/impermanence/issues/229 and + # https://github.com/nix-community/impermanence/pull/242 + (mkIf (any (f: f == "/etc/machine-id") (catAttrs "filePath" files)) { + boot.initrd.systemd.suppressedUnits = [ "systemd-machine-id-commit.service" ]; + systemd.services.systemd-machine-id-commit.unitConfig.ConditionFirstBoot = true; + }) + + # Assertions and warnings { - "createPersistentStorageDirs" = { - deps = [ "users" "groups" ]; - text = "${dirCreationScript}"; - }; - "persist-files" = { - deps = [ "createPersistentStorageDirs" ]; - text = "${persistFileScript}"; - }; - }; - - # Create the mountpoints of directories marked as needed for boot - # which are also persisted. For this to work, it has to run at - # early boot, before NixOS' filesystem mounting runs. Without - # this, initial boot fails when for example /var/lib/nixos is - # persisted but not created in persistent storage. - boot.initrd = - let - neededForBootFs = catAttrs "mountPoint" (filter fsNeededForBoot (attrValues config.fileSystems)); - neededForBootDirs = filter (dir: elem dir.dirPath neededForBootFs) directories; - getDevice = fs: - if fs.device != null then - fs.device - else if fs.label != null then - "/dev/disk/by-label/${fs.label}" - else - "none"; - mkMount = fs: + assertions = let - mountPoint = concatPaths [ "/persist-tmp-mnt" fs.mountPoint ]; - device = getDevice fs; - options = filter (o: (builtins.match "(x-.*\.mount)" o) == null) fs.options; - optionsFlag = optionalString (options != [ ]) ("-o " + escapeShellArg (concatStringsSep "," options)); + markedNeededForBoot = cond: fs: + if config.fileSystems ? ${fs} then + config.fileSystems.${fs}.neededForBoot == cond + else + cond; + + persistentStoragePaths = unique (catAttrs "persistentStoragePath" (files ++ directories)); + + submoduleAssertions = flatten allPersistentStoragePaths.assertions; + + fileAssertions = flatten (catAttrs "assertions" files); + + directoryAssertions = flatten (catAttrs "assertions" directories); in - '' - mkdir -p ${escapeShellArg mountPoint} - mount -t ${escapeShellArgs [ fs.fsType device mountPoint ]} ${optionsFlag} - ''; - mkDir = { persistentStoragePath, dirPath, ... }: '' - mkdir -p ${escapeShellArg (concatPaths [ "/persist-tmp-mnt" persistentStoragePath dirPath ])} - ''; - mkUnmount = fs: '' - umount ${escapeShellArg (concatPaths [ "/persist-tmp-mnt" fs.mountPoint ])} - ''; - fileSystems = + submoduleAssertions + ++ fileAssertions + ++ directoryAssertions + ++ [ + { + # Assert that all persistent storage volumes we use are + # marked with neededForBoot. + assertion = all (markedNeededForBoot true) persistentStoragePaths; + message = + let + offenders = filter (markedNeededForBoot false) persistentStoragePaths; + in + '' + environment.persistence: + All filesystems used for persistent storage must + have the flag neededForBoot set to true. + + Please fix or remove the following paths: + ${concatStringsSep "\n " offenders} + ''; + } + { + assertion = duplicates (catAttrs "filePath" files) == [ ]; + message = + let + offenders = duplicates (catAttrs "filePath" files); + in + '' + environment.persistence: + The following files were specified two or more + times: + ${concatStringsSep "\n " offenders} + ''; + } + { + assertion = duplicates (catAttrs "dirPath" directories) == [ ]; + message = + let + offenders = duplicates (catAttrs "dirPath" directories); + in + '' + environment.persistence: + The following directories were specified two or more + times: + ${concatStringsSep "\n " offenders} + ''; + } + ]; + + warnings = let - persistentStoragePaths = unique (catAttrs "persistentStoragePath" directories); - all = config.fileSystems // config.virtualisation.fileSystems; - matchFileSystems = fs: attrValues (filterAttrs (_: v: v.mountPoint or null == fs) all); - in - concatMap matchFileSystems persistentStoragePaths; - deviceUnits = unique - (concatMap - (fs: - # If the device path starts with “dev” or “sys”, - # it's a real device and should have an associated - # .device unit. If not, it's probably either a - # temporary file system lacking a backing device, a - # ZFS pool or a bind mount. + usersWithoutUid = attrNames (filterAttrs (n: u: u.uid == null) config.users.users); + groupsWithoutGid = attrNames (filterAttrs (n: g: g.gid == null) config.users.groups); + varLibNixosPersistent = let - device = getDevice fs; + varDirs = parentsOf "/var/lib/nixos" ++ [ "/var/lib/nixos" ]; + persistedDirs = catAttrs "dirPath" directories; + mountedDirs = catAttrs "mountPoint" (attrValues config.fileSystems); + persistedVarDirs = intersectLists varDirs persistedDirs; + mountedVarDirs = intersectLists varDirs mountedDirs; in - if elem (head (splitPath [ device ])) [ "dev" "sys" ] then - [ "${escapeSystemdPath device}.device" ] - else if device == "none" || device == fs.fsType then - [ ] - else if fs.fsType == "zfs" then - [ "zfs-import.target" ] - else - [ "${escapeSystemdPath device}.mount" ]) - fileSystems); - createNeededForBootDirs = '' - ${concatMapStrings mkMount fileSystems} - ${concatMapStrings mkDir neededForBootDirs} - ${concatMapStrings mkUnmount fileSystems} - ''; - in - { - systemd.services = mkIf config.boot.initrd.systemd.enable { - create-needed-for-boot-dirs = { - wantedBy = [ "initrd-root-device.target" ]; - requires = deviceUnits; - after = deviceUnits; - before = [ "sysroot.mount" ]; - serviceConfig.Type = "oneshot"; - unitConfig.DefaultDependencies = false; - script = createNeededForBootDirs; - }; - }; - postResumeCommands = mkIf (!config.boot.initrd.systemd.enable) - (mkAfter createNeededForBootDirs); - }; - } - - # Work around an issue with persisting /etc/machine-id where the - # systemd-machine-id-commit.service unit fails if the final - # /etc/machine-id is bind mounted from persistent storage. For - # more details, see - # https://github.com/nix-community/impermanence/issues/229 and - # https://github.com/nix-community/impermanence/pull/242 - (mkIf (any (f: f == "/etc/machine-id") (catAttrs "filePath" files)) { - boot.initrd.systemd.suppressedUnits = [ "systemd-machine-id-commit.service" ]; - systemd.services.systemd-machine-id-commit.unitConfig.ConditionFirstBoot = true; - }) - - # Assertions and warnings - { - assertions = - let - markedNeededForBoot = cond: fs: - if config.fileSystems ? ${fs} then - config.fileSystems.${fs}.neededForBoot == cond - else - cond; - persistentStoragePaths = attrNames cfg; - usersPerPath = allPersistentStoragePaths.users; - homeDirOffenders = - filterAttrs - (n: v: (v.home != config.users.users.${n}.home)); - in - [ - { - # Assert that all persistent storage volumes we use are - # marked with neededForBoot. - assertion = all (markedNeededForBoot true) persistentStoragePaths; - message = - let - offenders = filter (markedNeededForBoot false) persistentStoragePaths; - in - '' - environment.persistence: - All filesystems used for persistent storage must - have the flag neededForBoot set to true. - - Please fix or remove the following paths: - ${concatStringsSep "\n " offenders} - ''; - } - { - assertion = all (users: (homeDirOffenders users) == { }) usersPerPath; - message = - let - offendersPerPath = filter (users: (homeDirOffenders users) != { }) usersPerPath; - offendersText = - concatMapStringsSep - "\n " - (offenders: - concatMapStringsSep - "\n " - (n: "${n}: ${offenders.${n}.home} != ${config.users.users.${n}.home}") - (attrNames offenders)) - offendersPerPath; - in - '' - environment.persistence: - Users and home doesn't match: - ${offendersText} - - You probably want to set each - environment.persistence..users..home to - match the respective user's home directory as - defined by users.users..home. - ''; - } - { - assertion = duplicates (catAttrs "filePath" files) == [ ]; - message = - let - offenders = duplicates (catAttrs "filePath" files); - in - '' - environment.persistence: - The following files were specified two or more - times: - ${concatStringsSep "\n " offenders} - ''; - } - { - assertion = duplicates (catAttrs "dirPath" directories) == [ ]; - message = - let - offenders = duplicates (catAttrs "dirPath" directories); - in - '' - environment.persistence: - The following directories were specified two or more - times: - ${concatStringsSep "\n " offenders} - ''; - } - ]; - - warnings = - let - usersWithoutUid = attrNames (filterAttrs (n: u: u.uid == null) config.users.users); - groupsWithoutGid = attrNames (filterAttrs (n: g: g.gid == null) config.users.groups); - varLibNixosPersistent = - let - varDirs = parentsOf "/var/lib/nixos" ++ [ "/var/lib/nixos" ]; - persistedDirs = catAttrs "dirPath" directories; - mountedDirs = catAttrs "mountPoint" (attrValues config.fileSystems); - persistedVarDirs = intersectLists varDirs persistedDirs; - mountedVarDirs = intersectLists varDirs mountedDirs; + persistedVarDirs != [ ] || mountedVarDirs != [ ]; in - persistedVarDirs != [ ] || mountedVarDirs != [ ]; - in - mkIf (any id allPersistentStoragePaths.enableWarnings) - (mkMerge [ - (mkIf (!varLibNixosPersistent && (usersWithoutUid != [ ] || groupsWithoutGid != [ ])) [ - '' - environment.persistence: - Neither /var/lib/nixos nor any of its parents are - persisted. This means all users/groups without - specified uids/gids will have them reassigned on - reboot. - ${optionalString (usersWithoutUid != [ ]) '' - The following users are missing a uid: - ${concatStringsSep "\n " usersWithoutUid} - ''} - ${optionalString (groupsWithoutGid != [ ]) '' - The following groups are missing a gid: - ${concatStringsSep "\n " groupsWithoutGid} - ''} - '' - ]) - ]); - } - ]); + mkIf (any id allPersistentStoragePaths.enableWarnings) + (mkMerge [ + (mkIf (!varLibNixosPersistent && (usersWithoutUid != [ ] || groupsWithoutGid != [ ])) [ + '' + environment.persistence: + Neither /var/lib/nixos nor any of its parents are + persisted. This means all users/groups without + specified uids/gids will have them reassigned on + reboot. + ${optionalString (usersWithoutUid != [ ]) '' + The following users are missing a uid: + ${concatStringsSep "\n " usersWithoutUid} + ''} + ${optionalString (groupsWithoutGid != [ ]) '' + The following groups are missing a gid: + ${concatStringsSep "\n " groupsWithoutGid} + ''} + '' + ]) + ]); + } + ])) + ]; } diff --git a/submodule-options.nix b/submodule-options.nix new file mode 100644 index 0000000..1d65f97 --- /dev/null +++ b/submodule-options.nix @@ -0,0 +1,323 @@ +{ pkgs +, lib +, name +, config +, homeDir +, usersOpts ? false # Are the options used as users. submodule options? +, user # Default user name +, group # Default user group +}: +let + inherit (lib) + mkOption + mkDefault + mkIf + mapAttrsToList + types + mapAttrs + optionals + optionalAttrs + mkRemovedOptionModule + ; + + inherit (pkgs.callPackage ./lib.nix { }) + concatPaths + ; + + inherit (types) + bool + listOf + submodule + nullOr + path + enum + str + coercedTo + unspecified + ; + + defaultPerms = { + mode = "0755"; + inherit user group; + }; + + commonOpts = { + options = { + persistentStoragePath = mkOption { + type = path; + default = config.persistentStoragePath; + defaultText = "environment.persistence.‹name›.persistentStoragePath"; + description = '' + The path to persistent storage where the real + file or directory should be stored. + ''; + }; + home = mkOption { + type = nullOr path; + default = null; + internal = true; + description = '' + The path to the home directory the file or + directory is placed within. + ''; + }; + enableDebugging = mkOption { + type = bool; + default = config.enableDebugging; + defaultText = "environment.persistence.‹name›.enableDebugging"; + internal = true; + description = '' + Enable debug trace output when running + scripts. You only need to enable this if asked + to. + ''; + }; + assertions = mkOption { + type = listOf unspecified; + internal = true; + default = [ ]; + }; + }; + }; + dirPermsOpts = { + user = mkOption { + type = str; + description = '' + If the directory doesn't exist in persistent + storage it will be created and owned by the user + specified by this option. + ''; + }; + group = mkOption { + type = nullOr str; + description = '' + If the directory doesn't exist in persistent + storage it will be created and owned by the + group specified by this option. + ''; + }; + mode = mkOption { + type = str; + example = "0700"; + description = '' + If the directory doesn't exist in persistent + storage it will be created with the mode + specified by this option. + ''; + }; + }; + fileOpts = { + options = { + file = mkOption { + type = str; + description = '' + The path to the file. + ''; + }; + parentDirectory = + commonOpts.options // + mapAttrs + (_: x: + if x._type or null == "option" then + x // { internal = true; } + else + x) + dirOpts.options; + method = mkOption { + type = enum [ "auto" "symlink" ]; + default = "auto"; + description = '' + The method used to link to the target file. + `auto' will almost always do the right thing, + thus you should only set this if the default + doesn't work. + ''; + }; + filePath = mkOption { + type = path; + internal = true; + }; + }; + }; + dirOpts = { + options = { + directory = mkOption { + type = str; + description = '' + The path to the directory. + ''; + }; + hideMount = mkOption { + type = bool; + default = config.hideMounts; + defaultText = "environment.persistence.‹name›.hideMounts"; + example = true; + description = '' + Whether to hide bind mounts from showing up as + mounted drives. + ''; + }; + # Save the default permissions at the level the + # directory resides. This used when creating its + # parent directories, giving them reasonable + # default permissions unaffected by the + # directory's own. + defaultPerms = mapAttrs (_: x: x // { internal = true; }) dirPermsOpts; + dirPath = mkOption { + type = path; + internal = true; + }; + } // dirPermsOpts; + }; + file = submodule [ + commonOpts + fileOpts + (mkIf (homeDir != null) { home = homeDir; }) + { + parentDirectory = mkDefault defaultPerms; + } + ({ config, ... }: + let + home = if config.home != null then config.home else "/"; + directory = dirOf config.file; + in + { + parentDirectory = { + dirPath = concatPaths [ home directory ]; + inherit directory defaultPerms home; + inherit (config) persistentStoragePath; + }; + filePath = concatPaths [ home config.file ]; + }) + ]; + dir = submodule ([ + commonOpts + dirOpts + { + imports = [ + (mkRemovedOptionModule + [ "method" ] + '' + ▹ persistence."${name}": + As real bind mounts are now used instead of bindfs, changing the directory linking + method is deprecated. + '') + ]; + } + (mkIf (homeDir != null) { home = homeDir; }) + ({ config, ... }: + let + home = if config.home != null then config.home else "/"; + in + { + defaultPerms = mkDefault defaultPerms; + dirPath = concatPaths [ home config.directory ]; + }) + ] ++ (mapAttrsToList (n: v: { ${n} = mkDefault v; }) defaultPerms)); + +in +{ + imports = optionals (!usersOpts) [ + (mkRemovedOptionModule + [ "allowOther" ] + '' + ▹ persistence."${name}": + As real bind mounts are now used instead of bindfs, `allowOther' is no longer needed. + '') + (mkRemovedOptionModule + [ "removePrefixDirectory" ] + '' + ▹ persistence."${name}": + The use of prefix directories is deprecated and the functionality has been removed. + If you depend on this functionality, use the `home-manager-v1' branch. + '') + (mkRemovedOptionModule + [ "defaultDirectoryMethod" ] + '' + ▹ persistence."${name}": + As real bind mounts are now used instead of bindfs, changing the default directory linking + method is deprecated. + '') + ]; + options = + { + files = mkOption { + type = listOf (coercedTo str (f: { file = f; }) file); + default = [ ]; + example = [ + "/etc/machine-id" + "/etc/nix/id_rsa" + ]; + description = '' + Files that should be stored in persistent storage. + ''; + }; + + directories = mkOption { + type = listOf (coercedTo str (d: { directory = d; }) dir); + default = [ ]; + example = [ + "/var/log" + "/var/lib/bluetooth" + "/var/lib/nixos" + "/var/lib/systemd/coredump" + "/etc/NetworkManager/system-connections" + ]; + description = '' + Directories to bind mount to persistent storage. + ''; + }; + } // + optionalAttrs (!usersOpts) + { + enable = mkOption { + type = bool; + default = true; + description = "Whether to enable this persistent storage location."; + }; + + persistentStoragePath = mkOption { + type = path; + default = name; + defaultText = "‹name›"; + description = '' + The path to persistent storage where the real + files and directories should be stored. + ''; + }; + + hideMounts = mkOption { + type = bool; + default = false; + example = true; + description = '' + Whether to hide bind mounts from showing up as mounted drives. + ''; + }; + + enableDebugging = mkOption { + type = bool; + default = false; + internal = true; + description = '' + Enable debug trace output when running + scripts. You only need to enable this if asked + to. + ''; + }; + + enableWarnings = mkOption { + type = bool; + default = true; + description = '' + Enable non-critical warnings. + ''; + }; + + assertions = mkOption { + type = listOf unspecified; + internal = true; + default = [ ]; + }; + }; +}