1{
2 lib,
3 config,
4 utils,
5 pkgs,
6 ...
7}:
8
9let
10 inherit (lib)
11 all
12 any
13 concatLines
14 concatStringsSep
15 escapeShellArg
16 flatten
17 floatToString
18 foldl'
19 head
20 isAttrs
21 isDerivation
22 isFloat
23 isList
24 length
25 listToAttrs
26 match
27 mapAttrsToList
28 nameValuePair
29 removePrefix
30 tail
31 throwIf
32 ;
33
34 inherit (lib.options)
35 showDefs
36 showOption
37 ;
38
39 inherit (lib.strings)
40 escapeC
41 isConvertibleWithToString
42 ;
43
44 inherit (lib.path.subpath) join;
45
46 inherit (utils) escapeSystemdPath;
47
48 cfg = config.boot.kernel.sysfs;
49
50 sysfsAttrs = with lib.types; nullOr (either sysfsValue (attrsOf sysfsAttrs));
51 sysfsValue = lib.mkOptionType {
52 name = "sysfs value";
53 description = "sysfs attribute value";
54 descriptionClass = "noun";
55 check = v: isConvertibleWithToString v;
56 merge =
57 loc: defs:
58 if length defs == 1 then
59 (head defs).value
60 else
61 (foldl' (
62 first: def:
63 # merge definitions if they produce the same value string
64 throwIf (mkValueString first.value != mkValueString def.value)
65 "The option \"${showOption loc}\" has conflicting definition values:${
66 showDefs [
67 first
68 def
69 ]
70 }"
71 first
72 ) (head defs) (tail defs)).value;
73 };
74
75 mapAttrsToListRecursive =
76 fn: set:
77 let
78 recurse =
79 p: v:
80 if isAttrs v && !isDerivation v then mapAttrsToList (n: v: recurse (p ++ [ n ]) v) v else fn p v;
81 in
82 flatten (recurse [ ] set);
83
84 mkPath = p: "/sys" + removePrefix "." (join p);
85 hasGlob = p: any (n: match ''(.*[^\\])?[*?[].*'' n != null) p;
86
87 mkValueString =
88 v:
89 # true will be converted to "1" by toString, saving one branch
90 if v == false then
91 "0"
92 else if isFloat v then
93 floatToString v # warn about loss of precision
94 else if isList v then
95 concatStringsSep "," (map mkValueString v)
96 else
97 toString v;
98
99 # escape whitespace and linebreaks, as well as the escape character itself,
100 # to ensure that field boundaries are always preserved
101 escapeTmpfiles = escapeC [
102 "\t"
103 "\n"
104 "\r"
105 " "
106 "\\"
107 ];
108
109 tmpfiles = pkgs.runCommand "nixos-sysfs-tmpfiles.d" { } (
110 ''
111 mkdir "$out"
112 ''
113 + concatLines (
114 mapAttrsToListRecursive (
115 p: v:
116 let
117 path = mkPath p;
118 in
119 if v == null then
120 [ ]
121 else
122 ''
123 printf 'w %s - - - - %s\n' \
124 ${escapeShellArg (escapeTmpfiles path)} \
125 ${escapeShellArg (escapeTmpfiles (mkValueString v))} \
126 >"$out"/${escapeShellArg (escapeSystemdPath path)}.conf
127 ''
128 ) cfg
129 )
130 );
131in
132{
133 options = {
134 boot.kernel.sysfs = lib.mkOption {
135 type = lib.types.submodule {
136 freeformType = lib.types.attrsOf sysfsAttrs // {
137 description = "nested attribute set of null or sysfs attribute values";
138 };
139 };
140
141 description = ''
142 sysfs attributes to be set as soon as they become available.
143
144 Attribute names represent path components in the sysfs filesystem and
145 cannot be `.` or `..` nor contain any slash character (`/`).
146
147 Names may contain shell‐style glob patterns (`*`, `?` and `[…]`)
148 matching a single path component, these should however be used with
149 caution, as they may produce unexpected results if attribute paths
150 overlap.
151
152 Values will be converted to strings, with list elements concatenated
153 with commata and booleans converted to numeric values (`0` or `1`).
154
155 `null` values are ignored, allowing removal of values defined in other
156 modules, as are empty attribute sets.
157
158 List values defined in different modules will _not_ be concatenated.
159
160 This option may only be used for attributes which can be set
161 idempotently, as the configured values might be written more than once.
162 '';
163
164 default = { };
165
166 example = lib.literalExpression ''
167 {
168 # enable transparent hugepages with deferred defragmentaion
169 kernel.mm.transparent_hugepage = {
170 enabled = "always";
171 defrag = "defer";
172 shmem_enabled = "within_size";
173 };
174
175 devices.system.cpu = {
176 # configure powesave frequency governor for all CPUs
177 # the [0-9]* glob pattern ensures that other paths
178 # like cpufreq or cpuidle are not matched
179 "cpu[0-9]*" = {
180 scaling_governor = "powersave";
181 energy_performance_preference = 8;
182 };
183
184 # disable frequency boost
185 intel_pstate.no_turbo = true;
186 };
187 }
188 '';
189 };
190 };
191
192 config = lib.mkIf (cfg != { }) {
193 systemd = {
194 paths = {
195 "nixos-sysfs@" = {
196 description = "/%I attribute watcher";
197 pathConfig.PathExistsGlob = "/%I";
198 unitConfig.DefaultDependencies = false;
199 };
200 }
201 // listToAttrs (
202 mapAttrsToListRecursive (
203 p: v:
204 if v == null then
205 [ ]
206 else
207 nameValuePair "nixos-sysfs@${escapeSystemdPath (mkPath p)}" {
208 overrideStrategy = "asDropin";
209 wantedBy = [ "sysinit.target" ];
210 before = [ "sysinit.target" ];
211 }
212 ) cfg
213 );
214
215 services."nixos-sysfs@" = {
216 description = "/%I attribute setter";
217
218 unitConfig = {
219 DefaultDependencies = false;
220 AssertPathIsMountPoint = "/sys";
221 AssertPathExistsGlob = "/%I";
222 };
223
224 serviceConfig = {
225 Type = "oneshot";
226 RemainAfterExit = true;
227
228 # while we could be tempted to use simple shell script to set the
229 # sysfs attributes specified by the path or glob pattern, it is
230 # almost impossible to properly escape a glob pattern so that it
231 # can be used safely in a shell script
232 ExecStart = "${lib.getExe' config.systemd.package "systemd-tmpfiles"} --prefix=/sys --create ${tmpfiles}/%i.conf";
233
234 # hardening may be overkill for such a simple and short‐lived
235 # service, the following settings would however be suitable to deny
236 # access to anything but /sys
237 #ProtectProc = "noaccess";
238 #ProcSubset = "pid";
239 #ProtectSystem = "strict";
240 #PrivateDevices = true;
241 #SystemCallErrorNumber = "EPERM";
242 #SystemCallFilter = [
243 # "@basic-io"
244 # "@file-system"
245 #];
246 };
247 };
248 };
249
250 warnings = mapAttrsToListRecursive (
251 p: v:
252 if hasGlob p then
253 "Attribute path \"${concatStringsSep "." p}\" contains glob patterns. Please ensure that it does not overlap with other paths."
254 else
255 [ ]
256 ) cfg;
257
258 assertions = mapAttrsToListRecursive (p: v: {
259 assertion = all (n: match ''(\.\.?|.*/.*)'' n == null) p;
260 message = "Attribute path \"${concatStringsSep "." p}\" has invalid components.";
261 }) cfg;
262 };
263
264 meta.maintainers = with lib.maintainers; [ mvs ];
265}