1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 udev = config.systemd.package;
8
9 cfg = config.services.udev;
10
11 initrdUdevRules = pkgs.runCommand "initrd-udev-rules" {} ''
12 mkdir -p $out/etc/udev/rules.d
13 for f in 60-cdrom_id 60-persistent-storage 75-net-description 80-drivers 80-net-setup-link; do
14 ln -s ${config.boot.initrd.systemd.package}/lib/udev/rules.d/$f.rules $out/etc/udev/rules.d
15 done
16 '';
17
18
19 extraUdevRules = pkgs.writeTextFile {
20 name = "extra-udev-rules";
21 text = cfg.extraRules;
22 destination = "/etc/udev/rules.d/99-local.rules";
23 };
24
25 extraHwdbFile = pkgs.writeTextFile {
26 name = "extra-hwdb-file";
27 text = cfg.extraHwdb;
28 destination = "/etc/udev/hwdb.d/99-local.hwdb";
29 };
30
31 nixosRules = ''
32 # Miscellaneous devices.
33 KERNEL=="kvm", MODE="0666"
34
35 # Needed for gpm.
36 SUBSYSTEM=="input", KERNEL=="mice", TAG+="systemd"
37 '';
38
39 nixosInitrdRules = ''
40 # Mark dm devices as db_persist so that they are kept active after switching root
41 SUBSYSTEM=="block", KERNEL=="dm-[0-9]*", ACTION=="add|change", OPTIONS+="db_persist"
42 '';
43
44 # Perform substitutions in all udev rules files.
45 udevRulesFor = { name, udevPackages, udevPath, udev, systemd, binPackages, initrdBin ? null }: pkgs.runCommand name
46 { preferLocalBuild = true;
47 allowSubstitutes = false;
48 packages = unique (map toString udevPackages);
49 }
50 ''
51 mkdir -p $out
52 shopt -s nullglob
53 set +o pipefail
54
55 # Set a reasonable $PATH for programs called by udev rules.
56 echo 'ENV{PATH}="${udevPath}/bin:${udevPath}/sbin"' > $out/00-path.rules
57
58 # Add the udev rules from other packages.
59 for i in $packages; do
60 echo "Adding rules for package $i"
61 for j in $i/{etc,lib}/udev/rules.d/*; do
62 echo "Copying $j to $out/$(basename $j)"
63 cat $j > $out/$(basename $j)
64 done
65 done
66
67 # Fix some paths in the standard udev rules. Hacky.
68 for i in $out/*.rules; do
69 substituteInPlace $i \
70 --replace \"/sbin/modprobe \"${pkgs.kmod}/bin/modprobe \
71 --replace \"/sbin/mdadm \"${pkgs.mdadm}/sbin/mdadm \
72 --replace \"/sbin/blkid \"${pkgs.util-linux}/sbin/blkid \
73 --replace \"/bin/mount \"${pkgs.util-linux}/bin/mount \
74 --replace /usr/bin/readlink ${pkgs.coreutils}/bin/readlink \
75 --replace /usr/bin/basename ${pkgs.coreutils}/bin/basename 2>/dev/null
76 ${optionalString (initrdBin != null) ''
77 substituteInPlace $i --replace '/run/current-system/systemd' "${removeSuffix "/bin" initrdBin}"
78 ''}
79 done
80
81 echo -n "Checking that all programs called by relative paths in udev rules exist in ${udev}/lib/udev... "
82 import_progs=$(grep 'IMPORT{program}="[^/$]' $out/* |
83 sed -e 's/.*IMPORT{program}="\([^ "]*\)[ "].*/\1/' | uniq)
84 run_progs=$(grep -v '^[[:space:]]*#' $out/* | grep 'RUN+="[^/$]' |
85 sed -e 's/.*RUN+="\([^ "]*\)[ "].*/\1/' | uniq)
86 for i in $import_progs $run_progs; do
87 if [[ ! -x ${udev}/lib/udev/$i && ! $i =~ socket:.* ]]; then
88 echo "FAIL"
89 echo "$i is called in udev rules but not installed by udev"
90 exit 1
91 fi
92 done
93 echo "OK"
94
95 echo -n "Checking that all programs called by absolute paths in udev rules exist... "
96 import_progs=$(grep 'IMPORT{program}="\/' $out/* |
97 sed -e 's/.*IMPORT{program}="\([^ "]*\)[ "].*/\1/' | uniq)
98 run_progs=$(grep -v '^[[:space:]]*#' $out/* | grep 'RUN+="/' |
99 sed -e 's/.*RUN+="\([^ "]*\)[ "].*/\1/' | uniq)
100 for i in $import_progs $run_progs; do
101 # if the path refers to /run/current-system/systemd, replace with config.systemd.package
102 if [[ $i == /run/current-system/systemd* ]]; then
103 i="${systemd}/''${i#/run/current-system/systemd/}"
104 fi
105
106 if [[ ! -x $i ]]; then
107 echo "FAIL"
108 echo "$i is called in udev rules but is not executable or does not exist"
109 exit 1
110 fi
111 done
112 echo "OK"
113
114 filesToFixup="$(for i in "$out"/*; do
115 # list all files referring to (/usr)/bin paths, but allow references to /bin/sh.
116 grep -P -l '\B(?!\/bin\/sh\b)(\/usr)?\/bin(?:\/.*)?' "$i" || :
117 done)"
118
119 if [ -n "$filesToFixup" ]; then
120 echo "Consider fixing the following udev rules:"
121 echo "$filesToFixup" | while read localFile; do
122 remoteFile="origin unknown"
123 for i in ${toString binPackages}; do
124 for j in "$i"/*/udev/rules.d/*; do
125 [ -e "$out/$(basename "$j")" ] || continue
126 [ "$(basename "$j")" = "$(basename "$localFile")" ] || continue
127 remoteFile="originally from $j"
128 break 2
129 done
130 done
131 refs="$(
132 grep -o '\B\(/usr\)\?/s\?bin/[^ "]\+' "$localFile" \
133 | sed -e ':r;N;''${s/\n/ and /;br};s/\n/, /g;br'
134 )"
135 echo "$localFile ($remoteFile) contains references to $refs."
136 done
137 exit 1
138 fi
139
140 # If auto-configuration is disabled, then remove
141 # udev's 80-drivers.rules file, which contains rules for
142 # automatically calling modprobe.
143 ${optionalString (!config.boot.hardwareScan) ''
144 ln -s /dev/null $out/80-drivers.rules
145 ''}
146 '';
147
148 hwdbBin = pkgs.runCommand "hwdb.bin"
149 { preferLocalBuild = true;
150 allowSubstitutes = false;
151 packages = unique (map toString ([udev] ++ cfg.packages));
152 }
153 ''
154 mkdir -p etc/udev/hwdb.d
155 for i in $packages; do
156 echo "Adding hwdb files for package $i"
157 for j in $i/{etc,lib}/udev/hwdb.d/*; do
158 ln -s $j etc/udev/hwdb.d/$(basename $j)
159 done
160 done
161
162 echo "Generating hwdb database..."
163 # hwdb --update doesn't return error code even on errors!
164 res="$(${pkgs.buildPackages.systemd}/bin/systemd-hwdb --root=$(pwd) update 2>&1)"
165 echo "$res"
166 [ -z "$(echo "$res" | egrep '^Error')" ]
167 mv etc/udev/hwdb.bin $out
168 '';
169
170 compressFirmware = firmware:
171 let
172 inherit (config.boot.kernelPackages) kernelAtLeast;
173 in
174 if ! (firmware.compressFirmware or true) then
175 firmware
176 else
177 if kernelAtLeast "5.19" then pkgs.compressFirmwareZstd firmware
178 else if kernelAtLeast "5.3" then pkgs.compressFirmwareXz firmware
179 else firmware;
180
181 # Udev has a 512-character limit for ENV{PATH}, so create a symlink
182 # tree to work around this.
183 udevPath = pkgs.buildEnv {
184 name = "udev-path";
185 paths = cfg.path;
186 pathsToLink = [ "/bin" "/sbin" ];
187 ignoreCollisions = true;
188 };
189
190in
191
192{
193
194 ###### interface
195
196 options = {
197 boot.hardwareScan = mkOption {
198 type = types.bool;
199 default = true;
200 description = ''
201 Whether to try to load kernel modules for all detected hardware.
202 Usually this does a good job of providing you with the modules
203 you need, but sometimes it can crash the system or cause other
204 nasty effects.
205 '';
206 };
207
208 services.udev = {
209 enable = mkEnableOption "udev, a device manager for the Linux kernel" // {
210 default = true;
211 };
212
213 packages = mkOption {
214 type = types.listOf types.path;
215 default = [];
216 description = ''
217 List of packages containing {command}`udev` rules.
218 All files found in
219 {file}`«pkg»/etc/udev/rules.d` and
220 {file}`«pkg»/lib/udev/rules.d`
221 will be included.
222 '';
223 apply = map getBin;
224 };
225
226 path = mkOption {
227 type = types.listOf types.path;
228 default = [];
229 description = ''
230 Packages added to the {env}`PATH` environment variable when
231 executing programs from Udev rules.
232
233 coreutils, gnu{sed,grep}, util-linux and config.systemd.package are
234 automatically included.
235 '';
236 };
237
238 extraRules = mkOption {
239 default = "";
240 example = ''
241 ENV{ID_VENDOR_ID}=="046d", ENV{ID_MODEL_ID}=="0825", ENV{PULSE_IGNORE}="1"
242 '';
243 type = types.lines;
244 description = ''
245 Additional {command}`udev` rules. They'll be written
246 into file {file}`99-local.rules`. Thus they are
247 read and applied after all other rules.
248 '';
249 };
250
251 extraHwdb = mkOption {
252 default = "";
253 example = ''
254 evdev:input:b0003v05AFp8277*
255 KEYBOARD_KEY_70039=leftalt
256 KEYBOARD_KEY_700e2=leftctrl
257 '';
258 type = types.lines;
259 description = ''
260 Additional {command}`hwdb` files. They'll be written
261 into file {file}`99-local.hwdb`. Thus they are
262 read after all other files.
263 '';
264 };
265
266 };
267
268 hardware.firmware = mkOption {
269 type = types.listOf types.package;
270 default = [];
271 description = ''
272 List of packages containing firmware files. Such files
273 will be loaded automatically if the kernel asks for them
274 (i.e., when it has detected specific hardware that requires
275 firmware to function). If multiple packages contain firmware
276 files with the same name, the first package in the list takes
277 precedence. Note that you must rebuild your system if you add
278 files to any of these directories.
279 '';
280 apply = list: pkgs.buildEnv {
281 name = "firmware";
282 paths = map compressFirmware list;
283 pathsToLink = [ "/lib/firmware" ];
284 ignoreCollisions = true;
285 };
286 };
287
288 networking.usePredictableInterfaceNames = mkOption {
289 default = true;
290 type = types.bool;
291 description = ''
292 Whether to assign [predictable names to network interfaces](https://www.freedesktop.org/wiki/Software/systemd/PredictableNetworkInterfaceNames/).
293 If enabled, interfaces
294 are assigned names that contain topology information
295 (e.g. `wlp3s0`) and thus should be stable
296 across reboots. If disabled, names depend on the order in
297 which interfaces are discovered by the kernel, which may
298 change randomly across reboots; for instance, you may find
299 `eth0` and `eth1` flipping
300 unpredictably.
301 '';
302 };
303
304 boot.initrd.services.udev = {
305
306 packages = mkOption {
307 type = types.listOf types.path;
308 default = [];
309 description = ''
310 *This will only be used when systemd is used in stage 1.*
311
312 List of packages containing {command}`udev` rules that will be copied to stage 1.
313 All files found in
314 {file}`«pkg»/etc/udev/rules.d` and
315 {file}`«pkg»/lib/udev/rules.d`
316 will be included.
317 '';
318 };
319
320 binPackages = mkOption {
321 type = types.listOf types.path;
322 default = [];
323 description = ''
324 *This will only be used when systemd is used in stage 1.*
325
326 Packages to search for binaries that are referenced by the udev rules in stage 1.
327 This list always contains /bin of the initrd.
328 '';
329 apply = map getBin;
330 };
331
332 rules = mkOption {
333 default = "";
334 example = ''
335 SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="00:1D:60:B9:6D:4F", KERNEL=="eth*", NAME="my_fast_network_card"
336 '';
337 type = types.lines;
338 description = ''
339 {command}`udev` rules to include in the initrd
340 *only*. They'll be written into file
341 {file}`99-local.rules`. Thus they are read and applied
342 after the essential initrd rules.
343 '';
344 };
345
346 };
347
348 };
349
350
351 ###### implementation
352
353 config = mkIf cfg.enable {
354
355 services.udev.extraRules = nixosRules;
356
357 services.udev.packages = [ extraUdevRules extraHwdbFile ];
358
359 services.udev.path = [ pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.util-linux udev ];
360
361 boot.kernelParams = mkIf (!config.networking.usePredictableInterfaceNames) [ "net.ifnames=0" ];
362
363 boot.initrd.extraUdevRulesCommands = mkIf (!config.boot.initrd.systemd.enable && config.boot.initrd.services.udev.rules != "")
364 ''
365 cat <<'EOF' > $out/99-local.rules
366 ${config.boot.initrd.services.udev.rules}
367 EOF
368 '';
369
370 boot.initrd.services.udev.rules = nixosInitrdRules;
371
372 boot.initrd.systemd.additionalUpstreamUnits = [
373 "initrd-udevadm-cleanup-db.service"
374 "systemd-udevd-control.socket"
375 "systemd-udevd-kernel.socket"
376 "systemd-udevd.service"
377 "systemd-udev-settle.service"
378 "systemd-udev-trigger.service"
379 ];
380 boot.initrd.systemd.storePaths = [
381 "${config.boot.initrd.systemd.package}/lib/systemd/systemd-udevd"
382 "${config.boot.initrd.systemd.package}/lib/udev/ata_id"
383 "${config.boot.initrd.systemd.package}/lib/udev/cdrom_id"
384 "${config.boot.initrd.systemd.package}/lib/udev/scsi_id"
385 "${config.boot.initrd.systemd.package}/lib/udev/rules.d"
386 ] ++ map (x: "${x}/bin") config.boot.initrd.services.udev.binPackages;
387
388 # Generate the udev rules for the initrd
389 boot.initrd.systemd.contents = {
390 "/etc/udev/rules.d".source = udevRulesFor {
391 name = "initrd-udev-rules";
392 initrdBin = config.boot.initrd.systemd.contents."/bin".source;
393 udevPackages = config.boot.initrd.services.udev.packages;
394 udevPath = config.boot.initrd.systemd.contents."/bin".source;
395 udev = config.boot.initrd.systemd.package;
396 systemd = config.boot.initrd.systemd.package;
397 binPackages = config.boot.initrd.services.udev.binPackages ++ [ config.boot.initrd.systemd.contents."/bin".source ];
398 };
399 };
400 # Insert initrd rules
401 boot.initrd.services.udev.packages = [
402 initrdUdevRules
403 (mkIf (config.boot.initrd.services.udev.rules != "") (pkgs.writeTextFile {
404 name = "initrd-udev-rules";
405 destination = "/etc/udev/rules.d/99-local.rules";
406 text = config.boot.initrd.services.udev.rules;
407 }))
408 ];
409
410 environment.etc = {
411 "udev/rules.d".source = udevRulesFor {
412 name = "udev-rules";
413 udevPackages = cfg.packages;
414 systemd = config.systemd.package;
415 binPackages = cfg.packages;
416 inherit udevPath udev;
417 };
418 "udev/hwdb.bin".source = hwdbBin;
419 } // lib.optionalAttrs config.boot.modprobeConfig.enable {
420 # We don't place this into `extraModprobeConfig` so that stage-1 ramdisk doesn't bloat.
421 "modprobe.d/firmware.conf".text = "options firmware_class path=${config.hardware.firmware}/lib/firmware";
422 };
423
424 system.requiredKernelConfig = with config.lib.kernelConfig; [
425 (isEnabled "UNIX")
426 (isYes "INOTIFY_USER")
427 (isYes "NET")
428 ];
429
430 system.activationScripts.udevd = lib.mkIf config.boot.kernel.enable ''
431 # The deprecated hotplug uevent helper is not used anymore
432 if [ -e /proc/sys/kernel/hotplug ]; then
433 echo "" > /proc/sys/kernel/hotplug
434 fi
435
436 # Allow the kernel to find our firmware.
437 if [ -e /sys/module/firmware_class/parameters/path ]; then
438 echo -n "${config.hardware.firmware}/lib/firmware" > /sys/module/firmware_class/parameters/path
439 fi
440 '';
441
442 systemd.services.systemd-udevd =
443 { restartTriggers = cfg.packages;
444 };
445
446 };
447
448 imports = [
449 (mkRenamedOptionModule [ "services" "udev" "initrdRules" ] [ "boot" "initrd" "services" "udev" "rules" ])
450 ];
451}