1{ lib
2}:
3
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.
8
9 update.nix currently accepts the following type:
10
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 }
27*/
28
29let
30 /*
31 type ShellArg = String | { __rawShell : String }
32 */
33
34 /*
35 Quotes all arguments to be safely passed to the Bourne shell.
36
37 escapeShellArgs' : [ShellArg] -> String
38 */
39 escapeShellArgs' = lib.concatMapStringsSep " " (arg: if arg ? __rawShell then arg.__rawShell else lib.escapeShellArg arg);
40
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;
81
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 "-c"
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;
97in
98rec {
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 };
109
110 /*
111 sequence : [UpdateScript] → UpdateScript
112 EXPERIMENTAL! Combines multiple update scripts to run in sequence.
113 */
114 sequence =
115 scripts:
116
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 validateFeatures =
124 if hasCommitSupport then
125 ({ supportedFeatures, ... }: supportedFeatures == [ "commit" ] || supportedFeatures == [ "silent" ])
126 else
127 ({ supportedFeatures, ... }: supportedFeatures == [ ]);
128 in
129
130 assert lib.assertMsg (lib.all validateFeatures scripts) "Combining update scripts with features enabled (other than a single script with “commit” and all other with “silent”) is currently unsupported.";
131 assert lib.assertMsg (builtins.length (lib.unique (builtins.map ({ attrPath ? null, ... }: attrPath) scripts)) == 1) "Combining update scripts with different attr paths is currently unsupported.";
132
133 {
134 command = commandsToShellInvocation (builtins.map ({ command, ... }: command) scripts);
135 supportedFeatures = lib.optionals hasCommitSupport [
136 "commit"
137 ];
138 };
139
140 /*
141 copyAttrOutputToFile : String → FilePath → UpdateScript
142 EXPERIMENTAL! Simple update script that copies the output of Nix derivation built by `attr` to `path`.
143 */
144 copyAttrOutputToFile =
145 attr:
146 path:
147
148 {
149 command = [
150 "sh"
151 "-c"
152 "cp --no-preserve=all \"$(nix-build -A ${attr})\" \"$0\" > /dev/null"
153 path
154 ];
155 supportedFeatures = [ "silent" ];
156 };
157
158}