1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 udev = config.systemd.package;
8
9 cfg = config.services.udev;
10
11 extraUdevRules = pkgs.writeTextFile {
12 name = "extra-udev-rules";
13 text = cfg.extraRules;
14 destination = "/etc/udev/rules.d/99-local.rules";
15 };
16
17 extraHwdbFile = pkgs.writeTextFile {
18 name = "extra-hwdb-file";
19 text = cfg.extraHwdb;
20 destination = "/etc/udev/hwdb.d/99-local.hwdb";
21 };
22
23 nixosRules = ''
24 # Miscellaneous devices.
25 KERNEL=="kvm", MODE="0666"
26 KERNEL=="kqemu", MODE="0666"
27
28 # Needed for gpm.
29 SUBSYSTEM=="input", KERNEL=="mice", TAG+="systemd"
30 '';
31
32 # Perform substitutions in all udev rules files.
33 udevRules = pkgs.runCommand "udev-rules"
34 { preferLocalBuild = true;
35 allowSubstitutes = false;
36 packages = unique (map toString cfg.packages);
37 }
38 ''
39 mkdir -p $out
40 shopt -s nullglob
41 set +o pipefail
42
43 # Set a reasonable $PATH for programs called by udev rules.
44 echo 'ENV{PATH}="${udevPath}/bin:${udevPath}/sbin"' > $out/00-path.rules
45
46 # Add the udev rules from other packages.
47 for i in $packages; do
48 echo "Adding rules for package $i"
49 for j in $i/{etc,lib}/udev/rules.d/*; do
50 echo "Copying $j to $out/$(basename $j)"
51 cat $j > $out/$(basename $j)
52 done
53 done
54
55 # Fix some paths in the standard udev rules. Hacky.
56 for i in $out/*.rules; do
57 substituteInPlace $i \
58 --replace \"/sbin/modprobe \"${pkgs.kmod}/bin/modprobe \
59 --replace \"/sbin/mdadm \"${pkgs.mdadm}/sbin/mdadm \
60 --replace \"/sbin/blkid \"${pkgs.util-linux}/sbin/blkid \
61 --replace \"/bin/mount \"${pkgs.util-linux}/bin/mount \
62 --replace /usr/bin/readlink ${pkgs.coreutils}/bin/readlink \
63 --replace /usr/bin/basename ${pkgs.coreutils}/bin/basename
64 done
65
66 echo -n "Checking that all programs called by relative paths in udev rules exist in ${udev}/lib/udev... "
67 import_progs=$(grep 'IMPORT{program}="[^/$]' $out/* |
68 sed -e 's/.*IMPORT{program}="\([^ "]*\)[ "].*/\1/' | uniq)
69 run_progs=$(grep -v '^[[:space:]]*#' $out/* | grep 'RUN+="[^/$]' |
70 sed -e 's/.*RUN+="\([^ "]*\)[ "].*/\1/' | uniq)
71 for i in $import_progs $run_progs; do
72 if [[ ! -x ${udev}/lib/udev/$i && ! $i =~ socket:.* ]]; then
73 echo "FAIL"
74 echo "$i is called in udev rules but not installed by udev"
75 exit 1
76 fi
77 done
78 echo "OK"
79
80 echo -n "Checking that all programs called by absolute paths in udev rules exist... "
81 import_progs=$(grep 'IMPORT{program}="\/' $out/* |
82 sed -e 's/.*IMPORT{program}="\([^ "]*\)[ "].*/\1/' | uniq)
83 run_progs=$(grep -v '^[[:space:]]*#' $out/* | grep 'RUN+="/' |
84 sed -e 's/.*RUN+="\([^ "]*\)[ "].*/\1/' | uniq)
85 for i in $import_progs $run_progs; do
86 # if the path refers to /run/current-system/systemd, replace with config.systemd.package
87 if [[ $i == /run/current-system/systemd* ]]; then
88 i="${config.systemd.package}/''${i#/run/current-system/systemd/}"
89 fi
90 if [[ ! -x $i ]]; then
91 echo "FAIL"
92 echo "$i is called in udev rules but is not executable or does not exist"
93 exit 1
94 fi
95 done
96 echo "OK"
97
98 filesToFixup="$(for i in "$out"/*; do
99 grep -l '\B\(/usr\)\?/s\?bin' "$i" || :
100 done)"
101
102 if [ -n "$filesToFixup" ]; then
103 echo "Consider fixing the following udev rules:"
104 echo "$filesToFixup" | while read localFile; do
105 remoteFile="origin unknown"
106 for i in ${toString cfg.packages}; do
107 for j in "$i"/*/udev/rules.d/*; do
108 [ -e "$out/$(basename "$j")" ] || continue
109 [ "$(basename "$j")" = "$(basename "$localFile")" ] || continue
110 remoteFile="originally from $j"
111 break 2
112 done
113 done
114 refs="$(
115 grep -o '\B\(/usr\)\?/s\?bin/[^ "]\+' "$localFile" \
116 | sed -e ':r;N;''${s/\n/ and /;br};s/\n/, /g;br'
117 )"
118 echo "$localFile ($remoteFile) contains references to $refs."
119 done
120 exit 1
121 fi
122
123 # If auto-configuration is disabled, then remove
124 # udev's 80-drivers.rules file, which contains rules for
125 # automatically calling modprobe.
126 ${optionalString (!config.boot.hardwareScan) ''
127 ln -s /dev/null $out/80-drivers.rules
128 ''}
129 ''; # */
130
131 hwdbBin = pkgs.runCommand "hwdb.bin"
132 { preferLocalBuild = true;
133 allowSubstitutes = false;
134 packages = unique (map toString ([udev] ++ cfg.packages));
135 }
136 ''
137 mkdir -p etc/udev/hwdb.d
138 for i in $packages; do
139 echo "Adding hwdb files for package $i"
140 for j in $i/{etc,lib}/udev/hwdb.d/*; do
141 ln -s $j etc/udev/hwdb.d/$(basename $j)
142 done
143 done
144
145 echo "Generating hwdb database..."
146 # hwdb --update doesn't return error code even on errors!
147 res="$(${pkgs.buildPackages.udev}/bin/udevadm hwdb --update --root=$(pwd) 2>&1)"
148 echo "$res"
149 [ -z "$(echo "$res" | egrep '^Error')" ]
150 mv etc/udev/hwdb.bin $out
151 '';
152
153 # Udev has a 512-character limit for ENV{PATH}, so create a symlink
154 # tree to work around this.
155 udevPath = pkgs.buildEnv {
156 name = "udev-path";
157 paths = cfg.path;
158 pathsToLink = [ "/bin" "/sbin" ];
159 ignoreCollisions = true;
160 };
161
162in
163
164{
165
166 ###### interface
167
168 options = {
169
170 boot.hardwareScan = mkOption {
171 type = types.bool;
172 default = true;
173 description = ''
174 Whether to try to load kernel modules for all detected hardware.
175 Usually this does a good job of providing you with the modules
176 you need, but sometimes it can crash the system or cause other
177 nasty effects.
178 '';
179 };
180
181 services.udev = {
182
183 packages = mkOption {
184 type = types.listOf types.path;
185 default = [];
186 description = ''
187 List of packages containing <command>udev</command> rules.
188 All files found in
189 <filename><replaceable>pkg</replaceable>/etc/udev/rules.d</filename> and
190 <filename><replaceable>pkg</replaceable>/lib/udev/rules.d</filename>
191 will be included.
192 '';
193 apply = map getBin;
194 };
195
196 path = mkOption {
197 type = types.listOf types.path;
198 default = [];
199 description = ''
200 Packages added to the <envar>PATH</envar> environment variable when
201 executing programs from Udev rules.
202 '';
203 };
204
205 initrdRules = mkOption {
206 default = "";
207 example = ''
208 SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="00:1D:60:B9:6D:4F", KERNEL=="eth*", NAME="my_fast_network_card"
209 '';
210 type = types.lines;
211 description = ''
212 <command>udev</command> rules to include in the initrd
213 <emphasis>only</emphasis>. They'll be written into file
214 <filename>99-local.rules</filename>. Thus they are read and applied
215 after the essential initrd rules.
216 '';
217 };
218
219 extraRules = mkOption {
220 default = "";
221 example = ''
222 ENV{ID_VENDOR_ID}=="046d", ENV{ID_MODEL_ID}=="0825", ENV{PULSE_IGNORE}="1"
223 '';
224 type = types.lines;
225 description = ''
226 Additional <command>udev</command> rules. They'll be written
227 into file <filename>99-local.rules</filename>. Thus they are
228 read and applied after all other rules.
229 '';
230 };
231
232 extraHwdb = mkOption {
233 default = "";
234 example = ''
235 evdev:input:b0003v05AFp8277*
236 KEYBOARD_KEY_70039=leftalt
237 KEYBOARD_KEY_700e2=leftctrl
238 '';
239 type = types.lines;
240 description = ''
241 Additional <command>hwdb</command> files. They'll be written
242 into file <filename>99-local.hwdb</filename>. Thus they are
243 read after all other files.
244 '';
245 };
246
247 };
248
249 hardware.firmware = mkOption {
250 type = types.listOf types.package;
251 default = [];
252 description = ''
253 List of packages containing firmware files. Such files
254 will be loaded automatically if the kernel asks for them
255 (i.e., when it has detected specific hardware that requires
256 firmware to function). If multiple packages contain firmware
257 files with the same name, the first package in the list takes
258 precedence. Note that you must rebuild your system if you add
259 files to any of these directories.
260 '';
261 apply = list: pkgs.buildEnv {
262 name = "firmware";
263 paths = list;
264 pathsToLink = [ "/lib/firmware" ];
265 ignoreCollisions = true;
266 };
267 };
268
269 networking.usePredictableInterfaceNames = mkOption {
270 default = true;
271 type = types.bool;
272 description = ''
273 Whether to assign <link
274 xlink:href='http://www.freedesktop.org/wiki/Software/systemd/PredictableNetworkInterfaceNames'>predictable
275 names to network interfaces</link>. If enabled, interfaces
276 are assigned names that contain topology information
277 (e.g. <literal>wlp3s0</literal>) and thus should be stable
278 across reboots. If disabled, names depend on the order in
279 which interfaces are discovered by the kernel, which may
280 change randomly across reboots; for instance, you may find
281 <literal>eth0</literal> and <literal>eth1</literal> flipping
282 unpredictably.
283 '';
284 };
285
286 };
287
288
289 ###### implementation
290
291 config = mkIf (!config.boot.isContainer) {
292
293 services.udev.extraRules = nixosRules;
294
295 services.udev.packages = [ extraUdevRules extraHwdbFile ];
296
297 services.udev.path = [ pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.util-linux udev ];
298
299 boot.kernelParams = mkIf (!config.networking.usePredictableInterfaceNames) [ "net.ifnames=0" ];
300
301 boot.initrd.extraUdevRulesCommands = optionalString (cfg.initrdRules != "")
302 ''
303 cat <<'EOF' > $out/99-local.rules
304 ${cfg.initrdRules}
305 EOF
306 '';
307
308 environment.etc =
309 {
310 "udev/rules.d".source = udevRules;
311 "udev/hwdb.bin".source = hwdbBin;
312 };
313
314 system.requiredKernelConfig = with config.lib.kernelConfig; [
315 (isEnabled "UNIX")
316 (isYes "INOTIFY_USER")
317 (isYes "NET")
318 ];
319
320 boot.extraModprobeConfig = "options firmware_class path=${config.hardware.firmware}/lib/firmware";
321
322 system.activationScripts.udevd =
323 ''
324 # The deprecated hotplug uevent helper is not used anymore
325 if [ -e /proc/sys/kernel/hotplug ]; then
326 echo "" > /proc/sys/kernel/hotplug
327 fi
328
329 # Allow the kernel to find our firmware.
330 if [ -e /sys/module/firmware_class/parameters/path ]; then
331 echo -n "${config.hardware.firmware}/lib/firmware" > /sys/module/firmware_class/parameters/path
332 fi
333 '';
334
335 systemd.services.systemd-udevd =
336 { restartTriggers = cfg.packages;
337 };
338
339 };
340}