1{ config, lib, pkgs, utils, ... }:
2
3with lib;
4with utils;
5
6let
7
8 addCheckDesc = desc: elemType: check: types.addCheck elemType check
9 // { description = "${elemType.description} (with check: ${desc})"; };
10
11 isNonEmpty = s: (builtins.match "[ \t\n]*" s) == null;
12 nonEmptyStr = addCheckDesc "non-empty" types.str isNonEmpty;
13
14 fileSystems' = toposort fsBefore (attrValues config.fileSystems);
15
16 fileSystems = if fileSystems' ? result
17 then # use topologically sorted fileSystems everywhere
18 fileSystems'.result
19 else # the assertion below will catch this,
20 # but we fall back to the original order
21 # anyway so that other modules could check
22 # their assertions too
23 (attrValues config.fileSystems);
24
25 specialFSTypes = [ "proc" "sysfs" "tmpfs" "ramfs" "devtmpfs" "devpts" ];
26
27 nonEmptyWithoutTrailingSlash = addCheckDesc "non-empty without trailing slash" types.str
28 (s: isNonEmpty s && (builtins.match ".+/" s) == null);
29
30 coreFileSystemOpts = { name, config, ... }: {
31
32 options = {
33 mountPoint = mkOption {
34 example = "/mnt/usb";
35 type = nonEmptyWithoutTrailingSlash;
36 description = lib.mdDoc "Location of the mounted the file system.";
37 };
38
39 device = mkOption {
40 default = null;
41 example = "/dev/sda";
42 type = types.nullOr nonEmptyStr;
43 description = lib.mdDoc "Location of the device.";
44 };
45
46 fsType = mkOption {
47 default = "auto";
48 example = "ext3";
49 type = nonEmptyStr;
50 description = lib.mdDoc "Type of the file system.";
51 };
52
53 options = mkOption {
54 default = [ "defaults" ];
55 example = [ "data=journal" ];
56 description = lib.mdDoc "Options used to mount the file system.";
57 type = types.listOf nonEmptyStr;
58 };
59
60 depends = mkOption {
61 default = [ ];
62 example = [ "/persist" ];
63 type = types.listOf nonEmptyWithoutTrailingSlash;
64 description = lib.mdDoc ''
65 List of paths that should be mounted before this one. This filesystem's
66 {option}`device` and {option}`mountPoint` are always
67 checked and do not need to be included explicitly. If a path is added
68 to this list, any other filesystem whose mount point is a parent of
69 the path will be mounted before this filesystem. The paths do not need
70 to actually be the {option}`mountPoint` of some other filesystem.
71 '';
72 };
73
74 };
75
76 config = {
77 mountPoint = mkDefault name;
78 device = mkIf (elem config.fsType specialFSTypes) (mkDefault config.fsType);
79 };
80
81 };
82
83 fileSystemOpts = { config, ... }: {
84
85 options = {
86
87 label = mkOption {
88 default = null;
89 example = "root-partition";
90 type = types.nullOr nonEmptyStr;
91 description = lib.mdDoc "Label of the device (if any).";
92 };
93
94 autoFormat = mkOption {
95 default = false;
96 type = types.bool;
97 description = lib.mdDoc ''
98 If the device does not currently contain a filesystem (as
99 determined by {command}`blkid`, then automatically
100 format it with the filesystem type specified in
101 {option}`fsType`. Use with caution.
102 '';
103 };
104
105 formatOptions = mkOption {
106 default = "";
107 type = types.str;
108 description = lib.mdDoc ''
109 If {option}`autoFormat` option is set specifies
110 extra options passed to mkfs.
111 '';
112 };
113
114 autoResize = mkOption {
115 default = false;
116 type = types.bool;
117 description = lib.mdDoc ''
118 If set, the filesystem is grown to its maximum size before
119 being mounted. (This is typically the size of the containing
120 partition.) This is currently only supported for ext2/3/4
121 filesystems that are mounted during early boot.
122 '';
123 };
124
125 noCheck = mkOption {
126 default = false;
127 type = types.bool;
128 description = lib.mdDoc "Disable running fsck on this filesystem.";
129 };
130
131 };
132
133 config = let
134 defaultFormatOptions =
135 # -F needed to allow bare block device without partitions
136 if (builtins.substring 0 3 config.fsType) == "ext" then "-F"
137 # -q needed for non-interactive operations
138 else if config.fsType == "jfs" then "-q"
139 # (same here)
140 else if config.fsType == "reiserfs" then "-q"
141 else null;
142 in {
143 options = mkIf config.autoResize [ "x-nixos.autoresize" ];
144 formatOptions = mkIf (defaultFormatOptions != null) (mkDefault defaultFormatOptions);
145 };
146
147 };
148
149 # Makes sequence of `specialMount device mountPoint options fsType` commands.
150 # `systemMount` should be defined in the sourcing script.
151 makeSpecialMounts = mounts:
152 pkgs.writeText "mounts.sh" (concatMapStringsSep "\n" (mount: ''
153 specialMount "${mount.device}" "${mount.mountPoint}" "${concatStringsSep "," mount.options}" "${mount.fsType}"
154 '') mounts);
155
156 makeFstabEntries =
157 let
158 fsToSkipCheck = [ "none" "bindfs" "btrfs" "zfs" "tmpfs" "nfs" "nfs4" "vboxsf" "glusterfs" "apfs" "9p" "cifs" "prl_fs" "vmhgfs" ];
159 isBindMount = fs: builtins.elem "bind" fs.options;
160 skipCheck = fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck || isBindMount fs;
161 # https://wiki.archlinux.org/index.php/fstab#Filepath_spaces
162 escape = string: builtins.replaceStrings [ " " "\t" ] [ "\\040" "\\011" ] string;
163 in fstabFileSystems: { rootPrefix ? "", excludeChecks ? false, extraOpts ? (fs: []) }: concatMapStrings (fs:
164 (optionalString (isBindMount fs) (escape rootPrefix))
165 + (if fs.device != null then escape fs.device
166 else if fs.label != null then "/dev/disk/by-label/${escape fs.label}"
167 else throw "No device specified for mount point ‘${fs.mountPoint}’.")
168 + " " + escape (rootPrefix + fs.mountPoint)
169 + " " + fs.fsType
170 + " " + escape (builtins.concatStringsSep "," (fs.options ++ (extraOpts fs)))
171 + " " + (optionalString (!excludeChecks)
172 ("0 " + (if skipCheck fs then "0" else if fs.mountPoint == "/" then "1" else "2")))
173 + "\n"
174 ) fstabFileSystems;
175
176 initrdFstab = pkgs.writeText "initrd-fstab" (makeFstabEntries (filter utils.fsNeededForBoot fileSystems) {
177 rootPrefix = "/sysroot";
178 excludeChecks = true;
179 extraOpts = fs:
180 (optional fs.autoResize "x-systemd.growfs")
181 ++ (optional fs.autoFormat "x-systemd.makefs");
182 });
183
184in
185
186{
187
188 ###### interface
189
190 options = {
191
192 fileSystems = mkOption {
193 default = {};
194 example = literalExpression ''
195 {
196 "/".device = "/dev/hda1";
197 "/data" = {
198 device = "/dev/hda2";
199 fsType = "ext3";
200 options = [ "data=journal" ];
201 };
202 "/bigdisk".label = "bigdisk";
203 }
204 '';
205 type = types.attrsOf (types.submodule [coreFileSystemOpts fileSystemOpts]);
206 description = lib.mdDoc ''
207 The file systems to be mounted. It must include an entry for
208 the root directory (`mountPoint = "/"`). Each
209 entry in the list is an attribute set with the following fields:
210 `mountPoint`, `device`,
211 `fsType` (a file system type recognised by
212 {command}`mount`; defaults to
213 `"auto"`), and `options`
214 (the mount options passed to {command}`mount` using the
215 {option}`-o` flag; defaults to `[ "defaults" ]`).
216
217 Instead of specifying `device`, you can also
218 specify a volume label (`label`) for file
219 systems that support it, such as ext2/ext3 (see {command}`mke2fs -L`).
220 '';
221 };
222
223 system.fsPackages = mkOption {
224 internal = true;
225 default = [ ];
226 description = lib.mdDoc "Packages supplying file system mounters and checkers.";
227 };
228
229 boot.supportedFilesystems = mkOption {
230 default = [ ];
231 example = [ "btrfs" ];
232 type = types.listOf types.str;
233 description = lib.mdDoc "Names of supported filesystem types.";
234 };
235
236 boot.specialFileSystems = mkOption {
237 default = {};
238 type = types.attrsOf (types.submodule coreFileSystemOpts);
239 internal = true;
240 description = lib.mdDoc ''
241 Special filesystems that are mounted very early during boot.
242 '';
243 };
244
245 boot.devSize = mkOption {
246 default = "5%";
247 example = "32m";
248 type = types.str;
249 description = lib.mdDoc ''
250 Size limit for the /dev tmpfs. Look at mount(8), tmpfs size option,
251 for the accepted syntax.
252 '';
253 };
254
255 boot.devShmSize = mkOption {
256 default = "50%";
257 example = "256m";
258 type = types.str;
259 description = lib.mdDoc ''
260 Size limit for the /dev/shm tmpfs. Look at mount(8), tmpfs size option,
261 for the accepted syntax.
262 '';
263 };
264
265 boot.runSize = mkOption {
266 default = "25%";
267 example = "256m";
268 type = types.str;
269 description = lib.mdDoc ''
270 Size limit for the /run tmpfs. Look at mount(8), tmpfs size option,
271 for the accepted syntax.
272 '';
273 };
274 };
275
276
277 ###### implementation
278
279 config = {
280
281 assertions = let
282 ls = sep: concatMapStringsSep sep (x: x.mountPoint);
283 notAutoResizable = fs: fs.autoResize && !(hasPrefix "ext" fs.fsType || fs.fsType == "f2fs");
284 in [
285 { assertion = ! (fileSystems' ? cycle);
286 message = "The ‘fileSystems’ option can't be topologically sorted: mountpoint dependency path ${ls " -> " fileSystems'.cycle} loops to ${ls ", " fileSystems'.loops}";
287 }
288 { assertion = ! (any notAutoResizable fileSystems);
289 message = let
290 fs = head (filter notAutoResizable fileSystems);
291 in
292 "Mountpoint '${fs.mountPoint}': 'autoResize = true' is not supported for 'fsType = \"${fs.fsType}\"':${if fs.fsType == "auto" then " fsType has to be explicitly set and" else ""} only the ext filesystems and f2fs support it.";
293 }
294 ];
295
296 # Export for use in other modules
297 system.build.fileSystems = fileSystems;
298 system.build.earlyMountScript = makeSpecialMounts (toposort fsBefore (attrValues config.boot.specialFileSystems)).result;
299
300 boot.supportedFilesystems = map (fs: fs.fsType) fileSystems;
301
302 # Add the mount helpers to the system path so that `mount' can find them.
303 system.fsPackages = [ pkgs.dosfstools ];
304
305 environment.systemPackages = with pkgs; [ fuse3 fuse ] ++ config.system.fsPackages;
306
307 environment.etc.fstab.text =
308 let
309 swapOptions = sw: concatStringsSep "," (
310 sw.options
311 ++ optional (sw.priority != null) "pri=${toString sw.priority}"
312 ++ optional (sw.discardPolicy != null) "discard${optionalString (sw.discardPolicy != "both") "=${toString sw.discardPolicy}"}"
313 );
314 in ''
315 # This is a generated file. Do not edit!
316 #
317 # To make changes, edit the fileSystems and swapDevices NixOS options
318 # in your /etc/nixos/configuration.nix file.
319 #
320 # <file system> <mount point> <type> <options> <dump> <pass>
321
322 # Filesystems.
323 ${makeFstabEntries fileSystems {}}
324
325 # Swap devices.
326 ${flip concatMapStrings config.swapDevices (sw:
327 "${sw.realDevice} none swap ${swapOptions sw}\n"
328 )}
329 '';
330
331 boot.initrd.systemd.contents."/etc/fstab".source = initrdFstab;
332
333 # Provide a target that pulls in all filesystems.
334 systemd.targets.fs =
335 { description = "All File Systems";
336 wants = [ "local-fs.target" "remote-fs.target" ];
337 };
338
339 systemd.services =
340
341 # Emit systemd services to format requested filesystems.
342 let
343 formatDevice = fs:
344 let
345 mountPoint' = "${escapeSystemdPath fs.mountPoint}.mount";
346 device' = escapeSystemdPath fs.device;
347 device'' = "${device'}.device";
348 in nameValuePair "mkfs-${device'}"
349 { description = "Initialisation of Filesystem ${fs.device}";
350 wantedBy = [ mountPoint' ];
351 before = [ mountPoint' "systemd-fsck@${device'}.service" ];
352 requires = [ device'' ];
353 after = [ device'' ];
354 path = [ pkgs.util-linux ] ++ config.system.fsPackages;
355 script =
356 ''
357 if ! [ -e "${fs.device}" ]; then exit 1; fi
358 # FIXME: this is scary. The test could be more robust.
359 type=$(blkid -p -s TYPE -o value "${fs.device}" || true)
360 if [ -z "$type" ]; then
361 echo "creating ${fs.fsType} filesystem on ${fs.device}..."
362 mkfs.${fs.fsType} ${fs.formatOptions} "${fs.device}"
363 fi
364 '';
365 unitConfig.RequiresMountsFor = [ "${dirOf fs.device}" ];
366 unitConfig.DefaultDependencies = false; # needed to prevent a cycle
367 serviceConfig.Type = "oneshot";
368 };
369 in listToAttrs (map formatDevice (filter (fs: fs.autoFormat && !(utils.fsNeededForBoot fs)) fileSystems)) // {
370 # Mount /sys/fs/pstore for evacuating panic logs and crashdumps from persistent storage onto the disk using systemd-pstore.
371 # This cannot be done with the other special filesystems because the pstore module (which creates the mount point) is not loaded then.
372 "mount-pstore" = {
373 serviceConfig = {
374 Type = "oneshot";
375 # skip on kernels without the pstore module
376 ExecCondition = "${pkgs.kmod}/bin/modprobe -b pstore";
377 ExecStart = pkgs.writeShellScript "mount-pstore.sh" ''
378 set -eu
379 # if the pstore module is builtin it will have mounted the persistent store automatically. it may also be already mounted for other reasons.
380 ${pkgs.util-linux}/bin/mountpoint -q /sys/fs/pstore || ${pkgs.util-linux}/bin/mount -t pstore -o nosuid,noexec,nodev pstore /sys/fs/pstore
381 # 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.
382 TRIES=15
383 while [ "$(cat /sys/module/pstore/parameters/backend)" = "(null)" ]; do
384 if (( $TRIES )); then
385 sleep 0.1
386 TRIES=$((TRIES-1))
387 else
388 echo "Persistent Storage backend was not registered in time." >&2
389 break
390 fi
391 done
392 '';
393 RemainAfterExit = true;
394 };
395 unitConfig = {
396 ConditionVirtualization = "!container";
397 DefaultDependencies = false; # needed to prevent a cycle
398 };
399 before = [ "systemd-pstore.service" ];
400 wantedBy = [ "systemd-pstore.service" ];
401 };
402 };
403
404 systemd.tmpfiles.rules = [
405 "d /run/keys 0750 root ${toString config.ids.gids.keys}"
406 "z /run/keys 0750 root ${toString config.ids.gids.keys}"
407 ];
408
409 # Sync mount options with systemd's src/core/mount-setup.c: mount_table.
410 boot.specialFileSystems = {
411 "/proc" = { fsType = "proc"; options = [ "nosuid" "noexec" "nodev" ]; };
412 "/run" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=755" "size=${config.boot.runSize}" ]; };
413 "/dev" = { fsType = "devtmpfs"; options = [ "nosuid" "strictatime" "mode=755" "size=${config.boot.devSize}" ]; };
414 "/dev/shm" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=1777" "size=${config.boot.devShmSize}" ]; };
415 "/dev/pts" = { fsType = "devpts"; options = [ "nosuid" "noexec" "mode=620" "ptmxmode=0666" "gid=${toString config.ids.gids.tty}" ]; };
416
417 # To hold secrets that shouldn't be written to disk
418 "/run/keys" = { fsType = "ramfs"; options = [ "nosuid" "nodev" "mode=750" ]; };
419 } // optionalAttrs (!config.boot.isContainer) {
420 # systemd-nspawn populates /sys by itself, and remounting it causes all
421 # kinds of weird issues (most noticeably, waiting for host disk device
422 # nodes).
423 "/sys" = { fsType = "sysfs"; options = [ "nosuid" "noexec" "nodev" ]; };
424 };
425
426 };
427
428}