···475475<programlisting>
476476passthru.updateScript = [ ../../update.sh pname "--requested-release=unstable" ];
477477</programlisting>
478478- </para>
479479- <para>
480480- 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.
478478+ The script will be run with <variable>UPDATE_NIX_ATTR_PATH</variable> environment variable set to the attribute path it is supposed to update.
479479+ <note>
480480+ <para>
481481+ 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.
482482+ </para>
483483+ </note>
481484 </para>
482485 <para>
483486 For information about how to run the updates, execute <command>nix-shell maintainers/scripts/update.nix</command>.
+79-44
maintainers/scripts/update.nix
···44, max-workers ? null
55, include-overlays ? false
66, keep-going ? null
77+, commit ? null
78}:
89910# TODO: add assert statements
···3132 in
3233 [x] ++ nubOn f xs;
33343434- packagesWithPath = relativePath: cond: return: pathContent:
3535- let
3636- result = builtins.tryEval pathContent;
3535+ /* Recursively find all packages (derivations) in `pkgs` matching `cond` predicate.
37363838- dedupResults = lst: nubOn (pkg: pkg.updateScript) (lib.concatLists lst);
3939- in
4040- if result.success then
3737+ Type: packagesWithPath :: AttrPath → (AttrPath → derivation → bool) → (AttrSet | List) → List<AttrSet{attrPath :: str; package :: derivation; }>
3838+ AttrPath :: [str]
3939+4040+ The packages will be returned as a list of named pairs comprising of:
4141+ - attrPath: stringified attribute path (based on `rootPath`)
4242+ - package: corresponding derivation
4343+ */
4444+ packagesWithPath = rootPath: cond: pkgs:
4545+ let
4646+ packagesWithPathInner = path: pathContent:
4147 let
4242- pathContent = result.value;
4848+ result = builtins.tryEval pathContent;
4949+5050+ dedupResults = lst: nubOn ({ package, attrPath }: package.updateScript) (lib.concatLists lst);
4351 in
4444- if lib.isDerivation pathContent then
4545- lib.optional (cond relativePath pathContent) (return relativePath pathContent)
4646- else if lib.isAttrs pathContent then
4747- # If user explicitly points to an attrSet or it is marked for recursion, we recur.
4848- if relativePath == [] || pathContent.recurseForDerivations or false || pathContent.recurseForRelease or false then
4949- dedupResults (lib.mapAttrsToList (name: elem: packagesWithPath (relativePath ++ [name]) cond return elem) pathContent)
5050- else []
5151- else if lib.isList pathContent then
5252- dedupResults (lib.imap0 (i: elem: packagesWithPath (relativePath ++ [i]) cond return elem) pathContent)
5353- else []
5454- else [];
5252+ if result.success then
5353+ let
5454+ evaluatedPathContent = result.value;
5555+ in
5656+ if lib.isDerivation evaluatedPathContent then
5757+ lib.optional (cond path evaluatedPathContent) { attrPath = lib.concatStringsSep "." path; package = evaluatedPathContent; }
5858+ else if lib.isAttrs evaluatedPathContent then
5959+ # If user explicitly points to an attrSet or it is marked for recursion, we recur.
6060+ if path == rootPath || evaluatedPathContent.recurseForDerivations or false || evaluatedPathContent.recurseForRelease or false then
6161+ dedupResults (lib.mapAttrsToList (name: elem: packagesWithPathInner (path ++ [name]) elem) evaluatedPathContent)
6262+ else []
6363+ else if lib.isList evaluatedPathContent then
6464+ dedupResults (lib.imap0 (i: elem: packagesWithPathInner (path ++ [i]) elem) evaluatedPathContent)
6565+ else []
6666+ else [];
6767+ in
6868+ packagesWithPathInner rootPath pkgs;
55697070+ /* Recursively find all packages (derivations) in `pkgs` matching `cond` predicate.
7171+ */
5672 packagesWith = packagesWithPath [];
57737474+ /* Recursively find all packages in `pkgs` with updateScript by given maintainer.
7575+ */
5876 packagesWithUpdateScriptAndMaintainer = maintainer':
5977 let
6078 maintainer =
···6381 else
6482 builtins.getAttr maintainer' lib.maintainers;
6583 in
6666- packagesWith (relativePath: pkg: builtins.hasAttr "updateScript" pkg &&
6767- (if builtins.hasAttr "maintainers" pkg.meta
6868- then (if builtins.isList pkg.meta.maintainers
6969- then builtins.elem maintainer pkg.meta.maintainers
7070- else maintainer == pkg.meta.maintainers
7171- )
7272- else false
7373- )
7474- )
7575- (relativePath: pkg: pkg)
7676- pkgs;
8484+ packagesWith (path: pkg: builtins.hasAttr "updateScript" pkg &&
8585+ (if builtins.hasAttr "maintainers" pkg.meta
8686+ then (if builtins.isList pkg.meta.maintainers
8787+ then builtins.elem maintainer pkg.meta.maintainers
8888+ else maintainer == pkg.meta.maintainers
8989+ )
9090+ else false
9191+ )
9292+ );
77937878- packagesWithUpdateScript = path:
9494+ /* Recursively find all packages under `path` in `pkgs` with updateScript.
9595+ */
9696+ packagesWithUpdateScript = path: pkgs:
7997 let
8080- pathContent = lib.attrByPath (lib.splitString "." path) null pkgs;
9898+ prefix = lib.splitString "." path;
9999+ pathContent = lib.attrByPath prefix null pkgs;
81100 in
82101 if pathContent == null then
83102 builtins.throw "Attribute path `${path}` does not exists."
84103 else
8585- packagesWith (relativePath: pkg: builtins.hasAttr "updateScript" pkg)
8686- (relativePath: pkg: pkg)
104104+ packagesWithPath prefix (path: pkg: builtins.hasAttr "updateScript" pkg)
87105 pathContent;
881068989- packageByName = name:
107107+ /* Find a package under `path` in `pkgs` and require that it has an updateScript.
108108+ */
109109+ packageByName = path: pkgs:
90110 let
9191- package = lib.attrByPath (lib.splitString "." name) null pkgs;
111111+ package = lib.attrByPath (lib.splitString "." path) null pkgs;
92112 in
93113 if package == null then
9494- builtins.throw "Package with an attribute name `${name}` does not exists."
114114+ builtins.throw "Package with an attribute name `${path}` does not exists."
95115 else if ! builtins.hasAttr "updateScript" package then
9696- builtins.throw "Package with an attribute name `${name}` does not have a `passthru.updateScript` attribute defined."
116116+ builtins.throw "Package with an attribute name `${path}` does not have a `passthru.updateScript` attribute defined."
97117 else
9898- package;
118118+ { attrPath = path; inherit package; };
99119120120+ /* List of packages matched based on the CLI arguments.
121121+ */
100122 packages =
101123 if package != null then
102102- [ (packageByName package) ]
124124+ [ (packageByName package pkgs) ]
103125 else if maintainer != null then
104104- packagesWithUpdateScriptAndMaintainer maintainer
126126+ packagesWithUpdateScriptAndMaintainer maintainer pkgs
105127 else if path != null then
106106- packagesWithUpdateScript path
128128+ packagesWithUpdateScript path pkgs
107129 else
108130 builtins.throw "No arguments provided.\n\n${helpText}";
109131···132154 --argstr keep-going true
133155134156 to continue running when a single update fails.
157157+158158+ You can also make the updater automatically commit on your behalf from updateScripts
159159+ that support it by adding
160160+161161+ --argstr commit true
135162 '';
136163137137- packageData = package: {
164164+ /* Transform a matched package into an object for update.py.
165165+ */
166166+ packageData = { package, attrPath }: {
138167 name = package.name;
139168 pname = lib.getName package;
140140- updateScript = map builtins.toString (lib.toList package.updateScript);
169169+ oldVersion = lib.getVersion package;
170170+ updateScript = map builtins.toString (lib.toList (package.updateScript.command or package.updateScript));
171171+ supportedFeatures = package.updateScript.supportedFeatures or [];
172172+ attrPath = package.updateScript.attrPath or attrPath;
141173 };
142174175175+ /* JSON file with data for update.py.
176176+ */
143177 packagesJson = pkgs.writeText "packages.json" (builtins.toJSON (map packageData packages));
144178145179 optionalArgs =
146180 lib.optional (max-workers != null) "--max-workers=${max-workers}"
147147- ++ lib.optional (keep-going == "true") "--keep-going";
181181+ ++ lib.optional (keep-going == "true") "--keep-going"
182182+ ++ lib.optional (commit == "true") "--commit";
148183149184 args = [ packagesJson ] ++ optionalArgs;
150185
+178-35
maintainers/scripts/update.py
···11+from __future__ import annotations
22+from typing import Dict, Generator, List, Optional, Tuple
13import argparse
22-import concurrent.futures
44+import asyncio
55+import contextlib
36import json
47import os
88+import re
59import subprocess
610import sys
1111+import tempfile
71288-updates = {}
1313+class CalledProcessError(Exception):
1414+ process: asyncio.subprocess.Process
9151016def eprint(*args, **kwargs):
1117 print(*args, file=sys.stderr, **kwargs)
12181313-def run_update_script(package):
1919+async def check_subprocess(*args, **kwargs):
2020+ """
2121+ Emulate check argument of subprocess.run function.
2222+ """
2323+ process = await asyncio.create_subprocess_exec(*args, **kwargs)
2424+ returncode = await process.wait()
2525+2626+ if returncode != 0:
2727+ error = CalledProcessError()
2828+ error.process = process
2929+3030+ raise error
3131+3232+ return process
3333+3434+async def run_update_script(nixpkgs_root: str, merge_lock: asyncio.Lock, temp_dir: Optional[Tuple[str, str]], package: Dict, keep_going: bool):
3535+ worktree: Optional[str] = None
3636+3737+ update_script_command = package['updateScript']
3838+3939+ if temp_dir is not None:
4040+ worktree, _branch = temp_dir
4141+4242+ # Update scripts can use $(dirname $0) to get their location but we want to run
4343+ # their clones in the git worktree, not in the main nixpkgs repo.
4444+ update_script_command = map(lambda arg: re.sub(r'^{0}'.format(re.escape(nixpkgs_root)), worktree, arg), update_script_command)
4545+1446 eprint(f" - {package['name']}: UPDATING ...")
15471616- subprocess.run(package['updateScript'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=True)
4848+ try:
4949+ 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)
5050+ update_info = await update_process.stdout.read()
5151+5252+ await merge_changes(merge_lock, package, update_info, temp_dir)
5353+ except KeyboardInterrupt as e:
5454+ eprint('Cancelling…')
5555+ raise asyncio.exceptions.CancelledError()
5656+ except CalledProcessError as e:
5757+ eprint(f" - {package['name']}: ERROR")
5858+ eprint()
5959+ eprint(f"--- SHOWING ERROR LOG FOR {package['name']} ----------------------")
6060+ eprint()
6161+ stderr = await e.process.stderr.read()
6262+ eprint(stderr.decode('utf-8'))
6363+ with open(f"{package['pname']}.log", 'wb') as logfile:
6464+ logfile.write(stderr)
6565+ eprint()
6666+ eprint(f"--- SHOWING ERROR LOG FOR {package['name']} ----------------------")
6767+6868+ if not keep_going:
6969+ raise asyncio.exceptions.CancelledError()
7070+7171+@contextlib.contextmanager
7272+def make_worktree() -> Generator[Tuple[str, str], None, None]:
7373+ with tempfile.TemporaryDirectory() as wt:
7474+ branch_name = f'update-{os.path.basename(wt)}'
7575+ target_directory = f'{wt}/nixpkgs'
7676+7777+ subprocess.run(['git', 'worktree', 'add', '-b', branch_name, target_directory])
7878+ yield (target_directory, branch_name)
7979+ subprocess.run(['git', 'worktree', 'remove', '--force', target_directory])
8080+ subprocess.run(['git', 'branch', '-D', branch_name])
8181+8282+async def commit_changes(name: str, merge_lock: asyncio.Lock, worktree: str, branch: str, changes: List[Dict]) -> None:
8383+ for change in changes:
8484+ # Git can only handle a single index operation at a time
8585+ async with merge_lock:
8686+ await check_subprocess('git', 'add', *change['files'], cwd=worktree)
8787+ commit_message = '{attrPath}: {oldVersion} → {newVersion}'.format(**change)
8888+ await check_subprocess('git', 'commit', '--quiet', '-m', commit_message, cwd=worktree)
8989+ await check_subprocess('git', 'cherry-pick', branch)
9090+9191+async def check_changes(package: Dict, worktree: str, update_info: str):
9292+ if 'commit' in package['supportedFeatures']:
9393+ changes = json.loads(update_info)
9494+ else:
9595+ changes = [{}]
9696+9797+ # Try to fill in missing attributes when there is just a single change.
9898+ if len(changes) == 1:
9999+ # Dynamic data from updater take precedence over static data from passthru.updateScript.
100100+ if 'attrPath' not in changes[0]:
101101+ # update.nix is always passing attrPath
102102+ changes[0]['attrPath'] = package['attrPath']
103103+104104+ if 'oldVersion' not in changes[0]:
105105+ # update.nix is always passing oldVersion
106106+ changes[0]['oldVersion'] = package['oldVersion']
107107+108108+ if 'newVersion' not in changes[0]:
109109+ attr_path = changes[0]['attrPath']
110110+ 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)
111111+ changes[0]['newVersion'] = json.loads((await obtain_new_version_process.stdout.read()).decode('utf-8'))
112112+113113+ if 'files' not in changes[0]:
114114+ changed_files_process = await check_subprocess('git', 'diff', '--name-only', stdout=asyncio.subprocess.PIPE, cwd=worktree)
115115+ changed_files = (await changed_files_process.stdout.read()).splitlines()
116116+ changes[0]['files'] = changed_files
117117+118118+ if len(changed_files) == 0:
119119+ return []
120120+121121+ return changes
122122+123123+async def merge_changes(merge_lock: asyncio.Lock, package: Dict, update_info: str, temp_dir: Optional[Tuple[str, str]]) -> None:
124124+ if temp_dir is not None:
125125+ worktree, branch = temp_dir
126126+ changes = await check_changes(package, worktree, update_info)
127127+128128+ if len(changes) > 0:
129129+ await commit_changes(package['name'], merge_lock, worktree, branch, changes)
130130+ else:
131131+ eprint(f" - {package['name']}: DONE, no changes.")
132132+ else:
133133+ eprint(f" - {package['name']}: DONE.")
134134+135135+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):
136136+ while True:
137137+ package = await packages_to_update.get()
138138+ if package is None:
139139+ # A sentinel received, we are done.
140140+ return
141141+142142+ if not ('commit' in package['supportedFeatures'] or 'attrPath' in package):
143143+ temp_dir = None
17144145145+ await run_update_script(nixpkgs_root, merge_lock, temp_dir, package, keep_going)
181461919-def main(max_workers, keep_going, packages):
2020- with open(sys.argv[1]) as f:
147147+async def start_updates(max_workers: int, keep_going: bool, commit: bool, packages: List[Dict]):
148148+ merge_lock = asyncio.Lock()
149149+ packages_to_update: asyncio.Queue[Optional[Dict]] = asyncio.Queue()
150150+151151+ with contextlib.ExitStack() as stack:
152152+ temp_dirs: List[Optional[Tuple[str, str]]] = []
153153+154154+ # Do not create more workers than there are packages.
155155+ num_workers = min(max_workers, len(packages))
156156+157157+ nixpkgs_root_process = await check_subprocess('git', 'rev-parse', '--show-toplevel', stdout=asyncio.subprocess.PIPE)
158158+ nixpkgs_root = (await nixpkgs_root_process.stdout.read()).decode('utf-8').strip()
159159+160160+ # Set up temporary directories when using auto-commit.
161161+ for i in range(num_workers):
162162+ temp_dir = stack.enter_context(make_worktree()) if commit else None
163163+ temp_dirs.append(temp_dir)
164164+165165+ # Fill up an update queue,
166166+ for package in packages:
167167+ await packages_to_update.put(package)
168168+169169+ # Add sentinels, one for each worker.
170170+ # A workers will terminate when it gets sentinel from the queue.
171171+ for i in range(num_workers):
172172+ await packages_to_update.put(None)
173173+174174+ # Prepare updater workers for each temp_dir directory.
175175+ # At most `num_workers` instances of `run_update_script` will be running at one time.
176176+ updaters = asyncio.gather(*[updater(nixpkgs_root, temp_dir, merge_lock, packages_to_update, keep_going, commit) for temp_dir in temp_dirs])
177177+178178+ try:
179179+ # Start updater workers.
180180+ await updaters
181181+ except asyncio.exceptions.CancelledError as e:
182182+ # When one worker is cancelled, cancel the others too.
183183+ updaters.cancel()
184184+185185+def main(max_workers: int, keep_going: bool, commit: bool, packages_path: str) -> None:
186186+ with open(packages_path) as f:
21187 packages = json.load(f)
2218823189 eprint()
···31197 eprint()
32198 eprint('Running update for:')
331993434- with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as executor:
3535- for package in packages:
3636- updates[executor.submit(run_update_script, package)] = package
3737-3838- for future in concurrent.futures.as_completed(updates):
3939- package = updates[future]
4040-4141- try:
4242- future.result()
4343- eprint(f" - {package['name']}: DONE.")
4444- except subprocess.CalledProcessError as e:
4545- eprint(f" - {package['name']}: ERROR")
4646- eprint()
4747- eprint(f"--- SHOWING ERROR LOG FOR {package['name']} ----------------------")
4848- eprint()
4949- eprint(e.stdout.decode('utf-8'))
5050- with open(f"{package['pname']}.log", 'wb') as f:
5151- f.write(e.stdout)
5252- eprint()
5353- eprint(f"--- SHOWING ERROR LOG FOR {package['name']} ----------------------")
5454-5555- if not keep_going:
5656- sys.exit(1)
200200+ asyncio.run(start_updates(max_workers, keep_going, commit, packages))
5720158202 eprint()
59203 eprint('Packages updated!')
···65209parser = argparse.ArgumentParser(description='Update packages')
66210parser.add_argument('--max-workers', '-j', dest='max_workers', type=int, help='Number of updates to run concurrently', nargs='?', default=4)
67211parser.add_argument('--keep-going', '-k', dest='keep_going', action='store_true', help='Do not stop after first failure')
212212+parser.add_argument('--commit', '-c', dest='commit', action='store_true', help='Commit the changes')
68213parser.add_argument('packages', help='JSON file containing the list of package names and their update scripts')
6921470215if __name__ == '__main__':
71216 args = parser.parse_args()
7221773218 try:
7474- main(args.max_workers, args.keep_going, args.packages)
7575- except (KeyboardInterrupt, SystemExit) as e:
7676- for update in updates:
7777- update.cancel()
7878-7979- sys.exit(e.code if isinstance(e, SystemExit) else 130)
219219+ main(args.max_workers, args.keep_going, args.commit, args.packages)
220220+ except KeyboardInterrupt as e:
221221+ # Let’s cancel outside of the main loop too.
222222+ sys.exit(130)
+11-1
pkgs/common-updater/scripts/update-source-version
···1111usage() {
1212 echo "Usage: $scriptName <attr> <version> [<new-source-hash>] [<new-source-url>]"
1313 echo " [--version-key=<version-key>] [--system=<system>] [--file=<file-to-update>]"
1414- echo " [--ignore-same-hash]"
1414+ echo " [--ignore-same-hash] [--print-changes]"
1515}
16161717args=()
···3232 ;;
3333 --ignore-same-hash)
3434 ignoreSameHash="true"
3535+ ;;
3636+ --print-changes)
3737+ printChanges="true"
3538 ;;
3639 --help)
3740 usage
···102105103106if [[ "$oldVersion" = "$newVersion" ]]; then
104107 echo "$scriptName: New version same as old version, nothing to do." >&2
108108+ if [ -n "$printChanges" ]; then
109109+ printf '[]\n'
110110+ fi
105111 exit 0
106112fi
107113···197203198204rm -f "$nixFile.bak"
199205rm -f "$attr.fetchlog"
206206+207207+if [ -n "$printChanges" ]; then
208208+ printf '[{"attrPath":"%s","oldVersion":"%s","newVersion":"%s","files":["%s"]}]\n' "$attr" "$oldVersion" "$newVersion" "$nixFile"
209209+fi