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