├── .github └── workflows │ └── ci.yml ├── LICENSE ├── README.org ├── create-directories.bash ├── flake.nix ├── home-manager.nix ├── lib.nix ├── mount-file.bash └── nixos.nix /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request, merge_group] 3 | jobs: 4 | nix_parsing: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - uses: cachix/install-nix-action@v12 9 | - name: Check Nix parsing 10 | run: | 11 | find . -name "*.nix" -exec nix-instantiate --parse --quiet {} >/dev/null + 12 | nix_formatting: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: cachix/install-nix-action@v12 17 | with: 18 | nix_path: nixpkgs=channel:nixos-unstable 19 | - name: Check Nix formatting 20 | run: | 21 | nix-shell -p nixpkgs-fmt --run "nixpkgs-fmt --check ." 22 | shell_formatting: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: cachix/install-nix-action@v12 27 | with: 28 | nix_path: nixpkgs=channel:nixos-unstable 29 | - name: Check shell script formatting 30 | run: | 31 | find . -name "*.*sh" -exec nix-shell -p shfmt --run "shfmt -i 4 -d {}" \; 32 | shell_error_checking: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v2 36 | - uses: cachix/install-nix-action@v12 37 | with: 38 | nix_path: nixpkgs=channel:nixos-unstable 39 | - name: Check for shell script errors 40 | run: | 41 | find . -name "*.*sh" -exec nix-shell -p shellcheck --run "shellcheck {}" \; 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nix Community Projects 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | #+TITLE: Impermanence 2 | 3 | Lets you choose what files and directories you want to keep between 4 | reboots - the rest are thrown away. 5 | 6 | Why would you want this? 7 | 8 | - It keeps your system clean by default. 9 | 10 | - It forces you to declare settings you want to keep. 11 | 12 | - It lets you experiment with new software without cluttering up 13 | your system. 14 | 15 | There are a few different things to set up for this to work: 16 | 17 | - A root filesystem which somehow gets wiped on reboot. There are a 18 | few ways to achieve this. See the [[#system-setup][System setup]] section for more info. 19 | 20 | - At least one mounted volume where the files and directories you 21 | want to keep are stored permanently. 22 | 23 | - At least one of the modules in this repository, which take care of 24 | linking or bind mounting files between the persistent storage 25 | mount point and the root file system. See the [[#module-usage][Module usage]] section 26 | for more info. 27 | 28 | * Contact 29 | 30 | Join the [[https://matrix.to/#/#impermanence:nixos.org][matrix room]] to chat about the project. 31 | 32 | * System setup 33 | 34 | There are many ways to wipe your root partition between boots. This 35 | section lists a few common ways to accomplish this, but is by no 36 | means an exhaustive list. 37 | 38 | *** tmpfs 39 | 40 | The easiest method is to use a tmpfs filesystem for the 41 | root. This is the easiest way to set up impermanence on systems 42 | which currently use a traditional filesystem (ext4, xfs, etc) as 43 | the root filesystem, since you don't have to repartition. 44 | 45 | All data stored in tmpfs only resides in system memory, not on 46 | disk. This automatically takes care of cleaning up between boots, 47 | but also comes with some pretty significant drawbacks: 48 | 49 | - Downloading big files or trying programs that generate large 50 | amounts of data can easily result in either an out-of-memory or 51 | disk-full scenario. 52 | 53 | - If the system crashes or loses power before you've had a chance 54 | to move files you want to keep to persistent storage, they're 55 | gone forever. 56 | 57 | Using tmpfs as the root filesystem, the filesystem setup would 58 | look something like this: 59 | 60 | #+begin_src nix 61 | { 62 | fileSystems."/" = { 63 | device = "none"; 64 | fsType = "tmpfs"; 65 | options = [ "defaults" "size=25%" "mode=755" ]; 66 | }; 67 | 68 | fileSystems."/persistent" = { 69 | device = "/dev/root_vg/root"; 70 | neededForBoot = true; 71 | fsType = "btrfs"; 72 | options = [ "subvol=persistent" ]; 73 | }; 74 | 75 | fileSystems."/nix" = { 76 | device = "/dev/root_vg/root"; 77 | fsType = "btrfs"; 78 | options = [ "subvol=nix" ]; 79 | }; 80 | 81 | fileSystems."/boot" = { 82 | device = "/dev/disk/by-uuid/XXXX-XXXX"; 83 | fsType = "vfat"; 84 | }; 85 | } 86 | #+end_src 87 | 88 | where the ~size~ option determines how much system memory is allowed 89 | to be used by the filesystem. 90 | 91 | *** BTRFS subvolumes 92 | 93 | A more advanced solution which doesn't have the same drawbacks as 94 | using tmpfs is to use a regular filesystem, but clean it up 95 | between boots. A relatively easy way to do this is to use BTRFS 96 | and create a new subvolume to use as root on boot. This also 97 | allows you to keep a number of old roots around, in case of 98 | crashes, power outages or other accidents. 99 | 100 | A setup which would automatically remove roots that are 101 | older than 30 days could look like this: 102 | 103 | #+begin_src nix 104 | { 105 | fileSystems."/" = { 106 | device = "/dev/root_vg/root"; 107 | fsType = "btrfs"; 108 | options = [ "subvol=root" ]; 109 | }; 110 | 111 | boot.initrd.postResumeCommands = lib.mkAfter '' 112 | mkdir /btrfs_tmp 113 | mount /dev/root_vg/root /btrfs_tmp 114 | if [[ -e /btrfs_tmp/root ]]; then 115 | mkdir -p /btrfs_tmp/old_roots 116 | timestamp=$(date --date="@$(stat -c %Y /btrfs_tmp/root)" "+%Y-%m-%-d_%H:%M:%S") 117 | mv /btrfs_tmp/root "/btrfs_tmp/old_roots/$timestamp" 118 | fi 119 | 120 | delete_subvolume_recursively() { 121 | IFS=$'\n' 122 | for i in $(btrfs subvolume list -o "$1" | cut -f 9- -d ' '); do 123 | delete_subvolume_recursively "/btrfs_tmp/$i" 124 | done 125 | btrfs subvolume delete "$1" 126 | } 127 | 128 | for i in $(find /btrfs_tmp/old_roots/ -maxdepth 1 -mtime +30); do 129 | delete_subvolume_recursively "$i" 130 | done 131 | 132 | btrfs subvolume create /btrfs_tmp/root 133 | umount /btrfs_tmp 134 | ''; 135 | 136 | fileSystems."/persistent" = { 137 | device = "/dev/root_vg/root"; 138 | neededForBoot = true; 139 | fsType = "btrfs"; 140 | options = [ "subvol=persistent" ]; 141 | }; 142 | 143 | fileSystems."/nix" = { 144 | device = "/dev/root_vg/root"; 145 | fsType = "btrfs"; 146 | options = [ "subvol=nix" ]; 147 | }; 148 | 149 | fileSystems."/boot" = { 150 | device = "/dev/disk/by-uuid/XXXX-XXXX"; 151 | fsType = "vfat"; 152 | }; 153 | } 154 | #+end_src 155 | 156 | This assumes the BTRFS filesystem can be found in an LVM volume 157 | group called ~root_vg~. Adjust the path as necessary. 158 | 159 | * Module usage 160 | 161 | There are currently two modules: one for ~NixOS~ and one for ~home-manager~. 162 | 163 | *** NixOS 164 | 165 | To use the module, import it into your configuration with 166 | 167 | #+begin_src nix 168 | { 169 | imports = [ /path/to/impermanence/nixos.nix ]; 170 | } 171 | #+end_src 172 | 173 | or use the provided ~nixosModules.impermanence~ flake output: 174 | 175 | #+begin_src nix 176 | { 177 | inputs = { 178 | impermanence.url = "github:nix-community/impermanence"; 179 | }; 180 | 181 | outputs = { self, nixpkgs, impermanence, ... }: 182 | { 183 | nixosConfigurations.sythe = nixpkgs.lib.nixosSystem { 184 | system = "x86_64-linux"; 185 | modules = [ 186 | impermanence.nixosModules.impermanence 187 | ./machines/sythe/configuration.nix 188 | ]; 189 | }; 190 | }; 191 | } 192 | #+end_src 193 | 194 | This adds the ~environment.persistence~ option, which is an 195 | attribute set of submodules, where the attribute name is the path 196 | to persistent storage. 197 | 198 | Usage is shown best with an example: 199 | 200 | #+begin_src nix 201 | { 202 | environment.persistence."/persistent" = { 203 | enable = true; # NB: Defaults to true, not needed 204 | hideMounts = true; 205 | directories = [ 206 | "/var/log" 207 | "/var/lib/bluetooth" 208 | "/var/lib/nixos" 209 | "/var/lib/systemd/coredump" 210 | "/etc/NetworkManager/system-connections" 211 | { directory = "/var/lib/colord"; user = "colord"; group = "colord"; mode = "u=rwx,g=rx,o="; } 212 | ]; 213 | files = [ 214 | "/etc/machine-id" 215 | { file = "/var/keys/secret_file"; parentDirectory = { mode = "u=rwx,g=,o="; }; } 216 | ]; 217 | users.talyz = { 218 | directories = [ 219 | "Downloads" 220 | "Music" 221 | "Pictures" 222 | "Documents" 223 | "Videos" 224 | "VirtualBox VMs" 225 | { directory = ".gnupg"; mode = "0700"; } 226 | { directory = ".ssh"; mode = "0700"; } 227 | { directory = ".nixops"; mode = "0700"; } 228 | { directory = ".local/share/keyrings"; mode = "0700"; } 229 | ".local/share/direnv" 230 | ]; 231 | files = [ 232 | ".screenrc" 233 | ]; 234 | }; 235 | }; 236 | } 237 | #+end_src 238 | 239 | - ~"/persistent"~ is the path to your persistent storage location 240 | 241 | This allows for multiple different persistent storage 242 | locations. If you, for example, have one location you back up 243 | and one you don't, you can use both by defining two separate 244 | attributes under ~environment.persistence~. 245 | 246 | - ~enable~ determines whether the persistent storage location should 247 | be enabled or not. Useful when sharing configurations between 248 | systems with and without impermanence setups. Defaults to ~true~. 249 | 250 | - ~hideMounts~ allows you to specify whether to hide the 251 | bind mounts from showing up as mounted drives in the file 252 | manager. If enabled, it sets the mount option ~x-gvfs-hide~ 253 | on all the bind mounts. 254 | 255 | - ~directories~ are all directories you want to bind mount to 256 | persistent storage. A directory can be represented either as a 257 | string, simply denoting its path, or as a submodule. The 258 | submodule representation is useful when the default assumptions, 259 | mainly regarding permissions, are incorrect. The available 260 | options are: 261 | 262 | - ~directory~, the path to the directory you want to bind mount 263 | to persistent storage. Only setting this option is 264 | equivalent to the string representation. 265 | 266 | - ~persistentStoragePath~, the path to persistent 267 | storage. Defaults to the ~environment.persistence~ submodule 268 | name, i.e. ~"/persistent"~ in the example. This should most 269 | likely be left to its default value - don't change it unless 270 | you're certain you really need to. 271 | 272 | - ~user~, the user who should own the directory. If the directory 273 | doesn't already exist in persistent storage, it will be 274 | created and this user will be its owner. This also applies to 275 | any parent directories which don't yet exist. Changing this 276 | once the directory has been created has no effect. 277 | 278 | - ~group~, the group who should own the directory. If the 279 | directory doesn't already exist in persistent storage, it will 280 | be created and this group will be its owner. This also applies 281 | to any parent directories which don't yet exist. Changing this 282 | once the directory has been created has no effect. 283 | 284 | - ~mode~, the permissions to set for the directory. If the 285 | directory doesn't already exist in persistent storage, it will 286 | be created with this mode. Can be either an octal mode 287 | (e.g. ~0700~) or a symbolic mode (e.g. ~u=rwx,g=,o=~). Parent 288 | directories that don't yet exist are created with default 289 | permissions. Changing this once the directory has been created 290 | has no effect. 291 | 292 | - ~files~ are all files you want to link or bind to persistent 293 | storage. A file can be represented either as a string, simply 294 | denoting its path, or as a submodule. The submodule 295 | representation is useful when the default assumptions, mainly 296 | regarding the permissions of its parent directory, are 297 | incorrect. The available options are: 298 | 299 | - ~file~, the path to the file you want to bind mount to 300 | persistent storage. Only setting this option is equivalent to 301 | the string representation. 302 | 303 | - ~persistentStoragePath~, the path to persistent 304 | storage. Defaults to the ~environment.persistence~ submodule 305 | name, i.e. ~"/persistent"~ in the example. This should most 306 | likely be left to its default value - don't change it unless 307 | you're certain you really need to. 308 | 309 | - ~parentDirectory~, the permissions that should be applied to the 310 | file's parent directory, if it doesn't already 311 | exist. Available options are ~user~, ~group~ and ~mode~. See their 312 | definition in ~directories~ above. 313 | 314 | If the file exists in persistent storage, it will be bind 315 | mounted to the target path; otherwise it will be symlinked. 316 | 317 | - ~users.talyz~ handles files and directories in ~talyz~'s home 318 | directory 319 | 320 | The ~users~ option defines a set of submodules which correspond to 321 | the users' names. The ~directories~ and ~files~ options of each 322 | submodule work like their root counterparts, but the paths are 323 | automatically prefixed with with the user's home directory. 324 | 325 | If the user has a non-standard home directory (i.e. not 326 | ~/home/~), the ~users..home~ option has to be 327 | set to this path - it can't currently be automatically deduced 328 | due to a limitation in ~nixpkgs~. 329 | 330 | /Important note:/ Make sure your persistent volumes are marked with 331 | ~neededForBoot~, otherwise you will run into problems. 332 | 333 | *** home-manager 334 | 335 | Usage of the ~home-manager~ module is very similar to the one of the 336 | ~NixOS~ module - the key differences are that the ~persistence~ option 337 | is now under ~home~, rather than ~environment~, and the addition of 338 | the submodule option ~removePrefixDirectory~. 339 | 340 | /Important note:/ You have to use the ~home-manager~ ~NixOS~ module (in 341 | the ~nixos~ directory of ~home-manager~'s repo) in order for this 342 | module to work as intended. 343 | 344 | To use the module, import it into your configuration with 345 | 346 | #+begin_src nix 347 | { 348 | imports = [ /path/to/impermanence/home-manager.nix ]; 349 | } 350 | #+end_src 351 | 352 | or use the provided ~homeManagerModules.impermanence~ flake output: 353 | 354 | #+begin_src nix 355 | { 356 | inputs = { 357 | home-manager.url = "github:nix-community/home-manager"; 358 | impermanence.url = "github:nix-community/impermanence"; 359 | }; 360 | 361 | outputs = 362 | { 363 | home-manager, 364 | nixpkgs, 365 | impermanence, 366 | ... 367 | }: 368 | { 369 | nixosConfigurations.sythe = nixpkgs.lib.nixosSystem { 370 | system = "x86_64-linux"; 371 | modules = [ 372 | { 373 | imports = [ home-manager.nixosModules.home-manager ]; 374 | 375 | home-manager.users.username = 376 | { ... }: 377 | { 378 | imports = [ 379 | impermanence.homeManagerModules.impermanence 380 | ./home/impermanence.nix # Your home-manager impermanence-configuration 381 | ]; 382 | }; 383 | } 384 | ]; 385 | }; 386 | }; 387 | } 388 | #+end_src 389 | 390 | This adds the ~home.persistence~ option, which is an attribute set 391 | of submodules, where the attribute name is the path to persistent 392 | storage. 393 | 394 | Usage is shown best with an example: 395 | 396 | #+begin_src nix 397 | { 398 | home.persistence."/persistent/home/talyz" = { 399 | directories = [ 400 | "Downloads" 401 | "Music" 402 | "Pictures" 403 | "Documents" 404 | "Videos" 405 | "VirtualBox VMs" 406 | ".gnupg" 407 | ".ssh" 408 | ".nixops" 409 | ".local/share/keyrings" 410 | ".local/share/direnv" 411 | { 412 | directory = ".local/share/Steam"; 413 | method = "symlink"; 414 | } 415 | ]; 416 | files = [ 417 | ".screenrc" 418 | ]; 419 | allowOther = true; 420 | }; 421 | } 422 | #+end_src 423 | 424 | - ~"/persistent/home/talyz"~ is the path to your persistent storage location 425 | - ~directories~ are all directories you want to link to persistent storage 426 | - It is possible to switch the linking ~method~ between bindfs (the 427 | default) and symbolic links. 428 | - ~files~ are all files you want to link to persistent storage. These are 429 | symbolic links to their target location. 430 | - ~allowOther~ allows other users, such as ~root~, to access files 431 | through the bind mounted directories listed in 432 | ~directories~. Useful for ~sudo~ operations, Docker, etc. Requires 433 | the NixOS configuration ~programs.fuse.userAllowOther = true~. 434 | 435 | Additionally, the ~home-manager~ module allows for compatibility 436 | with ~dotfiles~ repos structured for use with [[https://www.gnu.org/software/stow/][GNU Stow]], where the 437 | files linked to are one level deeper than where they should end 438 | up. This can be achieved by setting ~removePrefixDirectory~ to ~true~: 439 | 440 | #+begin_src nix 441 | { 442 | home.persistence."/etc/nixos/home-talyz-nixpkgs/dotfiles" = { 443 | removePrefixDirectory = true; 444 | files = [ 445 | "screen/.screenrc" 446 | ]; 447 | directories = [ 448 | "fish/.config/fish" 449 | ]; 450 | }; 451 | } 452 | #+end_src 453 | 454 | In the example, the ~.screenrc~ file and ~.config/fish~ directory 455 | should be linked to from the home directory; ~removePrefixDirectory~ 456 | removes the first part of the path when deciding where to put the 457 | links. 458 | 459 | /Note:/ When using ~bindfs~ fuse filesystem for directories, the names of 460 | the directories you add will be visible in the ~/etc/mtab~ file and in the 461 | output of ~mount~ to all users. 462 | 463 | ** Further reading 464 | The following blog posts provide more information on the concept of ephemeral 465 | roots: 466 | 467 | - https://elis.nu/blog/2020/05/nixos-tmpfs-as-root/ --- [[https://github.com/etu/][@etu]]'s blog post walks 468 | the reader through a NixOS-on-tmpfs installation. 469 | - https://grahamc.com/blog/erase-your-darlings --- [[https://github.com/grahamc/][@grahamc]]'s blog post details 470 | why one would want to erase their state at every boot, as well as how to 471 | achieve this using ZFS snapshots. 472 | - https://willbush.dev/blog/impermanent-nixos/ --- [[https://github.com/willbush/][@willbush]]'s blog post 473 | provides a detailed NixOS-on-tmpfs guide with optional LUKS encryption, and 474 | utilizing nix flakes for an opinionated install. 475 | -------------------------------------------------------------------------------- /create-directories.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o nounset # Fail on use of unset variable. 4 | set -o errexit # Exit on command failure. 5 | set -o pipefail # Exit on failure of any command in a pipeline. 6 | set -o errtrace # Trap errors in functions and subshells. 7 | set -o noglob # Disable filename expansion (globbing), 8 | # since it could otherwise happen during 9 | # path splitting. 10 | shopt -s inherit_errexit # Inherit the errexit option status in subshells. 11 | 12 | # Print a useful trace when an error occurs 13 | trap 'echo Error when executing ${BASH_COMMAND} at line ${LINENO}! >&2' ERR 14 | 15 | # Given a source directory, /source, and a target directory, 16 | # /target/foo/bar/bazz, we want to "clone" the target structure 17 | # from source into the target. Essentially, we want both 18 | # /source/target/foo/bar/bazz and /target/foo/bar/bazz to exist 19 | # on the filesystem. More concretely, we'd like to map 20 | # /state/etc/ssh/example.key to /etc/ssh/example.key 21 | # 22 | # To achieve this, we split the target's path into parts -- target, foo, 23 | # bar, bazz -- and iterate over them while accumulating the path 24 | # (/target/, /target/foo/, /target/foo/bar, and so on); then, for each of 25 | # these increasingly qualified paths we: 26 | # 1. Ensure both /source/qualifiedPath and qualifiedPath exist 27 | # 2. Copy the ownership of the source path to the target path 28 | # 3. Copy the mode of the source path to the target path 29 | 30 | # Get inputs from command line arguments 31 | if [[ $# != 6 ]]; then 32 | printf "Error: 'create-directories.bash' requires *six* args.\n" >&2 33 | exit 1 34 | fi 35 | sourceBase="$1" 36 | target="$2" 37 | user="$3" 38 | group="$4" 39 | mode="$5" 40 | debug="$6" 41 | 42 | if (( debug )); then 43 | set -o xtrace 44 | fi 45 | 46 | # check that the source exists and warn the user if it doesn't, then 47 | # create them with the specified permissions 48 | realSource="$(realpath -m "$sourceBase$target")" 49 | if [[ ! -d $realSource ]]; then 50 | printf "Warning: Source directory '%s' does not exist; it will be created for you with the following permissions: owner: '%s:%s', mode: '%s'.\n" "$realSource" "$user" "$group" "$mode" 51 | mkdir --mode="$mode" "$realSource" 52 | chown "$user:$group" "$realSource" 53 | fi 54 | 55 | if [[ $sourceBase ]]; then 56 | [[ -d $target ]] || mkdir "$target" 57 | 58 | # synchronize perms between source and target 59 | chown --reference="$realSource" "$target" 60 | chmod --reference="$realSource" "$target" 61 | fi 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | outputs = { self }: { 3 | nixosModules.default = self.nixosModules.impermanence; 4 | nixosModules.impermanence = import ./nixos.nix; 5 | 6 | homeManagerModules.default = self.homeManagerModules.impermanence; 7 | homeManagerModules.impermanence = import ./home-manager.nix; 8 | 9 | # Deprecated 10 | nixosModule = self.nixosModules.impermanence; 11 | nixosModules.home-manager.impermanence = self.homeManagerModules.impermanence; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /home-manager.nix: -------------------------------------------------------------------------------- 1 | { pkgs, config, lib, ... }: 2 | 3 | with lib; 4 | let 5 | cfg = config.home.persistence; 6 | 7 | persistentStorageNames = (filter (path: cfg.${path}.enable) (attrNames cfg)); 8 | 9 | inherit (pkgs.callPackage ./lib.nix { }) 10 | splitPath 11 | dirListToPath 12 | concatPaths 13 | sanitizeName 14 | ; 15 | 16 | mount = "${pkgs.util-linux}/bin/mount"; 17 | unmountScript = mountPoint: tries: sleep: '' 18 | triesLeft=${toString tries} 19 | if ${mount} | grep -F ${mountPoint}' ' >/dev/null; then 20 | while (( triesLeft > 0 )); do 21 | if fusermount -u ${mountPoint}; then 22 | break 23 | else 24 | (( triesLeft-- )) 25 | if (( triesLeft == 0 )); then 26 | echo "Couldn't perform regular unmount of ${mountPoint}. Attempting lazy unmount." 27 | fusermount -uz ${mountPoint} 28 | else 29 | sleep ${toString sleep} 30 | fi 31 | fi 32 | done 33 | fi 34 | ''; 35 | in 36 | { 37 | options = { 38 | 39 | home.persistence = mkOption { 40 | default = { }; 41 | type = with types; attrsOf ( 42 | submodule ({ name, config, ... }: { 43 | options = 44 | { 45 | persistentStoragePath = mkOption { 46 | type = path; 47 | default = name; 48 | description = '' 49 | The path to persistent storage where the real 50 | files and directories should be stored. 51 | ''; 52 | }; 53 | 54 | enable = mkOption { 55 | type = bool; 56 | default = true; 57 | description = "Whether to enable this persistent storage location."; 58 | }; 59 | 60 | defaultDirectoryMethod = mkOption { 61 | type = types.enum [ "bindfs" "symlink" ]; 62 | default = "bindfs"; 63 | description = '' 64 | The linking method that should be used for directories. 65 | 66 | - bindfs is very transparent, and thus used as a safe 67 | default. It has, however, a significant performance impact in 68 | IO-heavy situations. 69 | 70 | - symlinks have great performance but may be treated 71 | specially by some programs that may e.g. generate 72 | errors/warnings, or replace them. 73 | 74 | This can be overridden on a per entry basis. 75 | ''; 76 | }; 77 | 78 | directories = mkOption { 79 | type = types.listOf ( 80 | types.coercedTo types.str (directory: { inherit directory; }) (submodule { 81 | options = { 82 | directory = mkOption { 83 | type = str; 84 | description = "The directory path to be linked."; 85 | }; 86 | method = mkOption { 87 | type = types.enum [ "bindfs" "symlink" ]; 88 | default = config.defaultDirectoryMethod; 89 | description = '' 90 | The linking method to be used for this specific 91 | directory entry. See 92 | defaultDirectoryMethod for more 93 | information on the tradeoffs. 94 | ''; 95 | }; 96 | }; 97 | }) 98 | ); 99 | default = [ ]; 100 | example = [ 101 | "Downloads" 102 | "Music" 103 | "Pictures" 104 | "Documents" 105 | "Videos" 106 | "VirtualBox VMs" 107 | ".gnupg" 108 | ".ssh" 109 | ".local/share/keyrings" 110 | ".local/share/direnv" 111 | { 112 | directory = ".local/share/Steam"; 113 | method = "symlink"; 114 | } 115 | ]; 116 | description = '' 117 | A list of directories in your home directory that 118 | you want to link to persistent storage. You may optionally 119 | specify the linking method each directory should use. 120 | ''; 121 | }; 122 | 123 | files = mkOption { 124 | type = with types; listOf str; 125 | default = [ ]; 126 | example = [ 127 | ".screenrc" 128 | ]; 129 | description = '' 130 | A list of files in your home directory you want to 131 | link to persistent storage. 132 | ''; 133 | }; 134 | 135 | allowOther = mkOption { 136 | type = with types; nullOr bool; 137 | default = null; 138 | example = true; 139 | apply = x: 140 | if x == null then 141 | warn '' 142 | home.persistence."${name}".allowOther not set; assuming 'false'. 143 | See https://github.com/nix-community/impermanence#home-manager for more info. 144 | '' 145 | false 146 | else 147 | x; 148 | description = '' 149 | Whether to allow other users, such as 150 | root, access to files through the 151 | bind mounted directories listed in 152 | directories. Requires the NixOS 153 | configuration parameter 154 | programs.fuse.userAllowOther to 155 | be true. 156 | ''; 157 | }; 158 | 159 | removePrefixDirectory = mkOption { 160 | type = types.bool; 161 | default = false; 162 | example = true; 163 | description = '' 164 | Note: This is mainly useful if you have a dotfiles 165 | repo structured for use with GNU Stow; if you don't, 166 | you can likely ignore it. 167 | 168 | Whether to remove the first directory when linking 169 | or mounting; e.g. for the path 170 | "screen/.screenrc", the 171 | screen/ is ignored for the path 172 | linked to in your home directory. 173 | ''; 174 | }; 175 | }; 176 | }) 177 | ); 178 | description = '' 179 | A set of persistent storage location submodules listing the 180 | files and directories to link to their respective persistent 181 | storage location. 182 | 183 | Each attribute name should be the path relative to the user's 184 | home directory. 185 | 186 | For detailed usage, check the documentation. 188 | ''; 189 | example = literalExpression '' 190 | { 191 | "/persistent/home/talyz" = { 192 | directories = [ 193 | "Downloads" 194 | "Music" 195 | "Pictures" 196 | "Documents" 197 | "Videos" 198 | "VirtualBox VMs" 199 | ".gnupg" 200 | ".ssh" 201 | ".nixops" 202 | ".local/share/keyrings" 203 | ".local/share/direnv" 204 | { 205 | directory = ".local/share/Steam"; 206 | method = "symlink"; 207 | } 208 | ]; 209 | files = [ 210 | ".screenrc" 211 | ]; 212 | allowOther = true; 213 | }; 214 | } 215 | ''; 216 | }; 217 | 218 | }; 219 | 220 | config = { 221 | home.file = 222 | let 223 | link = file: 224 | pkgs.runCommand 225 | "${sanitizeName file}" 226 | { } 227 | "ln -s '${file}' $out"; 228 | 229 | mkLinkNameValuePair = persistentStorageName: fileOrDir: { 230 | name = 231 | if cfg.${persistentStorageName}.removePrefixDirectory then 232 | dirListToPath (tail (splitPath [ fileOrDir ])) 233 | else 234 | fileOrDir; 235 | value = { source = link (concatPaths [ cfg.${persistentStorageName}.persistentStoragePath fileOrDir ]); }; 236 | }; 237 | 238 | mkLinksToPersistentStorage = persistentStorageName: 239 | listToAttrs (map 240 | (mkLinkNameValuePair persistentStorageName) 241 | (cfg.${persistentStorageName}.files ++ (map (v: v.directory) 242 | (filter (v: v.method == "symlink") cfg.${persistentStorageName}.directories))) 243 | ); 244 | in 245 | foldl' recursiveUpdate { } (map mkLinksToPersistentStorage persistentStorageNames); 246 | 247 | systemd.user.services = 248 | let 249 | mkBindMountService = persistentStorageName: dir: 250 | let 251 | mountDir = 252 | if cfg.${persistentStorageName}.removePrefixDirectory then 253 | dirListToPath (tail (splitPath [ dir ])) 254 | else 255 | dir; 256 | targetDir = escapeShellArg (concatPaths [ cfg.${persistentStorageName}.persistentStoragePath dir ]); 257 | mountPoint = escapeShellArg (concatPaths [ config.home.homeDirectory mountDir ]); 258 | name = "bindMount-${sanitizeName targetDir}"; 259 | bindfsOptions = concatStringsSep "," ( 260 | optional (!cfg.${persistentStorageName}.allowOther) "no-allow-other" 261 | ++ optional (versionAtLeast pkgs.bindfs.version "1.14.9") "fsname=${targetDir}" 262 | ); 263 | bindfsOptionFlag = optionalString (bindfsOptions != "") (" -o " + bindfsOptions); 264 | bindfs = "bindfs" + bindfsOptionFlag; 265 | startScript = pkgs.writeShellScript name '' 266 | set -eu 267 | if ! mount | grep -F ${mountPoint}' ' && ! mount | grep -F ${mountPoint}/; then 268 | mkdir -p ${mountPoint} 269 | exec ${bindfs} ${targetDir} ${mountPoint} 270 | else 271 | echo "There is already an active mount at or below ${mountPoint}!" >&2 272 | exit 1 273 | fi 274 | ''; 275 | stopScript = pkgs.writeShellScript "unmount-${name}" '' 276 | set -eu 277 | ${unmountScript mountPoint 6 5} 278 | ''; 279 | in 280 | { 281 | inherit name; 282 | value = { 283 | Unit = { 284 | Description = "Bind mount ${targetDir} at ${mountPoint}"; 285 | 286 | # Don't restart the unit, it could corrupt data and 287 | # crash programs currently reading from the mount. 288 | X-RestartIfChanged = false; 289 | 290 | # Don't add an implicit After=basic.target. 291 | DefaultDependencies = false; 292 | 293 | Before = [ 294 | "bluetooth.target" 295 | "basic.target" 296 | "default.target" 297 | "paths.target" 298 | "sockets.target" 299 | "timers.target" 300 | ]; 301 | }; 302 | 303 | Install.WantedBy = [ "paths.target" ]; 304 | 305 | Service = { 306 | Type = "forking"; 307 | ExecStart = "${startScript}"; 308 | ExecStop = "${stopScript}"; 309 | Environment = "PATH=${makeBinPath [ pkgs.coreutils pkgs.util-linux pkgs.gnugrep pkgs.bindfs ]}:/run/wrappers/bin"; 310 | }; 311 | }; 312 | }; 313 | 314 | mkBindMountServicesForPath = persistentStorageName: 315 | listToAttrs (map 316 | (mkBindMountService persistentStorageName) 317 | (map (v: v.directory) (filter (v: v.method == "bindfs") cfg.${persistentStorageName}.directories)) 318 | ); 319 | in 320 | builtins.foldl' 321 | recursiveUpdate 322 | { } 323 | (map mkBindMountServicesForPath persistentStorageNames); 324 | 325 | home.activation = 326 | let 327 | dag = config.lib.dag; 328 | mount = "${pkgs.util-linux}/bin/mount"; 329 | 330 | # The name of the activation script entry responsible for 331 | # reloading systemd user services. The name was initially 332 | # `reloadSystemD` but has been changed to `reloadSystemd`. 333 | reloadSystemd = 334 | if config.home.activation ? reloadSystemD then 335 | "reloadSystemD" 336 | else 337 | "reloadSystemd"; 338 | 339 | mkBindMount = persistentStorageName: dir: 340 | let 341 | mountDir = 342 | if cfg.${persistentStorageName}.removePrefixDirectory then 343 | dirListToPath (tail (splitPath [ dir ])) 344 | else 345 | dir; 346 | targetDir = escapeShellArg (concatPaths [ cfg.${persistentStorageName}.persistentStoragePath dir ]); 347 | mountPoint = escapeShellArg (concatPaths [ config.home.homeDirectory mountDir ]); 348 | bindfsOptions = concatStringsSep "," ( 349 | optional (!cfg.${persistentStorageName}.allowOther) "no-allow-other" 350 | ++ optional (versionAtLeast pkgs.bindfs.version "1.14.9") "fsname=${targetDir}" 351 | ); 352 | bindfsOptionFlag = optionalString (bindfsOptions != "") (" -o " + bindfsOptions); 353 | bindfs = "${pkgs.bindfs}/bin/bindfs" + bindfsOptionFlag; 354 | systemctl = "XDG_RUNTIME_DIR=\${XDG_RUNTIME_DIR:-/run/user/$(id -u)} ${config.systemd.user.systemctlPath}"; 355 | in 356 | '' 357 | mkdir -p ${targetDir} 358 | mkdir -p ${mountPoint} 359 | 360 | if ${mount} | grep -F ${mountPoint}' ' >/dev/null; then 361 | if ! ${mount} | grep -F ${mountPoint}' ' | grep -F bindfs; then 362 | if ! ${mount} | grep -F ${mountPoint}' ' | grep -F ${targetDir}' ' >/dev/null; then 363 | # The target directory changed, so we need to remount 364 | echo "remounting ${mountPoint}" 365 | ${systemctl} --user stop bindMount-${sanitizeName targetDir} 366 | ${bindfs} ${targetDir} ${mountPoint} 367 | mountedPaths[${mountPoint}]=1 368 | fi 369 | fi 370 | elif ${mount} | grep -F ${mountPoint}/ >/dev/null; then 371 | echo "Something is mounted below ${mountPoint}, not creating bind mount to ${targetDir}" >&2 372 | else 373 | ${bindfs} ${targetDir} ${mountPoint} 374 | mountedPaths[${mountPoint}]=1 375 | fi 376 | ''; 377 | 378 | mkBindMountsForPath = persistentStorageName: 379 | concatMapStrings 380 | (mkBindMount persistentStorageName) 381 | (map (v: v.directory) (filter (v: v.method == "bindfs") cfg.${persistentStorageName}.directories)); 382 | 383 | mkUnmount = persistentStorageName: dir: 384 | let 385 | mountDir = 386 | if cfg.${persistentStorageName}.removePrefixDirectory then 387 | dirListToPath (tail (splitPath [ dir ])) 388 | else 389 | dir; 390 | mountPoint = escapeShellArg (concatPaths [ config.home.homeDirectory mountDir ]); 391 | in 392 | '' 393 | if [[ -n ''${mountedPaths[${mountPoint}]+x} ]]; then 394 | ${unmountScript mountPoint 3 1} 395 | fi 396 | ''; 397 | 398 | mkUnmountsForPath = persistentStorageName: 399 | concatMapStrings 400 | (mkUnmount persistentStorageName) 401 | (map (v: v.directory) (filter (v: v.method == "bindfs") cfg.${persistentStorageName}.directories)); 402 | 403 | mkLinkCleanup = persistentStorageName: dir: 404 | let 405 | mountDir = 406 | if cfg.${persistentStorageName}.removePrefixDirectory then 407 | dirListToPath (tail (splitPath [ dir ])) 408 | else 409 | dir; 410 | mountPoint = escapeShellArg (concatPaths [ config.home.homeDirectory mountDir ]); 411 | in 412 | '' 413 | # Unmount if it's mounted. Ensures smooth transition: bindfs -> symlink 414 | ${unmountScript mountPoint 3 1} 415 | 416 | # If it is a directory and it's empty 417 | if [ -d ${mountPoint} ] && [ -z "$(ls -A ${mountPoint})" ]; then 418 | echo "Removing empty directory ${mountPoint}" 419 | rm -d ${mountPoint} 420 | fi 421 | ''; 422 | 423 | mkLinkCleanupForPath = persistentStorageName: 424 | concatMapStrings 425 | (mkLinkCleanup persistentStorageName) 426 | (map (v: v.directory) (filter (v: v.method == "symlink") cfg.${persistentStorageName}.directories)); 427 | 428 | 429 | in 430 | mkMerge [ 431 | (mkIf (any (path: (filter (v: v.method == "symlink") cfg.${path}.directories) != [ ]) persistentStorageNames) { 432 | # Clean up existing empty directories in the way of links 433 | cleanEmptyLinkTargets = 434 | dag.entryBefore 435 | [ "checkLinkTargets" ] 436 | '' 437 | ${concatMapStrings mkLinkCleanupForPath persistentStorageNames} 438 | ''; 439 | }) 440 | (mkIf (any (path: (filter (v: v.method == "bindfs") cfg.${path}.directories) != [ ]) persistentStorageNames) { 441 | createAndMountPersistentStoragePaths = 442 | dag.entryBefore 443 | [ "writeBoundary" ] 444 | '' 445 | declare -A mountedPaths 446 | ${(concatMapStrings mkBindMountsForPath persistentStorageNames)} 447 | ''; 448 | 449 | unmountPersistentStoragePaths = 450 | dag.entryBefore 451 | [ "createAndMountPersistentStoragePaths" ] 452 | '' 453 | PATH=$PATH:/run/wrappers/bin 454 | unmountBindMounts() { 455 | ${concatMapStrings mkUnmountsForPath persistentStorageNames} 456 | } 457 | 458 | # Run the unmount function on error to clean up stray 459 | # bind mounts 460 | trap "unmountBindMounts" ERR 461 | ''; 462 | 463 | runUnmountPersistentStoragePaths = 464 | dag.entryBefore 465 | [ reloadSystemd ] 466 | '' 467 | unmountBindMounts 468 | ''; 469 | }) 470 | (mkIf (any (path: (cfg.${path}.files != [ ]) || ((filter (v: v.method == "symlink") cfg.${path}.directories) != [ ])) persistentStorageNames) { 471 | createTargetFileDirectories = 472 | dag.entryBefore 473 | [ "writeBoundary" ] 474 | (concatMapStrings 475 | (persistentStorageName: 476 | concatMapStrings 477 | (targetFilePath: '' 478 | mkdir -p ${escapeShellArg (concatPaths [ cfg.${persistentStorageName}.persistentStoragePath (dirOf targetFilePath) ])} 479 | '') 480 | (cfg.${persistentStorageName}.files ++ (map (v: v.directory) (filter (v: v.method == "symlink") cfg.${persistentStorageName}.directories)))) 481 | persistentStorageNames); 482 | }) 483 | ]; 484 | }; 485 | 486 | } 487 | -------------------------------------------------------------------------------- /lib.nix: -------------------------------------------------------------------------------- 1 | { lib }: 2 | let 3 | inherit (lib) 4 | filter 5 | concatMap 6 | concatStringsSep 7 | hasPrefix 8 | head 9 | replaceStrings 10 | optionalString 11 | removePrefix 12 | foldl' 13 | elem 14 | take 15 | length 16 | last 17 | ; 18 | inherit (lib.strings) 19 | sanitizeDerivationName 20 | ; 21 | 22 | # ["/home/user/" "/.screenrc"] -> ["home" "user" ".screenrc"] 23 | splitPath = paths: 24 | (filter 25 | (s: builtins.typeOf s == "string" && s != "") 26 | (concatMap (builtins.split "/") paths) 27 | ); 28 | 29 | # ["home" "user" ".screenrc"] -> "home/user/.screenrc" 30 | dirListToPath = dirList: (concatStringsSep "/" dirList); 31 | 32 | # ["/home/user/" "/.screenrc"] -> "/home/user/.screenrc" 33 | concatPaths = paths: 34 | let 35 | prefix = optionalString (hasPrefix "/" (head paths)) "/"; 36 | path = dirListToPath (splitPath paths); 37 | in 38 | prefix + path; 39 | 40 | 41 | parentsOf = path: 42 | let 43 | prefix = optionalString (hasPrefix "/" path) "/"; 44 | split = splitPath [ path ]; 45 | parents = take ((length split) - 1) split; 46 | in 47 | foldl' 48 | (state: item: 49 | state ++ [ 50 | (concatPaths [ 51 | (if state != [ ] then last state else prefix) 52 | item 53 | ]) 54 | ]) 55 | [ ] 56 | parents; 57 | 58 | sanitizeName = name: 59 | replaceStrings 60 | [ "." ] [ "" ] 61 | (sanitizeDerivationName (removePrefix "/" name)); 62 | 63 | duplicates = list: 64 | let 65 | result = 66 | foldl' 67 | (state: item: 68 | if elem item state.items then 69 | { 70 | items = state.items ++ [ item ]; 71 | duplicates = state.duplicates ++ [ item ]; 72 | } 73 | else 74 | state // { 75 | items = state.items ++ [ item ]; 76 | }) 77 | { items = [ ]; duplicates = [ ]; } 78 | list; 79 | in 80 | result.duplicates; 81 | in 82 | { 83 | inherit 84 | splitPath 85 | dirListToPath 86 | concatPaths 87 | parentsOf 88 | sanitizeName 89 | duplicates 90 | ; 91 | } 92 | -------------------------------------------------------------------------------- /mount-file.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o nounset # Fail on use of unset variable. 4 | set -o errexit # Exit on command failure. 5 | set -o pipefail # Exit on failure of any command in a pipeline. 6 | set -o errtrace # Trap errors in functions and subshells. 7 | shopt -s inherit_errexit # Inherit the errexit option status in subshells. 8 | 9 | # Print a useful trace when an error occurs 10 | trap 'echo Error when executing ${BASH_COMMAND} at line ${LINENO}! >&2' ERR 11 | 12 | # Get inputs from command line arguments 13 | if [[ $# != 3 ]]; then 14 | echo "Error: 'mount-file.bash' requires *three* args." >&2 15 | exit 1 16 | fi 17 | 18 | mountPoint="$1" 19 | targetFile="$2" 20 | debug="$3" 21 | 22 | trace() { 23 | if (( debug )); then 24 | echo "$@" 25 | fi 26 | } 27 | if (( debug )); then 28 | set -o xtrace 29 | fi 30 | 31 | if [[ -L $mountPoint && $(readlink -f "$mountPoint") == "$targetFile" ]]; then 32 | trace "$mountPoint already links to $targetFile, ignoring" 33 | elif findmnt "$mountPoint" >/dev/null; then 34 | trace "mount already exists at $mountPoint, ignoring" 35 | elif [[ -s $mountPoint ]]; then 36 | echo "A file already exists at $mountPoint!" >&2 37 | exit 1 38 | elif [[ -e $targetFile ]]; then 39 | touch "$mountPoint" 40 | mount -o bind "$targetFile" "$mountPoint" 41 | elif [[ $mountPoint == "/etc/machine-id" ]]; then 42 | # Work around an issue with persisting /etc/machine-id. For more 43 | # details, see https://github.com/nix-community/impermanence/pull/242 44 | echo "Creating initial /etc/machine-id" 45 | echo "uninitialized" > "$targetFile" 46 | touch "$mountPoint" 47 | mount -o bind "$targetFile" "$mountPoint" 48 | else 49 | ln -s "$targetFile" "$mountPoint" 50 | fi 51 | -------------------------------------------------------------------------------- /nixos.nix: -------------------------------------------------------------------------------- 1 | { pkgs, config, lib, utils, ... }: 2 | 3 | let 4 | inherit (lib) 5 | attrNames 6 | attrValues 7 | zipAttrsWith 8 | flatten 9 | mkAfter 10 | mkOption 11 | mkDefault 12 | mkIf 13 | mkMerge 14 | mapAttrsToList 15 | types 16 | foldl' 17 | unique 18 | concatMap 19 | concatMapStrings 20 | listToAttrs 21 | escapeShellArg 22 | escapeShellArgs 23 | recursiveUpdate 24 | all 25 | filter 26 | filterAttrs 27 | concatStringsSep 28 | concatMapStringsSep 29 | catAttrs 30 | optional 31 | optionalString 32 | literalExpression 33 | elem 34 | mapAttrs 35 | intersectLists 36 | any 37 | id 38 | head 39 | ; 40 | 41 | inherit (utils) 42 | escapeSystemdPath 43 | fsNeededForBoot 44 | ; 45 | 46 | inherit (pkgs.callPackage ./lib.nix { }) 47 | splitPath 48 | concatPaths 49 | parentsOf 50 | duplicates 51 | ; 52 | 53 | cfg = config.environment.persistence; 54 | users = config.users.users; 55 | allPersistentStoragePaths = zipAttrsWith (_name: flatten) (filter (v: v.enable) (attrValues cfg)); 56 | inherit (allPersistentStoragePaths) files directories; 57 | mountFile = pkgs.runCommand "impermanence-mount-file" { buildInputs = [ pkgs.bash ]; } '' 58 | cp ${./mount-file.bash} $out 59 | patchShebangs $out 60 | ''; 61 | 62 | defaultPerms = { 63 | mode = "0755"; 64 | user = "root"; 65 | group = "root"; 66 | }; 67 | 68 | # Create fileSystems bind mount entry. 69 | mkBindMountNameValuePair = { dirPath, persistentStoragePath, hideMount, ... }: { 70 | name = concatPaths [ "/" dirPath ]; 71 | value = { 72 | device = concatPaths [ persistentStoragePath dirPath ]; 73 | noCheck = true; 74 | options = [ "bind" "X-fstrim.notrim" ] 75 | ++ optional hideMount "x-gvfs-hide"; 76 | depends = [ persistentStoragePath ]; 77 | }; 78 | }; 79 | 80 | # Create all fileSystems bind mount entries for a specific 81 | # persistent storage path. 82 | bindMounts = listToAttrs (map mkBindMountNameValuePair directories); 83 | in 84 | { 85 | options = { 86 | 87 | environment.persistence = mkOption { 88 | default = { }; 89 | type = 90 | let 91 | inherit (types) 92 | attrsOf 93 | bool 94 | listOf 95 | submodule 96 | nullOr 97 | path 98 | str 99 | coercedTo 100 | ; 101 | in 102 | attrsOf ( 103 | submodule ( 104 | { name, config, ... }: 105 | let 106 | commonOpts = { 107 | options = { 108 | persistentStoragePath = mkOption { 109 | type = path; 110 | default = config.persistentStoragePath; 111 | defaultText = "environment.persistence.‹name›.persistentStoragePath"; 112 | description = '' 113 | The path to persistent storage where the real 114 | file or directory should be stored. 115 | ''; 116 | }; 117 | home = mkOption { 118 | type = nullOr path; 119 | default = null; 120 | internal = true; 121 | description = '' 122 | The path to the home directory the file is 123 | placed within. 124 | ''; 125 | }; 126 | enableDebugging = mkOption { 127 | type = bool; 128 | default = config.enableDebugging; 129 | defaultText = "environment.persistence.‹name›.enableDebugging"; 130 | internal = true; 131 | description = '' 132 | Enable debug trace output when running 133 | scripts. You only need to enable this if asked 134 | to. 135 | ''; 136 | }; 137 | }; 138 | }; 139 | dirPermsOpts = { 140 | user = mkOption { 141 | type = str; 142 | description = '' 143 | If the directory doesn't exist in persistent 144 | storage it will be created and owned by the user 145 | specified by this option. 146 | ''; 147 | }; 148 | group = mkOption { 149 | type = str; 150 | description = '' 151 | If the directory doesn't exist in persistent 152 | storage it will be created and owned by the 153 | group specified by this option. 154 | ''; 155 | }; 156 | mode = mkOption { 157 | type = str; 158 | example = "0700"; 159 | description = '' 160 | If the directory doesn't exist in persistent 161 | storage it will be created with the mode 162 | specified by this option. 163 | ''; 164 | }; 165 | }; 166 | fileOpts = { 167 | options = { 168 | file = mkOption { 169 | type = str; 170 | description = '' 171 | The path to the file. 172 | ''; 173 | }; 174 | parentDirectory = 175 | commonOpts.options // 176 | mapAttrs 177 | (_: x: 178 | if x._type or null == "option" then 179 | x // { internal = true; } 180 | else 181 | x) 182 | dirOpts.options; 183 | filePath = mkOption { 184 | type = path; 185 | internal = true; 186 | }; 187 | }; 188 | }; 189 | dirOpts = { 190 | options = { 191 | directory = mkOption { 192 | type = str; 193 | description = '' 194 | The path to the directory. 195 | ''; 196 | }; 197 | hideMount = mkOption { 198 | type = bool; 199 | default = config.hideMounts; 200 | defaultText = "environment.persistence.‹name›.hideMounts"; 201 | example = true; 202 | description = '' 203 | Whether to hide bind mounts from showing up as 204 | mounted drives. 205 | ''; 206 | }; 207 | # Save the default permissions at the level the 208 | # directory resides. This used when creating its 209 | # parent directories, giving them reasonable 210 | # default permissions unaffected by the 211 | # directory's own. 212 | defaultPerms = mapAttrs (_: x: x // { internal = true; }) dirPermsOpts; 213 | dirPath = mkOption { 214 | type = path; 215 | internal = true; 216 | }; 217 | } // dirPermsOpts; 218 | }; 219 | rootFile = submodule [ 220 | commonOpts 221 | fileOpts 222 | ({ config, ... }: { 223 | parentDirectory = mkDefault (defaultPerms // rec { 224 | directory = dirOf config.file; 225 | dirPath = directory; 226 | inherit (config) persistentStoragePath; 227 | inherit defaultPerms; 228 | }); 229 | filePath = mkDefault config.file; 230 | }) 231 | ]; 232 | rootDir = submodule ([ 233 | commonOpts 234 | dirOpts 235 | ({ config, ... }: { 236 | defaultPerms = mkDefault defaultPerms; 237 | dirPath = mkDefault config.directory; 238 | }) 239 | ] ++ (mapAttrsToList (n: v: { ${n} = mkDefault v; }) defaultPerms)); 240 | in 241 | { 242 | options = 243 | { 244 | enable = mkOption { 245 | type = bool; 246 | default = true; 247 | description = "Whether to enable this persistent storage location."; 248 | }; 249 | 250 | persistentStoragePath = mkOption { 251 | type = path; 252 | default = name; 253 | defaultText = "‹name›"; 254 | description = '' 255 | The path to persistent storage where the real 256 | files and directories should be stored. 257 | ''; 258 | }; 259 | 260 | users = mkOption { 261 | type = attrsOf ( 262 | submodule ( 263 | { name, config, ... }: 264 | let 265 | userDefaultPerms = { 266 | inherit (defaultPerms) mode; 267 | user = name; 268 | group = users.${userDefaultPerms.user}.group; 269 | }; 270 | fileConfig = 271 | { config, ... }: 272 | { 273 | parentDirectory = rec { 274 | directory = dirOf config.file; 275 | dirPath = concatPaths [ config.home directory ]; 276 | inherit (config) persistentStoragePath home; 277 | defaultPerms = userDefaultPerms; 278 | }; 279 | filePath = concatPaths [ config.home config.file ]; 280 | }; 281 | userFile = submodule [ 282 | commonOpts 283 | fileOpts 284 | { inherit (config) home; } 285 | { 286 | parentDirectory = mkDefault userDefaultPerms; 287 | } 288 | fileConfig 289 | ]; 290 | dirConfig = 291 | { config, ... }: 292 | { 293 | defaultPerms = mkDefault userDefaultPerms; 294 | dirPath = concatPaths [ config.home config.directory ]; 295 | }; 296 | userDir = submodule ([ 297 | commonOpts 298 | dirOpts 299 | { inherit (config) home; } 300 | dirConfig 301 | ] ++ (mapAttrsToList (n: v: { ${n} = mkDefault v; }) userDefaultPerms)); 302 | in 303 | { 304 | options = 305 | { 306 | # Needed because defining fileSystems 307 | # based on values from users.users 308 | # results in infinite recursion. 309 | home = mkOption { 310 | type = path; 311 | default = "/home/${userDefaultPerms.user}"; 312 | defaultText = "/home/"; 313 | description = '' 314 | The user's home directory. Only 315 | useful for users with a custom home 316 | directory path. 317 | 318 | Cannot currently be automatically 319 | deduced due to a limitation in 320 | nixpkgs. 321 | ''; 322 | }; 323 | 324 | files = mkOption { 325 | type = listOf (coercedTo str (f: { file = f; }) userFile); 326 | default = [ ]; 327 | example = [ 328 | ".screenrc" 329 | ]; 330 | description = '' 331 | Files that should be stored in 332 | persistent storage. 333 | ''; 334 | }; 335 | 336 | directories = mkOption { 337 | type = listOf (coercedTo str (d: { directory = d; }) userDir); 338 | default = [ ]; 339 | example = [ 340 | "Downloads" 341 | "Music" 342 | "Pictures" 343 | "Documents" 344 | "Videos" 345 | ]; 346 | description = '' 347 | Directories to bind mount to 348 | persistent storage. 349 | ''; 350 | }; 351 | }; 352 | } 353 | ) 354 | ); 355 | default = { }; 356 | description = '' 357 | A set of user submodules listing the files and 358 | directories to link to their respective user's 359 | home directories. 360 | 361 | Each attribute name should be the name of the 362 | user. 363 | 364 | For detailed usage, check the documentation. 366 | ''; 367 | example = literalExpression '' 368 | { 369 | talyz = { 370 | directories = [ 371 | "Downloads" 372 | "Music" 373 | "Pictures" 374 | "Documents" 375 | "Videos" 376 | "VirtualBox VMs" 377 | { directory = ".gnupg"; mode = "0700"; } 378 | { directory = ".ssh"; mode = "0700"; } 379 | { directory = ".nixops"; mode = "0700"; } 380 | { directory = ".local/share/keyrings"; mode = "0700"; } 381 | ".local/share/direnv" 382 | ]; 383 | files = [ 384 | ".screenrc" 385 | ]; 386 | }; 387 | } 388 | ''; 389 | }; 390 | 391 | files = mkOption { 392 | type = listOf (coercedTo str (f: { file = f; }) rootFile); 393 | default = [ ]; 394 | example = [ 395 | "/etc/machine-id" 396 | "/etc/nix/id_rsa" 397 | ]; 398 | description = '' 399 | Files that should be stored in persistent storage. 400 | ''; 401 | }; 402 | 403 | directories = mkOption { 404 | type = listOf (coercedTo str (d: { directory = d; }) rootDir); 405 | default = [ ]; 406 | example = [ 407 | "/var/log" 408 | "/var/lib/bluetooth" 409 | "/var/lib/nixos" 410 | "/var/lib/systemd/coredump" 411 | "/etc/NetworkManager/system-connections" 412 | ]; 413 | description = '' 414 | Directories to bind mount to persistent storage. 415 | ''; 416 | }; 417 | 418 | hideMounts = mkOption { 419 | type = bool; 420 | default = false; 421 | example = true; 422 | description = '' 423 | Whether to hide bind mounts from showing up as mounted drives. 424 | ''; 425 | }; 426 | 427 | enableDebugging = mkOption { 428 | type = bool; 429 | default = false; 430 | internal = true; 431 | description = '' 432 | Enable debug trace output when running 433 | scripts. You only need to enable this if asked 434 | to. 435 | ''; 436 | }; 437 | 438 | enableWarnings = mkOption { 439 | type = bool; 440 | default = true; 441 | description = '' 442 | Enable non-critical warnings. 443 | ''; 444 | }; 445 | }; 446 | config = 447 | let 448 | allUsers = zipAttrsWith (_name: flatten) (attrValues config.users); 449 | in 450 | { 451 | files = allUsers.files or [ ]; 452 | directories = allUsers.directories or [ ]; 453 | }; 454 | } 455 | ) 456 | ); 457 | description = '' 458 | A set of persistent storage location submodules listing the 459 | files and directories to link to their respective persistent 460 | storage location. 461 | 462 | Each attribute name should be the full path to a persistent 463 | storage location. 464 | 465 | For detailed usage, check the documentation. 467 | ''; 468 | example = literalExpression '' 469 | { 470 | "/persistent" = { 471 | directories = [ 472 | "/var/log" 473 | "/var/lib/bluetooth" 474 | "/var/lib/nixos" 475 | "/var/lib/systemd/coredump" 476 | "/etc/NetworkManager/system-connections" 477 | { directory = "/var/lib/colord"; user = "colord"; group = "colord"; mode = "u=rwx,g=rx,o="; } 478 | ]; 479 | files = [ 480 | "/etc/machine-id" 481 | { file = "/etc/nix/id_rsa"; parentDirectory = { mode = "u=rwx,g=,o="; }; } 482 | ]; 483 | }; 484 | users.talyz = { ... }; # See the dedicated example 485 | } 486 | ''; 487 | }; 488 | 489 | # Forward declare a dummy option for VM filesystems since the real one won't exist 490 | # unless the VM module is actually imported. 491 | virtualisation.fileSystems = mkOption { }; 492 | }; 493 | 494 | config = mkIf (allPersistentStoragePaths != { }) 495 | (mkMerge [ 496 | { 497 | systemd.services = 498 | let 499 | mkPersistFileService = { filePath, persistentStoragePath, enableDebugging, ... }: 500 | let 501 | targetFile = escapeShellArg (concatPaths [ persistentStoragePath filePath ]); 502 | mountPoint = escapeShellArg filePath; 503 | in 504 | { 505 | "persist-${escapeSystemdPath targetFile}" = { 506 | description = "Bind mount or link ${targetFile} to ${mountPoint}"; 507 | wantedBy = [ "local-fs.target" ]; 508 | before = [ "local-fs.target" ]; 509 | path = [ pkgs.util-linux ]; 510 | unitConfig.DefaultDependencies = false; 511 | serviceConfig = { 512 | Type = "oneshot"; 513 | RemainAfterExit = true; 514 | ExecStart = "${mountFile} ${mountPoint} ${targetFile} ${escapeShellArg enableDebugging}"; 515 | ExecStop = pkgs.writeShellScript "unbindOrUnlink-${escapeSystemdPath targetFile}" '' 516 | set -eu 517 | if [[ -L ${mountPoint} ]]; then 518 | rm ${mountPoint} 519 | else 520 | umount ${mountPoint} 521 | rm ${mountPoint} 522 | fi 523 | ''; 524 | }; 525 | }; 526 | }; 527 | in 528 | foldl' recursiveUpdate { } (map mkPersistFileService files); 529 | 530 | fileSystems = mkIf (directories != [ ]) bindMounts; 531 | # So the mounts still make it into a VM built from `system.build.vm` 532 | virtualisation.fileSystems = mkIf (directories != [ ]) bindMounts; 533 | 534 | system.activationScripts = 535 | let 536 | # Script to create directories in persistent and ephemeral 537 | # storage. The directory structure's mode and ownership mirror 538 | # those of persistentStoragePath/dir. 539 | createDirectories = pkgs.runCommand "impermanence-create-directories" { buildInputs = [ pkgs.bash ]; } '' 540 | cp ${./create-directories.bash} $out 541 | patchShebangs $out 542 | ''; 543 | 544 | mkDirWithPerms = 545 | { dirPath 546 | , persistentStoragePath 547 | , user 548 | , group 549 | , mode 550 | , enableDebugging 551 | , ... 552 | }: 553 | let 554 | args = [ 555 | persistentStoragePath 556 | dirPath 557 | user 558 | group 559 | mode 560 | enableDebugging 561 | ]; 562 | in 563 | '' 564 | ${createDirectories} ${escapeShellArgs args} 565 | ''; 566 | 567 | # Build an activation script which creates all persistent 568 | # storage directories we want to bind mount. 569 | dirCreationScript = 570 | let 571 | # The parent directories of files. 572 | fileDirs = unique (catAttrs "parentDirectory" files); 573 | 574 | # All the directories actually listed by the user and the 575 | # parent directories of listed files. 576 | explicitDirs = directories ++ fileDirs; 577 | 578 | # Home directories have to be handled specially, since 579 | # they're at the permissions boundary where they 580 | # themselves should be owned by the user and have stricter 581 | # permissions than regular directories, whereas its parent 582 | # should be owned by root and have regular permissions. 583 | # 584 | # This simply collects all the home directories and sets 585 | # the appropriate permissions and ownership. 586 | homeDirs = 587 | foldl' 588 | (state: dir: 589 | let 590 | homeDir = { 591 | directory = dir.home; 592 | dirPath = dir.home; 593 | home = null; 594 | mode = "0700"; 595 | user = dir.user; 596 | group = users.${dir.user}.group; 597 | inherit defaultPerms; 598 | inherit (dir) persistentStoragePath enableDebugging; 599 | }; 600 | in 601 | if dir.home != null then 602 | if !(elem homeDir state) then 603 | state ++ [ homeDir ] 604 | else 605 | state 606 | else 607 | state 608 | ) 609 | [ ] 610 | explicitDirs; 611 | 612 | # Persistent storage directories. These need to be created 613 | # unless they're at the root of a filesystem. 614 | persistentStorageDirs = 615 | foldl' 616 | (state: dir: 617 | let 618 | persistentStorageDir = { 619 | directory = dir.persistentStoragePath; 620 | dirPath = dir.persistentStoragePath; 621 | persistentStoragePath = ""; 622 | home = null; 623 | inherit (dir) defaultPerms enableDebugging; 624 | inherit (dir.defaultPerms) user group mode; 625 | }; 626 | in 627 | if dir.home == null && !(elem persistentStorageDir state) then 628 | state ++ [ persistentStorageDir ] 629 | else 630 | state 631 | ) 632 | [ ] 633 | (explicitDirs ++ homeDirs); 634 | 635 | # Generate entries for all parent directories of the 636 | # argument directories, listed in the order they need to 637 | # be created. The parent directories are assigned default 638 | # permissions. 639 | mkParentDirs = dirs: 640 | let 641 | # Create a new directory item from `dir`, the child 642 | # directory item to inherit properties from and 643 | # `path`, the parent directory path. 644 | mkParent = dir: path: { 645 | directory = path; 646 | dirPath = 647 | if dir.home != null then 648 | concatPaths [ dir.home path ] 649 | else 650 | path; 651 | inherit (dir) persistentStoragePath home enableDebugging; 652 | inherit (dir.defaultPerms) user group mode; 653 | }; 654 | # Create new directory items for all parent 655 | # directories of a directory. 656 | mkParents = dir: 657 | map (mkParent dir) (parentsOf dir.directory); 658 | in 659 | unique (flatten (map mkParents dirs)); 660 | 661 | persistentStorageDirParents = mkParentDirs persistentStorageDirs; 662 | 663 | # Parent directories of home folders. This is usually only 664 | # /home, unless the user's home is in a non-standard 665 | # location. 666 | homeDirParents = mkParentDirs homeDirs; 667 | 668 | # Parent directories of all explicitly listed directories. 669 | parentDirs = mkParentDirs explicitDirs; 670 | 671 | # All directories in the order they should be created. 672 | allDirs = 673 | persistentStorageDirParents 674 | ++ persistentStorageDirs 675 | ++ homeDirParents 676 | ++ homeDirs 677 | ++ parentDirs 678 | ++ explicitDirs; 679 | in 680 | pkgs.writeShellScript "impermanence-run-create-directories" '' 681 | _status=0 682 | trap "_status=1" ERR 683 | ${concatMapStrings mkDirWithPerms allDirs} 684 | exit $_status 685 | ''; 686 | 687 | mkPersistFile = { filePath, persistentStoragePath, enableDebugging, ... }: 688 | let 689 | mountPoint = filePath; 690 | targetFile = concatPaths [ persistentStoragePath filePath ]; 691 | args = escapeShellArgs [ 692 | mountPoint 693 | targetFile 694 | enableDebugging 695 | ]; 696 | in 697 | '' 698 | ${mountFile} ${args} 699 | ''; 700 | 701 | persistFileScript = 702 | pkgs.writeShellScript "impermanence-persist-files" '' 703 | _status=0 704 | trap "_status=1" ERR 705 | ${concatMapStrings mkPersistFile files} 706 | exit $_status 707 | ''; 708 | in 709 | { 710 | "createPersistentStorageDirs" = { 711 | deps = [ "users" "groups" ]; 712 | text = "${dirCreationScript}"; 713 | }; 714 | "persist-files" = { 715 | deps = [ "createPersistentStorageDirs" ]; 716 | text = "${persistFileScript}"; 717 | }; 718 | }; 719 | 720 | # Create the mountpoints of directories marked as needed for boot 721 | # which are also persisted. For this to work, it has to run at 722 | # early boot, before NixOS' filesystem mounting runs. Without 723 | # this, initial boot fails when for example /var/lib/nixos is 724 | # persisted but not created in persistent storage. 725 | boot.initrd = 726 | let 727 | neededForBootFs = catAttrs "mountPoint" (filter fsNeededForBoot (attrValues config.fileSystems)); 728 | neededForBootDirs = filter (dir: elem dir.dirPath neededForBootFs) directories; 729 | getDevice = fs: 730 | if fs.device != null then 731 | fs.device 732 | else if fs.label != null then 733 | "/dev/disk/by-label/${fs.label}" 734 | else 735 | "none"; 736 | mkMount = fs: 737 | let 738 | mountPoint = concatPaths [ "/persist-tmp-mnt" fs.mountPoint ]; 739 | device = getDevice fs; 740 | options = filter (o: (builtins.match "(x-.*\.mount)" o) == null) fs.options; 741 | optionsFlag = optionalString (options != [ ]) ("-o " + escapeShellArg (concatStringsSep "," options)); 742 | in 743 | '' 744 | mkdir -p ${escapeShellArg mountPoint} 745 | mount -t ${escapeShellArgs [ fs.fsType device mountPoint ]} ${optionsFlag} 746 | ''; 747 | mkDir = { persistentStoragePath, dirPath, ... }: '' 748 | mkdir -p ${escapeShellArg (concatPaths [ "/persist-tmp-mnt" persistentStoragePath dirPath ])} 749 | ''; 750 | mkUnmount = fs: '' 751 | umount ${escapeShellArg (concatPaths [ "/persist-tmp-mnt" fs.mountPoint ])} 752 | ''; 753 | fileSystems = 754 | let 755 | persistentStoragePaths = unique (catAttrs "persistentStoragePath" directories); 756 | all = config.fileSystems // config.virtualisation.fileSystems; 757 | matchFileSystems = fs: attrValues (filterAttrs (_: v: v.mountPoint or null == fs) all); 758 | in 759 | concatMap matchFileSystems persistentStoragePaths; 760 | deviceUnits = unique 761 | (concatMap 762 | (fs: 763 | # If the device path starts with “dev” or “sys”, 764 | # it's a real device and should have an associated 765 | # .device unit. If not, it's probably either a 766 | # temporary file system lacking a backing device, a 767 | # ZFS pool or a bind mount. 768 | let 769 | device = getDevice fs; 770 | in 771 | if elem (head (splitPath [ device ])) [ "dev" "sys" ] then 772 | [ "${escapeSystemdPath device}.device" ] 773 | else if device == "none" || device == fs.fsType then 774 | [ ] 775 | else if fs.fsType == "zfs" then 776 | [ "zfs-import.target" ] 777 | else 778 | [ "${escapeSystemdPath device}.mount" ]) 779 | fileSystems); 780 | createNeededForBootDirs = '' 781 | ${concatMapStrings mkMount fileSystems} 782 | ${concatMapStrings mkDir neededForBootDirs} 783 | ${concatMapStrings mkUnmount fileSystems} 784 | ''; 785 | in 786 | { 787 | systemd.services = mkIf config.boot.initrd.systemd.enable { 788 | create-needed-for-boot-dirs = { 789 | wantedBy = [ "initrd-root-device.target" ]; 790 | requires = deviceUnits; 791 | after = deviceUnits; 792 | before = [ "sysroot.mount" ]; 793 | serviceConfig.Type = "oneshot"; 794 | unitConfig.DefaultDependencies = false; 795 | script = createNeededForBootDirs; 796 | }; 797 | }; 798 | postResumeCommands = mkIf (!config.boot.initrd.systemd.enable) 799 | (mkAfter createNeededForBootDirs); 800 | }; 801 | } 802 | 803 | # Work around an issue with persisting /etc/machine-id where the 804 | # systemd-machine-id-commit.service unit fails if the final 805 | # /etc/machine-id is bind mounted from persistent storage. For 806 | # more details, see 807 | # https://github.com/nix-community/impermanence/issues/229 and 808 | # https://github.com/nix-community/impermanence/pull/242 809 | (mkIf (any (f: f == "/etc/machine-id") (catAttrs "filePath" files)) { 810 | boot.initrd.systemd.suppressedUnits = [ "systemd-machine-id-commit.service" ]; 811 | systemd.services.systemd-machine-id-commit.unitConfig.ConditionFirstBoot = true; 812 | }) 813 | 814 | # Assertions and warnings 815 | { 816 | assertions = 817 | let 818 | markedNeededForBoot = cond: fs: 819 | if config.fileSystems ? ${fs} then 820 | config.fileSystems.${fs}.neededForBoot == cond 821 | else 822 | cond; 823 | persistentStoragePaths = attrNames cfg; 824 | usersPerPath = allPersistentStoragePaths.users; 825 | homeDirOffenders = 826 | filterAttrs 827 | (n: v: (v.home != config.users.users.${n}.home)); 828 | in 829 | [ 830 | { 831 | # Assert that all persistent storage volumes we use are 832 | # marked with neededForBoot. 833 | assertion = all (markedNeededForBoot true) persistentStoragePaths; 834 | message = 835 | let 836 | offenders = filter (markedNeededForBoot false) persistentStoragePaths; 837 | in 838 | '' 839 | environment.persistence: 840 | All filesystems used for persistent storage must 841 | have the flag neededForBoot set to true. 842 | 843 | Please fix or remove the following paths: 844 | ${concatStringsSep "\n " offenders} 845 | ''; 846 | } 847 | { 848 | assertion = all (users: (homeDirOffenders users) == { }) usersPerPath; 849 | message = 850 | let 851 | offendersPerPath = filter (users: (homeDirOffenders users) != { }) usersPerPath; 852 | offendersText = 853 | concatMapStringsSep 854 | "\n " 855 | (offenders: 856 | concatMapStringsSep 857 | "\n " 858 | (n: "${n}: ${offenders.${n}.home} != ${config.users.users.${n}.home}") 859 | (attrNames offenders)) 860 | offendersPerPath; 861 | in 862 | '' 863 | environment.persistence: 864 | Users and home doesn't match: 865 | ${offendersText} 866 | 867 | You probably want to set each 868 | environment.persistence..users..home to 869 | match the respective user's home directory as 870 | defined by users.users..home. 871 | ''; 872 | } 873 | { 874 | assertion = duplicates (catAttrs "filePath" files) == [ ]; 875 | message = 876 | let 877 | offenders = duplicates (catAttrs "filePath" files); 878 | in 879 | '' 880 | environment.persistence: 881 | The following files were specified two or more 882 | times: 883 | ${concatStringsSep "\n " offenders} 884 | ''; 885 | } 886 | { 887 | assertion = duplicates (catAttrs "dirPath" directories) == [ ]; 888 | message = 889 | let 890 | offenders = duplicates (catAttrs "dirPath" directories); 891 | in 892 | '' 893 | environment.persistence: 894 | The following directories were specified two or more 895 | times: 896 | ${concatStringsSep "\n " offenders} 897 | ''; 898 | } 899 | ]; 900 | 901 | warnings = 902 | let 903 | usersWithoutUid = attrNames (filterAttrs (n: u: u.uid == null) config.users.users); 904 | groupsWithoutGid = attrNames (filterAttrs (n: g: g.gid == null) config.users.groups); 905 | varLibNixosPersistent = 906 | let 907 | varDirs = parentsOf "/var/lib/nixos" ++ [ "/var/lib/nixos" ]; 908 | persistedDirs = catAttrs "dirPath" directories; 909 | mountedDirs = catAttrs "mountPoint" (attrValues config.fileSystems); 910 | persistedVarDirs = intersectLists varDirs persistedDirs; 911 | mountedVarDirs = intersectLists varDirs mountedDirs; 912 | in 913 | persistedVarDirs != [ ] || mountedVarDirs != [ ]; 914 | in 915 | mkIf (any id allPersistentStoragePaths.enableWarnings) 916 | (mkMerge [ 917 | (mkIf (!varLibNixosPersistent && (usersWithoutUid != [ ] || groupsWithoutGid != [ ])) [ 918 | '' 919 | environment.persistence: 920 | Neither /var/lib/nixos nor any of its parents are 921 | persisted. This means all users/groups without 922 | specified uids/gids will have them reassigned on 923 | reboot. 924 | ${optionalString (usersWithoutUid != [ ]) '' 925 | The following users are missing a uid: 926 | ${concatStringsSep "\n " usersWithoutUid} 927 | ''} 928 | ${optionalString (groupsWithoutGid != [ ]) '' 929 | The following groups are missing a gid: 930 | ${concatStringsSep "\n " groupsWithoutGid} 931 | ''} 932 | '' 933 | ]) 934 | ]); 935 | } 936 | ]); 937 | 938 | } 939 | --------------------------------------------------------------------------------