1/*
2 To run:
3
4 nix-shell maintainers/scripts/update.nix
5
6 See https://nixos.org/manual/nixpkgs/unstable/#var-passthru-updateScript
7*/
8{
9 package ? null,
10 maintainer ? null,
11 predicate ? null,
12 get-script ? pkg: pkg.updateScript or null,
13 path ? null,
14 max-workers ? null,
15 include-overlays ? false,
16 keep-going ? null,
17 commit ? null,
18 skip-prompt ? null,
19 order ? null,
20}:
21
22let
23 pkgs = import ./../../default.nix (
24 (
25 if include-overlays == false then
26 { overlays = [ ]; }
27 else if include-overlays == true then
28 { } # Let Nixpkgs include overlays impurely.
29 else
30 { overlays = include-overlays; }
31 )
32 // {
33 config.allowAliases = false;
34 }
35 );
36
37 inherit (pkgs) lib;
38
39 # Remove duplicate elements from the list based on some extracted value. O(n^2) complexity.
40 nubOn =
41 f: list:
42 if list == [ ] then
43 [ ]
44 else
45 let
46 x = lib.head list;
47 xs = lib.filter (p: f x != f p) (lib.drop 1 list);
48 in
49 [ x ] ++ nubOn f xs;
50
51 /*
52 Recursively find all packages (derivations) in `pkgs` matching `cond` predicate.
53
54 Type: packagesWithPath :: AttrPath → (AttrPath → derivation → bool) → AttrSet → List<AttrSet{attrPath :: str; package :: derivation; }>
55 AttrPath :: [str]
56
57 The packages will be returned as a list of named pairs comprising of:
58 - attrPath: stringified attribute path (based on `rootPath`)
59 - package: corresponding derivation
60 */
61 packagesWithPath =
62 rootPath: cond: pkgs:
63 let
64 packagesWithPathInner =
65 path: pathContent:
66 let
67 result = builtins.tryEval pathContent;
68
69 somewhatUniqueRepresentant =
70 { package, attrPath }:
71 {
72 updateScript = (get-script package);
73 # Some updaters use the same `updateScript` value for all packages.
74 # Also compare `meta.description`.
75 position = package.meta.position or null;
76 # We cannot always use `meta.position` since it might not be available
77 # or it might be shared among multiple packages.
78 };
79
80 dedupResults = lst: nubOn somewhatUniqueRepresentant (lib.concatLists lst);
81 in
82 if result.success then
83 let
84 evaluatedPathContent = result.value;
85 in
86 if lib.isDerivation evaluatedPathContent then
87 lib.optional (cond path evaluatedPathContent) {
88 attrPath = lib.concatStringsSep "." path;
89 package = evaluatedPathContent;
90 }
91 else if lib.isAttrs evaluatedPathContent then
92 # If user explicitly points to an attrSet or it is marked for recursion, we recur.
93 if
94 path == rootPath
95 || evaluatedPathContent.recurseForDerivations or false
96 || evaluatedPathContent.recurseForRelease or false
97 then
98 dedupResults (
99 lib.mapAttrsToList (name: elem: packagesWithPathInner (path ++ [ name ]) elem) evaluatedPathContent
100 )
101 else
102 [ ]
103 else
104 [ ]
105 else
106 [ ];
107 in
108 packagesWithPathInner rootPath pkgs;
109
110 # Recursively find all packages (derivations) in `pkgs` matching `cond` predicate.
111 packagesWith = packagesWithPath [ ];
112
113 # Recursively find all packages in `pkgs` with updateScript matching given predicate.
114 packagesWithUpdateScriptMatchingPredicate =
115 cond: packagesWith (path: pkg: (get-script pkg != null) && cond path pkg);
116
117 # Recursively find all packages in `pkgs` with updateScript by given maintainer.
118 packagesWithUpdateScriptAndMaintainer =
119 maintainer':
120 let
121 maintainer =
122 if !builtins.hasAttr maintainer' lib.maintainers then
123 builtins.throw "Maintainer with name `${maintainer'} does not exist in `maintainers/maintainer-list.nix`."
124 else
125 builtins.getAttr maintainer' lib.maintainers;
126 in
127 packagesWithUpdateScriptMatchingPredicate (
128 path: pkg:
129 (
130 if builtins.hasAttr "maintainers" pkg.meta then
131 (
132 if builtins.isList pkg.meta.maintainers then
133 builtins.elem maintainer pkg.meta.maintainers
134 else
135 maintainer == pkg.meta.maintainers
136 )
137 else
138 false
139 )
140 );
141
142 # Recursively find all packages under `path` in `pkgs` with updateScript.
143 packagesWithUpdateScript =
144 path: pkgs:
145 let
146 prefix = lib.splitString "." path;
147 pathContent = lib.attrByPath prefix null pkgs;
148 in
149 if pathContent == null then
150 builtins.throw "Attribute path `${path}` does not exist."
151 else
152 packagesWithPath prefix (path: pkg: (get-script pkg != null)) pathContent;
153
154 # Find a package under `path` in `pkgs` and require that it has an updateScript.
155 packageByName =
156 path: pkgs:
157 let
158 package = lib.attrByPath (lib.splitString "." path) null pkgs;
159 in
160 if package == null then
161 builtins.throw "Package with an attribute name `${path}` does not exist."
162 else if get-script package == null then
163 builtins.throw "Package with an attribute name `${path}` does not have a `passthru.updateScript` attribute defined."
164 else
165 {
166 attrPath = path;
167 inherit package;
168 };
169
170 # List of packages matched based on the CLI arguments.
171 packages =
172 if package != null then
173 [ (packageByName package pkgs) ]
174 else if predicate != null then
175 packagesWithUpdateScriptMatchingPredicate predicate pkgs
176 else if maintainer != null then
177 packagesWithUpdateScriptAndMaintainer maintainer pkgs
178 else if path != null then
179 packagesWithUpdateScript path pkgs
180 else
181 builtins.throw "No arguments provided.\n\n${helpText}";
182
183 helpText = ''
184 Please run:
185
186 % nix-shell maintainers/scripts/update.nix --argstr maintainer garbas
187
188 to run all update scripts for all packages that lists \`garbas\` as a maintainer
189 and have \`updateScript\` defined, or:
190
191 % nix-shell maintainers/scripts/update.nix --argstr package nautilus
192
193 to run update script for specific package, or
194
195 % nix-shell maintainers/scripts/update.nix --arg predicate '(path: pkg: pkg.updateScript.name or null == "gnome-update-script")'
196
197 to run update script for all packages matching given predicate, or
198
199 % nix-shell maintainers/scripts/update.nix --argstr path gnome
200
201 to run update script for all package under an attribute path.
202
203 You can also add
204
205 --argstr max-workers 8
206
207 to increase the number of jobs in parallel, or
208
209 --argstr keep-going true
210
211 to continue running when a single update fails.
212
213 You can also make the updater automatically commit on your behalf from updateScripts
214 that support it by adding
215
216 --argstr commit true
217
218 to skip prompt:
219
220 --argstr skip-prompt true
221
222 By default, the updater will update the packages in arbitrary order. Alternately, you can force a specific order based on the packages’ dependency relations:
223
224 - Reverse topological order (e.g. {"gnome-text-editor", "gimp"}, {"gtk3", "gtk4"}, {"glib"}) is useful when you want checkout each commit one by one to build each package individually but some of the packages to be updated would cause a mass rebuild for the others. Of course, this requires that none of the updated dependents require a new version of the dependency.
225
226 --argstr order reverse-topological
227
228 - Topological order (e.g. {"glib"}, {"gtk3", "gtk4"}, {"gnome-text-editor", "gimp"}) is useful when the updated dependents require a new version of updated dependency.
229
230 --argstr order topological
231
232 Note that sorting requires instantiating each package and then querying Nix store for requisites so it will be pretty slow with large number of packages.
233 '';
234
235 # Transform a matched package into an object for update.py.
236 packageData =
237 { package, attrPath }:
238 let
239 updateScript = get-script package;
240 in
241 {
242 name = package.name;
243 pname = lib.getName package;
244 oldVersion = lib.getVersion package;
245 updateScript = map builtins.toString (lib.toList (updateScript.command or updateScript));
246 supportedFeatures = updateScript.supportedFeatures or [ ];
247 attrPath = updateScript.attrPath or attrPath;
248 };
249
250 # JSON file with data for update.py.
251 packagesJson = pkgs.writeText "packages.json" (builtins.toJSON (map packageData packages));
252
253 optionalArgs =
254 lib.optional (max-workers != null) "--max-workers=${max-workers}"
255 ++ lib.optional (keep-going == "true") "--keep-going"
256 ++ lib.optional (commit == "true") "--commit"
257 ++ lib.optional (skip-prompt == "true") "--skip-prompt"
258 ++ lib.optional (order != null) "--order=${order}";
259
260 args = [ packagesJson ] ++ optionalArgs;
261
262in
263pkgs.stdenv.mkDerivation {
264 name = "nixpkgs-update-script";
265 buildCommand = ''
266 echo ""
267 echo "----------------------------------------------------------------"
268 echo ""
269 echo "Not possible to update packages using \`nix-build\`"
270 echo ""
271 echo "${helpText}"
272 echo "----------------------------------------------------------------"
273 exit 1
274 '';
275 shellHook = ''
276 unset shellHook # do not contaminate nested shells
277 exec ${pkgs.python3.interpreter} ${./update.py} ${builtins.concatStringsSep " " args}
278 '';
279 nativeBuildInputs = [
280 pkgs.git
281 pkgs.nix
282 pkgs.cacert
283 ];
284}