blob: b45be0aad8392193051ae7d8fef6e0e429e70d67 [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
18from assert_not_in_chroot import VerifyOutsideChroot
Salud Lemus42235702019-08-23 16:49:28 -070019from get_llvm_hash import CreateTempLLVMRepo
Salud Lemus055838a2019-08-19 13:37:28 -070020from get_llvm_hash import LLVMHash
21from modify_a_tryjob import AddTryjob
Salud Lemus055838a2019-08-19 13:37:28 -070022from update_tryjob_status import FindTryjobIndex
23from update_tryjob_status import TryjobStatus
24
25
Salud Lemusc856b682019-08-29 11:46:49 -070026class BisectionExitStatus(enum.Enum):
27 """Exit code when performing bisection."""
28
29 # Means that there are no more revisions available to bisect.
30 BISECTION_COMPLETE = 126
31
32
Salud Lemus055838a2019-08-19 13:37:28 -070033def is_file_and_json(json_file):
34 """Validates that the file exists and is a JSON file."""
35 return os.path.isfile(json_file) and json_file.endswith('.json')
36
37
38def GetCommandLineArgs():
39 """Parses the command line for the command line arguments."""
40
41 # Default path to the chroot if a path is not specified.
42 cros_root = os.path.expanduser('~')
43 cros_root = os.path.join(cros_root, 'chromiumos')
44
45 # Create parser and add optional command-line arguments.
46 parser = argparse.ArgumentParser(
47 description='Bisects LLVM via tracking a JSON file.')
48
49 # Add argument for other change lists that want to run alongside the tryjob
50 # which has a change list of updating a package's git hash.
51 parser.add_argument(
52 '--parallel',
53 type=int,
54 default=3,
55 help='How many tryjobs to create between the last good version and '
56 'the first bad version (default: %(default)s)')
57
58 # Add argument for the good LLVM revision for bisection.
59 parser.add_argument(
60 '--start_rev',
61 required=True,
62 type=int,
63 help='The good revision for the bisection.')
64
65 # Add argument for the bad LLVM revision for bisection.
66 parser.add_argument(
67 '--end_rev',
68 required=True,
69 type=int,
70 help='The bad revision for the bisection.')
71
72 # Add argument for the absolute path to the file that contains information on
73 # the previous tested svn version.
74 parser.add_argument(
75 '--last_tested',
76 required=True,
77 help='the absolute path to the file that contains the tryjobs')
78
79 # Add argument for the absolute path to the LLVM source tree.
80 parser.add_argument(
81 '--src_path',
82 help='the path to the LLVM source tree to use (used for retrieving the '
83 'git hash of each version between the last good version and first bad '
84 'version)')
85
86 # Add argument for other change lists that want to run alongside the tryjob
87 # which has a change list of updating a package's git hash.
88 parser.add_argument(
89 '--extra_change_lists',
90 type=int,
91 nargs='+',
92 help='change lists that would like to be run alongside the change list '
93 'of updating the packages')
94
95 # Add argument for custom options for the tryjob.
96 parser.add_argument(
97 '--options',
98 required=False,
99 nargs='+',
100 help='options to use for the tryjob testing')
101
102 # Add argument for the builder to use for the tryjob.
103 parser.add_argument(
104 '--builder', required=True, help='builder to use for the tryjob testing')
105
106 # Add argument for the description of the tryjob.
107 parser.add_argument(
108 '--description',
109 required=False,
110 nargs='+',
111 help='the description of the tryjob')
112
113 # Add argument for a specific chroot path.
114 parser.add_argument(
115 '--chroot_path',
116 default=cros_root,
117 help='the path to the chroot (default: %(default)s)')
118
Salud Lemus6270ccc2019-09-04 18:15:35 -0700119 # Add argument for whether to display command contents to `stdout`.
Salud Lemus055838a2019-08-19 13:37:28 -0700120 parser.add_argument(
Salud Lemus6270ccc2019-09-04 18:15:35 -0700121 '--verbose',
122 action='store_true',
123 help='display contents of a command to the terminal '
124 '(default: %(default)s)')
Salud Lemus055838a2019-08-19 13:37:28 -0700125
126 args_output = parser.parse_args()
127
128 assert args_output.start_rev < args_output.end_rev, (
129 'Start revision %d is >= end revision %d' % (args_output.start_rev,
130 args_output.end_rev))
131
132 if args_output.last_tested and not args_output.last_tested.endswith('.json'):
133 raise ValueError(
Salud Lemus6270ccc2019-09-04 18:15:35 -0700134 'Filed provided %s does not end in ".json"' % args_output.last_tested)
Salud Lemus055838a2019-08-19 13:37:28 -0700135
136 return args_output
137
138
139def _ValidateStartAndEndAgainstJSONStartAndEnd(start, end, json_start,
140 json_end):
141 """Valides that the command line arguments are the same as the JSON."""
142
143 if start != json_start or end != json_end:
144 raise ValueError('The start %d or the end %d version provided is '
Salud Lemus6270ccc2019-09-04 18:15:35 -0700145 'different than "start" %d or "end" %d in the .JSON '
Salud Lemus055838a2019-08-19 13:37:28 -0700146 'file' % (start, end, json_start, json_end))
147
148
149def GetStartAndEndRevision(start, end, tryjobs):
150 """Gets the start and end intervals in 'json_file'.
151
152 Args:
153 start: The start version of the bisection provided via the command line.
154 end: The end version of the bisection provided via the command line.
155 tryjobs: A list of tryjobs where each element is in the following format:
156 [
157 {[TRYJOB_INFORMATION]},
158 {[TRYJOB_INFORMATION]},
159 ...,
160 {[TRYJOB_INFORMATION]}
161 ]
162
163 Returns:
Salud Lemusffed65d2019-08-26 14:52:02 -0700164 The new start version and end version for bisection, a set of revisions
165 that are 'pending' and a set of revisions that are to be skipped.
Salud Lemus055838a2019-08-19 13:37:28 -0700166
167 Raises:
168 ValueError: The value for 'status' is missing or there is a mismatch
169 between 'start' and 'end' compared to the 'start' and 'end' in the JSON
170 file.
171 AssertionError: The new start version is >= than the new end version.
172 """
173
174 if not tryjobs:
Salud Lemus02630632019-08-28 15:27:59 -0700175 return start, end, {}, {}
Salud Lemus055838a2019-08-19 13:37:28 -0700176
177 # Verify that each tryjob has a value for the 'status' key.
178 for cur_tryjob_dict in tryjobs:
179 if not cur_tryjob_dict.get('status', None):
Salud Lemus6270ccc2019-09-04 18:15:35 -0700180 raise ValueError('"status" is missing or has no value, please '
Salud Lemus055838a2019-08-19 13:37:28 -0700181 'go to %s and update it' % cur_tryjob_dict['link'])
182
183 all_bad_revisions = [end]
184 all_bad_revisions.extend(cur_tryjob['rev']
185 for cur_tryjob in tryjobs
186 if cur_tryjob['status'] == TryjobStatus.BAD.value)
187
188 # The minimum value for the 'bad' field in the tryjobs is the new end
189 # version.
190 bad_rev = min(all_bad_revisions)
191
192 all_good_revisions = [start]
193 all_good_revisions.extend(cur_tryjob['rev']
194 for cur_tryjob in tryjobs
195 if cur_tryjob['status'] == TryjobStatus.GOOD.value)
196
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
214 if tryjob['status'] == TryjobStatus.PENDING.value and
215 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
226 if tryjob['status'] == TryjobStatus.SKIP.value and
227 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
256 new_llvm = LLVMHash()
257
258 valid_revisions = []
259
260 # Start at ('start' + 1) because 'start' is the good revision.
261 #
262 # FIXME: Searching for each revision from ('start' + 1) up to 'end' in the
263 # LLVM source tree is a quadratic algorithm. It's a good idea to optimize
264 # this.
265 for cur_revision in range(start + 1, end):
266 try:
Salud Lemusffed65d2019-08-26 14:52:02 -0700267 if cur_revision not in pending_revisions and \
268 cur_revision not in skip_revisions:
Salud Lemus055838a2019-08-19 13:37:28 -0700269 # Verify that the current revision exists by finding its corresponding
270 # git hash in the LLVM source tree.
271 new_llvm.GetGitHashForVersion(src_path, cur_revision)
272 valid_revisions.append(cur_revision)
273 except ValueError:
274 # Could not find the git hash for the current revision.
275 continue
276
277 # ('parallel' + 1) so that the last revision in the list is not close to
278 # 'end' (have a bit more coverage).
279 index_step = len(valid_revisions) // (parallel + 1)
280
281 if not index_step:
282 index_step = 1
283
Salud Lemus055838a2019-08-19 13:37:28 -0700284 result = [valid_revisions[index] \
Salud Lemusffed65d2019-08-26 14:52:02 -0700285 for index in range(0, len(valid_revisions), index_step)]
Salud Lemus055838a2019-08-19 13:37:28 -0700286
287 return result
288
289
290def GetRevisionsListAndHashList(start, end, parallel, src_path,
Salud Lemusffed65d2019-08-26 14:52:02 -0700291 pending_revisions, skip_revisions):
Salud Lemus055838a2019-08-19 13:37:28 -0700292 """Determines the revisions between start and end."""
293
294 new_llvm = LLVMHash()
295
296 with new_llvm.CreateTempDirectory() as temp_dir:
Salud Lemus42235702019-08-23 16:49:28 -0700297 with CreateTempLLVMRepo(temp_dir) as new_repo:
298 if not src_path:
299 src_path = new_repo
Salud Lemus055838a2019-08-19 13:37:28 -0700300
Salud Lemus42235702019-08-23 16:49:28 -0700301 # Get a list of revisions between start and end.
Salud Lemusffed65d2019-08-26 14:52:02 -0700302 revisions = GetRevisionsBetweenBisection(
303 start, end, parallel, src_path, pending_revisions, skip_revisions)
Salud Lemus055838a2019-08-19 13:37:28 -0700304
Salud Lemus42235702019-08-23 16:49:28 -0700305 git_hashes = [
306 new_llvm.GetGitHashForVersion(src_path, rev) for rev in revisions
307 ]
Salud Lemus055838a2019-08-19 13:37:28 -0700308
Salud Lemus055838a2019-08-19 13:37:28 -0700309 return revisions, git_hashes
310
311
Salud Lemusc856b682019-08-29 11:46:49 -0700312def DieWithNoRevisionsError(start, end, skip_revisions, pending_revisions):
313 """Raises a ValueError exception with useful information."""
314
315 no_revisions_message = ('No revisions between start %d and end '
316 '%d to create tryjobs' % (start, end))
317
318 if pending_revisions:
319 no_revisions_message += '\nThe following tryjobs are pending:\n' \
320 + '\n'.join(str(rev) for rev in pending_revisions)
321
322 if skip_revisions:
323 no_revisions_message += '\nThe following tryjobs were skipped:\n' \
324 + '\n'.join(str(rev) for rev in skip_revisions)
325
326 raise ValueError(no_revisions_message)
327
328
329def CheckForExistingTryjobsInRevisionsToLaunch(revisions, jobs):
330 """Checks if a revision in 'revisions' exists in 'jobs' list."""
331
332 for rev in revisions:
333 if FindTryjobIndex(rev, jobs) is not None:
Salud Lemus6270ccc2019-09-04 18:15:35 -0700334 raise ValueError('Revision %d exists already in "jobs"' % rev)
Salud Lemusc856b682019-08-29 11:46:49 -0700335
336
337def UpdateBisection(revisions, git_hashes, bisect_contents, last_tested,
338 update_packages, chroot_path, patch_metadata_file,
Salud Lemus6270ccc2019-09-04 18:15:35 -0700339 extra_change_lists, options, builder, verbose):
Salud Lemusc856b682019-08-29 11:46:49 -0700340 """Adds tryjobs and updates the status file with the new tryjobs."""
341
342 try:
343 for svn_revision, git_hash in zip(revisions, git_hashes):
344 tryjob_dict = AddTryjob(update_packages, git_hash, svn_revision,
345 chroot_path, patch_metadata_file,
Salud Lemus6270ccc2019-09-04 18:15:35 -0700346 extra_change_lists, options, builder, verbose,
Salud Lemusc856b682019-08-29 11:46:49 -0700347 svn_revision)
348
349 bisect_contents['jobs'].append(tryjob_dict)
350 finally:
351 # Do not want to lose progress if there is an exception.
352 if last_tested:
353 new_file = '%s.new' % last_tested
354 with open(new_file, 'w') as json_file:
355 json.dump(bisect_contents, json_file, indent=4, separators=(',', ': '))
356
357 os.rename(new_file, last_tested)
358
359
360def _NoteCompletedBisection(last_tested, src_path, end):
361 """Prints that bisection is complete."""
362
363 print('Finished bisecting for %s' % last_tested)
364
365 if src_path:
366 bad_llvm_hash = LLVMHash().GetGitHashForVersion(src_path, end)
367 else:
368 bad_llvm_hash = LLVMHash().GetLLVMHash(end)
369
370 print(
371 'The bad revision is %d and its commit hash is %s' % (end, bad_llvm_hash))
372
373
Salud Lemus97c1e1c2019-09-10 16:28:47 -0700374def LoadStatusFile(last_tested, start, end):
375 """Loads the status file for bisection."""
376
377 try:
378 with open(last_tested) as f:
379 return json.load(f)
380 except IOError as err:
381 if err.errno != errno.ENOENT:
382 raise
383
384 return {'start': start, 'end': end, 'jobs': []}
385
386
Salud Lemusc856b682019-08-29 11:46:49 -0700387def main(args_output):
Salud Lemus055838a2019-08-19 13:37:28 -0700388 """Bisects LLVM based off of a .JSON file.
389
390 Raises:
391 AssertionError: The script was run inside the chroot.
392 """
393
394 VerifyOutsideChroot()
395
Salud Lemus055838a2019-08-19 13:37:28 -0700396 update_packages = [
397 'sys-devel/llvm', 'sys-libs/compiler-rt', 'sys-libs/libcxx',
398 'sys-libs/libcxxabi', 'sys-libs/llvm-libunwind'
399 ]
400
401 patch_metadata_file = 'PATCHES.json'
402
403 start = args_output.start_rev
404 end = args_output.end_rev
405
Salud Lemus97c1e1c2019-09-10 16:28:47 -0700406 bisect_contents = LoadStatusFile(args_output.last_tested, start, end)
Salud Lemus055838a2019-08-19 13:37:28 -0700407
408 _ValidateStartAndEndAgainstJSONStartAndEnd(
409 start, end, bisect_contents['start'], bisect_contents['end'])
Salud Lemusc856b682019-08-29 11:46:49 -0700410
Salud Lemusffed65d2019-08-26 14:52:02 -0700411 # Pending and skipped revisions are between 'start_revision' and
412 # 'end_revision'.
413 start_revision, end_revision, pending_revisions, skip_revisions = \
414 GetStartAndEndRevision(start, end, bisect_contents['jobs'])
Salud Lemus055838a2019-08-19 13:37:28 -0700415
416 revisions, git_hashes = GetRevisionsListAndHashList(
417 start_revision, end_revision, args_output.parallel, args_output.src_path,
Salud Lemusffed65d2019-08-26 14:52:02 -0700418 pending_revisions, skip_revisions)
419
Salud Lemusc856b682019-08-29 11:46:49 -0700420 # No more revisions between 'start_revision' and 'end_revision', so
421 # bisection is complete.
422 #
423 # This is determined by finding all valid revisions between 'start_revision'
424 # and 'end_revision' and that are NOT in the 'pending' and 'skipped' set.
Salud Lemusffed65d2019-08-26 14:52:02 -0700425 if not revisions:
Salud Lemusc856b682019-08-29 11:46:49 -0700426 # Successfully completed bisection where there are 2 cases:
427 # 1) 'start_revision' and 'end_revision' are back-to-back (example:
428 # 'start_revision' is 369410 and 'end_revision' is 369411).
429 #
430 # 2) 'start_revision' and 'end_revision' are NOT back-to-back, so there must
431 # be tryjobs in between which are labeled as 'skip' for their 'status'
432 # value.
433 #
434 # In either case, there are no 'pending' jobs.
435 if not pending_revisions:
436 _NoteCompletedBisection(args_output.last_tested, args_output.src_path,
437 end_revision)
Salud Lemusffed65d2019-08-26 14:52:02 -0700438
Salud Lemusc856b682019-08-29 11:46:49 -0700439 if skip_revisions:
440 skip_revisions_message = ('\nThe following revisions were skipped:\n' +
441 '\n'.join(str(rev) for rev in skip_revisions))
Salud Lemusffed65d2019-08-26 14:52:02 -0700442
Salud Lemusc856b682019-08-29 11:46:49 -0700443 print(skip_revisions_message)
Salud Lemus055838a2019-08-19 13:37:28 -0700444
Salud Lemusc856b682019-08-29 11:46:49 -0700445 return BisectionExitStatus.BISECTION_COMPLETE.value
Salud Lemus055838a2019-08-19 13:37:28 -0700446
Salud Lemusc856b682019-08-29 11:46:49 -0700447 # Some tryjobs are not finished which may change the actual bad
448 # commit/revision when those tryjobs are finished.
449 DieWithNoRevisionsError(start_revision, end_revision, skip_revisions,
450 pending_revisions)
Salud Lemus055838a2019-08-19 13:37:28 -0700451
Salud Lemusc856b682019-08-29 11:46:49 -0700452 CheckForExistingTryjobsInRevisionsToLaunch(revisions, bisect_contents['jobs'])
Salud Lemus055838a2019-08-19 13:37:28 -0700453
Salud Lemusc856b682019-08-29 11:46:49 -0700454 UpdateBisection(revisions, git_hashes, bisect_contents,
455 args_output.last_tested, update_packages,
456 args_output.chroot_path, patch_metadata_file,
457 args_output.extra_change_lists, args_output.options,
Salud Lemus6270ccc2019-09-04 18:15:35 -0700458 args_output.builder, args_output.verbose)
Salud Lemus055838a2019-08-19 13:37:28 -0700459
460
461if __name__ == '__main__':
Salud Lemusc856b682019-08-29 11:46:49 -0700462 args_output = GetCommandLineArgs()
463 sys.exit(main(args_output))