at 24.11-pre 15 kB view raw
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}