{ config, lib, pkgs, utils, ... }@moduleArgs: with lib; with utils; let addCheckDesc = desc: elemType: check: types.addCheck elemType check // { description = "${elemType.description} (with check: ${desc})"; }; isNonEmpty = s: (builtins.match "[ \t\n]*" s) == null; nonEmptyStr = addCheckDesc "non-empty" types.str isNonEmpty; fileSystems' = toposort fsBefore (attrValues config.fileSystems); fileSystems = if fileSystems' ? result then # use topologically sorted fileSystems everywhere fileSystems'.result else # the assertion below will catch this, # but we fall back to the original order # anyway so that other modules could check # their assertions too (attrValues config.fileSystems); specialFSTypes = [ "proc" "sysfs" "tmpfs" "ramfs" "devtmpfs" "devpts" ]; nonEmptyWithoutTrailingSlash = addCheckDesc "non-empty without trailing slash" types.str (s: isNonEmpty s && (builtins.match ".+/" s) == null); coreFileSystemOpts = { name, config, ... }: { options = { mountPoint = mkOption { example = "/mnt/usb"; type = nonEmptyWithoutTrailingSlash; description = "Location of the mounted file system."; }; stratis.poolUuid = lib.mkOption { type = types.uniq (types.nullOr types.str); description = '' UUID of the stratis pool that the fs is located in ''; example = "04c68063-90a5-4235-b9dd-6180098a20d9"; default = null; }; device = mkOption { default = null; example = "/dev/sda"; type = types.nullOr nonEmptyStr; description = "Location of the device."; }; fsType = mkOption { default = "auto"; example = "ext3"; type = nonEmptyStr; description = "Type of the file system."; }; options = mkOption { default = [ "defaults" ]; example = [ "data=journal" ]; description = "Options used to mount the file system."; type = types.nonEmptyListOf nonEmptyStr; }; depends = mkOption { default = [ ]; example = [ "/persist" ]; type = types.listOf nonEmptyWithoutTrailingSlash; description = '' List of paths that should be mounted before this one. This filesystem's {option}`device` and {option}`mountPoint` are always checked and do not need to be included explicitly. If a path is added to this list, any other filesystem whose mount point is a parent of the path will be mounted before this filesystem. The paths do not need to actually be the {option}`mountPoint` of some other filesystem. ''; }; }; config = { mountPoint = mkDefault name; device = mkIf (elem config.fsType specialFSTypes) (mkDefault config.fsType); }; }; fileSystemOpts = { config, ... }: { options = { label = mkOption { default = null; example = "root-partition"; type = types.nullOr nonEmptyStr; description = "Label of the device (if any)."; }; autoFormat = mkOption { default = false; type = types.bool; description = '' If the device does not currently contain a filesystem (as determined by {command}`blkid`), then automatically format it with the filesystem type specified in {option}`fsType`. Use with caution. ''; }; formatOptions = mkOption { visible = false; type = types.unspecified; default = null; }; autoResize = mkOption { default = false; type = types.bool; description = '' If set, the filesystem is grown to its maximum size before being mounted. (This is typically the size of the containing partition.) This is currently only supported for ext2/3/4 filesystems that are mounted during early boot. ''; }; noCheck = mkOption { default = false; type = types.bool; description = "Disable running fsck on this filesystem."; }; }; config.options = let inInitrd = utils.fsNeededForBoot config; in mkMerge [ (mkIf config.autoResize [ "x-systemd.growfs" ]) (mkIf config.autoFormat [ "x-systemd.makefs" ]) (mkIf (utils.fsNeededForBoot config) [ "x-initrd.mount" ]) (mkIf # With scripted stage 1, depends is implemented by sorting 'config.system.build.fileSystems' (lib.length config.depends > 0 && (inInitrd -> moduleArgs.config.boot.initrd.systemd.enable)) ( map ( x: "x-systemd.requires-mounts-for=${optionalString inInitrd "/sysroot"}${x}" ) config.depends ) ) ]; }; # Makes sequence of `specialMount device mountPoint options fsType` commands. # `systemMount` should be defined in the sourcing script. makeSpecialMounts = mounts: pkgs.writeText "mounts.sh" (concatMapStringsSep "\n" (mount: '' specialMount "${mount.device}" "${mount.mountPoint}" "${concatStringsSep "," mount.options}" "${mount.fsType}" '') mounts); makeFstabEntries = let fsToSkipCheck = [ "none" "auto" "overlay" "iso9660" "bindfs" "udf" "btrfs" "zfs" "tmpfs" "bcachefs" "nfs" "nfs4" "nilfs2" "vboxsf" "squashfs" "glusterfs" "apfs" "9p" "cifs" "prl_fs" "vmhgfs" ] ++ lib.optionals (!config.boot.initrd.checkJournalingFS) [ "ext3" "ext4" "reiserfs" "xfs" "jfs" "f2fs" ]; isBindMount = fs: builtins.elem "bind" fs.options; skipCheck = fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck || isBindMount fs; # https://wiki.archlinux.org/index.php/fstab#Filepath_spaces escape = string: builtins.replaceStrings [ " " "\t" ] [ "\\040" "\\011" ] string; in fstabFileSystems: { }: concatMapStrings (fs: (if fs.device != null then escape fs.device else if fs.label != null then "/dev/disk/by-label/${escape fs.label}" else throw "No device specified for mount point ‘${fs.mountPoint}’.") + " " + escape fs.mountPoint + " " + fs.fsType + " " + escape (builtins.concatStringsSep "," fs.options) + " 0 " + (if skipCheck fs then "0" else if fs.mountPoint == "/" then "1" else "2") + "\n" ) fstabFileSystems; initrdFstab = pkgs.writeText "initrd-fstab" (makeFstabEntries (filter utils.fsNeededForBoot fileSystems) { }); in { ###### interface options = { fileSystems = mkOption { default = {}; example = literalExpression '' { "/".device = "/dev/hda1"; "/data" = { device = "/dev/hda2"; fsType = "ext3"; options = [ "data=journal" ]; }; "/bigdisk".label = "bigdisk"; } ''; type = types.attrsOf (types.submodule [coreFileSystemOpts fileSystemOpts]); description = '' The file systems to be mounted. It must include an entry for the root directory (`mountPoint = "/"`). Each entry in the list is an attribute set with the following fields: `mountPoint`, `device`, `fsType` (a file system type recognised by {command}`mount`; defaults to `"auto"`), and `options` (the mount options passed to {command}`mount` using the {option}`-o` flag; defaults to `[ "defaults" ]`). Instead of specifying `device`, you can also specify a volume label (`label`) for file systems that support it, such as ext2/ext3 (see {command}`mke2fs -L`). ''; }; system.fsPackages = mkOption { internal = true; default = [ ]; description = "Packages supplying file system mounters and checkers."; }; boot.supportedFilesystems = mkOption { default = { }; example = lib.literalExpression '' { btrfs = true; zfs = lib.mkForce false; } ''; type = types.coercedTo (types.listOf types.str) (enabled: lib.listToAttrs (map (fs: lib.nameValuePair fs true) enabled)) (types.attrsOf types.bool); description = '' Names of supported filesystem types, or an attribute set of file system types and their state. The set form may be used together with `lib.mkForce` to explicitly disable support for specific filesystems, e.g. to disable ZFS with an unsupported kernel. ''; }; boot.specialFileSystems = mkOption { default = {}; type = types.attrsOf (types.submodule coreFileSystemOpts); internal = true; description = '' Special filesystems that are mounted very early during boot. ''; }; boot.devSize = mkOption { default = "5%"; example = "32m"; type = types.str; description = '' Size limit for the /dev tmpfs. Look at mount(8), tmpfs size option, for the accepted syntax. ''; }; boot.devShmSize = mkOption { default = "50%"; example = "256m"; type = types.str; description = '' Size limit for the /dev/shm tmpfs. Look at mount(8), tmpfs size option, for the accepted syntax. ''; }; boot.runSize = mkOption { default = "25%"; example = "256m"; type = types.str; description = '' Size limit for the /run tmpfs. Look at mount(8), tmpfs size option, for the accepted syntax. ''; }; }; ###### implementation config = { assertions = let ls = sep: concatMapStringsSep sep (x: x.mountPoint); resizableFSes = [ "ext3" "ext4" "btrfs" "xfs" ]; notAutoResizable = fs: fs.autoResize && !(builtins.elem fs.fsType resizableFSes); in [ { assertion = ! (fileSystems' ? cycle); message = "The ‘fileSystems’ option can't be topologically sorted: mountpoint dependency path ${ls " -> " fileSystems'.cycle} loops to ${ls ", " fileSystems'.loops}"; } { assertion = ! (any notAutoResizable fileSystems); message = let fs = head (filter notAutoResizable fileSystems); in '' Mountpoint '${fs.mountPoint}': 'autoResize = true' is not supported for 'fsType = "${fs.fsType}"' ${optionalString (fs.fsType == "auto") "fsType has to be explicitly set and"} only the following support it: ${lib.concatStringsSep ", " resizableFSes}. ''; } { assertion = ! (any (fs: fs.formatOptions != null) fileSystems); message = let fs = head (filter (fs: fs.formatOptions != null) fileSystems); in '' 'fileSystems..formatOptions' has been removed, since systemd-makefs does not support any way to provide formatting options. ''; } ]; # Export for use in other modules system.build.fileSystems = fileSystems; system.build.earlyMountScript = makeSpecialMounts (toposort fsBefore (attrValues config.boot.specialFileSystems)).result; boot.supportedFilesystems = map (fs: fs.fsType) fileSystems; # Add the mount helpers to the system path so that `mount' can find them. system.fsPackages = [ pkgs.dosfstools ]; environment.systemPackages = with pkgs; [ fuse3 fuse ] ++ config.system.fsPackages; environment.etc.fstab.text = let swapOptions = sw: concatStringsSep "," ( sw.options ++ optional (sw.priority != null) "pri=${toString sw.priority}" ++ optional (sw.discardPolicy != null) "discard${optionalString (sw.discardPolicy != "both") "=${toString sw.discardPolicy}"}" ); in '' # This is a generated file. Do not edit! # # To make changes, edit the fileSystems and swapDevices NixOS options # in your /etc/nixos/configuration.nix file. # # # Filesystems. ${makeFstabEntries fileSystems {}} # Swap devices. ${flip concatMapStrings config.swapDevices (sw: "${sw.realDevice} none swap ${swapOptions sw}\n" )} ''; boot.initrd.systemd.storePaths = [initrdFstab]; boot.initrd.systemd.managerEnvironment.SYSTEMD_SYSROOT_FSTAB = initrdFstab; boot.initrd.systemd.services.initrd-parse-etc.environment.SYSTEMD_SYSROOT_FSTAB = initrdFstab; # Provide a target that pulls in all filesystems. systemd.targets.fs = { description = "All File Systems"; wants = [ "local-fs.target" "remote-fs.target" ]; }; systemd.services = { # Mount /sys/fs/pstore for evacuating panic logs and crashdumps from persistent storage onto the disk using systemd-pstore. # This cannot be done with the other special filesystems because the pstore module (which creates the mount point) is not loaded then. "mount-pstore" = { serviceConfig = { Type = "oneshot"; # skip on kernels without the pstore module ExecCondition = "${pkgs.kmod}/bin/modprobe -b pstore"; ExecStart = pkgs.writeShellScript "mount-pstore.sh" '' set -eu # if the pstore module is builtin it will have mounted the persistent store automatically. it may also be already mounted for other reasons. ${pkgs.util-linux}/bin/mountpoint -q /sys/fs/pstore || ${pkgs.util-linux}/bin/mount -t pstore -o nosuid,noexec,nodev pstore /sys/fs/pstore # wait up to 1.5 seconds for the backend to be registered and the files to appear. a systemd path unit cannot detect this happening; and succeeding after a restart would not start dependent units. TRIES=15 while [ "$(cat /sys/module/pstore/parameters/backend)" = "(null)" ]; do if (( $TRIES )); then sleep 0.1 TRIES=$((TRIES-1)) else echo "Persistent Storage backend was not registered in time." >&2 break fi done ''; RemainAfterExit = true; }; unitConfig = { ConditionVirtualization = "!container"; DefaultDependencies = false; # needed to prevent a cycle }; before = [ "systemd-pstore.service" "shutdown.target" ]; conflicts = [ "shutdown.target" ]; wantedBy = [ "systemd-pstore.service" ]; }; }; systemd.tmpfiles.rules = [ "d /run/keys 0750 root ${toString config.ids.gids.keys}" "z /run/keys 0750 root ${toString config.ids.gids.keys}" ]; # Sync mount options with systemd's src/core/mount-setup.c: mount_table. boot.specialFileSystems = { "/proc" = { fsType = "proc"; options = [ "nosuid" "noexec" "nodev" ]; }; "/run" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=755" "size=${config.boot.runSize}" ]; }; "/dev" = { fsType = "devtmpfs"; options = [ "nosuid" "strictatime" "mode=755" "size=${config.boot.devSize}" ]; }; "/dev/shm" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=1777" "size=${config.boot.devShmSize}" ]; }; "/dev/pts" = { fsType = "devpts"; options = [ "nosuid" "noexec" "mode=620" "ptmxmode=0666" "gid=${toString config.ids.gids.tty}" ]; }; # To hold secrets that shouldn't be written to disk "/run/keys" = { fsType = "ramfs"; options = [ "nosuid" "nodev" "mode=750" ]; }; } // optionalAttrs (!config.boot.isContainer) { # systemd-nspawn populates /sys by itself, and remounting it causes all # kinds of weird issues (most noticeably, waiting for host disk device # nodes). "/sys" = { fsType = "sysfs"; options = [ "nosuid" "noexec" "nodev" ]; }; }; }; }