pytrainer: unpin python 3.10
[NixPkgs.git] / pkgs / common-updater / combinators.nix
bloba774328380bb65067d1f63aff2061e0643c5d69a
1 { lib
2 }:
4 /*
5   This is a set of tools to manipulate update scripts as recognized by update.nix.
6   It is still very experimental with **instability** almost guaranteed so any use
7   outside Nixpkgs is discouraged.
9   update.nix currently accepts the following type:
11   type UpdateScript
12     // Simple path to script to execute script
13     = FilePath
14     // Path to execute plus arguments to pass it
15     | [ (FilePath | String) ]
16     // Advanced attribue set (experimental)
17     | {
18       // Script to execute (same as basic update script above)
19       command : (FilePath | [ (FilePath | String) ])
20       // Features that the script supports
21       // - commit: (experimental) returns commit message in stdout
22       // - silent: (experimental) returns no stdout
23       supportedFeatures : ?[ ("commit" | "silent") ]
24       // Override attribute path detected by update.nix
25       attrPath : ?String
26     }
29 let
30   /*
31     type ShellArg = String | { __rawShell : String }
32   */
34   /*
35     Quotes all arguments to be safely passed to the Bourne shell.
37     escapeShellArgs' : [ShellArg] -> String
38   */
39   escapeShellArgs' = lib.concatMapStringsSep " " (arg: if arg ? __rawShell then arg.__rawShell else lib.escapeShellArg arg);
41   /*
42     processArg : { maxArgIndex : Int, args : [ShellArg], paths : [FilePath] } → (String|FilePath) → { maxArgIndex : Int, args : [ShellArg], paths : [FilePath] }
43     Helper reducer function for building a command arguments where file paths are replaced with argv[x] reference.
44   */
45   processArg =
46     { maxArgIndex, args, paths }:
47     arg:
48     if builtins.isPath arg then {
49       args = args ++ [ { __rawShell = "\"\$${builtins.toString maxArgIndex}\""; } ];
50       maxArgIndex = maxArgIndex + 1;
51       paths = paths ++ [ arg ];
52     } else {
53       args = args ++ [ arg ];
54       inherit maxArgIndex paths;
55     };
56   /*
57     extractPaths : Int → [ (String|FilePath) ] → { maxArgIndex : Int, args : [ShellArg], paths : [FilePath] }
58     Helper function that extracts file paths from command arguments and replaces them with argv[x] references.
59   */
60   extractPaths = maxArgIndex: command: builtins.foldl' processArg { inherit maxArgIndex; args = [ ]; paths = [ ]; } command;
61   /*
62     processCommand : { maxArgIndex : Int, commands : [[ShellArg]], paths : [FilePath] } → [ (String|FilePath) ] → { maxArgIndex : Int, commands : [[ShellArg]], paths : [FilePath] }
63     Helper reducer function for extracting file paths from individual commands.
64   */
65   processCommand =
66     { maxArgIndex, commands, paths }:
67     command:
68     let
69       new = extractPaths maxArgIndex command;
70     in
71     {
72       commands = commands ++ [ new.args ];
73       paths = paths ++ new.paths;
74       maxArgIndex = new.maxArgIndex;
75     };
76   /*
77     extractCommands : Int → [[ (String|FilePath) ]] → { maxArgIndex : Int, commands : [[ShellArg]], paths : [FilePath] }
78     Helper function for extracting file paths from a list of commands and replacing them with argv[x] references.
79   */
80   extractCommands = maxArgIndex: commands: builtins.foldl' processCommand { inherit maxArgIndex; commands = [ ]; paths = [ ]; } commands;
82   /*
83     commandsToShellInvocation : [[ (String|FilePath) ]] → [ (String|FilePath) ]
84     Converts a list of commands into a single command by turning them into a shell script and passing them to `sh -c`.
85   */
86   commandsToShellInvocation = commands:
87     let
88       extracted = extractCommands 0 commands;
89     in
90     [
91       "sh"
92       "-ec"
93       (lib.concatMapStringsSep ";" escapeShellArgs' extracted.commands)
94       # We need paths as separate arguments so that update.nix can ensure they refer to the local directory
95       # rather than a store path.
96     ] ++ extracted.paths;
98 rec {
99   /*
100     normalize : UpdateScript → UpdateScript
101     EXPERIMENTAL! Converts a basic update script to the experimental attribute set form.
102   */
103   normalize = updateScript: {
104     command = lib.toList (updateScript.command or updateScript);
105     supportedFeatures = updateScript.supportedFeatures or [ ];
106   } // lib.optionalAttrs (updateScript ? attrPath) {
107     inherit (updateScript) attrPath;
108   };
110   /*
111     sequence : [UpdateScript] → UpdateScript
112     EXPERIMENTAL! Combines multiple update scripts to run in sequence.
113   */
114   sequence =
115     scripts:
117     let
118       scriptsNormalized = builtins.map normalize scripts;
119     in
120     let
121       scripts = scriptsNormalized;
122       hasCommitSupport = lib.findSingle ({ supportedFeatures, ... }: supportedFeatures == [ "commit" ]) null null scripts != null;
123       hasSilentSupport = lib.findFirst ({ supportedFeatures, ... }: supportedFeatures == [ "silent" ]) null scripts != null;
124       # Supported features currently only describe the format of the standard output of the update script.
125       # Here we ensure that the standard output of the combined update script is well formed.
126       validateFeatures =
127         if hasCommitSupport then
128           # Exactly one update script declares only “commit” feature and all the rest declare only “silent” feature.
129           ({ supportedFeatures, ... }: supportedFeatures == [ "commit" ] || supportedFeatures == [ "silent" ])
130         else if hasSilentSupport then
131           # All update scripts declare only “silent” feature.
132           ({ supportedFeatures, ... }: supportedFeatures == [ "silent" ])
133         else
134           # No update script declares any supported feature to fail loudly on unknown features rather than silently discard them.
135           ({ supportedFeatures, ... }: supportedFeatures == [ ]);
136     in
138     assert lib.assertMsg (lib.all validateFeatures scripts) "Combining update scripts with features enabled (other than “silent” scripts and an optional single script with “commit”) is currently unsupported.";
139     assert lib.assertMsg (builtins.length (lib.unique (builtins.map ({ attrPath ? null, ... }: attrPath) scripts)) == 1) "Combining update scripts with different attr paths is currently unsupported.";
141     {
142       command = commandsToShellInvocation (builtins.map ({ command, ... }: command) scripts);
143       supportedFeatures =
144         if hasCommitSupport then
145           [ "commit" ]
146         else if hasSilentSupport then
147           [ "silent" ]
148         else
149           [ ];
150     };
152   /*
153     copyAttrOutputToFile : String → FilePath → UpdateScript
154     EXPERIMENTAL! Simple update script that copies the output of Nix derivation built by `attr` to `path`.
155   */
156   copyAttrOutputToFile =
157     attr:
158     path:
160     {
161       command = [
162         "sh"
163         "-c"
164         "cp --no-preserve=all \"$(nix-build -A ${attr})\" \"$0\" > /dev/null"
165         path
166       ];
167       supportedFeatures = [ "silent" ];
168     };