1{ lib, config, pkgs }:
2
3let
4 inherit (lib)
5 any
6 attrNames
7 concatMapStringsSep
8 concatStringsSep
9 elem
10 escapeShellArg
11 filter
12 flatten
13 getName
14 hasPrefix
15 hasSuffix
16 imap0
17 imap1
18 isAttrs
19 isDerivation
20 isFloat
21 isInt
22 isList
23 isPath
24 isString
25 listToAttrs
26 nameValuePair
27 optionalString
28 removePrefix
29 removeSuffix
30 replaceStrings
31 stringToCharacters
32 types
33 ;
34
35 inherit (lib.strings) toJSON normalizePath escapeC;
36in
37
38let
39utils = rec {
40
41 # Copy configuration files to avoid having the entire sources in the system closure
42 copyFile = filePath: pkgs.runCommand (builtins.unsafeDiscardStringContext (baseNameOf filePath)) {} ''
43 cp ${filePath} $out
44 '';
45
46 # Check whenever fileSystem is needed for boot. NOTE: Make sure
47 # pathsNeededForBoot is closed under the parent relationship, i.e. if /a/b/c
48 # is in the list, put /a and /a/b in as well.
49 pathsNeededForBoot = [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/var/lib/nixos" "/etc" "/usr" ];
50 fsNeededForBoot = fs: fs.neededForBoot || elem fs.mountPoint pathsNeededForBoot;
51
52 # Check whenever `b` depends on `a` as a fileSystem
53 fsBefore = a: b:
54 let
55 # normalisePath adds a slash at the end of the path if it didn't already
56 # have one.
57 #
58 # The reason slashes are added at the end of each path is to prevent `b`
59 # from accidentally depending on `a` in cases like
60 # a = { mountPoint = "/aaa"; ... }
61 # b = { device = "/aaaa"; ... }
62 # Here a.mountPoint *is* a prefix of b.device even though a.mountPoint is
63 # *not* a parent of b.device. If we add a slash at the end of each string,
64 # though, this is not a problem: "/aaa/" is not a prefix of "/aaaa/".
65 normalisePath = path: "${path}${optionalString (!(hasSuffix "/" path)) "/"}";
66 normalise = mount: mount // { device = normalisePath (toString mount.device);
67 mountPoint = normalisePath mount.mountPoint;
68 depends = map normalisePath mount.depends;
69 };
70
71 a' = normalise a;
72 b' = normalise b;
73
74 in hasPrefix a'.mountPoint b'.device
75 || hasPrefix a'.mountPoint b'.mountPoint
76 || any (hasPrefix a'.mountPoint) b'.depends;
77
78 # Escape a path according to the systemd rules. FIXME: slow
79 # The rules are described in systemd.unit(5) as follows:
80 # The escaping algorithm operates as follows: given a string, any "/" character is replaced by "-", and all other characters which are not ASCII alphanumerics, ":", "_" or "." are replaced by C-style "\x2d" escapes. In addition, "." is replaced with such a C-style escape when it would appear as the first character in the escaped string.
81 # When the input qualifies as absolute file system path, this algorithm is extended slightly: the path to the root directory "/" is encoded as single dash "-". In addition, any leading, trailing or duplicate "/" characters are removed from the string before transformation. Example: /foo//bar/baz/ becomes "foo-bar-baz".
82 escapeSystemdPath = s: let
83 replacePrefix = p: r: s: (if (hasPrefix p s) then r + (removePrefix p s) else s);
84 trim = s: removeSuffix "/" (removePrefix "/" s);
85 normalizedPath = normalizePath s;
86 in
87 replaceStrings ["/"] ["-"]
88 (replacePrefix "." (escapeC ["."] ".")
89 (escapeC (stringToCharacters " !\"#$%&'()*+,;<=>=@[\\]^`{|}~-")
90 (if normalizedPath == "/" then normalizedPath else trim normalizedPath)));
91
92 # Quotes an argument for use in Exec* service lines.
93 # systemd accepts "-quoted strings with escape sequences, toJSON produces
94 # a subset of these.
95 # Additionally we escape % to disallow expansion of % specifiers. Any lone ;
96 # in the input will be turned it ";" and thus lose its special meaning.
97 # Every $ is escaped to $$, this makes it unnecessary to disable environment
98 # substitution for the directive.
99 escapeSystemdExecArg = arg:
100 let
101 s = if isPath arg then "${arg}"
102 else if isString arg then arg
103 else if isInt arg || isFloat arg || isDerivation arg then toString arg
104 else throw "escapeSystemdExecArg only allows strings, paths, numbers and derivations";
105 in
106 replaceStrings [ "%" "$" ] [ "%%" "$$" ] (toJSON s);
107
108 # Quotes a list of arguments into a single string for use in a Exec*
109 # line.
110 escapeSystemdExecArgs = concatMapStringsSep " " escapeSystemdExecArg;
111
112 # Returns a system path for a given shell package
113 toShellPath = shell:
114 if types.shellPackage.check shell then
115 "/run/current-system/sw${shell.shellPath}"
116 else if types.package.check shell then
117 throw "${shell} is not a shell package"
118 else
119 shell;
120
121 /* Recurse into a list or an attrset, searching for attrs named like
122 the value of the "attr" parameter, and return an attrset where the
123 names are the corresponding jq path where the attrs were found and
124 the values are the values of the attrs.
125
126 Example:
127 recursiveGetAttrWithJqPrefix {
128 example = [
129 {
130 irrelevant = "not interesting";
131 }
132 {
133 ignored = "ignored attr";
134 relevant = {
135 secret = {
136 _secret = "/path/to/secret";
137 };
138 };
139 }
140 ];
141 } "_secret" -> { ".example[1].relevant.secret" = "/path/to/secret"; }
142 */
143 recursiveGetAttrWithJqPrefix = item: attr:
144 let
145 recurse = prefix: item:
146 if item ? ${attr} then
147 nameValuePair prefix item.${attr}
148 else if isDerivation item then []
149 else if isAttrs item then
150 map (name:
151 let
152 escapedName = ''"${replaceStrings [''"'' "\\"] [''\"'' "\\\\"] name}"'';
153 in
154 recurse (prefix + "." + escapedName) item.${name}) (attrNames item)
155 else if isList item then
156 imap0 (index: item: recurse (prefix + "[${toString index}]") item) item
157 else
158 [];
159 in listToAttrs (flatten (recurse "" item));
160
161 /* Takes an attrset and a file path and generates a bash snippet that
162 outputs a JSON file at the file path with all instances of
163
164 { _secret = "/path/to/secret" }
165
166 in the attrset replaced with the contents of the file
167 "/path/to/secret" in the output JSON.
168
169 When a configuration option accepts an attrset that is finally
170 converted to JSON, this makes it possible to let the user define
171 arbitrary secret values.
172
173 Example:
174 If the file "/path/to/secret" contains the string
175 "topsecretpassword1234",
176
177 genJqSecretsReplacementSnippet {
178 example = [
179 {
180 irrelevant = "not interesting";
181 }
182 {
183 ignored = "ignored attr";
184 relevant = {
185 secret = {
186 _secret = "/path/to/secret";
187 };
188 };
189 }
190 ];
191 } "/path/to/output.json"
192
193 would generate a snippet that, when run, outputs the following
194 JSON file at "/path/to/output.json":
195
196 {
197 "example": [
198 {
199 "irrelevant": "not interesting"
200 },
201 {
202 "ignored": "ignored attr",
203 "relevant": {
204 "secret": "topsecretpassword1234"
205 }
206 }
207 ]
208 }
209 */
210 genJqSecretsReplacementSnippet = genJqSecretsReplacementSnippet' "_secret";
211
212 # Like genJqSecretsReplacementSnippet, but allows the name of the
213 # attr which identifies the secret to be changed.
214 genJqSecretsReplacementSnippet' = attr: set: output:
215 let
216 secrets = recursiveGetAttrWithJqPrefix set attr;
217 stringOrDefault = str: def: if str == "" then def else str;
218 in ''
219 if [[ -h '${output}' ]]; then
220 rm '${output}'
221 fi
222
223 inherit_errexit_enabled=0
224 shopt -pq inherit_errexit && inherit_errexit_enabled=1
225 shopt -s inherit_errexit
226 ''
227 + concatStringsSep
228 "\n"
229 (imap1 (index: name: ''
230 secret${toString index}=$(<'${secrets.${name}}')
231 export secret${toString index}
232 '')
233 (attrNames secrets))
234 + "\n"
235 + "${pkgs.jq}/bin/jq >'${output}' "
236 + escapeShellArg (stringOrDefault
237 (concatStringsSep
238 " | "
239 (imap1 (index: name: ''${name} = $ENV.secret${toString index}'')
240 (attrNames secrets)))
241 ".")
242 + ''
243 <<'EOF'
244 ${toJSON set}
245 EOF
246 (( ! $inherit_errexit_enabled )) && shopt -u inherit_errexit
247 '';
248
249 /* Remove packages of packagesToRemove from packages, based on their names.
250 Relies on package names and has quadratic complexity so use with caution!
251
252 Type:
253 removePackagesByName :: [package] -> [package] -> [package]
254
255 Example:
256 removePackagesByName [ nautilus file-roller ] [ file-roller totem ]
257 => [ nautilus ]
258 */
259 removePackagesByName = packages: packagesToRemove:
260 let
261 namesToRemove = map getName packagesToRemove;
262 in
263 filter (x: !(elem (getName x) namesToRemove)) packages;
264
265 systemdUtils = {
266 lib = import ./systemd-lib.nix { inherit lib config pkgs utils; };
267 unitOptions = import ./systemd-unit-options.nix { inherit lib systemdUtils; };
268 types = import ./systemd-types.nix { inherit lib systemdUtils pkgs; };
269 network = {
270 units = import ./systemd-network-units.nix { inherit lib systemdUtils; };
271 };
272 };
273};
274in utils