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