1 { lib, config, pkgs }: with lib;
5 # Copy configuration files to avoid having the entire sources in the system closure
6 copyFile = filePath: pkgs.runCommand (builtins.unsafeDiscardStringContext (builtins.baseNameOf filePath)) {} ''
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;
16 # Check whenever `b` depends on `a` as a fileSystem
19 # normalisePath adds a slash at the end of the path if it didn't already
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;
38 in hasPrefix a'.mountPoint b'.device
39 || hasPrefix a'.mountPoint b'.mountPoint
40 || any (hasPrefix a'.mountPoint) b'.depends;
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;
51 replaceStrings ["/"] ["-"]
52 (replacePrefix "." (strings.escapeC ["."] ".")
53 (strings.escapeC (stringToCharacters " !\"#$%&'()*+,;<=>=@[\\]^`{|}~-")
54 (if normalizedPath == "/" then normalizedPath else trim normalizedPath)));
56 # Quotes an argument for use in Exec* service lines.
57 # systemd accepts "-quoted strings with escape sequences, toJSON produces
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:
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";
70 replaceStrings [ "%" "$" ] [ "%%" "$$" ] (builtins.toJSON s);
72 # Quotes a list of arguments into a single string for use in a Exec*
74 escapeSystemdExecArgs = concatMapStringsSep " " escapeSystemdExecArg;
76 # Returns a system path for a given shell package
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"
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.
91 recursiveGetAttrWithJqPrefix {
94 irrelevant = "not interesting";
97 ignored = "ignored attr";
100 _secret = "/path/to/secret";
105 } "_secret" -> { ".example[1].relevant.secret" = "/path/to/secret"; }
107 recursiveGetAttrWithJqPrefix = item: attr:
109 recurse = prefix: item:
110 if item ? ${attr} then
111 nameValuePair prefix item.${attr}
112 else if isAttrs item then
115 escapedName = ''"${replaceStrings [''"'' "\\"] [''\"'' "\\\\"] name}"'';
117 recurse (prefix + "." + escapedName) item.${name}) (attrNames item)
118 else if isList item then
119 imap0 (index: item: recurse (prefix + "[${toString index}]") item) item
122 in listToAttrs (flatten (recurse "" item));
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
127 { _secret = "/path/to/secret" }
129 in the attrset replaced with the contents of the file
130 "/path/to/secret" in the output JSON.
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.
137 If the file "/path/to/secret" contains the string
138 "topsecretpassword1234",
140 genJqSecretsReplacementSnippet {
143 irrelevant = "not interesting";
146 ignored = "ignored attr";
149 _secret = "/path/to/secret";
154 } "/path/to/output.json"
156 would generate a snippet that, when run, outputs the following
157 JSON file at "/path/to/output.json":
162 "irrelevant": "not interesting"
165 "ignored": "ignored attr",
167 "secret": "topsecretpassword1234"
173 genJqSecretsReplacementSnippet = genJqSecretsReplacementSnippet' "_secret";
175 # Like genJqSecretsReplacementSnippet, but allows the name of the
176 # attr which identifies the secret to be changed.
177 genJqSecretsReplacementSnippet' = attr: set: output:
179 secrets = recursiveGetAttrWithJqPrefix set attr;
180 stringOrDefault = str: def: if str == "" then def else str;
182 if [[ -h '${output}' ]]; then
186 inherit_errexit_enabled=0
187 shopt -pq inherit_errexit && inherit_errexit_enabled=1
188 shopt -s inherit_errexit
192 (imap1 (index: name: ''
193 secret${toString index}=$(<'${secrets.${name}}')
194 export secret${toString index}
198 + "${pkgs.jq}/bin/jq >'${output}' "
199 + lib.escapeShellArg (stringOrDefault
202 (imap1 (index: name: ''${name} = $ENV.secret${toString index}'')
203 (attrNames secrets)))
207 ${builtins.toJSON set}
209 (( ! $inherit_errexit_enabled )) && shopt -u inherit_errexit
212 /* Remove packages of packagesToRemove from packages, based on their names.
213 Relies on package names and has quadratic complexity so use with caution!
216 removePackagesByName :: [package] -> [package] -> [package]
219 removePackagesByName [ nautilus file-roller ] [ file-roller totem ]
222 removePackagesByName = packages: packagesToRemove:
224 namesToRemove = map lib.getName packagesToRemove;
226 lib.filter (x: !(builtins.elem (lib.getName x) namesToRemove)) packages;
229 lib = import ./systemd-lib.nix { inherit lib config pkgs; };
230 unitOptions = import ./systemd-unit-options.nix { inherit lib systemdUtils; };
231 types = import ./systemd-types.nix { inherit lib systemdUtils pkgs; };
233 units = import ./systemd-network-units.nix { inherit lib systemdUtils; };