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