blob: 2772ca4878f876e3af4b41123c4d62b20acfb09c [file] [log] [blame]
Salud Lemus6270ccc2019-09-04 18:15:35 -07001#!/usr/bin/env python3
Salud Lemus055838a2019-08-19 13:37:28 -07002# -*- coding: utf-8 -*-
3# Copyright 2019 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
7"""Performs bisection on LLVM based off a .JSON file."""
8
Salud Lemus055838a2019-08-19 13:37:28 -07009from __future__ import print_function
10
11import argparse
Salud Lemusc856b682019-08-29 11:46:49 -070012import enum
Salud Lemus055838a2019-08-19 13:37:28 -070013import errno
14import json
15import os
Salud Lemusc856b682019-08-29 11:46:49 -070016import sys
Salud Lemus055838a2019-08-19 13:37:28 -070017
Jian Caic16daa12020-04-15 17:53:41 -070018import chroot
19import get_llvm_hash
20import modify_a_tryjob
21import update_tryjob_status
Salud Lemus055838a2019-08-19 13:37:28 -070022
23
Salud Lemusc856b682019-08-29 11:46:49 -070024class BisectionExitStatus(enum.Enum):
25 """Exit code when performing bisection."""
26
27 # Means that there are no more revisions available to bisect.
28 BISECTION_COMPLETE = 126
29
30
Salud Lemus055838a2019-08-19 13:37:28 -070031def is_file_and_json(json_file):
32 """Validates that the file exists and is a JSON file."""
33 return os.path.isfile(json_file) and json_file.endswith('.json')
34
35
36def GetCommandLineArgs():
37 """Parses the command line for the command line arguments."""
38
39 # Default path to the chroot if a path is not specified.
40 cros_root = os.path.expanduser('~')
41 cros_root = os.path.join(cros_root, 'chromiumos')
42
43 # Create parser and add optional command-line arguments.
44 parser = argparse.ArgumentParser(
45 description='Bisects LLVM via tracking a JSON file.')
46
47 # Add argument for other change lists that want to run alongside the tryjob
48 # which has a change list of updating a package's git hash.
49 parser.add_argument(
50 '--parallel',
51 type=int,
52 default=3,
53 help='How many tryjobs to create between the last good version and '
54 'the first bad version (default: %(default)s)')
55
56 # Add argument for the good LLVM revision for bisection.
57 parser.add_argument(
58 '--start_rev',
59 required=True,
60 type=int,
61 help='The good revision for the bisection.')
62
63 # Add argument for the bad LLVM revision for bisection.
64 parser.add_argument(
65 '--end_rev',
66 required=True,
67 type=int,
68 help='The bad revision for the bisection.')
69
70 # Add argument for the absolute path to the file that contains information on
71 # the previous tested svn version.
72 parser.add_argument(
73 '--last_tested',
74 required=True,
75 help='the absolute path to the file that contains the tryjobs')
76
77 # Add argument for the absolute path to the LLVM source tree.
78 parser.add_argument(
79 '--src_path',
80 help='the path to the LLVM source tree to use (used for retrieving the '
81 'git hash of each version between the last good version and first bad '
82 'version)')
83
84 # Add argument for other change lists that want to run alongside the tryjob
85 # which has a change list of updating a package's git hash.
86 parser.add_argument(
87 '--extra_change_lists',
88 type=int,
89 nargs='+',
90 help='change lists that would like to be run alongside the change list '
91 'of updating the packages')
92
93 # Add argument for custom options for the tryjob.
94 parser.add_argument(
95 '--options',
96 required=False,
97 nargs='+',
98 help='options to use for the tryjob testing')
99
100 # Add argument for the builder to use for the tryjob.
101 parser.add_argument(
102 '--builder', required=True, help='builder to use for the tryjob testing')
103
104 # Add argument for the description of the tryjob.
105 parser.add_argument(
106 '--description',
107 required=False,
108 nargs='+',
109 help='the description of the tryjob')
110
111 # Add argument for a specific chroot path.
112 parser.add_argument(
113 '--chroot_path',
114 default=cros_root,
115 help='the path to the chroot (default: %(default)s)')
116
Salud Lemus6270ccc2019-09-04 18:15:35 -0700117 # Add argument for whether to display command contents to `stdout`.
Salud Lemus055838a2019-08-19 13:37:28 -0700118 parser.add_argument(
Salud Lemus6270ccc2019-09-04 18:15:35 -0700119 '--verbose',
120 action='store_true',
121 help='display contents of a command to the terminal '
122 '(default: %(default)s)')
Salud Lemus055838a2019-08-19 13:37:28 -0700123
124 args_output = parser.parse_args()
125
126 assert args_output.start_rev < args_output.end_rev, (
127 'Start revision %d is >= end revision %d' % (args_output.start_rev,
128 args_output.end_rev))
129
130 if args_output.last_tested and not args_output.last_tested.endswith('.json'):
131 raise ValueError(
Salud Lemus6270ccc2019-09-04 18:15:35 -0700132 'Filed provided %s does not end in ".json"' % args_output.last_tested)
Salud Lemus055838a2019-08-19 13:37:28 -0700133
134 return args_output
135
136
137def _ValidateStartAndEndAgainstJSONStartAndEnd(start, end, json_start,
138 json_end):
139 """Valides that the command line arguments are the same as the JSON."""
140
141 if start != json_start or end != json_end:
142 raise ValueError('The start %d or the end %d version provided is '
Salud Lemus6270ccc2019-09-04 18:15:35 -0700143 'different than "start" %d or "end" %d in the .JSON '
Salud Lemus055838a2019-08-19 13:37:28 -0700144 'file' % (start, end, json_start, json_end))
145
146
147def GetStartAndEndRevision(start, end, tryjobs):
148 """Gets the start and end intervals in 'json_file'.
149
150 Args:
151 start: The start version of the bisection provided via the command line.
152 end: The end version of the bisection provided via the command line.
153 tryjobs: A list of tryjobs where each element is in the following format:
154 [
155 {[TRYJOB_INFORMATION]},
156 {[TRYJOB_INFORMATION]},
157 ...,
158 {[TRYJOB_INFORMATION]}
159 ]
160
161 Returns:
Salud Lemusffed65d2019-08-26 14:52:02 -0700162 The new start version and end version for bisection, a set of revisions
163 that are 'pending' and a set of revisions that are to be skipped.
Salud Lemus055838a2019-08-19 13:37:28 -0700164
165 Raises:
166 ValueError: The value for 'status' is missing or there is a mismatch
167 between 'start' and 'end' compared to the 'start' and 'end' in the JSON
168 file.
169 AssertionError: The new start version is >= than the new end version.
170 """
171
172 if not tryjobs:
Salud Lemus02630632019-08-28 15:27:59 -0700173 return start, end, {}, {}
Salud Lemus055838a2019-08-19 13:37:28 -0700174
175 # Verify that each tryjob has a value for the 'status' key.
176 for cur_tryjob_dict in tryjobs:
177 if not cur_tryjob_dict.get('status', None):
Salud Lemus6270ccc2019-09-04 18:15:35 -0700178 raise ValueError('"status" is missing or has no value, please '
Salud Lemus055838a2019-08-19 13:37:28 -0700179 'go to %s and update it' % cur_tryjob_dict['link'])
180
181 all_bad_revisions = [end]
Jian Caic16daa12020-04-15 17:53:41 -0700182 all_bad_revisions.extend(
183 cur_tryjob['rev']
184 for cur_tryjob in tryjobs
185 if cur_tryjob['status'] == update_tryjob_status.TryjobStatus.BAD.value)
Salud Lemus055838a2019-08-19 13:37:28 -0700186
187 # The minimum value for the 'bad' field in the tryjobs is the new end
188 # version.
189 bad_rev = min(all_bad_revisions)
190
191 all_good_revisions = [start]
Jian Caic16daa12020-04-15 17:53:41 -0700192 all_good_revisions.extend(
193 cur_tryjob['rev']
194 for cur_tryjob in tryjobs
195 if cur_tryjob['status'] == update_tryjob_status.TryjobStatus.GOOD.value)
Salud Lemus055838a2019-08-19 13:37:28 -0700196
197 # The maximum value for the 'good' field in the tryjobs is the new start
198 # version.
199 good_rev = max(all_good_revisions)
200
201 # The good version should always be strictly less than the bad version;
202 # otherwise, bisection is broken.
203 assert good_rev < bad_rev, ('Bisection is broken because %d (good) is >= '
204 '%d (bad)' % (good_rev, bad_rev))
205
206 # Find all revisions that are 'pending' within 'good_rev' and 'bad_rev'.
207 #
208 # NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev'
Salud Lemusffed65d2019-08-26 14:52:02 -0700209 # that have already been launched (this set is used when constructing the
Salud Lemus055838a2019-08-19 13:37:28 -0700210 # list of revisions to launch tryjobs for).
Salud Lemusffed65d2019-08-26 14:52:02 -0700211 pending_revisions = {
Salud Lemus055838a2019-08-19 13:37:28 -0700212 tryjob['rev']
213 for tryjob in tryjobs
Jian Caic16daa12020-04-15 17:53:41 -0700214 if tryjob['status'] == update_tryjob_status.TryjobStatus.PENDING.value and
Salud Lemus055838a2019-08-19 13:37:28 -0700215 good_rev < tryjob['rev'] < bad_rev
Salud Lemusffed65d2019-08-26 14:52:02 -0700216 }
Salud Lemus055838a2019-08-19 13:37:28 -0700217
Salud Lemusffed65d2019-08-26 14:52:02 -0700218 # Find all revisions that are to be skipped within 'good_rev' and 'bad_rev'.
219 #
220 # NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev'
221 # that have already been marked as 'skip' (this set is used when constructing
222 # the list of revisions to launch tryjobs for).
223 skip_revisions = {
224 tryjob['rev']
225 for tryjob in tryjobs
Jian Caic16daa12020-04-15 17:53:41 -0700226 if tryjob['status'] == update_tryjob_status.TryjobStatus.SKIP.value and
Salud Lemusffed65d2019-08-26 14:52:02 -0700227 good_rev < tryjob['rev'] < bad_rev
228 }
229
230 return good_rev, bad_rev, pending_revisions, skip_revisions
Salud Lemus055838a2019-08-19 13:37:28 -0700231
232
233def GetRevisionsBetweenBisection(start, end, parallel, src_path,
Salud Lemusffed65d2019-08-26 14:52:02 -0700234 pending_revisions, skip_revisions):
Salud Lemus055838a2019-08-19 13:37:28 -0700235 """Gets the revisions between 'start' and 'end'.
236
237 Sometimes, the LLVM source tree's revisions do not increment by 1 (there is
238 a jump), so need to construct a list of all revisions that are NOT missing
239 between 'start' and 'end'. Then, the step amount (i.e. length of the list
240 divided by ('parallel' + 1)) will be used for indexing into the list.
241
242 Args:
243 start: The start revision.
244 end: The end revision.
245 parallel: The number of tryjobs to create between 'start' and 'end'.
246 src_path: The absolute path to the LLVM source tree to use.
Salud Lemusffed65d2019-08-26 14:52:02 -0700247 pending_revisions: A set containing 'pending' revisions that are between
248 'start' and 'end'.
249 skip_revisions: A set containing revisions between 'start' and 'end' that
250 are to be skipped.
Salud Lemus055838a2019-08-19 13:37:28 -0700251
252 Returns:
253 A list of revisions between 'start' and 'end'.
254 """
255
Salud Lemus055838a2019-08-19 13:37:28 -0700256 valid_revisions = []
257
258 # Start at ('start' + 1) because 'start' is the good revision.
259 #
260 # FIXME: Searching for each revision from ('start' + 1) up to 'end' in the
261 # LLVM source tree is a quadratic algorithm. It's a good idea to optimize
262 # this.
263 for cur_revision in range(start + 1, end):
264 try:
Salud Lemusffed65d2019-08-26 14:52:02 -0700265 if cur_revision not in pending_revisions and \
266 cur_revision not in skip_revisions:
Salud Lemus055838a2019-08-19 13:37:28 -0700267 # Verify that the current revision exists by finding its corresponding
268 # git hash in the LLVM source tree.
Jian Cai121dbe52019-11-20 15:32:56 -0800269 get_llvm_hash.GetGitHashFrom(src_path, cur_revision)
Salud Lemus055838a2019-08-19 13:37:28 -0700270 valid_revisions.append(cur_revision)
271 except ValueError:
272 # Could not find the git hash for the current revision.
273 continue
274
275 # ('parallel' + 1) so that the last revision in the list is not close to
276 # 'end' (have a bit more coverage).
277 index_step = len(valid_revisions) // (parallel + 1)
278
279 if not index_step:
280 index_step = 1
281
Salud Lemus055838a2019-08-19 13:37:28 -0700282 result = [valid_revisions[index] \
Salud Lemusffed65d2019-08-26 14:52:02 -0700283 for index in range(0, len(valid_revisions), index_step)]
Salud Lemus055838a2019-08-19 13:37:28 -0700284
285 return result
286
287
288def GetRevisionsListAndHashList(start, end, parallel, src_path,
Salud Lemusffed65d2019-08-26 14:52:02 -0700289 pending_revisions, skip_revisions):
Salud Lemus055838a2019-08-19 13:37:28 -0700290 """Determines the revisions between start and end."""
291
Jian Caic16daa12020-04-15 17:53:41 -0700292 new_llvm = get_llvm_hash.LLVMHash()
Salud Lemus055838a2019-08-19 13:37:28 -0700293
294 with new_llvm.CreateTempDirectory() as temp_dir:
Jian Caic16daa12020-04-15 17:53:41 -0700295 with get_llvm_hash.CreateTempLLVMRepo(temp_dir) as new_repo:
Salud Lemus42235702019-08-23 16:49:28 -0700296 if not src_path:
297 src_path = new_repo
Salud Lemus055838a2019-08-19 13:37:28 -0700298
Salud Lemus42235702019-08-23 16:49:28 -0700299 # Get a list of revisions between start and end.
Salud Lemusffed65d2019-08-26 14:52:02 -0700300 revisions = GetRevisionsBetweenBisection(
301 start, end, parallel, src_path, pending_revisions, skip_revisions)
Salud Lemus055838a2019-08-19 13:37:28 -0700302
Jian Caic16daa12020-04-15 17:53:41 -0700303 git_hashes = [
304 get_llvm_hash.GetGitHashFrom(src_path, rev) for rev in revisions
305 ]
Salud Lemus055838a2019-08-19 13:37:28 -0700306
Salud Lemus055838a2019-08-19 13:37:28 -0700307 return revisions, git_hashes
308
309
Salud Lemusc856b682019-08-29 11:46:49 -0700310def DieWithNoRevisionsError(start, end, skip_revisions, pending_revisions):
311 """Raises a ValueError exception with useful information."""
312
313 no_revisions_message = ('No revisions between start %d and end '
314 '%d to create tryjobs' % (start, end))
315
316 if pending_revisions:
317 no_revisions_message += '\nThe following tryjobs are pending:\n' \
318 + '\n'.join(str(rev) for rev in pending_revisions)
319
320 if skip_revisions:
321 no_revisions_message += '\nThe following tryjobs were skipped:\n' \
322 + '\n'.join(str(rev) for rev in skip_revisions)
323
324 raise ValueError(no_revisions_message)
325
326
327def CheckForExistingTryjobsInRevisionsToLaunch(revisions, jobs):
328 """Checks if a revision in 'revisions' exists in 'jobs' list."""
329
330 for rev in revisions:
Jian Caic16daa12020-04-15 17:53:41 -0700331 if update_tryjob_status.FindTryjobIndex(rev, jobs) is not None:
Salud Lemus6270ccc2019-09-04 18:15:35 -0700332 raise ValueError('Revision %d exists already in "jobs"' % rev)
Salud Lemusc856b682019-08-29 11:46:49 -0700333
334
335def UpdateBisection(revisions, git_hashes, bisect_contents, last_tested,
336 update_packages, chroot_path, patch_metadata_file,
Salud Lemus6270ccc2019-09-04 18:15:35 -0700337 extra_change_lists, options, builder, verbose):
Salud Lemusc856b682019-08-29 11:46:49 -0700338 """Adds tryjobs and updates the status file with the new tryjobs."""
339
340 try:
341 for svn_revision, git_hash in zip(revisions, git_hashes):
Jian Caic16daa12020-04-15 17:53:41 -0700342 tryjob_dict = modify_a_tryjob.AddTryjob(
343 update_packages, git_hash, svn_revision, chroot_path,
344 patch_metadata_file, extra_change_lists, options, builder, verbose,
345 svn_revision)
Salud Lemusc856b682019-08-29 11:46:49 -0700346
347 bisect_contents['jobs'].append(tryjob_dict)
348 finally:
349 # Do not want to lose progress if there is an exception.
350 if last_tested:
351 new_file = '%s.new' % last_tested
352 with open(new_file, 'w') as json_file:
353 json.dump(bisect_contents, json_file, indent=4, separators=(',', ': '))
354
355 os.rename(new_file, last_tested)
356
357
358def _NoteCompletedBisection(last_tested, src_path, end):
359 """Prints that bisection is complete."""
360
361 print('Finished bisecting for %s' % last_tested)
362
363 if src_path:
Jian Cai121dbe52019-11-20 15:32:56 -0800364 bad_llvm_hash = get_llvm_hash.GetGitHashFrom(src_path, end)
Salud Lemusc856b682019-08-29 11:46:49 -0700365 else:
Jian Caic16daa12020-04-15 17:53:41 -0700366 bad_llvm_hash = get_llvm_hash.LLVMHash().GetLLVMHash(end)
Salud Lemusc856b682019-08-29 11:46:49 -0700367
368 print(
369 'The bad revision is %d and its commit hash is %s' % (end, bad_llvm_hash))
370
371
Salud Lemus97c1e1c2019-09-10 16:28:47 -0700372def LoadStatusFile(last_tested, start, end):
373 """Loads the status file for bisection."""
374
375 try:
376 with open(last_tested) as f:
377 return json.load(f)
378 except IOError as err:
379 if err.errno != errno.ENOENT:
380 raise
381
382 return {'start': start, 'end': end, 'jobs': []}
383
384
Salud Lemusc856b682019-08-29 11:46:49 -0700385def main(args_output):
Salud Lemus055838a2019-08-19 13:37:28 -0700386 """Bisects LLVM based off of a .JSON file.
387
388 Raises:
389 AssertionError: The script was run inside the chroot.
390 """
391
Jian Caic16daa12020-04-15 17:53:41 -0700392 chroot.VerifyOutsideChroot()
Salud Lemus055838a2019-08-19 13:37:28 -0700393
Salud Lemus055838a2019-08-19 13:37:28 -0700394 update_packages = [
395 'sys-devel/llvm', 'sys-libs/compiler-rt', 'sys-libs/libcxx',
396 'sys-libs/libcxxabi', 'sys-libs/llvm-libunwind'
397 ]
398
399 patch_metadata_file = 'PATCHES.json'
400
401 start = args_output.start_rev
402 end = args_output.end_rev
403
Salud Lemus97c1e1c2019-09-10 16:28:47 -0700404 bisect_contents = LoadStatusFile(args_output.last_tested, start, end)
Salud Lemus055838a2019-08-19 13:37:28 -0700405
406 _ValidateStartAndEndAgainstJSONStartAndEnd(
407 start, end, bisect_contents['start'], bisect_contents['end'])
Salud Lemusc856b682019-08-29 11:46:49 -0700408
Salud Lemusffed65d2019-08-26 14:52:02 -0700409 # Pending and skipped revisions are between 'start_revision' and
410 # 'end_revision'.
411 start_revision, end_revision, pending_revisions, skip_revisions = \
412 GetStartAndEndRevision(start, end, bisect_contents['jobs'])
Salud Lemus055838a2019-08-19 13:37:28 -0700413
414 revisions, git_hashes = GetRevisionsListAndHashList(
415 start_revision, end_revision, args_output.parallel, args_output.src_path,
Salud Lemusffed65d2019-08-26 14:52:02 -0700416 pending_revisions, skip_revisions)
417
Salud Lemusc856b682019-08-29 11:46:49 -0700418 # No more revisions between 'start_revision' and 'end_revision', so
419 # bisection is complete.
420 #
421 # This is determined by finding all valid revisions between 'start_revision'
422 # and 'end_revision' and that are NOT in the 'pending' and 'skipped' set.
Salud Lemusffed65d2019-08-26 14:52:02 -0700423 if not revisions:
Salud Lemusc856b682019-08-29 11:46:49 -0700424 # Successfully completed bisection where there are 2 cases:
425 # 1) 'start_revision' and 'end_revision' are back-to-back (example:
426 # 'start_revision' is 369410 and 'end_revision' is 369411).
427 #
428 # 2) 'start_revision' and 'end_revision' are NOT back-to-back, so there must
429 # be tryjobs in between which are labeled as 'skip' for their 'status'
430 # value.
431 #
432 # In either case, there are no 'pending' jobs.
433 if not pending_revisions:
434 _NoteCompletedBisection(args_output.last_tested, args_output.src_path,
435 end_revision)
Salud Lemusffed65d2019-08-26 14:52:02 -0700436
Salud Lemusc856b682019-08-29 11:46:49 -0700437 if skip_revisions:
438 skip_revisions_message = ('\nThe following revisions were skipped:\n' +
439 '\n'.join(str(rev) for rev in skip_revisions))
Salud Lemusffed65d2019-08-26 14:52:02 -0700440
Salud Lemusc856b682019-08-29 11:46:49 -0700441 print(skip_revisions_message)
Salud Lemus055838a2019-08-19 13:37:28 -0700442
Salud Lemusc856b682019-08-29 11:46:49 -0700443 return BisectionExitStatus.BISECTION_COMPLETE.value
Salud Lemus055838a2019-08-19 13:37:28 -0700444
Salud Lemusc856b682019-08-29 11:46:49 -0700445 # Some tryjobs are not finished which may change the actual bad
446 # commit/revision when those tryjobs are finished.
447 DieWithNoRevisionsError(start_revision, end_revision, skip_revisions,
448 pending_revisions)
Salud Lemus055838a2019-08-19 13:37:28 -0700449
Salud Lemusc856b682019-08-29 11:46:49 -0700450 CheckForExistingTryjobsInRevisionsToLaunch(revisions, bisect_contents['jobs'])
Salud Lemus055838a2019-08-19 13:37:28 -0700451
Salud Lemusc856b682019-08-29 11:46:49 -0700452 UpdateBisection(revisions, git_hashes, bisect_contents,
453 args_output.last_tested, update_packages,
454 args_output.chroot_path, patch_metadata_file,
455 args_output.extra_change_lists, args_output.options,
Salud Lemus6270ccc2019-09-04 18:15:35 -0700456 args_output.builder, args_output.verbose)
Salud Lemus055838a2019-08-19 13:37:28 -0700457
458
459if __name__ == '__main__':
Jian Caic16daa12020-04-15 17:53:41 -0700460 sys.exit(main(GetCommandLineArgs()))