Skip to content

Commit 8f23631

Browse files
author
Doug Coleman
committed
utils: Make update-checkout run in parallel.
1 parent 33f1e11 commit 8f23631

File tree

2 files changed

+190
-101
lines changed

2 files changed

+190
-101
lines changed

utils/swift_build_support/swift_build_support/shell.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import shutil
2121
import subprocess
2222
import sys
23+
from multiprocessing import Pool, Lock, cpu_count
2324
from contextlib import contextmanager
2425

2526
from . import diagnostics
@@ -170,3 +171,71 @@ def copytree(src, dest, dry_run=None, echo=True):
170171
if dry_run:
171172
return
172173
shutil.copytree(src, dest)
174+
175+
176+
def run(*args, **kwargs):
177+
msg = None
178+
repo_path = os.getcwd()
179+
try:
180+
echo_output = kwargs.pop('echo', False)
181+
msg = kwargs.pop('msg', False)
182+
dry_run = kwargs.pop('dry_run', False)
183+
env = kwargs.pop('env', None)
184+
allow_non_zero_exit = kwargs.pop('allow_non_zero_exit', False)
185+
if dry_run:
186+
_echo_command(dry_run, *args, env=env)
187+
return(None, 0, args)
188+
189+
out = subprocess.check_output(*args, stderr=subprocess.STDOUT, **kwargs)
190+
lock.acquire()
191+
192+
if out and echo_output:
193+
print(repo_path)
194+
_echo_command(dry_run, *args, env=env)
195+
print(out)
196+
lock.release()
197+
return (out, 0, args)
198+
except subprocess.CalledProcessError as e:
199+
if allow_non_zero_exit:
200+
print("non-fatal error running ``%s`` on ``%s``" % (" ".join(*args), repo_path))
201+
return (out, 0, args)
202+
print("error running ``%s`` on ``%s``" % (" ".join(*args), repo_path))
203+
# Workaround for exception bug in 2.7.12
204+
# http://bugs.python.org/issue9400
205+
eout = Exception(str(e))
206+
eout.repo_path = repo_path
207+
if msg is not None and msg is not False:
208+
print(msg)
209+
eout.msg = msg
210+
raise eout
211+
212+
213+
def run_parallel(fn, pool_args, n_processes=0):
214+
def init(l):
215+
global lock
216+
lock = l
217+
218+
if n_processes == 0:
219+
n_processes = cpu_count()
220+
221+
l = Lock()
222+
pool = Pool(processes=n_processes, initializer=init, initargs=(l,))
223+
results = pool.map_async(func=fn, iterable=pool_args).get(9999999)
224+
pool.close()
225+
pool.join()
226+
return results
227+
228+
229+
def check_parallel_results(results, op):
230+
fail_count = 0
231+
if results is None:
232+
return 0
233+
for r in results:
234+
if r is not None:
235+
if fail_count == 0:
236+
print("======%s FAILURES======" % op)
237+
print("%s failed: %s" % (r.repo_path, r))
238+
fail_count += 1
239+
if r.msg:
240+
print(r.msg)
241+
return fail_count

utils/update-checkout

Lines changed: 121 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ import json
1616
import os
1717
import re
1818
import sys
19+
import traceback
20+
import subprocess
1921

2022

2123
from functools import reduce
24+
from multiprocessing import freeze_support
25+
2226

2327
sys.path.append(os.path.dirname(__file__))
2428

@@ -34,56 +38,53 @@ sys.path.append(os.path.join(SCRIPT_DIR, 'swift_build_support'))
3438
from swift_build_support import shell # noqa (E402)
3539

3640

37-
def update_single_repository(repo_path, branch, reset_to_remote, should_clean,
38-
cross_repo):
41+
def update_single_repository(args):
42+
repo_path, branch, reset_to_remote, should_clean, cross_repo = args
3943
if not os.path.isdir(repo_path):
4044
return
4145

