jetbrains: 2024.1 -> 2024.2.7 (#351041)
[NixPkgs.git] / nixos / lib / utils.nix
blob82bbfae0178b0e793e0be98eb6c5e7a1ff1f5aca
1 { lib, config, pkgs }:
3 let
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     mapAttrs
27     nameValuePair
28     optionalString
29     removePrefix
30     removeSuffix
31     replaceStrings
32     stringToCharacters
33     types
34     ;
36   inherit (lib.strings) toJSON normalizePath escapeC;
39 let
40 utils = rec {
42   # Copy configuration files to avoid having the entire sources in the system closure
43   copyFile = filePath: pkgs.runCommand (builtins.unsafeDiscardStringContext (baseNameOf filePath)) {} ''
44     cp ${filePath} $out
45   '';
47   # Check whenever fileSystem is needed for boot.  NOTE: Make sure
48   # pathsNeededForBoot is closed under the parent relationship, i.e. if /a/b/c
49   # is in the list, put /a and /a/b in as well.
50   pathsNeededForBoot = [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/var/lib/nixos" "/etc" "/usr" ];
51   fsNeededForBoot = fs: fs.neededForBoot || elem fs.mountPoint pathsNeededForBoot;
53   # Check whenever `b` depends on `a` as a fileSystem
54   fsBefore = a: b:
55     let
56       # normalisePath adds a slash at the end of the path if it didn't already
57       # have one.
58       #
59       # The reason slashes are added at the end of each path is to prevent `b`
60       # from accidentally depending on `a` in cases like
61       #    a = { mountPoint = "/aaa"; ... }
62       #    b = { device     = "/aaaa"; ... }
63       # Here a.mountPoint *is* a prefix of b.device even though a.mountPoint is
64       # *not* a parent of b.device. If we add a slash at the end of each string,
65       # though, this is not a problem: "/aaa/" is not a prefix of "/aaaa/".
66       normalisePath = path: "${path}${optionalString (!(hasSuffix "/" path)) "/"}";
67       normalise = mount: mount // { device = normalisePath (toString mount.device);
68                                     mountPoint = normalisePath mount.mountPoint;
69                                     depends = map normalisePath mount.depends;
70                                   };
72       a' = normalise a;
73       b' = normalise b;
75     in hasPrefix a'.mountPoint b'.device
76     || hasPrefix a'.mountPoint b'.mountPoint
77     || any (hasPrefix a'.mountPoint) b'.depends;
79   # Escape a path according to the systemd rules. FIXME: slow
80   # The rules are described in systemd.unit(5) as follows:
81   # 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.
82   # 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".
83   escapeSystemdPath = s: let
84     replacePrefix = p: r: s: (if (hasPrefix p s) then r + (removePrefix p s) else s);
85     trim = s: removeSuffix "/" (removePrefix "/" s);
86     normalizedPath = normalizePath s;
87   in
88     replaceStrings ["/"] ["-"]
89     (replacePrefix "." (escapeC ["."] ".")
90     (escapeC (stringToCharacters " !\"#$%&'()*+,;<=>=@[\\]^`{|}~-")
91     (if normalizedPath == "/" then normalizedPath else trim normalizedPath)));
93   # Quotes an argument for use in Exec* service lines.
94   # systemd accepts "-quoted strings with escape sequences, toJSON produces
95   # a subset of these.
96   # Additionally we escape % to disallow expansion of % specifiers. Any lone ;
97   # in the input will be turned it ";" and thus lose its special meaning.
98   # Every $ is escaped to $$, this makes it unnecessary to disable environment
99   # substitution for the directive.
100   escapeSystemdExecArg = arg:
101     let
102       s = if isPath arg then "${arg}"
103         else if isString arg then arg
104         else if isInt arg || isFloat arg || isDerivation arg then toString arg
105         else throw "escapeSystemdExecArg only allows strings, paths, numbers and derivations";
106     in
107       replaceStrings [ "%" "$" ] [ "%%" "$$" ] (toJSON s);
109   # Quotes a list of arguments into a single string for use in a Exec*
110   # line.
111   escapeSystemdExecArgs = concatMapStringsSep " " escapeSystemdExecArg;
113   # Returns a system path for a given shell package
114   toShellPath = shell:
115     if types.shellPackage.check shell then
116       "/run/current-system/sw${shell.shellPath}"
117     else if types.package.check shell then
118       throw "${shell} is not a shell package"
119     else
120       shell;
122   /* Recurse into a list or an attrset, searching for attrs named like
123      the value of the "attr" parameter, and return an attrset where the
124      names are the corresponding jq path where the attrs were found and
125      the values are the values of the attrs.
127      Example:
128        recursiveGetAttrWithJqPrefix {
129          example = [
130            {
131              irrelevant = "not interesting";
132            }
133            {
134              ignored = "ignored attr";
135              relevant = {
136                secret = {
137                  _secret = "/path/to/secret";
138                };
139              };
140            }
141          ];
142        } "_secret" -> { ".example[1].relevant.secret" = "/path/to/secret"; }
143   */
144   recursiveGetAttrWithJqPrefix = item: attr: mapAttrs (_name: set: set.${attr}) (recursiveGetAttrsetWithJqPrefix item attr);
146   /* Similar to `recursiveGetAttrWithJqPrefix`, but returns the whole
147      attribute set containing `attr` instead of the value of `attr` in
148      the set.
150      Example:
151        recursiveGetAttrsetWithJqPrefix {
152          example = [
153            {
154              irrelevant = "not interesting";
155            }
156            {
157              ignored = "ignored attr";
158              relevant = {
159                secret = {
160                  _secret = "/path/to/secret";
161                  quote = true;
162                };
163              };
164            }
165          ];
166        } "_secret" -> { ".example[1].relevant.secret" = { _secret = "/path/to/secret"; quote = true; }; }
167   */
168   recursiveGetAttrsetWithJqPrefix = item: attr:
169     let
170       recurse = prefix: item:
171         if item ? ${attr} then
172           nameValuePair prefix item
173         else if isDerivation item then []
174         else if isAttrs item then
175           map (name:
176             let
177               escapedName = ''"${replaceStrings [''"'' "\\"] [''\"'' "\\\\"] name}"'';
178             in
179               recurse (prefix + "." + escapedName) item.${name}) (attrNames item)
180         else if isList item then
181           imap0 (index: item: recurse (prefix + "[${toString index}]") item) item
182         else
183           [];
184     in listToAttrs (flatten (recurse "" item));
186   /* Takes an attrset and a file path and generates a bash snippet that
187      outputs a JSON file at the file path with all instances of
189      { _secret = "/path/to/secret" }
191      in the attrset replaced with the contents of the file
192      "/path/to/secret" in the output JSON.
194      When a configuration option accepts an attrset that is finally
195      converted to JSON, this makes it possible to let the user define
196      arbitrary secret values.
198      Example:
199        If the file "/path/to/secret" contains the string
200        "topsecretpassword1234",
202        genJqSecretsReplacementSnippet {
203          example = [
204            {
205              irrelevant = "not interesting";
206            }
207            {
208              ignored = "ignored attr";
209              relevant = {
210                secret = {
211                  _secret = "/path/to/secret";
212                };
213              };
214            }
215          ];
216        } "/path/to/output.json"
218        would generate a snippet that, when run, outputs the following
219        JSON file at "/path/to/output.json":
221        {
222          "example": [
223            {
224              "irrelevant": "not interesting"
225            },
226            {
227              "ignored": "ignored attr",
228              "relevant": {
229                "secret": "topsecretpassword1234"
230              }
231            }
232          ]
233        }
235      The attribute set { _secret = "/path/to/secret"; } can contain extra
236      options, currently it accepts the `quote = true|false` option.
238      If `quote = true` (default behavior), the content of the secret file will
239      be quoted as a string and embedded.  Otherwise, if `quote = false`, the
240      content of the secret file will be parsed to JSON and then embedded.
242      Example:
243        If the file "/path/to/secret" contains the JSON document:
245        [
246          { "a": "topsecretpassword1234" },
247          { "b": "topsecretpassword5678" }
248        ]
250        genJqSecretsReplacementSnippet {
251          example = [
252            {
253              irrelevant = "not interesting";
254            }
255            {
256              ignored = "ignored attr";
257              relevant = {
258                secret = {
259                  _secret = "/path/to/secret";
260                  quote = false;
261                };
262              };
263            }
264          ];
265        } "/path/to/output.json"
267        would generate a snippet that, when run, outputs the following
268        JSON file at "/path/to/output.json":
270        {
271          "example": [
272            {
273              "irrelevant": "not interesting"
274            },
275            {
276              "ignored": "ignored attr",
277              "relevant": {
278                "secret": [
279                  { "a": "topsecretpassword1234" },
280                  { "b": "topsecretpassword5678" }
281                ]
282              }
283            }
284          ]
285        }
286   */
287   genJqSecretsReplacementSnippet = genJqSecretsReplacementSnippet' "_secret";
289   # Like genJqSecretsReplacementSnippet, but allows the name of the
290   # attr which identifies the secret to be changed.
291   genJqSecretsReplacementSnippet' = attr: set: output:
292     let
293       secretsRaw = recursiveGetAttrsetWithJqPrefix set attr;
294       # Set default option values
295       secrets = mapAttrs (_name: set: {
296         quote = true;
297       } // set) secretsRaw;
298       stringOrDefault = str: def: if str == "" then def else str;
299     in ''
300       if [[ -h '${output}' ]]; then
301         rm '${output}'
302       fi
304       inherit_errexit_enabled=0
305       shopt -pq inherit_errexit && inherit_errexit_enabled=1
306       shopt -s inherit_errexit
307     ''
308     + concatStringsSep
309         "\n"
310         (imap1 (index: name: ''
311                   secret${toString index}=$(<'${secrets.${name}.${attr}}')
312                   export secret${toString index}
313                 '')
314                (attrNames secrets))
315     + "\n"
316     + "${pkgs.jq}/bin/jq >'${output}' "
317     + escapeShellArg (stringOrDefault
318           (concatStringsSep
319             " | "
320             (imap1 (index: name: ''${name} = ($ENV.secret${toString index}${optionalString (!secrets.${name}.quote) " | fromjson"})'')
321                    (attrNames secrets)))
322           ".")
323     + ''
324        <<'EOF'
325       ${toJSON set}
326       EOF
327       (( ! $inherit_errexit_enabled )) && shopt -u inherit_errexit
328     '';
330   /* Remove packages of packagesToRemove from packages, based on their names.
331      Relies on package names and has quadratic complexity so use with caution!
333      Type:
334        removePackagesByName :: [package] -> [package] -> [package]
336      Example:
337        removePackagesByName [ nautilus file-roller ] [ file-roller totem ]
338        => [ nautilus ]
339   */
340   removePackagesByName = packages: packagesToRemove:
341     let
342       namesToRemove = map getName packagesToRemove;
343     in
344       filter (x: !(elem (getName x) namesToRemove)) packages;
346   systemdUtils = {
347     lib = import ./systemd-lib.nix { inherit lib config pkgs utils; };
348     unitOptions = import ./systemd-unit-options.nix { inherit lib systemdUtils; };
349     types = import ./systemd-types.nix { inherit lib systemdUtils pkgs; };
350     network = {
351       units = import ./systemd-network-units.nix { inherit lib systemdUtils; };
352     };
353   };
355 in utils