blob: 68a5429ed6787dabbde95447e8b8900b11be9d94 [file] [log] [blame]
Kuang-che Wu41e8b592018-09-25 17:01:30 +08001#!/usr/bin/env python2
2# -*- coding: utf-8 -*-
3# Copyright 2018 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6"""Helper script to prepare source trees for ChromeOS bisection.
7
8Typical usage:
9
10 Initial setup:
11 $ %(prog)s init --chromeos
12 $ %(prog)s init --chrome
13 $ %(prog)s init --android=pi-arc-dev
14
15 Sync code if necessary:
16 $ %(prog)s sync
17
18 Create source trees for bisection
19 $ %(prog)s new --session=12345
20
21 After bisection finished, delete trees
22 $ %(prog)s delete --session=12345
23"""
24from __future__ import print_function
25import argparse
26import csv
27import logging
28import os
Kuang-che Wu67be74b2018-10-15 14:17:26 +080029import time
Kuang-che Wu41e8b592018-09-25 17:01:30 +080030import urllib2
Kuang-che Wu67be74b2018-10-15 14:17:26 +080031import urlparse
Kuang-che Wu41e8b592018-09-25 17:01:30 +080032
33from bisect_kit import common
34from bisect_kit import configure
35from bisect_kit import gclient_util
36from bisect_kit import git_util
37from bisect_kit import repo_util
38from bisect_kit import util
39
40DEFAULT_MIRROR_BASE = os.path.expanduser('~/git-mirrors')
41DEFAULT_WORK_BASE = os.path.expanduser('~/bisect-workdir')
42CHECKOUT_TEMPLATE_NAME = 'template'
43
44logger = logging.getLogger(__name__)
45
46
47class DefaultProjectPathFactory(object):
48 """Factory for chromeos/chrome/android source tree paths."""
49
50 def __init__(self, mirror_base, work_base, session):
51 self.mirror_base = mirror_base
52 self.work_base = work_base
53 self.session = session
54
55 def get_chromeos_mirror(self):
56 return os.path.join(self.mirror_base, 'chromeos')
57
58 def get_chromeos_tree(self):
59 return os.path.join(self.work_base, self.session, 'chromeos')
60
61 def get_android_mirror(self, branch):
62 return os.path.join(self.mirror_base, 'android.%s' % branch)
63
64 def get_android_tree(self, branch):
65 return os.path.join(self.work_base, self.session, 'android.%s' % branch)
66
67 def get_chrome_cache(self):
68 return os.path.join(self.mirror_base, 'chrome')
69
70 def get_chrome_tree(self):
71 return os.path.join(self.work_base, self.session, 'chrome')
72
73
74def subvolume_or_makedirs(opts, path):
75 if os.path.exists(path):
76 return
77
78 path = os.path.abspath(path)
79 if opts.btrfs:
80 dirname, basename = os.path.split(path)
81 if not os.path.exists(dirname):
82 os.makedirs(dirname)
83 util.check_call('btrfs', 'subvolume', 'create', basename, cwd=dirname)
84 else:
85 os.makedirs(path)
86
87
88def is_btrfs_subvolume(path):
89 if util.check_output('stat', '-f', '--format=%T', path).strip() != 'btrfs':
90 return False
91 return util.check_output('stat', '--format=%i', path).strip() == '256'
92
93
94def snapshot_or_copytree(src, dst):
95 assert os.path.isdir(src), '%s does not exist' % src
96 assert os.path.isdir(os.path.dirname(dst))
97
98 # Make sure dst do not exist, otherwise it becomes "dst/name" (one extra
99 # depth) instead of "dst".
100 assert not os.path.exists(dst)
101
102 if is_btrfs_subvolume(src):
103 util.check_call('btrfs', 'subvolume', 'snapshot', src, dst)
104 else:
105 # -a for recursion and preserve all attributes.
106 util.check_call('cp', '-a', src, dst)
107
108
Kuang-che Wubfa64482018-10-16 11:49:49 +0800109def collect_removed_manifest_repos(repo_dir, last_sync_time, only_branch=None):
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800110 manifest_dir = os.path.join(repo_dir, '.repo', 'manifests')
Kuang-che Wu16cd6e22018-10-17 12:06:12 +0800111 util.check_call('git', 'fetch', cwd=manifest_dir)
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800112
113 manifest_path = 'default.xml'
114 manifest_full_path = os.path.join(manifest_dir, manifest_path)
115 # hack for chromeos symlink
116 if os.path.islink(manifest_full_path):
117 manifest_path = os.readlink(manifest_full_path)
118
119 parser = repo_util.ManifestParser(manifest_dir)
120 latest = None
121 removed = {}
122 for _, git_rev in reversed(
123 parser.enumerate_manifest_commits(last_sync_time, None, manifest_path)):
124 root = parser.parse_xml_recursive(git_rev, manifest_path)
Kuang-che Wubfa64482018-10-16 11:49:49 +0800125 if (only_branch and root.find('default') is not None and
126 root.find('default').get('revision') != only_branch):
127 break
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800128 entries = parser.process_parsed_result(root)
129 if latest is None:
130 assert entries is not None
131 latest = entries
132 continue
133
134 for path, path_spec in entries.items():
135 if path in latest:
136 continue
137 if path in removed:
138 continue
139 removed[path] = path_spec
140
141 return removed
142
143
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800144def setup_chromeos_repos(opts, path_factory):
145 chromeos_mirror = path_factory.get_chromeos_mirror()
146 chromeos_tree = path_factory.get_chromeos_tree()
147 subvolume_or_makedirs(opts, chromeos_mirror)
148 subvolume_or_makedirs(opts, chromeos_tree)
149
150 manifest_url = (
151 'https://chrome-internal.googlesource.com/chromeos/manifest-internal')
152 repo_url = 'https://chromium.googlesource.com/external/repo.git'
153
154 if os.path.exists(os.path.join(chromeos_mirror, '.repo', 'manifests')):
155 logger.warning(
156 '%s has already been initialized, assume it is setup properly',
157 chromeos_mirror)
158 else:
159 logger.info('repo init for chromeos mirror')
160 repo_util.init(
161 chromeos_mirror,
162 manifest_url=manifest_url,
163 repo_url=repo_url,
164 mirror=True)
165
166 local_manifest_dir = os.path.join(chromeos_mirror, '.repo',
167 'local_manifests')
168 os.mkdir(local_manifest_dir)
169 with open(os.path.join(local_manifest_dir, 'manifest-versions.xml'),
170 'w') as f:
171 f.write('''<?xml version="1.0" encoding="UTF-8"?>
172 <manifest>
173 <project name="chromeos/manifest-versions" remote="cros-internal" />
174 </manifest>
175 ''')
176
177 logger.info('repo init for chromeos tree')
178 repo_util.init(
179 chromeos_tree,
180 manifest_url=manifest_url,
181 repo_url=repo_url,
182 reference=chromeos_mirror)
183
184 logger.info('repo sync for chromeos mirror (this takes hours; be patient)')
185 repo_util.sync(chromeos_mirror)
186
187 logger.info('repo sync for chromeos tree')
188 repo_util.sync(chromeos_tree)
189
190
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800191def read_last_sync_time(repo_dir):
192 timestamp_path = os.path.join(repo_dir, 'last_sync_time')
193 if os.path.exists(timestamp_path):
194 with open(timestamp_path) as f:
195 return int(f.read())
196 else:
197 # 4 months should be enough for most bisect cases.
198 return int(time.time()) - 86400 * 120
199
200
201def write_sync_time(repo_dir, sync_time):
202 timestamp_path = os.path.join(repo_dir, 'last_sync_time')
203 with open(timestamp_path, 'w') as f:
204 f.write('%d\n' % sync_time)
205
206
207def write_extra_manifest_to_mirror(repo_dir, removed):
208 local_manifest_dir = os.path.join(repo_dir, '.repo', 'local_manifests')
209 if not os.path.exists(local_manifest_dir):
210 os.mkdir(local_manifest_dir)
211 with open(os.path.join(local_manifest_dir, 'deleted-repos.xml'), 'w') as f:
212 f.write('''<?xml version="1.0" encoding="UTF-8"?>\n<manifest>\n''')
213 remotes = {}
214 for path_spec in removed.values():
215 scheme, netloc, remote_path = urlparse.urlsplit(path_spec.repo_url)[:3]
216 assert remote_path[0] == '/'
217 remote_path = remote_path[1:]
218 if (scheme, netloc) not in remotes:
219 remote_name = 'remote_for_deleted_repo_%s' % (scheme + netloc)
220 remotes[scheme, netloc] = remote_name
221 f.write(''' <remote name="%s" fetch="%s" />\n''' %
222 (remote_name, '%s://%s' % (scheme, netloc)))
Kuang-che Wubfa64482018-10-16 11:49:49 +0800223 f.write(
224 ''' <project name="%s" path="%s" remote="%s" revision="%s" />\n''' %
225 (remote_path, path_spec.path, remotes[scheme, netloc], path_spec.at))
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800226 f.write('''</manifest>\n''')
227
228
Kuang-che Wubfa64482018-10-16 11:49:49 +0800229def generate_extra_manifest_for_deleted_repo(repo_dir, only_branch=None):
230 last_sync_time = read_last_sync_time(repo_dir)
231 removed = collect_removed_manifest_repos(
232 repo_dir, last_sync_time, only_branch=only_branch)
233 write_extra_manifest_to_mirror(repo_dir, removed)
234 logger.info('since last sync, %d repo got removed', len(removed))
235
236
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800237def sync_chromeos_code(opts, path_factory):
238 del opts # unused
239
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800240 start_sync_time = int(time.time())
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800241 chromeos_mirror = path_factory.get_chromeos_mirror()
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800242
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800243 logger.info('repo sync for chromeos mirror')
Kuang-che Wubfa64482018-10-16 11:49:49 +0800244 generate_extra_manifest_for_deleted_repo(chromeos_mirror)
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800245 repo_util.sync(chromeos_mirror)
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800246 write_sync_time(chromeos_mirror, start_sync_time)
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800247
248 logger.info('repo sync for chromeos tree')
249 chromeos_tree = path_factory.get_chromeos_tree()
250 repo_util.sync(chromeos_tree)
251
252 # test_that may use this ssh key and ssh complains its permission is too open
253 util.check_call(
254 'chmod',
255 'o-r,g-r',
256 'src/scripts/mod_for_test_scripts/ssh_keys/testing_rsa',
257 cwd=chromeos_tree)
258
259
260def query_chrome_latest_branch():
261 result = None
262 r = urllib2.urlopen('https://omahaproxy.appspot.com/all')
263 for row in csv.DictReader(r):
264 if row['true_branch'].isdigit():
265 result = max(result, int(row['true_branch']))
266 return result
267
268
269def setup_chrome_repos(opts, path_factory):
270 chrome_cache = path_factory.get_chrome_cache()
271 subvolume_or_makedirs(opts, chrome_cache)
272 chrome_tree = path_factory.get_chrome_tree()
273 subvolume_or_makedirs(opts, chrome_tree)
274
275 latest_branch = query_chrome_latest_branch()
276 logger.info('latest chrome branch is %d', latest_branch)
277 assert latest_branch
278 spec = '''
279solutions = [
280 { "name" : "buildspec",
281 "url" : "https://chrome-internal.googlesource.com/a/chrome/tools/buildspec.git",
282 "deps_file" : "branches/%d/DEPS",
283 "custom_deps" : {
284 },
285 "custom_vars": {'checkout_src_internal': True},
286 },
287]
288target_os = ['chromeos']
289cache_dir = %r
290''' % (latest_branch, chrome_cache)
291
292 logger.info('gclient config for chrome')
293 gclient_util.config(chrome_tree, spec=spec)
294
295 is_first_sync = not os.listdir(chrome_cache)
296 if is_first_sync:
297 logger.info('gclient sync for chrome (this takes hours; be patient)')
298 else:
299 logger.info('gclient sync for chrome')
300 gclient_util.sync(chrome_tree, with_branch_heads=True, with_tags=True)
301
302 # It's possible that some repos are removed from latest branch and thus their
303 # commit history is not fetched in recent gclient sync. So we call 'git
304 # fetch' for all existing git mirrors.
305 # TODO(kcwu): only sync repos not in DEPS files of latest branch
306 logger.info('additional sync for chrome mirror')
307 for git_repo_name in os.listdir(chrome_cache):
308 # another gclient is running or leftover of previous run; skip
309 if git_repo_name.startswith('_cache_tmp'):
310 continue
311 git_repo = os.path.join(chrome_cache, git_repo_name)
312 if not git_util.is_git_rev(git_repo):
313 continue
314 util.check_call('git', 'fetch', cwd=git_repo)
315
316
317def sync_chrome_code(opts, path_factory):
318 # The sync step is identical to the initial gclient config step.
319 setup_chrome_repos(opts, path_factory)
320
321
322def setup_android_repos(opts, path_factory, branch):
323 android_mirror = path_factory.get_android_mirror(branch)
324 android_tree = path_factory.get_android_tree(branch)
325 subvolume_or_makedirs(opts, android_mirror)
326 subvolume_or_makedirs(opts, android_tree)
327
328 manifest_url = ('persistent-https://googleplex-android.git.corp.google.com'
329 '/platform/manifest')
330 repo_url = 'https://gerrit.googlesource.com/git-repo'
331
332 if os.path.exists(os.path.join(android_mirror, '.repo', 'manifests')):
333 logger.warning(
334 '%s has already been initialized, assume it is setup properly',
335 android_mirror)
336 else:
337 logger.info('repo init for android mirror branch=%s', branch)
338 repo_util.init(
339 android_mirror,
340 manifest_url=manifest_url,
341 repo_url=repo_url,
342 manifest_branch=branch,
343 mirror=True)
344
345 logger.info('repo init for android tree branch=%s', branch)
346 repo_util.init(
347 android_tree,
348 manifest_url=manifest_url,
349 repo_url=repo_url,
350 manifest_branch=branch,
351 reference=android_mirror)
352
353 logger.info('repo sync for android mirror (this takes hours; be patient)')
354 repo_util.sync(android_mirror, current_branch=True)
355
356 logger.info('repo sync for android tree branch=%s', branch)
357 repo_util.sync(android_tree, current_branch=True)
358
359
360def sync_android_code(opts, path_factory, branch):
361 del opts # unused
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800362 start_sync_time = int(time.time())
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800363 android_mirror = path_factory.get_android_mirror(branch)
364 android_tree = path_factory.get_android_tree(branch)
365
366 logger.info('repo sync for android mirror branch=%s', branch)
Kuang-che Wubfa64482018-10-16 11:49:49 +0800367 # Android usually big jump between milestone releases and add/delete lots of
368 # repos when switch releases. Because it's infeasible to bisect between such
369 # big jump, the deleted repo is useless. In order to save disk, do not sync
370 # repos deleted in other branches.
371 generate_extra_manifest_for_deleted_repo(android_mirror, only_branch=branch)
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800372 repo_util.sync(android_mirror, current_branch=True)
Kuang-che Wu67be74b2018-10-15 14:17:26 +0800373 write_sync_time(android_mirror, start_sync_time)
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800374
375 logger.info('repo sync for android tree branch=%s', branch)
376 repo_util.sync(android_tree, current_branch=True)
377
378
379def cmd_init(opts):
380 path_factory = DefaultProjectPathFactory(opts.mirror_base, opts.work_base,
381 CHECKOUT_TEMPLATE_NAME)
382
383 if opts.chromeos:
384 setup_chromeos_repos(opts, path_factory)
385 if opts.chrome:
386 setup_chrome_repos(opts, path_factory)
387 for branch in opts.android:
388 setup_android_repos(opts, path_factory, branch)
389
390
391def enumerate_android_branches_available(base):
392 branches = []
393 for name in os.listdir(base):
394 if name.startswith('android.'):
395 branches.append(name.partition('.')[2])
396 return branches
397
398
399def cmd_sync(opts):
400 path_factory = DefaultProjectPathFactory(opts.mirror_base, opts.work_base,
401 CHECKOUT_TEMPLATE_NAME)
402
403 sync_all = False
404 if not opts.chromeos and not opts.chrome and not opts.android:
405 logger.info('sync trees for all')
406 sync_all = True
407
408 if sync_all or opts.chromeos:
409 sync_chromeos_code(opts, path_factory)
410 if sync_all or opts.chrome:
411 sync_chrome_code(opts, path_factory)
412
413 if sync_all:
414 android_branches = enumerate_android_branches_available(opts.mirror_base)
415 else:
416 android_branches = opts.android
417 for branch in android_branches:
418 sync_android_code(opts, path_factory, branch)
419
420
421def cmd_new(opts):
422 work_dir = os.path.join(opts.work_base, opts.session)
423 if not os.path.exists(work_dir):
424 os.makedirs(work_dir)
425
426 template_factory = DefaultProjectPathFactory(opts.mirror_base, opts.work_base,
427 CHECKOUT_TEMPLATE_NAME)
428 path_factory = DefaultProjectPathFactory(opts.mirror_base, opts.work_base,
429 opts.session)
430
431 prepare_all = False
432 if not opts.chromeos and not opts.chrome and not opts.android:
433 logger.info('prepare trees for all')
434 prepare_all = True
435
436 chromeos_template = template_factory.get_chromeos_tree()
437 if (prepare_all and os.path.exists(chromeos_template)) or opts.chromeos:
438 logger.info('prepare tree for chromeos, %s',
439 path_factory.get_chromeos_tree())
440 snapshot_or_copytree(chromeos_template, path_factory.get_chromeos_tree())
441
442 chrome_template = template_factory.get_chrome_tree()
443 if (prepare_all and os.path.exists(chrome_template)) or opts.chrome:
444 logger.info('prepare tree for chrome, %s', path_factory.get_chrome_tree())
445 snapshot_or_copytree(chrome_template, path_factory.get_chrome_tree())
446
447 if prepare_all:
448 android_branches = enumerate_android_branches_available(opts.mirror_base)
449 else:
450 android_branches = opts.android
451 for branch in android_branches:
452 logger.info('prepare tree for android branch=%s, %s', branch,
453 path_factory.get_android_tree(branch))
454 snapshot_or_copytree(
455 template_factory.get_android_tree(branch),
456 path_factory.get_android_tree(branch))
457
458
459def delete_tree(path):
460 if is_btrfs_subvolume(path):
461 util.check_call('btrfs', 'subvolume', 'delete', path)
462 else:
463 util.check_call('rm', '-rf', path)
464
465
466def cmd_list(opts):
467 print('%-20s %s' % ('Session', 'Path'))
468 for name in os.listdir(opts.work_base):
469 if name == CHECKOUT_TEMPLATE_NAME:
470 continue
471 path = os.path.join(opts.work_base, name)
472 print('%-20s %s' % (name, path))
473
474
475def cmd_delete(opts):
476 assert opts.session
477 path_factory = DefaultProjectPathFactory(opts.mirror_base, opts.work_base,
478 opts.session)
479
480 chromeos_tree = path_factory.get_chromeos_tree()
481 if os.path.exists(chromeos_tree):
482 if os.path.exists(os.path.join(chromeos_tree, 'chromite')):
483 # ignore error
484 util.call('cros_sdk', '--unmount', cwd=chromeos_tree)
485 delete_tree(chromeos_tree)
486
487 chrome_tree = path_factory.get_chrome_tree()
488 if os.path.exists(chrome_tree):
489 delete_tree(chrome_tree)
490
491 android_branches = enumerate_android_branches_available(opts.mirror_base)
492 for branch in android_branches:
493 android_tree = path_factory.get_android_tree(branch)
494 if os.path.exists(android_tree):
495 delete_tree(android_tree)
496
497 os.rmdir(os.path.join(opts.work_base, opts.session))
498
499
500def create_parser():
501 parser = argparse.ArgumentParser(
502 formatter_class=argparse.RawDescriptionHelpFormatter, description=__doc__)
503 parser.add_argument(
504 '--mirror_base',
505 metavar='MIRROR_BASE',
506 default=configure.get('MIRROR_BASE', DEFAULT_MIRROR_BASE),
507 help='Directory for mirrors (default: %(default)s)')
508 parser.add_argument(
509 '--work_base',
510 metavar='WORK_BASE',
511 default=configure.get('WORK_BASE', DEFAULT_WORK_BASE),
512 help='Directory for bisection working directories (default: %(default)s)')
513 common.add_common_arguments(parser)
514 subparsers = parser.add_subparsers(
515 dest='command', title='commands', metavar='<command>')
516
517 parser_init = subparsers.add_parser(
518 'init', help='Mirror source trees and create template checkout')
519 parser_init.add_argument(
520 '--chrome', action='store_true', help='init chrome mirror and tree')
521 parser_init.add_argument(
522 '--chromeos', action='store_true', help='init chromeos mirror and tree')
523 parser_init.add_argument(
524 '--android',
525 metavar='BRANCH',
526 action='append',
527 default=[],
528 help='init android mirror and tree of BRANCH')
529 parser_init.add_argument(
530 '--btrfs',
531 action='store_true',
532 help='create btrfs subvolume for source tree')
533 parser_init.set_defaults(func=cmd_init)
534
535 parser_sync = subparsers.add_parser(
536 'sync',
537 help='Sync source trees',
538 description='Sync all if no projects are specified '
539 '(--chrome, --chromeos, or --android)')
540 parser_sync.add_argument(
541 '--chrome', action='store_true', help='sync chrome mirror and tree')
542 parser_sync.add_argument(
543 '--chromeos', action='store_true', help='sync chromeos mirror and tree')
544 parser_sync.add_argument(
545 '--android',
546 metavar='BRANCH',
547 action='append',
548 default=[],
549 help='sync android mirror and tree of BRANCH')
550 parser_sync.set_defaults(func=cmd_sync)
551
552 parser_new = subparsers.add_parser(
553 'new',
554 help='Create new source checkout for bisect',
555 description='Create for all if no projects are specified '
556 '(--chrome, --chromeos, or --android)')
557 parser_new.add_argument('--session', required=True)
558 parser_new.add_argument(
559 '--chrome', action='store_true', help='create chrome checkout')
560 parser_new.add_argument(
561 '--chromeos', action='store_true', help='create chromeos checkout')
562 parser_new.add_argument(
563 '--android',
564 metavar='BRANCH',
565 action='append',
566 default=[],
567 help='create android checkout of BRANCH')
568 parser_new.set_defaults(func=cmd_new)
569
570 parser_list = subparsers.add_parser(
571 'list', help='List existing sessions with source checkout')
572 parser_list.set_defaults(func=cmd_list)
573
574 parser_delete = subparsers.add_parser('delete', help='Delete source checkout')
575 parser_delete.add_argument('--session', required=True)
576 parser_delete.set_defaults(func=cmd_delete)
577
578 return parser
579
580
581def main():
582 common.init()
583 parser = create_parser()
584 opts = parser.parse_args()
585 common.config_logging(opts)
586
587 opts.func(opts)
588
589
590if __name__ == '__main__':
591 main()