42-
print("--- Updating '" + repo_path + "' ---")
43-
with shell.pushd(repo_path, dry_run=False, echo=False):
44-
shell.call(["git", "fetch", "--recurse-submodules=yes"], echo=True)
45-
46-
if should_clean:
47-
shell.call(['git', 'clean', '-fdx'],
48-
echo=True)
49-
shell.call(['git', 'submodule', 'foreach', '--recursive', 'git',
50-
'clean', '-fdx'], echo=True)
51-
shell.call(['git', 'submodule', 'foreach', '--recursive', 'git',
52-
'reset', '--hard', 'HEAD'], echo=True)
53-
status = shell.call(['git', 'reset', '--hard', 'HEAD'],
54-
echo=True)
55-
if status:
56-
print("Please, commit your changes.")
57-
print(status)
58-
exit(1)
59-
60-
if branch:
61-
status = shell.capture(['git', 'status', '--porcelain', '-uno'],
62-
echo=False)
63-
if status:
64-
print("Please, commit your changes.")
65-
print(status)
66-
exit(1)
67-
shell.call(['git', 'checkout', branch], echo=True)
68-
69-
# If we were asked to reset to the specified branch, do the hard
70-
# reset and return.
71-
if reset_to_remote and not cross_repo:
72-
shell.call(['git', 'reset', '--hard', "origin/%s" % branch],
73-
echo=True)
74-
return
75-
76-
# Prior to Git 2.6, this is the way to do a "git pull
77-
# --rebase" that respects rebase.autostash. See
78-
# http://stackoverflow.com/a/30209750/125349
79-
if not cross_repo:
80-
shell.call(["git", "rebase", "FETCH_HEAD"], echo=True)
81-
shell.call(["git", "submodule", "update", "--recursive"],
82-
echo=True)
46+
try:
47+
print("--- Updating '" + repo_path + "' ---")
48+
with shell.pushd(repo_path, dry_run=False, echo=False):
49+
shell.run(["git", "fetch", "--recurse-submodules=yes"], echo=True)
50+
51+
if should_clean:
52+
shell.run(['git', 'clean', '-fdx'], echo=True)
53+
shell.run(['git', 'submodule', 'foreach', '--recursive', 'git',
54+
'clean', '-fdx'], echo=True)
55+
shell.run(['git', 'submodule', 'foreach', '--recursive', 'git',
56+
'reset', '--hard', 'HEAD'], echo=True)
57+
shell.run(['git', 'reset', '--hard', 'HEAD'],
58+
echo=True, msg="Please, commit your changes.")
59+
60+
if branch:
61+
shell.run(['git', 'status', '--porcelain', '-uno'],
62+
echo=False, msg="Please, commit your changes.")
63+
shell.run(['git', 'checkout', branch], echo=True)
64+
65+
# If we were asked to reset to the specified branch, do the hard
66+
# reset and return.
67+
if reset_to_remote and not cross_repo:
68+
shell.run(['git', 'reset', '--hard', "origin/%s" % branch],
69+
echo=True)
70+
return
71+
72+
# Prior to Git 2.6, this is the way to do a "git pull
73+
# --rebase" that respects rebase.autostash. See
74+
# http://stackoverflow.com/a/30209750/125349
75+
if not cross_repo:
76+
shell.run(["git", "rebase", "FETCH_HEAD"], echo=True,
77+
msg="Cannot rebase: You have unstaged changes. Please commit or stash them.")
78+
shell.run(["git", "submodule", "update", "--recursive"], echo=True)
79+
except:
80+
(type, value, tb) = sys.exc_info()
81+
print('Error on repo "%s": %s' % (repo_path, traceback.format_exc()))
82+
return value
8383

8484

