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