blob: 37320bafbfa02ebce3bbcca66e83460a8ac96855 [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
Jian Cai089db672020-08-23 20:09:02 -070020import git_llvm_rev
Jian Caic16daa12020-04-15 17:53:41 -070021import modify_a_tryjob
22import update_tryjob_status
Salud Lemus055838a2019-08-19 13:37:28 -070023
24
Salud Lemusc856b682019-08-29 11:46:49 -070025class BisectionExitStatus(enum.Enum):
26 """Exit code when performing bisection."""
27
28 # Means that there are no more revisions available to bisect.
29 BISECTION_COMPLETE = 126
30
31
Salud Lemus055838a2019-08-19 13:37:28 -070032def GetCommandLineArgs():
33 """Parses the command line for the command line arguments."""
34
35 # Default path to the chroot if a path is not specified.
36 cros_root = os.path.expanduser('~')
37 cros_root = os.path.join(cros_root, 'chromiumos')
38
39 # Create parser and add optional command-line arguments.
40 parser = argparse.ArgumentParser(
41 description='Bisects LLVM via tracking a JSON file.')
42
43 # Add argument for other change lists that want to run alongside the tryjob
44 # which has a change list of updating a package's git hash.
45 parser.add_argument(
46 '--parallel',
47 type=int,
48 default=3,
49 help='How many tryjobs to create between the last good version and '
50 'the first bad version (default: %(default)s)')
51
52 # Add argument for the good LLVM revision for bisection.
53 parser.add_argument(
54 '--start_rev',
55 required=True,
56 type=int,
57 help='The good revision for the bisection.')
58
59 # Add argument for the bad LLVM revision for bisection.
60 parser.add_argument(
61 '--end_rev',
62 required=True,
63 type=int,
64 help='The bad revision for the bisection.')
65
66 # Add argument for the absolute path to the file that contains information on
67 # the previous tested svn version.
68 parser.add_argument(
69 '--last_tested',
70 required=True,
71 help='the absolute path to the file that contains the tryjobs')
72
73 # Add argument for the absolute path to the LLVM source tree.
74 parser.add_argument(
75 '--src_path',
76 help='the path to the LLVM source tree to use (used for retrieving the '
77 'git hash of each version between the last good version and first bad '
78 'version)')
79
80 # Add argument for other change lists that want to run alongside the tryjob
81 # which has a change list of updating a package's git hash.
82 parser.add_argument(
83 '--extra_change_lists',
84 type=int,
85 nargs='+',
86 help='change lists that would like to be run alongside the change list '
87 'of updating the packages')
88
89 # Add argument for custom options for the tryjob.
90 parser.add_argument(
91 '--options',
92 required=False,
93 nargs='+',
94 help='options to use for the tryjob testing')
95
96 # Add argument for the builder to use for the tryjob.
97 parser.add_argument(
98 '--builder', required=True, help='builder to use for the tryjob testing')
99
100 # Add argument for the description of the tryjob.
101 parser.add_argument(
102 '--description',
103 required=False,
104 nargs='+',
105 help='the description of the tryjob')
106
107 # Add argument for a specific chroot path.
108 parser.add_argument(
109 '--chroot_path',
110 default=cros_root,
111 help='the path to the chroot (default: %(default)s)')
112
Salud Lemus6270ccc2019-09-04 18:15:35 -0700113 # Add argument for whether to display command contents to `stdout`.
Salud Lemus055838a2019-08-19 13:37:28 -0700114 parser.add_argument(
Salud Lemus6270ccc2019-09-04 18:15:35 -0700115 '--verbose',
116 action='store_true',
117 help='display contents of a command to the terminal '
118 '(default: %(default)s)')
Salud Lemus055838a2019-08-19 13:37:28 -0700119
120 args_output = parser.parse_args()
121
122 assert args_output.start_rev < args_output.end_rev, (
Jian Cai089db672020-08-23 20:09:02 -0700123 'Start revision %d is >= end revision %d' %
124 (args_output.start_rev, args_output.end_rev))
Salud Lemus055838a2019-08-19 13:37:28 -0700125
126 if args_output.last_tested and not args_output.last_tested.endswith('.json'):
Jian Cai089db672020-08-23 20:09:02 -0700127 raise ValueError('Filed provided %s does not end in ".json"' %
128 args_output.last_tested)
Salud Lemus055838a2019-08-19 13:37:28 -0700129
130 return args_output
131
132
Jian Cai089db672020-08-23 20:09:02 -0700133def GetRemainingRange(start, end, tryjobs):
Salud Lemus055838a2019-08-19 13:37:28 -0700134 """Gets the start and end intervals in 'json_file'.
135
136 Args:
137 start: The start version of the bisection provided via the command line.
138 end: The end version of the bisection provided via the command line.
139 tryjobs: A list of tryjobs where each element is in the following format:
140 [
141 {[TRYJOB_INFORMATION]},
142 {[TRYJOB_INFORMATION]},
143 ...,
144 {[TRYJOB_INFORMATION]}
145 ]
146
147 Returns:
Salud Lemusffed65d2019-08-26 14:52:02 -0700148 The new start version and end version for bisection, a set of revisions
149 that are 'pending' and a set of revisions that are to be skipped.
Salud Lemus055838a2019-08-19 13:37:28 -0700150
151 Raises:
152 ValueError: The value for 'status' is missing or there is a mismatch
153 between 'start' and 'end' compared to the 'start' and 'end' in the JSON
154 file.
155 AssertionError: The new start version is >= than the new end version.
156 """
157
158 if not tryjobs:
Salud Lemus02630632019-08-28 15:27:59 -0700159 return start, end, {}, {}
Salud Lemus055838a2019-08-19 13:37:28 -0700160
161 # Verify that each tryjob has a value for the 'status' key.
162 for cur_tryjob_dict in tryjobs:
163 if not cur_tryjob_dict.get('status', None):
Salud Lemus6270ccc2019-09-04 18:15:35 -0700164 raise ValueError('"status" is missing or has no value, please '
Salud Lemus055838a2019-08-19 13:37:28 -0700165 'go to %s and update it' % cur_tryjob_dict['link'])
166
167 all_bad_revisions = [end]
Jian Caic16daa12020-04-15 17:53:41 -0700168 all_bad_revisions.extend(
169 cur_tryjob['rev']
170 for cur_tryjob in tryjobs
171 if cur_tryjob['status'] == update_tryjob_status.TryjobStatus.BAD.value)
Salud Lemus055838a2019-08-19 13:37:28 -0700172
173 # The minimum value for the 'bad' field in the tryjobs is the new end
174 # version.
175 bad_rev = min(all_bad_revisions)
176
177 all_good_revisions = [start]
Jian Caic16daa12020-04-15 17:53:41 -0700178 all_good_revisions.extend(
179 cur_tryjob['rev']
180 for cur_tryjob in tryjobs
181 if cur_tryjob['status'] == update_tryjob_status.TryjobStatus.GOOD.value)
Salud Lemus055838a2019-08-19 13:37:28 -0700182
183 # The maximum value for the 'good' field in the tryjobs is the new start
184 # version.
185 good_rev = max(all_good_revisions)
186
187 # The good version should always be strictly less than the bad version;
188 # otherwise, bisection is broken.
189 assert good_rev < bad_rev, ('Bisection is broken because %d (good) is >= '
190 '%d (bad)' % (good_rev, bad_rev))
191
192 # Find all revisions that are 'pending' within 'good_rev' and 'bad_rev'.
193 #
194 # NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev'
Salud Lemusffed65d2019-08-26 14:52:02 -0700195 # that have already been launched (this set is used when constructing the
Salud Lemus055838a2019-08-19 13:37:28 -0700196 # list of revisions to launch tryjobs for).
Salud Lemusffed65d2019-08-26 14:52:02 -0700197 pending_revisions = {
Salud Lemus055838a2019-08-19 13:37:28 -0700198 tryjob['rev']
199 for tryjob in tryjobs
Jian Caic16daa12020-04-15 17:53:41 -0700200 if tryjob['status'] == update_tryjob_status.TryjobStatus.PENDING.value and
Salud Lemus055838a2019-08-19 13:37:28 -0700201 good_rev < tryjob['rev'] < bad_rev
Salud Lemusffed65d2019-08-26 14:52:02 -0700202 }
Salud Lemus055838a2019-08-19 13:37:28 -0700203
Salud Lemusffed65d2019-08-26 14:52:02 -0700204 # Find all revisions that are to be skipped within 'good_rev' and 'bad_rev'.
205 #
206 # NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev'
207 # that have already been marked as 'skip' (this set is used when constructing
208 # the list of revisions to launch tryjobs for).
209 skip_revisions = {
210 tryjob['rev']
211 for tryjob in tryjobs
Jian Caic16daa12020-04-15 17:53:41 -0700212 if tryjob['status'] == update_tryjob_status.TryjobStatus.SKIP.value and
Salud Lemusffed65d2019-08-26 14:52:02 -0700213 good_rev < tryjob['rev'] < bad_rev
214 }
215
216 return good_rev, bad_rev, pending_revisions, skip_revisions
Salud Lemus055838a2019-08-19 13:37:28 -0700217
218
Jian Cai089db672020-08-23 20:09:02 -0700219def GetCommitsBetween(start, end, parallel, src_path, pending_revisions,
220 skip_revisions):
Salud Lemus055838a2019-08-19 13:37:28 -0700221 """Determines the revisions between start and end."""
222
Jian Cai089db672020-08-23 20:09:02 -0700223 with get_llvm_hash.LLVMHash().CreateTempDirectory() as temp_dir:
224 # We have guaranteed contiguous revision numbers after this,
225 # and that guarnatee simplifies things considerably, so we don't
226 # support anything before it.
227 assert start >= git_llvm_rev.base_llvm_revision, f'{start} was too long ago'
Salud Lemus055838a2019-08-19 13:37:28 -0700228
Jian Caic16daa12020-04-15 17:53:41 -0700229 with get_llvm_hash.CreateTempLLVMRepo(temp_dir) as new_repo:
Salud Lemus42235702019-08-23 16:49:28 -0700230 if not src_path:
231 src_path = new_repo
Jian Cai089db672020-08-23 20:09:02 -0700232 index_step = (end - (start + 1)) // (parallel + 1)
233 if not index_step:
234 index_step = 1
235 revisions = [
236 rev for rev in range(start + 1, end, index_step)
237 if rev not in pending_revisions and rev not in skip_revisions
238 ]
Jian Caic16daa12020-04-15 17:53:41 -0700239 git_hashes = [
240 get_llvm_hash.GetGitHashFrom(src_path, rev) for rev in revisions
241 ]
Jian Cai089db672020-08-23 20:09:02 -0700242 return revisions, git_hashes
Salud Lemus055838a2019-08-19 13:37:28 -0700243
244
Jian Cai089db672020-08-23 20:09:02 -0700245def Bisect(revisions, git_hashes, bisect_state, last_tested, update_packages,
246 chroot_path, patch_metadata_file, extra_change_lists, options,
247 builder, verbose):
Salud Lemusc856b682019-08-29 11:46:49 -0700248 """Adds tryjobs and updates the status file with the new tryjobs."""
249
250 try:
251 for svn_revision, git_hash in zip(revisions, git_hashes):
Jian Cai089db672020-08-23 20:09:02 -0700252 tryjob_dict = modify_a_tryjob.AddTryjob(update_packages, git_hash,
253 svn_revision, chroot_path,
254 patch_metadata_file,
255 extra_change_lists, options,
256 builder, verbose, svn_revision)
Salud Lemusc856b682019-08-29 11:46:49 -0700257
Jian Cai089db672020-08-23 20:09:02 -0700258 bisect_state['jobs'].append(tryjob_dict)
Salud Lemusc856b682019-08-29 11:46:49 -0700259 finally:
260 # Do not want to lose progress if there is an exception.
261 if last_tested:
262 new_file = '%s.new' % last_tested
263 with open(new_file, 'w') as json_file:
Jian Cai089db672020-08-23 20:09:02 -0700264 json.dump(bisect_state, json_file, indent=4, separators=(',', ': '))
Salud Lemusc856b682019-08-29 11:46:49 -0700265
266 os.rename(new_file, last_tested)
267
268
Salud Lemus97c1e1c2019-09-10 16:28:47 -0700269def LoadStatusFile(last_tested, start, end):
270 """Loads the status file for bisection."""
271
272 try:
273 with open(last_tested) as f:
274 return json.load(f)
275 except IOError as err:
276 if err.errno != errno.ENOENT:
277 raise
278
279 return {'start': start, 'end': end, 'jobs': []}
280
281
Salud Lemusc856b682019-08-29 11:46:49 -0700282def main(args_output):
Jian Cai089db672020-08-23 20:09:02 -0700283 """Bisects LLVM commits.
Salud Lemus055838a2019-08-19 13:37:28 -0700284
285 Raises:
286 AssertionError: The script was run inside the chroot.
287 """
288
Jian Caic16daa12020-04-15 17:53:41 -0700289 chroot.VerifyOutsideChroot()
Salud Lemus055838a2019-08-19 13:37:28 -0700290 update_packages = [
291 'sys-devel/llvm', 'sys-libs/compiler-rt', 'sys-libs/libcxx',
292 'sys-libs/libcxxabi', 'sys-libs/llvm-libunwind'
293 ]
Salud Lemus055838a2019-08-19 13:37:28 -0700294 patch_metadata_file = 'PATCHES.json'
Salud Lemus055838a2019-08-19 13:37:28 -0700295 start = args_output.start_rev
296 end = args_output.end_rev
297
Jian Cai089db672020-08-23 20:09:02 -0700298 bisect_state = LoadStatusFile(args_output.last_tested, start, end)
299 if start != bisect_state['start'] or end != bisect_state['end']:
300 raise ValueError(f'The start {start} or the end {end} version provided is '
301 f'different than "start" {bisect_state["start"]} or "end" '
302 f'{bisect_state["end"]} in the .JSON file')
Salud Lemusc856b682019-08-29 11:46:49 -0700303
Salud Lemusffed65d2019-08-26 14:52:02 -0700304 # Pending and skipped revisions are between 'start_revision' and
305 # 'end_revision'.
306 start_revision, end_revision, pending_revisions, skip_revisions = \
Jian Cai089db672020-08-23 20:09:02 -0700307 GetRemainingRange(start, end, bisect_state['jobs'])
Salud Lemus055838a2019-08-19 13:37:28 -0700308
Jian Cai089db672020-08-23 20:09:02 -0700309 revisions, git_hashes = GetCommitsBetween(start_revision, end_revision,
310 args_output.parallel,
311 args_output.src_path,
312 pending_revisions, skip_revisions)
Salud Lemusffed65d2019-08-26 14:52:02 -0700313
Salud Lemusc856b682019-08-29 11:46:49 -0700314 # No more revisions between 'start_revision' and 'end_revision', so
315 # bisection is complete.
316 #
317 # This is determined by finding all valid revisions between 'start_revision'
318 # and 'end_revision' and that are NOT in the 'pending' and 'skipped' set.
Salud Lemusffed65d2019-08-26 14:52:02 -0700319 if not revisions:
Jian Cai089db672020-08-23 20:09:02 -0700320 if pending_revisions:
321 # Some tryjobs are not finished which may change the actual bad
322 # commit/revision when those tryjobs are finished.
323 no_revisions_message = (f'No revisions between start {start_revision} '
324 f'and end {end_revision} to create tryjobs\n')
325
326 if pending_revisions:
327 no_revisions_message += (
328 'The following tryjobs are pending:\n' +
329 '\n'.join(str(rev) for rev in pending_revisions) + '\n')
Salud Lemusffed65d2019-08-26 14:52:02 -0700330
Salud Lemusc856b682019-08-29 11:46:49 -0700331 if skip_revisions:
Jian Cai089db672020-08-23 20:09:02 -0700332 no_revisions_message += ('The following tryjobs were skipped:\n' +
333 '\n'.join(str(rev) for rev in skip_revisions) +
334 '\n')
Salud Lemusffed65d2019-08-26 14:52:02 -0700335
Jian Cai089db672020-08-23 20:09:02 -0700336 raise ValueError(no_revisions_message)
Salud Lemus055838a2019-08-19 13:37:28 -0700337
Jian Cai089db672020-08-23 20:09:02 -0700338 print(f'Finished bisecting for {args_output.last_tested}')
339 if args_output.src_path:
340 bad_llvm_hash = get_llvm_hash.GetGitHashFrom(args_output.src_path,
341 end_revision)
342 else:
343 bad_llvm_hash = get_llvm_hash.LLVMHash().GetLLVMHash(end_revision)
344 print(f'The bad revision is {end_revision} and its commit hash is '
345 f'{bad_llvm_hash}')
346 if skip_revisions:
347 skip_revisions_message = ('\nThe following revisions were skipped:\n' +
348 '\n'.join(str(rev) for rev in skip_revisions))
349 print(skip_revisions_message)
Salud Lemus055838a2019-08-19 13:37:28 -0700350
Jian Cai089db672020-08-23 20:09:02 -0700351 return BisectionExitStatus.BISECTION_COMPLETE.value
Salud Lemus055838a2019-08-19 13:37:28 -0700352
Jian Cai089db672020-08-23 20:09:02 -0700353 for rev in revisions:
354 if update_tryjob_status.FindTryjobIndex(rev,
355 bisect_state['jobs']) is not None:
356 raise ValueError(f'Revision {rev} exists already in "jobs"')
Salud Lemus055838a2019-08-19 13:37:28 -0700357
Jian Cai089db672020-08-23 20:09:02 -0700358 Bisect(revisions, git_hashes, bisect_state, args_output.last_tested,
359 update_packages, args_output.chroot_path, patch_metadata_file,
360 args_output.extra_change_lists, args_output.options,
361 args_output.builder, args_output.verbose)
Salud Lemus055838a2019-08-19 13:37:28 -0700362
363
364if __name__ == '__main__':
Jian Caic16daa12020-04-15 17:53:41 -0700365 sys.exit(main(GetCommandLineArgs()))