8585
def update_all_repositories(args, config, scheme_name, cross_repos_pr):
8686
repo_branch = scheme_name
87+
pool_args = []
8788
for repo_name in config['repos'].keys():
8889
cross_repo = False
8990
if repo_name in args.skip_repository_list:
@@ -105,70 +106,78 @@ def update_all_repositories(args, config, scheme_name, cross_repos_pr):
105106
pr_id = cross_repos_pr[remote_repo_id]
106107
repo_branch = "ci_pr_{0}".format(pr_id)
107108
with shell.pushd(repo_path, dry_run=False, echo=False):
108-
shell.call(["git", "checkout", v['repos'][repo_name]],
109+
shell.run(["git", "checkout", v['repos'][repo_name]],
109110
echo=True)
110-
shell.capture(["git", "branch", "-D", repo_branch],
111+
shell.run(["git", "branch", "-D", repo_branch],
111112
echo=True, allow_non_zero_exit=True)
112-
shell.call(["git", "fetch", "origin",
113+
shell.run(["git", "fetch", "origin",
113114
"pull/{0}/merge:{1}"
114115
.format(pr_id, repo_branch)], echo=True)
115116
break
116-
update_single_repository(repo_path,
117-
repo_branch,
118-
args.reset_to_remote,
119-
args.clean,
120-
cross_repo)
117+
pool_args.append([repo_path, repo_branch, args.reset_to_remote, args.clean, cross_repo])
118+
119+
return shell.run_parallel(update_single_repository, pool_args, args.n_processes)
121120

121+
def obtain_additional_swift_sources(pool_args):
122+
args, repo_name, repo_info, repo_branch, remote, with_ssh, scheme_name, skip_history, skip_repository_list = pool_args
122123

123-
def obtain_additional_swift_sources(
124-
config, with_ssh, scheme_name, skip_history, skip_repository_list):
125124
with shell.pushd(SWIFT_SOURCE_ROOT, dry_run=False,
126125
echo=False):
127-
for repo_name, repo_info in config['repos'].items():
128-
if repo_name in skip_repository_list:
129-
print("--- Skipping '" + repo_name + "' ---")
130-
continue
126+
print("--- Cloning '" + repo_name + "' ---")
127+
128+
if skip_history:
129+
shell.run(['git', 'clone', '--recursive', '--depth', '1',
130+
remote, repo_name], echo=True)
131+
else:
132+
shell.run(['git', 'clone', '--recursive', remote,
133+
repo_name], echo=True)
134+
if scheme_name:
135+
src_path = os.path.join(SWIFT_SOURCE_ROOT, repo_name, ".git")
136+
shell.run(['git', '--git-dir', src_path, '--work-tree',
137+
os.path.join(SWIFT_SOURCE_ROOT, repo_name),
138+
'checkout', repo_branch], echo=False)
139+
with shell.pushd(os.path.join(SWIFT_SOURCE_ROOT, repo_name),
140+
dry_run=False, echo=False):
141+
shell.run(["git", "submodule", "update", "--recursive"],
142+
echo=False)
131143

132-
if os.path.isdir(os.path.join(repo_name, ".git")):
133-
continue
144+
def obtain_all_additional_swift_sources(
145+
args, config, with_ssh, scheme_name, skip_history, skip_repository_list):
134146

135-
print("--- Cloning '" + repo_name + "' ---")
147+
pool_args = []
148+
for repo_name, repo_info in config['repos'].items():
149+
if repo_name in skip_repository_list:
150+
print("--- Skipping '" + repo_name + "' ---")
151+
continue
152+
153+
if os.path.isdir(os.path.join(repo_name, ".git")):
154+
continue
136155

137-
# If we have a url override, use that url instead of
138-
# interpolating.
139-
remote_repo_info = repo_info['remote']
140-
if 'url' in remote_repo_info:
141-
remote = remote_repo_info['url']
156+
# If we have a url override, use that url instead of
157+
# interpolating.
158+
remote_repo_info = repo_info['remote']
159+
if 'url' in remote_repo_info:
160+
remote = remote_repo_info['url']
161+
else:
162+
remote_repo_id = remote_repo_info['id']
163+
if with_ssh is True or 'https-clone-pattern' not in config:
164+
remote = config['ssh-clone-pattern'] % remote_repo_id
142165
else:
143-
remote_repo_id = remote_repo_info['id']
144-
if with_ssh is True or 'https-clone-pattern' not in config:
145-
remote = config['ssh-clone-pattern'] % remote_repo_id
146-
else:
147-
remote = config['https-clone-pattern'] % remote_repo_id
148-
149-
if skip_history:
150-
shell.call(['git', 'clone', '--recursive', '--depth', '1',
151-
remote, repo_name], echo=True)
166+
remote = config['https-clone-pattern'] % remote_repo_id
167+
168+
repo_branch = None
169+
if scheme_name:
170+
for v in config['branch-schemes'].values():
171+
if scheme_name not in v['aliases']:
172+
continue
173+
repo_branch = v['repos'][repo_name]
174+
break
152175
else:
153-
shell.call(['git', 'clone', '--recursive', remote,
154-
repo_name], echo=True)
155-
if scheme_name:
156-
for v in config['branch-schemes'].values():
157-
if scheme_name not in v['aliases']:
158-
continue
159-
repo_branch = v['repos'][repo_name]
160-
break
161-
else:
162-
repo_branch = scheme_name
163-
src_path = os.path.join(SWIFT_SOURCE_ROOT, repo_name,
164-
".git")
165-
shell.call(['git', '--git-dir', src_path, '--work-tree',
166-
os.path.join(SWIFT_SOURCE_ROOT, repo_name),
167-
'checkout', repo_branch], echo=False)
168-
with shell.pushd(os.path.join(SWIFT_SOURCE_ROOT, repo_name),
169-
dry_run=False, echo=False):
170-
shell.call(["git", "submodule", "update", "--recursive"],
171-
echo=False)
176+
repo_branch = scheme_name
177+
178+
pool_args.append([args, repo_name, repo_info, repo_branch, remote, with_ssh, scheme_name, skip_history, skip_repository_list])
179+
180+
return shell.run_parallel(obtain_additional_swift_sources, pool_args, args.n_processes)
172181

173182

174183
def validate_config(config):
@@ -192,7 +201,6 @@ def validate_config(config):
192201
raise RuntimeError('Configuration file has schemes with duplicate '
193202
'aliases?!')
194203

195-
196204
def main():
197205
parser = argparse.ArgumentParser(
198206
formatter_class=argparse.RawDescriptionHelpFormatter,
@@ -242,6 +250,12 @@ By default, updates your checkouts of Swift, SourceKit, LLDB, and SwiftPM.""")
242250
free-form GitHub-style comment.""",
243251
metavar='GITHUB-COMMENT',
244252
dest='github_comment')
253+
parser.add_argument(
254+
"-j", "--jobs",
255+
type=int,
256+
help="Number of threads to run at once",
257+
default=0,
258+
dest="n_processes")
245259
args = parser.parse_args()
246260

247261
clone = args.clone
@@ -262,19 +276,25 @@ By default, updates your checkouts of Swift, SourceKit, LLDB, and SwiftPM.""")
262276
repos_with_pr = [pr.replace('/pull/', '#') for pr in repos_with_pr]
263277
cross_repos_pr = dict(pr.split('#') for pr in repos_with_pr)
264278

279+
clone_results = None
265280
if clone or clone_with_ssh:
266281
# If branch is None, default to using the default branch alias
267282
# specified by our configuration file.
268283
if scheme is None:
269284
scheme = config['default-branch-scheme']
270285

271-
obtain_additional_swift_sources(
272-
config, clone_with_ssh, scheme, skip_history,
273-
args.skip_repository_list)
274-
275-
update_all_repositories(args, config, scheme, cross_repos_pr)
286+
clone_results = obtain_all_additional_swift_sources(args, config,
287+
clone_with_ssh, scheme, skip_history, args.skip_repository_list)
276288

277-
return 0
289+
update_results = update_all_repositories(args, config, scheme, cross_repos_pr)
290+
return (clone_results, update_results)
278291

279292
if __name__ == "__main__":
280-
sys.exit(main())
293+
freeze_support()
294+
clone_results, update_results = main()
295+
fail_count = 0
296+
fail_count += shell.check_parallel_results(clone_results, "CLONE")
297+
fail_count += shell.check_parallel_results(update_results, "UPDATE")
298+
if fail_count > 0:
299+
print("update-checkout failed, fix errors and try again")
300+
sys.exit(fail_count)

0 commit comments

Comments
 (0)