1{ lib, pkgs }:
2let
3 inherit (lib)
4 concatStringsSep
5 escape
6 flatten
7 id
8 isAttrs
9 isFloat
10 isInt
11 isList
12 isString
13 mapAttrs
14 mapAttrsToList
15 mkOption
16 optionalAttrs
17 optionalString
18 pipe
19 types
20 singleton
21 warn
22 ;
23
24 inherit (lib.generators)
25 mkValueStringDefault
26 toGitINI
27 toINI
28 toINIWithGlobalSection
29 toKeyValue
30 toLua
31 mkLuaInline
32 ;
33
34 inherit (lib.types)
35 attrsOf
36 atom
37 bool
38 coercedTo
39 either
40 float
41 int
42 listOf
43 luaInline
44 mkOptionType
45 nonEmptyListOf
46 nullOr
47 oneOf
48 path
49 str
50 submodule
51 ;
52
53 # Attributes added accidentally in https://github.com/NixOS/nixpkgs/pull/335232 (2024-08-18)
54 # Deprecated in https://github.com/NixOS/nixpkgs/pull/415666 (2025-06)
55 allowAliases = pkgs.config.allowAliases or false;
56 aliasWarning = name: warn "`formats.${name}` is deprecated; use `lib.types.${name}` instead.";
57 aliases = mapAttrs aliasWarning {
58 inherit
59 attrsOf
60 bool
61 coercedTo
62 either
63 float
64 int
65 listOf
66 luaInline
67 mkOptionType
68 nonEmptyListOf
69 nullOr
70 oneOf
71 path
72 str
73 ;
74 };
75in
76optionalAttrs allowAliases aliases
77// rec {
78
79 /*
80 Every following entry represents a format for program configuration files
81 used for `settings`-style options (see https://github.com/NixOS/rfcs/pull/42).
82 Each entry should look as follows:
83
84 <format> = <parameters>: {
85 # ^^ Parameters for controlling the format
86
87 # The module system type most suitable for representing such a format
88 # The description needs to be overwritten for recursive types
89 type = ...;
90
91 # Utility functions for convenience, or special interactions with the
92 # format (optional)
93 lib = {
94 exampleFunction = ...
95 # Types specific to the format (optional)
96 types = { ... };
97 ...
98 };
99
100 # generate :: Name -> Value -> Path
101 # A function for generating a file with a value of such a type
102 generate = ...;
103
104 });
105
106 Please note that `pkgs` may not always be available for use due to the split
107 options doc build introduced in fc614c37c653, so lazy evaluation of only the
108 'type' field is required.
109 */
110
111 inherit (import ./formats/java-properties/default.nix { inherit lib pkgs; })
112 javaProperties
113 ;
114
115 libconfig = (import ./formats/libconfig/default.nix { inherit lib pkgs; }).format;
116
117 hocon = (import ./formats/hocon/default.nix { inherit lib pkgs; }).format;
118
119 php = (import ./formats/php/default.nix { inherit lib pkgs; }).format;
120
121 json =
122 { }:
123 {
124
125 type =
126 let
127 valueType =
128 nullOr (oneOf [
129 bool
130 int
131 float
132 str
133 path
134 (attrsOf valueType)
135 (listOf valueType)
136 ])
137 // {
138 description = "JSON value";
139 };
140 in
141 valueType;
142
143 generate =
144 name: value:
145 pkgs.callPackage (
146 { runCommand, jq }:
147 runCommand name
148 {
149 nativeBuildInputs = [ jq ];
150 value = builtins.toJSON value;
151 passAsFile = [ "value" ];
152 preferLocalBuild = true;
153 }
154 ''
155 jq . "$valuePath"> $out
156 ''
157 ) { };
158
159 };
160
161 yaml = yaml_1_1;
162
163 yaml_1_1 =
164 { }:
165 {
166 generate =
167 name: value:
168 pkgs.callPackage (
169 { runCommand, remarshal_0_17 }:
170 runCommand name
171 {
172 nativeBuildInputs = [ remarshal_0_17 ];
173 value = builtins.toJSON value;
174 passAsFile = [ "value" ];
175 preferLocalBuild = true;
176 }
177 ''
178 json2yaml "$valuePath" "$out"
179 ''
180 ) { };
181
182 type =
183 let
184 valueType =
185 nullOr (oneOf [
186 bool
187 int
188 float
189 str
190 path
191 (attrsOf valueType)
192 (listOf valueType)
193 ])
194 // {
195 description = "YAML 1.1 value";
196 };
197 in
198 valueType;
199
200 };
201
202 yaml_1_2 =
203 { }:
204 {
205 generate =
206 name: value:
207 pkgs.callPackage (
208 { runCommand, remarshal }:
209 runCommand name
210 {
211 nativeBuildInputs = [ remarshal ];
212 value = builtins.toJSON value;
213 passAsFile = [ "value" ];
214 preferLocalBuild = true;
215 }
216 ''
217 json2yaml "$valuePath" "$out"
218 ''
219 ) { };
220
221 type =
222 let
223 valueType =
224 nullOr (oneOf [
225 bool
226 int
227 float
228 str
229 path
230 (attrsOf valueType)
231 (listOf valueType)
232 ])
233 // {
234 description = "YAML 1.2 value";
235 };
236 in
237 valueType;
238
239 };
240
241 # the ini formats share a lot of code
242 inherit
243 (
244 let
245 singleIniAtom =
246 nullOr (oneOf [
247 bool
248 int
249 float
250 str
251 ])
252 // {
253 description = "INI atom (null, bool, int, float or string)";
254 };
255 iniAtom =
256 {
257 listsAsDuplicateKeys,
258 listToValue,
259 atomsCoercedToLists,
260 }:
261 let
262 singleIniAtomOr =
263 if atomsCoercedToLists then coercedTo singleIniAtom singleton else either singleIniAtom;
264 in
265 if listsAsDuplicateKeys then
266 singleIniAtomOr (listOf singleIniAtom)
267 // {
268 description = singleIniAtom.description + " or a list of them for duplicate keys";
269 }
270 else if listToValue != null then
271 singleIniAtomOr (nonEmptyListOf singleIniAtom)
272 // {
273 description = singleIniAtom.description + " or a non-empty list of them";
274 }
275 else
276 singleIniAtom;
277 iniSection =
278 atom:
279 attrsOf atom
280 // {
281 description = "section of an INI file (attrs of " + atom.description + ")";
282 };
283
284 maybeToList =
285 listToValue:
286 if listToValue != null then
287 mapAttrs (key: val: if isList val then listToValue val else val)
288 else
289 id;
290 in
291 {
292 ini =
293 {
294 # Represents lists as duplicate keys
295 listsAsDuplicateKeys ? false,
296 # Alternative to listsAsDuplicateKeys, converts list to non-list
297 # listToValue :: [IniAtom] -> IniAtom
298 listToValue ? null,
299 # Merge multiple instances of the same key into a list
300 atomsCoercedToLists ? null,
301 ...
302 }@args:
303 assert listsAsDuplicateKeys -> listToValue == null;
304 assert atomsCoercedToLists != null -> (listsAsDuplicateKeys || listToValue != null);
305 let
306 atomsCoercedToLists' = if atomsCoercedToLists == null then false else atomsCoercedToLists;
307 atom = iniAtom {
308 inherit listsAsDuplicateKeys listToValue;
309 atomsCoercedToLists = atomsCoercedToLists';
310 };
311 in
312 {
313
314 type = attrsOf (iniSection atom);
315 lib.types.atom = atom;
316
317 generate =
318 name: value:
319 pipe value [
320 (mapAttrs (_: maybeToList listToValue))
321 (toINI (
322 removeAttrs args [
323 "listToValue"
324 "atomsCoercedToLists"
325 ]
326 ))
327 (pkgs.writeText name)
328 ];
329 };
330
331 iniWithGlobalSection =
332 {
333 # Represents lists as duplicate keys
334 listsAsDuplicateKeys ? false,
335 # Alternative to listsAsDuplicateKeys, converts list to non-list
336 # listToValue :: [IniAtom] -> IniAtom
337 listToValue ? null,
338 # Merge multiple instances of the same key into a list
339 atomsCoercedToLists ? null,
340 ...
341 }@args:
342 assert listsAsDuplicateKeys -> listToValue == null;
343 assert atomsCoercedToLists != null -> (listsAsDuplicateKeys || listToValue != null);
344 let
345 atomsCoercedToLists' = if atomsCoercedToLists == null then false else atomsCoercedToLists;
346 atom = iniAtom {
347 inherit listsAsDuplicateKeys listToValue;
348 atomsCoercedToLists = atomsCoercedToLists';
349 };
350 in
351 {
352 type = submodule {
353 options = {
354 sections = mkOption rec {
355 type = attrsOf (iniSection atom);
356 default = { };
357 description = type.description;
358 };
359 globalSection = mkOption rec {
360 type = iniSection atom;
361 default = { };
362 description = "global " + type.description;
363 };
364 };
365 };
366 lib.types.atom = atom;
367 generate =
368 name:
369 {
370 sections ? { },
371 globalSection ? { },
372 ...
373 }:
374 pkgs.writeText name (
375 toINIWithGlobalSection
376 (removeAttrs args [
377 "listToValue"
378 "atomsCoercedToLists"
379 ])
380 {
381 globalSection = maybeToList listToValue globalSection;
382 sections = mapAttrs (_: maybeToList listToValue) sections;
383 }
384 );
385 };
386
387 gitIni =
388 {
389 listsAsDuplicateKeys ? false,
390 ...
391 }@args:
392 let
393 atom = iniAtom {
394 inherit listsAsDuplicateKeys;
395 listToValue = null;
396 atomsCoercedToLists = false;
397 };
398 in
399 {
400 type = attrsOf (attrsOf (either atom (attrsOf atom)));
401 lib.types.atom = atom;
402 generate = name: value: pkgs.writeText name (toGitINI value);
403 };
404
405 }
406 )
407 ini
408 iniWithGlobalSection
409 gitIni
410 ;
411
412 # As defined by systemd.syntax(7)
413 #
414 # null does not set any value, which allows for RFC42 modules to specify
415 # optional config options.
416 systemd =
417 let
418 mkValueString = mkValueStringDefault { };
419 mkKeyValue = k: v: if v == null then "# ${k} is unset" else "${k} = ${mkValueString v}";
420 in
421 ini {
422 listsAsDuplicateKeys = true;
423 inherit mkKeyValue;
424 };
425
426 keyValue =
427 {
428 # Represents lists as duplicate keys
429 listsAsDuplicateKeys ? false,
430 # Alternative to listsAsDuplicateKeys, converts list to non-list
431 # listToValue :: [Atom] -> Atom
432 listToValue ? null,
433 ...
434 }@args:
435 assert listsAsDuplicateKeys -> listToValue == null;
436 {
437
438 type =
439 let
440
441 singleAtom =
442 nullOr (oneOf [
443 bool
444 int
445 float
446 str
447 ])
448 // {
449 description = "atom (null, bool, int, float or string)";
450 };
451
452 atom =
453 if listsAsDuplicateKeys then
454 coercedTo singleAtom singleton (listOf singleAtom)
455 // {
456 description = singleAtom.description + " or a list of them for duplicate keys";
457 }
458 else if listToValue != null then
459 coercedTo singleAtom singleton (nonEmptyListOf singleAtom)
460 // {
461 description = singleAtom.description + " or a non-empty list of them";
462 }
463 else
464 singleAtom;
465
466 in
467 attrsOf atom;
468
469 generate =
470 name: value:
471 let
472 transformedValue =
473 if listToValue != null then
474 mapAttrs (key: val: if isList val then listToValue val else val) value
475 else
476 value;
477 in
478 pkgs.writeText name (toKeyValue (removeAttrs args [ "listToValue" ]) transformedValue);
479
480 };
481
482 toml =
483 { }:
484 json { }
485 // {
486 type =
487 let
488 valueType =
489 oneOf [
490 bool
491 int
492 float
493 str
494 path
495 (attrsOf valueType)
496 (listOf valueType)
497 ]
498 // {
499 description = "TOML value";
500 };
501 in
502 valueType;
503
504 generate =
505 name: value:
506 pkgs.callPackage (
507 { runCommand, remarshal }:
508 runCommand name
509 {
510 nativeBuildInputs = [ remarshal ];
511 value = builtins.toJSON value;
512 passAsFile = [ "value" ];
513 preferLocalBuild = true;
514 }
515 ''
516 json2toml "$valuePath" "$out"
517 ''
518 ) { };
519
520 };
521
522 /*
523 dzikoysk's CDN format, see https://github.com/dzikoysk/cdn
524
525 The result is almost identical to YAML when there are no nested properties,
526 but differs enough in the other case to warrant a separate format.
527 (see https://github.com/dzikoysk/cdn#supported-formats)
528
529 Currently used by Panda, Reposilite, and FunnyGuilds (as per the repo's readme).
530 */
531 cdn =
532 { }:
533 json { }
534 // {
535 type =
536 let
537 valueType =
538 nullOr (oneOf [
539 bool
540 int
541 float
542 str
543 path
544 (attrsOf valueType)
545 (listOf valueType)
546 ])
547 // {
548 description = "CDN value";
549 };
550 in
551 valueType;
552
553 generate =
554 name: value:
555 pkgs.callPackage (
556 { runCommand, json2cdn }:
557 runCommand name
558 {
559 nativeBuildInputs = [ json2cdn ];
560 value = builtins.toJSON value;
561 passAsFile = [ "value" ];
562 preferLocalBuild = true;
563 }
564 ''
565 json2cdn "$valuePath" > $out
566 ''
567 ) { };
568 };
569
570 /*
571 For configurations of Elixir project, like config.exs or runtime.exs
572
573 Most Elixir project are configured using the [Config] Elixir DSL
574
575 Since Elixir has more types than Nix, we need a way to map Nix types to
576 more than 1 Elixir type. To that end, this format provides its own library,
577 and its own set of types.
578
579 To be more detailed, a Nix attribute set could correspond in Elixir to a
580 [Keyword list] (the more common type), or it could correspond to a [Map].
581
582 A Nix string could correspond in Elixir to a [String] (also called
583 "binary"), an [Atom], or a list of chars (usually discouraged).
584
585 A Nix array could correspond in Elixir to a [List] or a [Tuple].
586
587 Some more types exists, like records, regexes, but since they are less used,
588 we can leave the `mkRaw` function as an escape hatch.
589
590 For more information on how to use this format in modules, please refer to
591 the Elixir section of the Nixos documentation.
592
593 TODO: special Elixir values doesn't show up nicely in the documentation
594
595 [Config]: <https://hexdocs.pm/elixir/Config.html>
596 [Keyword list]: <https://hexdocs.pm/elixir/Keyword.html>
597 [Map]: <https://hexdocs.pm/elixir/Map.html>
598 [String]: <https://hexdocs.pm/elixir/String.html>
599 [Atom]: <https://hexdocs.pm/elixir/Atom.html>
600 [List]: <https://hexdocs.pm/elixir/List.html>
601 [Tuple]: <https://hexdocs.pm/elixir/Tuple.html>
602 */
603 elixirConf =
604 {
605 elixir ? pkgs.elixir,
606 }:
607 let
608 toElixir =
609 value:
610 if value == null then
611 "nil"
612 else if value == true then
613 "true"
614 else if value == false then
615 "false"
616 else if isInt value || isFloat value then
617 toString value
618 else if isString value then
619 string value
620 else if isAttrs value then
621 attrs value
622 else if isList value then
623 list value
624 else
625 abort "formats.elixirConf: should never happen (value = ${value})";
626
627 escapeElixir = escape [
628 "\\"
629 "#"
630 "\""
631 ];
632 string = value: "\"${escapeElixir value}\"";
633
634 attrs =
635 set:
636 if set ? _elixirType then
637 specialType set
638 else
639 let
640 toKeyword = name: value: "${name}: ${toElixir value}";
641 keywordList = concatStringsSep ", " (mapAttrsToList toKeyword set);
642 in
643 "[" + keywordList + "]";
644
645 listContent = values: concatStringsSep ", " (map toElixir values);
646
647 list = values: "[" + (listContent values) + "]";
648
649 specialType =
650 { value, _elixirType }:
651 if _elixirType == "raw" then
652 value
653 else if _elixirType == "atom" then
654 value
655 else if _elixirType == "map" then
656 elixirMap value
657 else if _elixirType == "tuple" then
658 tuple value
659 else
660 abort "formats.elixirConf: should never happen (_elixirType = ${_elixirType})";
661
662 elixirMap =
663 set:
664 let
665 toEntry = name: value: "${toElixir name} => ${toElixir value}";
666 entries = concatStringsSep ", " (mapAttrsToList toEntry set);
667 in
668 "%{${entries}}";
669
670 tuple = values: "{${listContent values}}";
671
672 toConf =
673 values:
674 let
675 keyConfig =
676 rootKey: key: value:
677 "config ${rootKey}, ${key}, ${toElixir value}";
678 keyConfigs = rootKey: values: mapAttrsToList (keyConfig rootKey) values;
679 rootConfigs = flatten (mapAttrsToList keyConfigs values);
680 in
681 ''
682 import Config
683
684 ${concatStringsSep "\n" rootConfigs}
685 '';
686 in
687 {
688 type =
689 let
690 valueType =
691 nullOr (oneOf [
692 bool
693 int
694 float
695 str
696 (attrsOf valueType)
697 (listOf valueType)
698 ])
699 // {
700 description = "Elixir value";
701 };
702 in
703 attrsOf (attrsOf (valueType));
704
705 lib =
706 let
707 mkRaw = value: {
708 inherit value;
709 _elixirType = "raw";
710 };
711
712 in
713 {
714 inherit mkRaw;
715
716 # Fetch an environment variable at runtime, with optional fallback
717 mkGetEnv =
718 {
719 envVariable,
720 fallback ? null,
721 }:
722 mkRaw "System.get_env(${toElixir envVariable}, ${toElixir fallback})";
723
724 /*
725 Make an Elixir atom.
726
727 Note: lowercase atoms still need to be prefixed by ':'
728 */
729 mkAtom = value: {
730 inherit value;
731 _elixirType = "atom";
732 };
733
734 # Make an Elixir tuple out of a list.
735 mkTuple = value: {
736 inherit value;
737 _elixirType = "tuple";
738 };
739
740 # Make an Elixir map out of an attribute set.
741 mkMap = value: {
742 inherit value;
743 _elixirType = "map";
744 };
745
746 /*
747 Contains Elixir types. Every type it exports can also be replaced
748 by raw Elixir code (i.e. every type is `either type rawElixir`).
749
750 It also reexports standard types, wrapping them so that they can
751 also be raw Elixir.
752 */
753 types =
754 let
755 isElixirType = type: x: (x._elixirType or "") == type;
756
757 rawElixir = mkOptionType {
758 name = "rawElixir";
759 description = "raw elixir";
760 check = isElixirType "raw";
761 };
762
763 elixirOr = other: either other rawElixir;
764 in
765 {
766 inherit rawElixir elixirOr;
767
768 atom = elixirOr (mkOptionType {
769 name = "elixirAtom";
770 description = "elixir atom";
771 check = isElixirType "atom";
772 });
773
774 tuple = elixirOr (mkOptionType {
775 name = "elixirTuple";
776 description = "elixir tuple";
777 check = isElixirType "tuple";
778 });
779
780 map = elixirOr (mkOptionType {
781 name = "elixirMap";
782 description = "elixir map";
783 check = isElixirType "map";
784 });
785 # Wrap standard types, since anything in the Elixir configuration
786 # can be raw Elixir
787 }
788 // mapAttrs (_name: type: elixirOr type) types;
789 };
790
791 generate =
792 name: value:
793 pkgs.runCommand name
794 {
795 value = toConf value;
796 passAsFile = [ "value" ];
797 nativeBuildInputs = [ elixir ];
798 preferLocalBuild = true;
799 }
800 ''
801 cp "$valuePath" "$out"
802 mix format "$out"
803 '';
804 };
805
806 lua =
807 {
808 asBindings ? false,
809 multiline ? true,
810 columnWidth ? 100,
811 indentWidth ? 2,
812 indentUsingTabs ? false,
813 }:
814 {
815 type =
816 let
817 valueType =
818 nullOr (oneOf [
819 bool
820 float
821 int
822 path
823 str
824 luaInline
825 (attrsOf valueType)
826 (listOf valueType)
827 ])
828 // {
829 description = "lua value";
830 descriptionClass = "noun";
831 };
832 in
833 if asBindings then attrsOf valueType else valueType;
834 generate =
835 name: value:
836 pkgs.callPackage (
837 { runCommand, stylua }:
838 runCommand name
839 {
840 nativeBuildInputs = [ stylua ];
841 inherit columnWidth;
842 inherit indentWidth;
843 indentType = if indentUsingTabs then "Tabs" else "Spaces";
844 value = toLua { inherit asBindings multiline; } value;
845 passAsFile = [ "value" ];
846 preferLocalBuild = true;
847 }
848 ''
849 ${optionalString (!asBindings) ''
850 echo -n 'return ' >> $out
851 ''}
852 cat $valuePath >> $out
853 stylua \
854 --no-editorconfig \
855 --line-endings Unix \
856 --column-width $columnWidth \
857 --indent-width $indentWidth \
858 --indent-type $indentType \
859 $out
860 ''
861 ) { };
862 # Alias for mkLuaInline
863 lib.mkRaw = lib.mkLuaInline;
864 };
865
866 # Outputs a succession of Python variable assignments
867 # Useful for many Django-based services
868 pythonVars =
869 { }:
870 {
871 type =
872 let
873 valueType =
874 nullOr (oneOf [
875 bool
876 float
877 int
878 path
879 str
880 (attrsOf valueType)
881 (listOf valueType)
882 ])
883 // {
884 description = "Python value";
885 };
886 in
887 attrsOf valueType;
888 generate =
889 name: value:
890 pkgs.callPackage (
891 {
892 runCommand,
893 python3,
894 black,
895 }:
896 runCommand name
897 {
898 nativeBuildInputs = [
899 python3
900 black
901 ];
902 value = builtins.toJSON value;
903 pythonGen = ''
904 import json
905 import os
906
907 with open(os.environ["valuePath"], "r") as f:
908 for key, value in json.load(f).items():
909 print(f"{key} = {repr(value)}")
910 '';
911 passAsFile = [
912 "value"
913 "pythonGen"
914 ];
915 preferLocalBuild = true;
916 }
917 ''
918 cat "$valuePath"
919 python3 "$pythonGenPath" > $out
920 black $out
921 ''
922 ) { };
923 };
924
925 xml =
926 {
927 format ? "badgerfish",
928 withHeader ? true,
929 }:
930 if format == "badgerfish" then
931 {
932 type =
933 let
934 valueType =
935 nullOr (oneOf [
936 bool
937 int
938 float
939 str
940 path
941 (attrsOf valueType)
942 (listOf valueType)
943 ])
944 // {
945 description = "XML value";
946 };
947 in
948 valueType;
949
950 generate =
951 name: value:
952 pkgs.callPackage (
953 {
954 runCommand,
955 libxml2Python,
956 python3Packages,
957 }:
958 runCommand name
959 {
960 nativeBuildInputs = [
961 python3Packages.xmltodict
962 libxml2Python
963 ];
964 value = builtins.toJSON value;
965 pythonGen = ''
966 import json
967 import os
968 import xmltodict
969
970 with open(os.environ["valuePath"], "r") as f:
971 print(xmltodict.unparse(json.load(f), full_document=${
972 if withHeader then "True" else "False"
973 }, pretty=True, indent=" " * 2))
974 '';
975 passAsFile = [
976 "value"
977 "pythonGen"
978 ];
979 preferLocalBuild = true;
980 }
981 ''
982 python3 "$pythonGenPath" > $out
983 xmllint $out > /dev/null
984 ''
985 ) { };
986 }
987 else
988 throw "pkgs.formats.xml: Unknown format: ${format}";
989
990}