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