blob: 4ff921d268ea187beed2d6942701a0a702d1c805 [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
Jian Cai121dbe52019-11-20 15:32:56 -080014import get_llvm_hash
Salud Lemus055838a2019-08-19 13:37:28 -070015import json
16import os
Salud Lemusc856b682019-08-29 11:46:49 -070017import sys
Salud Lemus055838a2019-08-19 13:37:28 -070018
19from assert_not_in_chroot import VerifyOutsideChroot
Salud Lemus42235702019-08-23 16:49:28 -070020from get_llvm_hash import CreateTempLLVMRepo
Salud Lemus055838a2019-08-19 13:37:28 -070021from get_llvm_hash import LLVMHash
22from modify_a_tryjob import AddTryjob
Salud Lemus055838a2019-08-19 13:37:28 -070023from update_tryjob_status import FindTryjobIndex
24from update_tryjob_status import TryjobStatus
25
26
Salud Lemusc856b682019-08-29 11:46:49 -070027class BisectionExitStatus(enum.Enum):
28 """Exit code when performing bisection."""
29
30 # Means that there are no more revisions available to bisect.
31 BISECTION_COMPLETE = 126
32
33
Salud Lemus055838a2019-08-19 13:37:28 -070034def is_file_and_json(json_file):
35 """Validates that the file exists and is a JSON file."""
36 return os.path.isfile(json_file) and json_file.endswith('.json')
37
38
39def GetCommandLineArgs():
40 """Parses the command line for the command line arguments."""
41
42 # Default path to the chroot if a path is not specified.
43 cros_root = os.path.expanduser('~')
44 cros_root = os.path.join(cros_root, 'chromiumos')
45
46 # Create parser and add optional command-line arguments.
47 parser = argparse.ArgumentParser(
48 description='Bisects LLVM via tracking a JSON file.')
49
50 # Add argument for other change lists that want to run alongside the tryjob
51 # which has a change list of updating a package's git hash.
52 parser.add_argument(
53 '--parallel',
54 type=int,
55 default=3,
56 help='How many tryjobs to create between the last good version and '
57 'the first bad version (default: %(default)s)')
58
59 # Add argument for the good LLVM revision for bisection.
60 parser.add_argument(
61 '--start_rev',
62 required=True,
63 type=int,
64 help='The good revision for the bisection.')
65
66 # Add argument for the bad LLVM revision for bisection.
67 parser.add_argument(
68 '--end_rev',
69 required=True,
70 type=int,
71 help='The bad revision for the bisection.')
72
73 # Add argument for the absolute path to the file that contains information on
74 # the previous tested svn version.
75 parser.add_argument(
76 '--last_tested',
77 required=True,
78 help='the absolute path to the file that contains the tryjobs')
79
80 # Add argument for the absolute path to the LLVM source tree.
81 parser.add_argument(
82 '--src_path',
83 help='the path to the LLVM source tree to use (used for retrieving the '
84 'git hash of each version between the last good version and first bad '
85 'version)')
86
87 # Add argument for other change lists that want to run alongside the tryjob
88 # which has a change list of updating a package's git hash.
89 parser.add_argument(
90 '--extra_change_lists',
91 type=int,
92 nargs='+',
93 help='change lists that would like to be run alongside the change list '
94 'of updating the packages')
95
96 # Add argument for custom options for the tryjob.
97 parser.add_argument(
98 '--options',
99 required=False,
100 nargs='+',
101 help='options to use for the tryjob testing')
102
103 # Add argument for the builder to use for the tryjob.
104 parser.add_argument(
105 '--builder', required=True, help='builder to use for the tryjob testing')
106
107 # Add argument for the description of the tryjob.
108 parser.add_argument(
109 '--description',
110 required=False,
111 nargs='+',
112 help='the description of the tryjob')
113
114 # Add argument for a specific chroot path.
115 parser.add_argument(
116 '--chroot_path',
117 default=cros_root,
118 help='the path to the chroot (default: %(default)s)')
119
Salud Lemus6270ccc2019-09-04 18:15:35 -0700120 # Add argument for whether to display command contents to `stdout`.
Salud Lemus055838a2019-08-19 13:37:28 -0700121 parser.add_argument(
Salud Lemus6270ccc2019-09-04 18:15:35 -0700122 '--verbose',
123 action='store_true',
124 help='display contents of a command to the terminal '
125 '(default: %(default)s)')
Salud Lemus055838a2019-08-19 13:37:28 -0700126
127 args_output = parser.parse_args()
128
129 assert args_output.start_rev < args_output.end_rev, (
130 'Start revision %d is >= end revision %d' % (args_output.start_rev,
131 args_output.end_rev))
132
133 if args_output.last_tested and not args_output.last_tested.endswith('.json'):
134 raise ValueError(
Salud Lemus6270ccc2019-09-04 18:15:35 -0700135 'Filed provided %s does not end in ".json"' % args_output.last_tested)
Salud Lemus055838a2019-08-19 13:37:28 -0700136
137 return args_output
138
139
140def _ValidateStartAndEndAgainstJSONStartAndEnd(start, end, json_start,
141 json_end):
142 """Valides that the command line arguments are the same as the JSON."""
143
144 if start != json_start or end != json_end:
145 raise ValueError('The start %d or the end %d version provided is '
Salud Lemus6270ccc2019-09-04 18:15:35 -0700146 'different than "start" %d or "end" %d in the .JSON '
Salud Lemus055838a2019-08-19 13:37:28 -0700147 'file' % (start, end, json_start, json_end))
148
149
150def GetStartAndEndRevision(start, end, tryjobs):
151 """Gets the start and end intervals in 'json_file'.
152
153 Args:
154 start: The start version of the bisection provided via the command line.
155 end: The end version of the bisection provided via the command line.
156 tryjobs: A list of tryjobs where each element is in the following format:
157 [
158 {[TRYJOB_INFORMATION]},
159 {[TRYJOB_INFORMATION]},
160 ...,
161 {[TRYJOB_INFORMATION]}
162 ]
163
164 Returns:
Salud Lemusffed65d2019-08-26 14:52:02 -0700165 The new start version and end version for bisection, a set of revisions
166 that are 'pending' and a set of revisions that are to be skipped.
Salud Lemus055838a2019-08-19 13:37:28 -0700167
168 Raises:
169 ValueError: The value for 'status' is missing or there is a mismatch
170 between 'start' and 'end' compared to the 'start' and 'end' in the JSON
171 file.
172 AssertionError: The new start version is >= than the new end version.
173 """
174
175 if not tryjobs:
Salud Lemus02630632019-08-28 15:27:59 -0700176 return start, end, {}, {}
Salud Lemus055838a2019-08-19 13:37:28 -0700177
178 # Verify that each tryjob has a value for the 'status' key.
179 for cur_tryjob_dict in tryjobs:
180 if not cur_tryjob_dict.get('status', None):
Salud Lemus6270ccc2019-09-04 18:15:35 -0700181 raise ValueError('"status" is missing or has no value, please '
Salud Lemus055838a2019-08-19 13:37:28 -0700182 'go to %s and update it' % cur_tryjob_dict['link'])
183
184 all_bad_revisions = [end]
185 all_bad_revisions.extend(cur_tryjob['rev']
186 for cur_tryjob in tryjobs
187 if cur_tryjob['status'] == TryjobStatus.BAD.value)
188
189 # The minimum value for the 'bad' field in the tryjobs is the new end
190 # version.
191 bad_rev = min(all_bad_revisions)
192
193 all_good_revisions = [start]
194 all_good_revisions.extend(cur_tryjob['rev']
195 for cur_tryjob in tryjobs
196 if cur_tryjob['status'] == TryjobStatus.GOOD.value)
197
198 # The maximum value for the 'good' field in the tryjobs is the new start
199 # version.
200 good_rev = max(all_good_revisions)
201
202 # The good version should always be strictly less than the bad version;
203 # otherwise, bisection is broken.
204 assert good_rev < bad_rev, ('Bisection is broken because %d (good) is >= '
205 '%d (bad)' % (good_rev, bad_rev))
206
207 # Find all revisions that are 'pending' within 'good_rev' and 'bad_rev'.
208 #
209 # NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev'
Salud Lemusffed65d2019-08-26 14:52:02 -0700210 # that have already been launched (this set is used when constructing the
Salud Lemus055838a2019-08-19 13:37:28 -0700211 # list of revisions to launch tryjobs for).
Salud Lemusffed65d2019-08-26 14:52:02 -0700212 pending_revisions = {
Salud Lemus055838a2019-08-19 13:37:28 -0700213 tryjob['rev']
214 for tryjob in tryjobs
215 if tryjob['status'] == TryjobStatus.PENDING.value and
216 good_rev < tryjob['rev'] < bad_rev
Salud Lemusffed65d2019-08-26 14:52:02 -0700217 }
Salud Lemus055838a2019-08-19 13:37:28 -0700218
Salud Lemusffed65d2019-08-26 14:52:02 -0700219 # Find all revisions that are to be skipped within 'good_rev' and 'bad_rev'.
220 #
221 # NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev'
222 # that have already been marked as 'skip' (this set is used when constructing
223 # the list of revisions to launch tryjobs for).
224 skip_revisions = {
225 tryjob['rev']
226 for tryjob in tryjobs
227 if tryjob['status'] == TryjobStatus.SKIP.value and
228 good_rev < tryjob['rev'] < bad_rev
229 }
230
231 return good_rev, bad_rev, pending_revisions, skip_revisions
Salud Lemus055838a2019-08-19 13:37:28 -0700232
233
234def GetRevisionsBetweenBisection(start, end, parallel, src_path,
Salud Lemusffed65d2019-08-26 14:52:02 -0700235 pending_revisions, skip_revisions):
Salud Lemus055838a2019-08-19 13:37:28 -0700236 """Gets the revisions between 'start' and 'end'.
237
238 Sometimes, the LLVM source tree's revisions do not increment by 1 (there is
239 a jump), so need to construct a list of all revisions that are NOT missing
240 between 'start' and 'end'. Then, the step amount (i.e. length of the list
241 divided by ('parallel' + 1)) will be used for indexing into the list.
242
243 Args:
244 start: The start revision.
245 end: The end revision.
246 parallel: The number of tryjobs to create between 'start' and 'end'.
247 src_path: The absolute path to the LLVM source tree to use.
Salud Lemusffed65d2019-08-26 14:52:02 -0700248 pending_revisions: A set containing 'pending' revisions that are between
249 'start' and 'end'.
250 skip_revisions: A set containing revisions between 'start' and 'end' that
251 are to be skipped.
Salud Lemus055838a2019-08-19 13:37:28 -0700252
253 Returns:
254 A list of revisions between 'start' and 'end'.
255 """
256
257 new_llvm = LLVMHash()
258
259 valid_revisions = []
260
261 # Start at ('start' + 1) because 'start' is the good revision.
262 #
263 # FIXME: Searching for each revision from ('start' + 1) up to 'end' in the
264 # LLVM source tree is a quadratic algorithm. It's a good idea to optimize
265 # this.
266 for cur_revision in range(start + 1, end):
267 try:
Salud Lemusffed65d2019-08-26 14:52:02 -0700268 if cur_revision not in pending_revisions and \
269 cur_revision not in skip_revisions:
Salud Lemus055838a2019-08-19 13:37:28 -0700270 # Verify that the current revision exists by finding its corresponding
271 # git hash in the LLVM source tree.
Jian Cai121dbe52019-11-20 15:32:56 -0800272 get_llvm_hash.GetGitHashFrom(src_path, cur_revision)
Salud Lemus055838a2019-08-19 13:37:28 -0700273 valid_revisions.append(cur_revision)
274 except ValueError:
275 # Could not find the git hash for the current revision.
276 continue
277
278 # ('parallel' + 1) so that the last revision in the list is not close to
279 # 'end' (have a bit more coverage).
280 index_step = len(valid_revisions) // (parallel + 1)
281
282 if not index_step:
283 index_step = 1
284
Salud Lemus055838a2019-08-19 13:37:28 -0700285 result = [valid_revisions[index] \
Salud Lemusffed65d2019-08-26 14:52:02 -0700286 for index in range(0, len(valid_revisions), index_step)]
Salud Lemus055838a2019-08-19 13:37:28 -0700287
288 return result
289
290
291def GetRevisionsListAndHashList(start, end, parallel, src_path,
Salud Lemusffed65d2019-08-26 14:52:02 -0700292 pending_revisions, skip_revisions):
Salud Lemus055838a2019-08-19 13:37:28 -0700293 """Determines the revisions between start and end."""
294
295 new_llvm = LLVMHash()
296
297 with new_llvm.CreateTempDirectory() as temp_dir:
Salud Lemus42235702019-08-23 16:49:28 -0700298 with CreateTempLLVMRepo(temp_dir) as new_repo:
299 if not src_path:
300 src_path = new_repo
Salud Lemus055838a2019-08-19 13:37:28 -0700301
Salud Lemus42235702019-08-23 16:49:28 -0700302 # Get a list of revisions between start and end.
Salud Lemusffed65d2019-08-26 14:52:02 -0700303 revisions = GetRevisionsBetweenBisection(
304 start, end, parallel, src_path, pending_revisions, skip_revisions)
Salud Lemus055838a2019-08-19 13:37:28 -0700305
Jian Cai121dbe52019-11-20 15:32:56 -0800306 git_hashes = [get_llvm_hash.GetGitHashFrom(src_path, rev) for rev in revisions]
Salud Lemus055838a2019-08-19 13:37:28 -0700307
Salud Lemus055838a2019-08-19 13:37:28 -0700308 return revisions, git_hashes
309
310
Salud Lemusc856b682019-08-29 11:46:49 -0700311def DieWithNoRevisionsError(start, end, skip_revisions, pending_revisions):
312 """Raises a ValueError exception with useful information."""
313
314 no_revisions_message = ('No revisions between start %d and end '
315 '%d to create tryjobs' % (start, end))
316
317 if pending_revisions:
318 no_revisions_message += '\nThe following tryjobs are pending:\n' \
319 + '\n'.join(str(rev) for rev in pending_revisions)
320
321 if skip_revisions:
322 no_revisions_message += '\nThe following tryjobs were skipped:\n' \
323 + '\n'.join(str(rev) for rev in skip_revisions)
324
325 raise ValueError(no_revisions_message)
326
327
328def CheckForExistingTryjobsInRevisionsToLaunch(revisions, jobs):
329 """Checks if a revision in 'revisions' exists in 'jobs' list."""
330
331 for rev in revisions:
332 if FindTryjobIndex(rev, jobs) is not None:
Salud Lemus6270ccc2019-09-04 18:15:35 -0700333 raise ValueError('Revision %d exists already in "jobs"' % rev)
Salud Lemusc856b682019-08-29 11:46:49 -0700334
335
336def UpdateBisection(revisions, git_hashes, bisect_contents, last_tested,
337 update_packages, chroot_path, patch_metadata_file,
Salud Lemus6270ccc2019-09-04 18:15:35 -0700338 extra_change_lists, options, builder, verbose):
Salud Lemusc856b682019-08-29 11:46:49 -0700339 """Adds tryjobs and updates the status file with the new tryjobs."""
340
341 try:
342 for svn_revision, git_hash in zip(revisions, git_hashes):
343 tryjob_dict = AddTryjob(update_packages, git_hash, svn_revision,
344 chroot_path, patch_metadata_file,
Salud Lemus6270ccc2019-09-04 18:15:35 -0700345 extra_change_lists, options, builder, verbose,
Salud Lemusc856b682019-08-29 11:46:49 -0700346 svn_revision)
347
348 bisect_contents['jobs'].append(tryjob_dict)
349 finally:
350 # Do not want to lose progress if there is an exception.
351 if last_tested:
352 new_file = '%s.new' % last_tested
353 with open(new_file, 'w') as json_file:
354 json.dump(bisect_contents, json_file, indent=4, separators=(',', ': '))
355
356 os.rename(new_file, last_tested)
357
358
359def _NoteCompletedBisection(last_tested, src_path, end):
360 """Prints that bisection is complete."""
361
362 print('Finished bisecting for %s' % last_tested)
363
364 if src_path:
Jian Cai121dbe52019-11-20 15:32:56 -0800365 bad_llvm_hash = get_llvm_hash.GetGitHashFrom(src_path, end)
Salud Lemusc856b682019-08-29 11:46:49 -0700366 else:
367 bad_llvm_hash = LLVMHash().GetLLVMHash(end)
368
369 print(
370 'The bad revision is %d and its commit hash is %s' % (end, bad_llvm_hash))
371
372
Salud Lemus97c1e1c2019-09-10 16:28:47 -0700373def LoadStatusFile(last_tested, start, end):
374 """Loads the status file for bisection."""
375
376 try:
377 with open(last_tested) as f:
378 return json.load(f)
379 except IOError as err:
380 if err.errno != errno.ENOENT:
381 raise
382
383 return {'start': start, 'end': end, 'jobs': []}
384
385
Salud Lemusc856b682019-08-29 11:46:49 -0700386def main(args_output):
Salud Lemus055838a2019-08-19 13:37:28 -0700387 """Bisects LLVM based off of a .JSON file.
388
389 Raises:
390 AssertionError: The script was run inside the chroot.
391 """
392
393 VerifyOutsideChroot()
394
Salud Lemus055838a2019-08-19 13:37:28 -0700395 update_packages = [
396 'sys-devel/llvm', 'sys-libs/compiler-rt', 'sys-libs/libcxx',
397 'sys-libs/libcxxabi', 'sys-libs/llvm-libunwind'
398 ]
399
400 patch_metadata_file = 'PATCHES.json'
401
402 start = args_output.start_rev
403 end = args_output.end_rev
404
Salud Lemus97c1e1c2019-09-10 16:28:47 -0700405 bisect_contents = LoadStatusFile(args_output.last_tested, start, end)
Salud Lemus055838a2019-08-19 13:37:28 -0700406
407 _ValidateStartAndEndAgainstJSONStartAndEnd(
408 start, end, bisect_contents['start'], bisect_contents['end'])
Salud Lemusc856b682019-08-29 11:46:49 -0700409
Salud Lemusffed65d2019-08-26 14:52:02 -0700410 # Pending and skipped revisions are between 'start_revision' and
411 # 'end_revision'.
412 start_revision, end_revision, pending_revisions, skip_revisions = \
413 GetStartAndEndRevision(start, end, bisect_contents['jobs'])
Salud Lemus055838a2019-08-19 13:37:28 -0700414
415 revisions, git_hashes = GetRevisionsListAndHashList(
416 start_revision, end_revision, args_output.parallel, args_output.src_path,
Salud Lemusffed65d2019-08-26 14:52:02 -0700417 pending_revisions, skip_revisions)
418
Salud Lemusc856b682019-08-29 11:46:49 -0700419 # No more revisions between 'start_revision' and 'end_revision', so
420 # bisection is complete.
421 #
422 # This is determined by finding all valid revisions between 'start_revision'
423 # and 'end_revision' and that are NOT in the 'pending' and 'skipped' set.
Salud Lemusffed65d2019-08-26 14:52:02 -0700424 if not revisions:
Salud Lemusc856b682019-08-29 11:46:49 -0700425 # Successfully completed bisection where there are 2 cases:
426 # 1) 'start_revision' and 'end_revision' are back-to-back (example:
427 # 'start_revision' is 369410 and 'end_revision' is 369411).
428 #
429 # 2) 'start_revision' and 'end_revision' are NOT back-to-back, so there must
430 # be tryjobs in between which are labeled as 'skip' for their 'status'
431 # value.
432 #
433 # In either case, there are no 'pending' jobs.
434 if not pending_revisions:
435 _NoteCompletedBisection(args_output.last_tested, args_output.src_path,
436 end_revision)
Salud Lemusffed65d2019-08-26 14:52:02 -0700437
Salud Lemusc856b682019-08-29 11:46:49 -0700438 if skip_revisions:
439 skip_revisions_message = ('\nThe following revisions were skipped:\n' +
440 '\n'.join(str(rev) for rev in skip_revisions))
Salud Lemusffed65d2019-08-26 14:52:02 -0700441
Salud Lemusc856b682019-08-29 11:46:49 -0700442 print(skip_revisions_message)
Salud Lemus055838a2019-08-19 13:37:28 -0700443
Salud Lemusc856b682019-08-29 11:46:49 -0700444 return BisectionExitStatus.BISECTION_COMPLETE.value
Salud Lemus055838a2019-08-19 13:37:28 -0700445
Salud Lemusc856b682019-08-29 11:46:49 -0700446 # Some tryjobs are not finished which may change the actual bad
447 # commit/revision when those tryjobs are finished.
448 DieWithNoRevisionsError(start_revision, end_revision, skip_revisions,
449 pending_revisions)
Salud Lemus055838a2019-08-19 13:37:28 -0700450
Salud Lemusc856b682019-08-29 11:46:49 -0700451 CheckForExistingTryjobsInRevisionsToLaunch(revisions, bisect_contents['jobs'])
Salud Lemus055838a2019-08-19 13:37:28 -0700452
Salud Lemusc856b682019-08-29 11:46:49 -0700453 UpdateBisection(revisions, git_hashes, bisect_contents,
454 args_output.last_tested, update_packages,
455 args_output.chroot_path, patch_metadata_file,
456 args_output.extra_change_lists, args_output.options,
Salud Lemus6270ccc2019-09-04 18:15:35 -0700457 args_output.builder, args_output.verbose)
Salud Lemus055838a2019-08-19 13:37:28 -0700458
459
460if __name__ == '__main__':
Salud Lemusc856b682019-08-29 11:46:49 -0700461 args_output = GetCommandLineArgs()
462 sys.exit(main(args_output))