···475<programlisting>
476passthru.updateScript = [ ../../update.sh pname "--requested-release=unstable" ];
477</programlisting>
478- </para>
479- <para>
480- The script will be usually run from the root of the Nixpkgs repository but you should not rely on that. Also note that the update scripts will be run in parallel by default; you should avoid running <command>git commit</command> or any other commands that cannot handle that.
000481 </para>
482 <para>
483 For information about how to run the updates, execute <command>nix-shell maintainers/scripts/update.nix</command>.
···475<programlisting>
476passthru.updateScript = [ ../../update.sh pname "--requested-release=unstable" ];
477</programlisting>
478+ The script will be run with <variable>UPDATE_NIX_ATTR_PATH</variable> environment variable set to the attribute path it is supposed to update.
479+ <note>
480+ <para>
481+ The script will be usually run from the root of the Nixpkgs repository but you should not rely on that. Also note that the update scripts will be run in parallel by default; you should avoid running <command>git commit</command> or any other commands that cannot handle that.
482+ </para>
483+ </note>
484 </para>
485 <para>
486 For information about how to run the updates, execute <command>nix-shell maintainers/scripts/update.nix</command>.
+79-44
maintainers/scripts/update.nix
···4, max-workers ? null
5, include-overlays ? false
6, keep-going ? null
07}:
89# TODO: add assert statements
···31 in
32 [x] ++ nubOn f xs;
3334- packagesWithPath = relativePath: cond: return: pathContent:
35- let
36- result = builtins.tryEval pathContent;
3738- dedupResults = lst: nubOn (pkg: pkg.updateScript) (lib.concatLists lst);
39- in
40- if result.success then
000000041 let
42- pathContent = result.value;
0043 in
44- if lib.isDerivation pathContent then
45- lib.optional (cond relativePath pathContent) (return relativePath pathContent)
46- else if lib.isAttrs pathContent then
47- # If user explicitly points to an attrSet or it is marked for recursion, we recur.
48- if relativePath == [] || pathContent.recurseForDerivations or false || pathContent.recurseForRelease or false then
49- dedupResults (lib.mapAttrsToList (name: elem: packagesWithPath (relativePath ++ [name]) cond return elem) pathContent)
50- else []
51- else if lib.isList pathContent then
52- dedupResults (lib.imap0 (i: elem: packagesWithPath (relativePath ++ [i]) cond return elem) pathContent)
53- else []
54- else [];
000000550056 packagesWith = packagesWithPath [];
570058 packagesWithUpdateScriptAndMaintainer = maintainer':
59 let
60 maintainer =
···63 else
64 builtins.getAttr maintainer' lib.maintainers;
65 in
66- packagesWith (relativePath: pkg: builtins.hasAttr "updateScript" pkg &&
67- (if builtins.hasAttr "maintainers" pkg.meta
68- then (if builtins.isList pkg.meta.maintainers
69- then builtins.elem maintainer pkg.meta.maintainers
70- else maintainer == pkg.meta.maintainers
71- )
72- else false
73- )
74- )
75- (relativePath: pkg: pkg)
76- pkgs;
7778- packagesWithUpdateScript = path:
0079 let
80- pathContent = lib.attrByPath (lib.splitString "." path) null pkgs;
081 in
82 if pathContent == null then
83 builtins.throw "Attribute path `${path}` does not exists."
84 else
85- packagesWith (relativePath: pkg: builtins.hasAttr "updateScript" pkg)
86- (relativePath: pkg: pkg)
87 pathContent;
8889- packageByName = name:
0090 let
91- package = lib.attrByPath (lib.splitString "." name) null pkgs;
92 in
93 if package == null then
94- builtins.throw "Package with an attribute name `${name}` does not exists."
95 else if ! builtins.hasAttr "updateScript" package then
96- builtins.throw "Package with an attribute name `${name}` does not have a `passthru.updateScript` attribute defined."
97 else
98- package;
9900100 packages =
101 if package != null then
102- [ (packageByName package) ]
103 else if maintainer != null then
104- packagesWithUpdateScriptAndMaintainer maintainer
105 else if path != null then
106- packagesWithUpdateScript path
107 else
108 builtins.throw "No arguments provided.\n\n${helpText}";
109···132 --argstr keep-going true
133134 to continue running when a single update fails.
00000135 '';
136137- packageData = package: {
00138 name = package.name;
139 pname = lib.getName package;
140- updateScript = map builtins.toString (lib.toList package.updateScript);
000141 };
14200143 packagesJson = pkgs.writeText "packages.json" (builtins.toJSON (map packageData packages));
144145 optionalArgs =
146 lib.optional (max-workers != null) "--max-workers=${max-workers}"
147- ++ lib.optional (keep-going == "true") "--keep-going";
0148149 args = [ packagesJson ] ++ optionalArgs;
150
···4, max-workers ? null
5, include-overlays ? false
6, keep-going ? null
7+, commit ? null
8}:
910# TODO: add assert statements
···32 in
33 [x] ++ nubOn f xs;
3435+ /* Recursively find all packages (derivations) in `pkgs` matching `cond` predicate.
003637+ Type: packagesWithPath :: AttrPath → (AttrPath → derivation → bool) → (AttrSet | List) → List<AttrSet{attrPath :: str; package :: derivation; }>
38+ AttrPath :: [str]
39+40+ The packages will be returned as a list of named pairs comprising of:
41+ - attrPath: stringified attribute path (based on `rootPath`)
42+ - package: corresponding derivation
43+ */
44+ packagesWithPath = rootPath: cond: pkgs:
45+ let
46+ packagesWithPathInner = path: pathContent:
47 let
48+ result = builtins.tryEval pathContent;
49+50+ dedupResults = lst: nubOn ({ package, attrPath }: package.updateScript) (lib.concatLists lst);
51 in
52+ if result.success then
53+ let
54+ evaluatedPathContent = result.value;
55+ in
56+ if lib.isDerivation evaluatedPathContent then
57+ lib.optional (cond path evaluatedPathContent) { attrPath = lib.concatStringsSep "." path; package = evaluatedPathContent; }
58+ else if lib.isAttrs evaluatedPathContent then
59+ # If user explicitly points to an attrSet or it is marked for recursion, we recur.
60+ if path == rootPath || evaluatedPathContent.recurseForDerivations or false || evaluatedPathContent.recurseForRelease or false then
61+ dedupResults (lib.mapAttrsToList (name: elem: packagesWithPathInner (path ++ [name]) elem) evaluatedPathContent)
62+ else []
63+ else if lib.isList evaluatedPathContent then
64+ dedupResults (lib.imap0 (i: elem: packagesWithPathInner (path ++ [i]) elem) evaluatedPathContent)
65+ else []
66+ else [];
67+ in
68+ packagesWithPathInner rootPath pkgs;
6970+ /* Recursively find all packages (derivations) in `pkgs` matching `cond` predicate.
71+ */
72 packagesWith = packagesWithPath [];
7374+ /* Recursively find all packages in `pkgs` with updateScript by given maintainer.
75+ */
76 packagesWithUpdateScriptAndMaintainer = maintainer':
77 let
78 maintainer =
···81 else
82 builtins.getAttr maintainer' lib.maintainers;
83 in
84+ packagesWith (path: pkg: builtins.hasAttr "updateScript" pkg &&
85+ (if builtins.hasAttr "maintainers" pkg.meta
86+ then (if builtins.isList pkg.meta.maintainers
87+ then builtins.elem maintainer pkg.meta.maintainers
88+ else maintainer == pkg.meta.maintainers
89+ )
90+ else false
91+ )
92+ );
009394+ /* Recursively find all packages under `path` in `pkgs` with updateScript.
95+ */
96+ packagesWithUpdateScript = path: pkgs:
97 let
98+ prefix = lib.splitString "." path;
99+ pathContent = lib.attrByPath prefix null pkgs;
100 in
101 if pathContent == null then
102 builtins.throw "Attribute path `${path}` does not exists."
103 else
104+ packagesWithPath prefix (path: pkg: builtins.hasAttr "updateScript" pkg)
0105 pathContent;
106107+ /* Find a package under `path` in `pkgs` and require that it has an updateScript.
108+ */
109+ packageByName = path: pkgs:
110 let
111+ package = lib.attrByPath (lib.splitString "." path) null pkgs;
112 in
113 if package == null then
114+ builtins.throw "Package with an attribute name `${path}` does not exists."
115 else if ! builtins.hasAttr "updateScript" package then
116+ builtins.throw "Package with an attribute name `${path}` does not have a `passthru.updateScript` attribute defined."
117 else
118+ { attrPath = path; inherit package; };
119120+ /* List of packages matched based on the CLI arguments.
121+ */
122 packages =
123 if package != null then
124+ [ (packageByName package pkgs) ]
125 else if maintainer != null then
126+ packagesWithUpdateScriptAndMaintainer maintainer pkgs
127 else if path != null then
128+ packagesWithUpdateScript path pkgs
129 else
130 builtins.throw "No arguments provided.\n\n${helpText}";
131···154 --argstr keep-going true
155156 to continue running when a single update fails.
157+158+ You can also make the updater automatically commit on your behalf from updateScripts
159+ that support it by adding
160+161+ --argstr commit true
162 '';
163164+ /* Transform a matched package into an object for update.py.
165+ */
166+ packageData = { package, attrPath }: {
167 name = package.name;
168 pname = lib.getName package;
169+ oldVersion = lib.getVersion package;
170+ updateScript = map builtins.toString (lib.toList (package.updateScript.command or package.updateScript));
171+ supportedFeatures = package.updateScript.supportedFeatures or [];
172+ attrPath = package.updateScript.attrPath or attrPath;
173 };
174175+ /* JSON file with data for update.py.
176+ */
177 packagesJson = pkgs.writeText "packages.json" (builtins.toJSON (map packageData packages));
178179 optionalArgs =
180 lib.optional (max-workers != null) "--max-workers=${max-workers}"
181+ ++ lib.optional (keep-going == "true") "--keep-going"
182+ ++ lib.optional (commit == "true") "--commit";
183184 args = [ packagesJson ] ++ optionalArgs;
185
+178-35
maintainers/scripts/update.py
···001import argparse
2-import concurrent.futures
03import json
4import os
05import subprocess
6import sys
078-updates = {}
0910def eprint(*args, **kwargs):
11 print(*args, file=sys.stderr, **kwargs)
1213-def run_update_script(package):
0000000000000000000000000014 eprint(f" - {package['name']}: UPDATING ...")
1516- subprocess.run(package['updateScript'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=True)
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001701819-def main(max_workers, keep_going, packages):
20- with open(sys.argv[1]) as f:
0000000000000000000000000000000000000021 packages = json.load(f)
2223 eprint()
···31 eprint()
32 eprint('Running update for:')
3334- with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as executor:
35- for package in packages:
36- updates[executor.submit(run_update_script, package)] = package
37-38- for future in concurrent.futures.as_completed(updates):
39- package = updates[future]
40-41- try:
42- future.result()
43- eprint(f" - {package['name']}: DONE.")
44- except subprocess.CalledProcessError as e:
45- eprint(f" - {package['name']}: ERROR")
46- eprint()
47- eprint(f"--- SHOWING ERROR LOG FOR {package['name']} ----------------------")
48- eprint()
49- eprint(e.stdout.decode('utf-8'))
50- with open(f"{package['pname']}.log", 'wb') as f:
51- f.write(e.stdout)
52- eprint()
53- eprint(f"--- SHOWING ERROR LOG FOR {package['name']} ----------------------")
54-55- if not keep_going:
56- sys.exit(1)
5758 eprint()
59 eprint('Packages updated!')
···65parser = argparse.ArgumentParser(description='Update packages')
66parser.add_argument('--max-workers', '-j', dest='max_workers', type=int, help='Number of updates to run concurrently', nargs='?', default=4)
67parser.add_argument('--keep-going', '-k', dest='keep_going', action='store_true', help='Do not stop after first failure')
068parser.add_argument('packages', help='JSON file containing the list of package names and their update scripts')
6970if __name__ == '__main__':
71 args = parser.parse_args()
7273 try:
74- main(args.max_workers, args.keep_going, args.packages)
75- except (KeyboardInterrupt, SystemExit) as e:
76- for update in updates:
77- update.cancel()
78-79- sys.exit(e.code if isinstance(e, SystemExit) else 130)
···1+from __future__ import annotations
2+from typing import Dict, Generator, List, Optional, Tuple
3import argparse
4+import asyncio
5+import contextlib
6import json
7import os
8+import re
9import subprocess
10import sys
11+import tempfile
1213+class CalledProcessError(Exception):
14+ process: asyncio.subprocess.Process
1516def eprint(*args, **kwargs):
17 print(*args, file=sys.stderr, **kwargs)
1819+async def check_subprocess(*args, **kwargs):
20+ """
21+ Emulate check argument of subprocess.run function.
22+ """
23+ process = await asyncio.create_subprocess_exec(*args, **kwargs)
24+ returncode = await process.wait()
25+26+ if returncode != 0:
27+ error = CalledProcessError()
28+ error.process = process
29+30+ raise error
31+32+ return process
33+34+async def run_update_script(nixpkgs_root: str, merge_lock: asyncio.Lock, temp_dir: Optional[Tuple[str, str]], package: Dict, keep_going: bool):
35+ worktree: Optional[str] = None
36+37+ update_script_command = package['updateScript']
38+39+ if temp_dir is not None:
40+ worktree, _branch = temp_dir
41+42+ # Update scripts can use $(dirname $0) to get their location but we want to run
43+ # their clones in the git worktree, not in the main nixpkgs repo.
44+ update_script_command = map(lambda arg: re.sub(r'^{0}'.format(re.escape(nixpkgs_root)), worktree, arg), update_script_command)
45+46 eprint(f" - {package['name']}: UPDATING ...")
4748+ try:
49+ update_process = await check_subprocess('env', f"UPDATE_NIX_ATTR_PATH={package['attrPath']}", *update_script_command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, cwd=worktree)
50+ update_info = await update_process.stdout.read()
51+52+ await merge_changes(merge_lock, package, update_info, temp_dir)
53+ except KeyboardInterrupt as e:
54+ eprint('Cancelling…')
55+ raise asyncio.exceptions.CancelledError()
56+ except CalledProcessError as e:
57+ eprint(f" - {package['name']}: ERROR")
58+ eprint()
59+ eprint(f"--- SHOWING ERROR LOG FOR {package['name']} ----------------------")
60+ eprint()
61+ stderr = await e.process.stderr.read()
62+ eprint(stderr.decode('utf-8'))
63+ with open(f"{package['pname']}.log", 'wb') as logfile:
64+ logfile.write(stderr)
65+ eprint()
66+ eprint(f"--- SHOWING ERROR LOG FOR {package['name']} ----------------------")
67+68+ if not keep_going:
69+ raise asyncio.exceptions.CancelledError()
70+71+@contextlib.contextmanager
72+def make_worktree() -> Generator[Tuple[str, str], None, None]:
73+ with tempfile.TemporaryDirectory() as wt:
74+ branch_name = f'update-{os.path.basename(wt)}'
75+ target_directory = f'{wt}/nixpkgs'
76+77+ subprocess.run(['git', 'worktree', 'add', '-b', branch_name, target_directory])
78+ yield (target_directory, branch_name)
79+ subprocess.run(['git', 'worktree', 'remove', '--force', target_directory])
80+ subprocess.run(['git', 'branch', '-D', branch_name])
81+82+async def commit_changes(name: str, merge_lock: asyncio.Lock, worktree: str, branch: str, changes: List[Dict]) -> None:
83+ for change in changes:
84+ # Git can only handle a single index operation at a time
85+ async with merge_lock:
86+ await check_subprocess('git', 'add', *change['files'], cwd=worktree)
87+ commit_message = '{attrPath}: {oldVersion} → {newVersion}'.format(**change)
88+ await check_subprocess('git', 'commit', '--quiet', '-m', commit_message, cwd=worktree)
89+ await check_subprocess('git', 'cherry-pick', branch)
90+91+async def check_changes(package: Dict, worktree: str, update_info: str):
92+ if 'commit' in package['supportedFeatures']:
93+ changes = json.loads(update_info)
94+ else:
95+ changes = [{}]
96+97+ # Try to fill in missing attributes when there is just a single change.
98+ if len(changes) == 1:
99+ # Dynamic data from updater take precedence over static data from passthru.updateScript.
100+ if 'attrPath' not in changes[0]:
101+ # update.nix is always passing attrPath
102+ changes[0]['attrPath'] = package['attrPath']
103+104+ if 'oldVersion' not in changes[0]:
105+ # update.nix is always passing oldVersion
106+ changes[0]['oldVersion'] = package['oldVersion']
107+108+ if 'newVersion' not in changes[0]:
109+ attr_path = changes[0]['attrPath']
110+ obtain_new_version_process = await check_subprocess('nix-instantiate', '--expr', f'with import ./. {{}}; lib.getVersion {attr_path}', '--eval', '--strict', '--json', stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, cwd=worktree)
111+ changes[0]['newVersion'] = json.loads((await obtain_new_version_process.stdout.read()).decode('utf-8'))
112+113+ if 'files' not in changes[0]:
114+ changed_files_process = await check_subprocess('git', 'diff', '--name-only', stdout=asyncio.subprocess.PIPE, cwd=worktree)
115+ changed_files = (await changed_files_process.stdout.read()).splitlines()
116+ changes[0]['files'] = changed_files
117+118+ if len(changed_files) == 0:
119+ return []
120+121+ return changes
122+123+async def merge_changes(merge_lock: asyncio.Lock, package: Dict, update_info: str, temp_dir: Optional[Tuple[str, str]]) -> None:
124+ if temp_dir is not None:
125+ worktree, branch = temp_dir
126+ changes = await check_changes(package, worktree, update_info)
127+128+ if len(changes) > 0:
129+ await commit_changes(package['name'], merge_lock, worktree, branch, changes)
130+ else:
131+ eprint(f" - {package['name']}: DONE, no changes.")
132+ else:
133+ eprint(f" - {package['name']}: DONE.")
134+135+async def updater(nixpkgs_root: str, temp_dir: Optional[Tuple[str, str]], merge_lock: asyncio.Lock, packages_to_update: asyncio.Queue[Optional[Dict]], keep_going: bool, commit: bool):
136+ while True:
137+ package = await packages_to_update.get()
138+ if package is None:
139+ # A sentinel received, we are done.
140+ return
141+142+ if not ('commit' in package['supportedFeatures'] or 'attrPath' in package):
143+ temp_dir = None
144145+ await run_update_script(nixpkgs_root, merge_lock, temp_dir, package, keep_going)
146147+async def start_updates(max_workers: int, keep_going: bool, commit: bool, packages: List[Dict]):
148+ merge_lock = asyncio.Lock()
149+ packages_to_update: asyncio.Queue[Optional[Dict]] = asyncio.Queue()
150+151+ with contextlib.ExitStack() as stack:
152+ temp_dirs: List[Optional[Tuple[str, str]]] = []
153+154+ # Do not create more workers than there are packages.
155+ num_workers = min(max_workers, len(packages))
156+157+ nixpkgs_root_process = await check_subprocess('git', 'rev-parse', '--show-toplevel', stdout=asyncio.subprocess.PIPE)
158+ nixpkgs_root = (await nixpkgs_root_process.stdout.read()).decode('utf-8').strip()
159+160+ # Set up temporary directories when using auto-commit.
161+ for i in range(num_workers):
162+ temp_dir = stack.enter_context(make_worktree()) if commit else None
163+ temp_dirs.append(temp_dir)
164+165+ # Fill up an update queue,
166+ for package in packages:
167+ await packages_to_update.put(package)
168+169+ # Add sentinels, one for each worker.
170+ # A workers will terminate when it gets sentinel from the queue.
171+ for i in range(num_workers):
172+ await packages_to_update.put(None)
173+174+ # Prepare updater workers for each temp_dir directory.
175+ # At most `num_workers` instances of `run_update_script` will be running at one time.
176+ updaters = asyncio.gather(*[updater(nixpkgs_root, temp_dir, merge_lock, packages_to_update, keep_going, commit) for temp_dir in temp_dirs])
177+178+ try:
179+ # Start updater workers.
180+ await updaters
181+ except asyncio.exceptions.CancelledError as e:
182+ # When one worker is cancelled, cancel the others too.
183+ updaters.cancel()
184+185+def main(max_workers: int, keep_going: bool, commit: bool, packages_path: str) -> None:
186+ with open(packages_path) as f:
187 packages = json.load(f)
188189 eprint()
···197 eprint()
198 eprint('Running update for:')
199200+ asyncio.run(start_updates(max_workers, keep_going, commit, packages))
0000000000000000000000201202 eprint()
203 eprint('Packages updated!')
···209parser = argparse.ArgumentParser(description='Update packages')
210parser.add_argument('--max-workers', '-j', dest='max_workers', type=int, help='Number of updates to run concurrently', nargs='?', default=4)
211parser.add_argument('--keep-going', '-k', dest='keep_going', action='store_true', help='Do not stop after first failure')
212+parser.add_argument('--commit', '-c', dest='commit', action='store_true', help='Commit the changes')
213parser.add_argument('packages', help='JSON file containing the list of package names and their update scripts')
214215if __name__ == '__main__':
216 args = parser.parse_args()
217218 try:
219+ main(args.max_workers, args.keep_going, args.commit, args.packages)
220+ except KeyboardInterrupt as e:
221+ # Let’s cancel outside of the main loop too.
222+ sys.exit(130)
00
+11-1
pkgs/common-updater/scripts/update-source-version
···11usage() {
12 echo "Usage: $scriptName <attr> <version> [<new-source-hash>] [<new-source-url>]"
13 echo " [--version-key=<version-key>] [--system=<system>] [--file=<file-to-update>]"
14- echo " [--ignore-same-hash]"
15}
1617args=()
···32 ;;
33 --ignore-same-hash)
34 ignoreSameHash="true"
00035 ;;
36 --help)
37 usage
···102103if [[ "$oldVersion" = "$newVersion" ]]; then
104 echo "$scriptName: New version same as old version, nothing to do." >&2
000105 exit 0
106fi
107···197198rm -f "$nixFile.bak"
199rm -f "$attr.fetchlog"
0000
···11usage() {
12 echo "Usage: $scriptName <attr> <version> [<new-source-hash>] [<new-source-url>]"
13 echo " [--version-key=<version-key>] [--system=<system>] [--file=<file-to-update>]"
14+ echo " [--ignore-same-hash] [--print-changes]"
15}
1617args=()
···32 ;;
33 --ignore-same-hash)
34 ignoreSameHash="true"
35+ ;;
36+ --print-changes)
37+ printChanges="true"
38 ;;
39 --help)
40 usage
···105106if [[ "$oldVersion" = "$newVersion" ]]; then
107 echo "$scriptName: New version same as old version, nothing to do." >&2
108+ if [ -n "$printChanges" ]; then
109+ printf '[]\n'
110+ fi
111 exit 0
112fi
113···203204rm -f "$nixFile.bak"
205rm -f "$attr.fetchlog"
206+207+if [ -n "$printChanges" ]; then
208+ printf '[{"attrPath":"%s","oldVersion":"%s","newVersion":"%s","files":["%s"]}]\n' "$attr" "$oldVersion" "$newVersion" "$nixFile"
209+fi