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 nonEmptyStr = addCheckDesc "non-empty" types.str
11 (x: x != "" && ! (all (c: c == " " || c == "\t") (stringToCharacters x)));
12
13 fileSystems' = toposort fsBefore (attrValues config.fileSystems);
14
15 fileSystems = if fileSystems' ? "result"
16 then # use topologically sorted fileSystems everywhere
17 fileSystems'.result
18 else # the assertion below will catch this,
19 # but we fall back to the original order
20 # anyway so that other modules could check
21 # their assertions too
22 (attrValues config.fileSystems);
23
24 prioOption = prio: optionalString (prio != null) " pri=${toString prio}";
25
26 specialFSTypes = [ "proc" "sysfs" "tmpfs" "ramfs" "devtmpfs" "devpts" ];
27
28 coreFileSystemOpts = { name, config, ... }: {
29
30 options = {
31
32 mountPoint = mkOption {
33 example = "/mnt/usb";
34 type = nonEmptyStr;
35 description = "Location of the mounted the file system.";
36 };
37
38 device = mkOption {
39 default = null;
40 example = "/dev/sda";
41 type = types.nullOr nonEmptyStr;
42 description = "Location of the device.";
43 };
44
45 fsType = mkOption {
46 default = "auto";
47 example = "ext3";
48 type = nonEmptyStr;
49 description = "Type of the file system.";
50 };
51
52 options = mkOption {
53 default = [ "defaults" ];
54 example = [ "data=journal" ];
55 description = "Options used to mount the file system.";
56 type = types.listOf nonEmptyStr;
57 };
58
59 };
60
61 config = {
62 mountPoint = mkDefault name;
63 device = mkIf (elem config.fsType specialFSTypes) (mkDefault config.fsType);
64 };
65
66 };
67
68 fileSystemOpts = { config, ... }: {
69
70 options = {
71
72 label = mkOption {
73 default = null;
74 example = "root-partition";
75 type = types.nullOr nonEmptyStr;
76 description = "Label of the device (if any).";
77 };
78
79 autoFormat = mkOption {
80 default = false;
81 type = types.bool;
82 description = ''
83 If the device does not currently contain a filesystem (as
84 determined by <command>blkid</command>, then automatically
85 format it with the filesystem type specified in
86 <option>fsType</option>. Use with caution.
87 '';
88 };
89
90 formatOptions = mkOption {
91 default = "";
92 type = types.str;
93 description = ''
94 If <option>autoFormat</option> option is set specifies
95 extra options passed to mkfs.
96 '';
97 };
98
99 autoResize = mkOption {
100 default = false;
101 type = types.bool;
102 description = ''
103 If set, the filesystem is grown to its maximum size before
104 being mounted. (This is typically the size of the containing
105 partition.) This is currently only supported for ext2/3/4
106 filesystems that are mounted during early boot.
107 '';
108 };
109
110 noCheck = mkOption {
111 default = false;
112 type = types.bool;
113 description = "Disable running fsck on this filesystem.";
114 };
115
116 };
117
118 config = let
119 defaultFormatOptions =
120 # -F needed to allow bare block device without partitions
121 if (builtins.substring 0 3 config.fsType) == "ext" then "-F"
122 # -q needed for non-interactive operations
123 else if config.fsType == "jfs" then "-q"
124 # (same here)
125 else if config.fsType == "reiserfs" then "-q"
126 else null;
127 in {
128 options = mkIf config.autoResize [ "x-nixos.autoresize" ];
129 formatOptions = mkIf (defaultFormatOptions != null) (mkDefault defaultFormatOptions);
130 };
131
132 };
133
134 # Makes sequence of `specialMount device mountPoint options fsType` commands.
135 # `systemMount` should be defined in the sourcing script.
136 makeSpecialMounts = mounts:
137 pkgs.writeText "mounts.sh" (concatMapStringsSep "\n" (mount: ''
138 specialMount "${mount.device}" "${mount.mountPoint}" "${concatStringsSep "," mount.options}" "${mount.fsType}"
139 '') mounts);
140
141in
142
143{
144
145 ###### interface
146
147 options = {
148
149 fileSystems = mkOption {
150 default = {};
151 example = literalExample ''
152 {
153 "/".device = "/dev/hda1";
154 "/data" = {
155 device = "/dev/hda2";
156 fsType = "ext3";
157 options = [ "data=journal" ];
158 };
159 "/bigdisk".label = "bigdisk";
160 }
161 '';
162 type = types.loaOf (types.submodule [coreFileSystemOpts fileSystemOpts]);
163 description = ''
164 The file systems to be mounted. It must include an entry for
165 the root directory (<literal>mountPoint = "/"</literal>). Each
166 entry in the list is an attribute set with the following fields:
167 <literal>mountPoint</literal>, <literal>device</literal>,
168 <literal>fsType</literal> (a file system type recognised by
169 <command>mount</command>; defaults to
170 <literal>"auto"</literal>), and <literal>options</literal>
171 (the mount options passed to <command>mount</command> using the
172 <option>-o</option> flag; defaults to <literal>[ "defaults" ]</literal>).
173
174 Instead of specifying <literal>device</literal>, you can also
175 specify a volume label (<literal>label</literal>) for file
176 systems that support it, such as ext2/ext3 (see <command>mke2fs
177 -L</command>).
178 '';
179 };
180
181 system.fsPackages = mkOption {
182 internal = true;
183 default = [ ];
184 description = "Packages supplying file system mounters and checkers.";
185 };
186
187 boot.supportedFilesystems = mkOption {
188 default = [ ];
189 example = [ "btrfs" ];
190 type = types.listOf types.str;
191 description = "Names of supported filesystem types.";
192 };
193
194 boot.specialFileSystems = mkOption {
195 default = {};
196 type = types.loaOf (types.submodule coreFileSystemOpts);
197 internal = true;
198 description = ''
199 Special filesystems that are mounted very early during boot.
200 '';
201 };
202
203 };
204
205
206 ###### implementation
207
208 config = {
209
210 assertions = let
211 ls = sep: concatMapStringsSep sep (x: x.mountPoint);
212 in [
213 { assertion = ! (fileSystems' ? "cycle");
214 message = "The ‘fileSystems’ option can't be topologically sorted: mountpoint dependency path ${ls " -> " fileSystems'.cycle} loops to ${ls ", " fileSystems'.loops}";
215 }
216 ];
217
218 # Export for use in other modules
219 system.build.fileSystems = fileSystems;
220 system.build.earlyMountScript = makeSpecialMounts (toposort fsBefore (attrValues config.boot.specialFileSystems)).result;
221
222 boot.supportedFilesystems = map (fs: fs.fsType) fileSystems;
223
224 # Add the mount helpers to the system path so that `mount' can find them.
225 system.fsPackages = [ pkgs.dosfstools ];
226
227 environment.systemPackages = with pkgs; [ fuse3 fuse ] ++ config.system.fsPackages;
228
229 environment.etc.fstab.text =
230 let
231 fsToSkipCheck = [ "none" "bindfs" "btrfs" "zfs" "tmpfs" "nfs" "vboxsf" "glusterfs" ];
232 skipCheck = fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck;
233 in ''
234 # This is a generated file. Do not edit!
235 #
236 # To make changes, edit the fileSystems and swapDevices NixOS options
237 # in your /etc/nixos/configuration.nix file.
238
239 # Filesystems.
240 ${concatMapStrings (fs:
241 (if fs.device != null then fs.device
242 else if fs.label != null then "/dev/disk/by-label/${fs.label}"
243 else throw "No device specified for mount point ‘${fs.mountPoint}’.")
244 + " " + fs.mountPoint
245 + " " + fs.fsType
246 + " " + builtins.concatStringsSep "," fs.options
247 + " 0"
248 + " " + (if skipCheck fs then "0" else
249 if fs.mountPoint == "/" then "1" else "2")
250 + "\n"
251 ) fileSystems}
252
253 # Swap devices.
254 ${flip concatMapStrings config.swapDevices (sw:
255 "${sw.realDevice} none swap${prioOption sw.priority}\n"
256 )}
257 '';
258
259 # Provide a target that pulls in all filesystems.
260 systemd.targets.fs =
261 { description = "All File Systems";
262 wants = [ "local-fs.target" "remote-fs.target" ];
263 };
264
265 # Emit systemd services to format requested filesystems.
266 systemd.services =
267 let
268
269 formatDevice = fs:
270 let
271 mountPoint' = "${escapeSystemdPath fs.mountPoint}.mount";
272 device' = escapeSystemdPath fs.device;
273 device'' = "${device'}.device";
274 in nameValuePair "mkfs-${device'}"
275 { description = "Initialisation of Filesystem ${fs.device}";
276 wantedBy = [ mountPoint' ];
277 before = [ mountPoint' "systemd-fsck@${device'}.service" ];
278 requires = [ device'' ];
279 after = [ device'' ];
280 path = [ pkgs.utillinux ] ++ config.system.fsPackages;
281 script =
282 ''
283 if ! [ -e "${fs.device}" ]; then exit 1; fi
284 # FIXME: this is scary. The test could be more robust.
285 type=$(blkid -p -s TYPE -o value "${fs.device}" || true)
286 if [ -z "$type" ]; then
287 echo "creating ${fs.fsType} filesystem on ${fs.device}..."
288 mkfs.${fs.fsType} ${fs.formatOptions} "${fs.device}"
289 fi
290 '';
291 unitConfig.RequiresMountsFor = [ "${dirOf fs.device}" ];
292 unitConfig.DefaultDependencies = false; # needed to prevent a cycle
293 serviceConfig.Type = "oneshot";
294 };
295
296 in listToAttrs (map formatDevice (filter (fs: fs.autoFormat) fileSystems));
297
298 # Sync mount options with systemd's src/core/mount-setup.c: mount_table.
299 boot.specialFileSystems = {
300 "/proc" = { fsType = "proc"; options = [ "nosuid" "noexec" "nodev" ]; };
301 "/run" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=755" "size=${config.boot.runSize}" ]; };
302 "/dev" = { fsType = "devtmpfs"; options = [ "nosuid" "strictatime" "mode=755" "size=${config.boot.devSize}" ]; };
303 "/dev/shm" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=1777" "size=${config.boot.devShmSize}" ]; };
304 "/dev/pts" = { fsType = "devpts"; options = [ "nosuid" "noexec" "mode=620" "ptmxmode=0666" "gid=${toString config.ids.gids.tty}" ]; };
305
306 # To hold secrets that shouldn't be written to disk (generally used for NixOps, harmless elsewhere)
307 "/run/keys" = { fsType = "ramfs"; options = [ "nosuid" "nodev" "mode=750" "gid=${toString config.ids.gids.keys}" ]; };
308 } // optionalAttrs (!config.boot.isContainer) {
309 # systemd-nspawn populates /sys by itself, and remounting it causes all
310 # kinds of weird issues (most noticeably, waiting for host disk device
311 # nodes).
312 "/sys" = { fsType = "sysfs"; options = [ "nosuid" "noexec" "nodev" ]; };
313 };
314
315 };
316
317}