blob: c8d694cdff2864dd80aa685cb3ccf52474b56e7d [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
Jian Caib9a42992020-08-23 20:09:02 -070016import subprocess
Salud Lemusc856b682019-08-29 11:46:49 -070017import sys
Salud Lemus055838a2019-08-19 13:37:28 -070018
Jian Caic16daa12020-04-15 17:53:41 -070019import chroot
20import get_llvm_hash
Jian Cai089db672020-08-23 20:09:02 -070021import git_llvm_rev
Jian Caic16daa12020-04-15 17:53:41 -070022import modify_a_tryjob
23import update_tryjob_status
Salud Lemus055838a2019-08-19 13:37:28 -070024
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 GetCommandLineArgs():
34 """Parses the command line for the command line arguments."""
35
36 # Default path to the chroot if a path is not specified.
37 cros_root = os.path.expanduser('~')
38 cros_root = os.path.join(cros_root, 'chromiumos')
39
40 # Create parser and add optional command-line arguments.
41 parser = argparse.ArgumentParser(
42 description='Bisects LLVM via tracking a JSON file.')
43
44 # Add argument for other change lists that want to run alongside the tryjob
45 # which has a change list of updating a package's git hash.
46 parser.add_argument(
47 '--parallel',
48 type=int,
49 default=3,
50 help='How many tryjobs to create between the last good version and '
51 'the first bad version (default: %(default)s)')
52
53 # Add argument for the good LLVM revision for bisection.
54 parser.add_argument(
55 '--start_rev',
56 required=True,
57 type=int,
58 help='The good revision for the bisection.')
59
60 # Add argument for the bad LLVM revision for bisection.
61 parser.add_argument(
62 '--end_rev',
63 required=True,
64 type=int,
65 help='The bad revision for the bisection.')
66
67 # Add argument for the absolute path to the file that contains information on
68 # the previous tested svn version.
69 parser.add_argument(
70 '--last_tested',
71 required=True,
72 help='the absolute path to the file that contains the tryjobs')
73
74 # Add argument for the absolute path to the LLVM source tree.
75 parser.add_argument(
76 '--src_path',
77 help='the path to the LLVM source tree to use (used for retrieving the '
78 'git hash of each version between the last good version and first bad '
79 'version)')
80
81 # Add argument for other change lists that want to run alongside the tryjob
82 # which has a change list of updating a package's git hash.
83 parser.add_argument(
84 '--extra_change_lists',
85 type=int,
86 nargs='+',
87 help='change lists that would like to be run alongside the change list '
88 'of updating the packages')
89
90 # Add argument for custom options for the tryjob.
91 parser.add_argument(
92 '--options',
93 required=False,
94 nargs='+',
95 help='options to use for the tryjob testing')
96
97 # Add argument for the builder to use for the tryjob.
98 parser.add_argument(
99 '--builder', required=True, help='builder to use for the tryjob testing')
100
101 # Add argument for the description of the tryjob.
102 parser.add_argument(
103 '--description',
104 required=False,
105 nargs='+',
106 help='the description of the tryjob')
107
108 # Add argument for a specific chroot path.
109 parser.add_argument(
110 '--chroot_path',
111 default=cros_root,
112 help='the path to the chroot (default: %(default)s)')
113
Salud Lemus6270ccc2019-09-04 18:15:35 -0700114 # Add argument for whether to display command contents to `stdout`.
Salud Lemus055838a2019-08-19 13:37:28 -0700115 parser.add_argument(
Salud Lemus6270ccc2019-09-04 18:15:35 -0700116 '--verbose',
117 action='store_true',
118 help='display contents of a command to the terminal '
119 '(default: %(default)s)')
Salud Lemus055838a2019-08-19 13:37:28 -0700120
Jian Caib9a42992020-08-23 20:09:02 -0700121 # Add argument for whether to display command contents to `stdout`.
122 parser.add_argument(
123 '--nocleanup',
124 action='store_false',
125 dest='cleanup',
126 help='Abandon CLs created for bisectoin')
127
Salud Lemus055838a2019-08-19 13:37:28 -0700128 args_output = parser.parse_args()
129
130 assert args_output.start_rev < args_output.end_rev, (
Jian Cai089db672020-08-23 20:09:02 -0700131 'Start revision %d is >= end revision %d' %
132 (args_output.start_rev, args_output.end_rev))
Salud Lemus055838a2019-08-19 13:37:28 -0700133
134 if args_output.last_tested and not args_output.last_tested.endswith('.json'):
Jian Cai089db672020-08-23 20:09:02 -0700135 raise ValueError('Filed provided %s does not end in ".json"' %
136 args_output.last_tested)
Salud Lemus055838a2019-08-19 13:37:28 -0700137
138 return args_output
139
140
Jian Cai089db672020-08-23 20:09:02 -0700141def GetRemainingRange(start, end, tryjobs):
Salud Lemus055838a2019-08-19 13:37:28 -0700142 """Gets the start and end intervals in 'json_file'.
143
144 Args:
145 start: The start version of the bisection provided via the command line.
146 end: The end version of the bisection provided via the command line.
147 tryjobs: A list of tryjobs where each element is in the following format:
148 [
149 {[TRYJOB_INFORMATION]},
150 {[TRYJOB_INFORMATION]},
151 ...,
152 {[TRYJOB_INFORMATION]}
153 ]
154
155 Returns:
Salud Lemusffed65d2019-08-26 14:52:02 -0700156 The new start version and end version for bisection, a set of revisions
157 that are 'pending' and a set of revisions that are to be skipped.
Salud Lemus055838a2019-08-19 13:37:28 -0700158
159 Raises:
160 ValueError: The value for 'status' is missing or there is a mismatch
161 between 'start' and 'end' compared to the 'start' and 'end' in the JSON
162 file.
163 AssertionError: The new start version is >= than the new end version.
164 """
165
166 if not tryjobs:
Salud Lemus02630632019-08-28 15:27:59 -0700167 return start, end, {}, {}
Salud Lemus055838a2019-08-19 13:37:28 -0700168
169 # Verify that each tryjob has a value for the 'status' key.
170 for cur_tryjob_dict in tryjobs:
171 if not cur_tryjob_dict.get('status', None):
Salud Lemus6270ccc2019-09-04 18:15:35 -0700172 raise ValueError('"status" is missing or has no value, please '
Salud Lemus055838a2019-08-19 13:37:28 -0700173 'go to %s and update it' % cur_tryjob_dict['link'])
174
175 all_bad_revisions = [end]
Jian Caic16daa12020-04-15 17:53:41 -0700176 all_bad_revisions.extend(
177 cur_tryjob['rev']
178 for cur_tryjob in tryjobs
179 if cur_tryjob['status'] == update_tryjob_status.TryjobStatus.BAD.value)
Salud Lemus055838a2019-08-19 13:37:28 -0700180
181 # The minimum value for the 'bad' field in the tryjobs is the new end
182 # version.
183 bad_rev = min(all_bad_revisions)
184
185 all_good_revisions = [start]
Jian Caic16daa12020-04-15 17:53:41 -0700186 all_good_revisions.extend(
187 cur_tryjob['rev']
188 for cur_tryjob in tryjobs
189 if cur_tryjob['status'] == update_tryjob_status.TryjobStatus.GOOD.value)
Salud Lemus055838a2019-08-19 13:37:28 -0700190
191 # The maximum value for the 'good' field in the tryjobs is the new start
192 # version.
193 good_rev = max(all_good_revisions)
194
195 # The good version should always be strictly less than the bad version;
196 # otherwise, bisection is broken.
197 assert good_rev < bad_rev, ('Bisection is broken because %d (good) is >= '
198 '%d (bad)' % (good_rev, bad_rev))
199
200 # Find all revisions that are 'pending' within 'good_rev' and 'bad_rev'.
201 #
202 # NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev'
Salud Lemusffed65d2019-08-26 14:52:02 -0700203 # that have already been launched (this set is used when constructing the
Salud Lemus055838a2019-08-19 13:37:28 -0700204 # list of revisions to launch tryjobs for).
Salud Lemusffed65d2019-08-26 14:52:02 -0700205 pending_revisions = {
Salud Lemus055838a2019-08-19 13:37:28 -0700206 tryjob['rev']
207 for tryjob in tryjobs
Jian Caic16daa12020-04-15 17:53:41 -0700208 if tryjob['status'] == update_tryjob_status.TryjobStatus.PENDING.value and
Salud Lemus055838a2019-08-19 13:37:28 -0700209 good_rev < tryjob['rev'] < bad_rev
Salud Lemusffed65d2019-08-26 14:52:02 -0700210 }
Salud Lemus055838a2019-08-19 13:37:28 -0700211
Salud Lemusffed65d2019-08-26 14:52:02 -0700212 # Find all revisions that are to be skipped within 'good_rev' and 'bad_rev'.
213 #
214 # NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev'
215 # that have already been marked as 'skip' (this set is used when constructing
216 # the list of revisions to launch tryjobs for).
217 skip_revisions = {
218 tryjob['rev']
219 for tryjob in tryjobs
Jian Caic16daa12020-04-15 17:53:41 -0700220 if tryjob['status'] == update_tryjob_status.TryjobStatus.SKIP.value and
Salud Lemusffed65d2019-08-26 14:52:02 -0700221 good_rev < tryjob['rev'] < bad_rev
222 }
223
224 return good_rev, bad_rev, pending_revisions, skip_revisions
Salud Lemus055838a2019-08-19 13:37:28 -0700225
226
Jian Cai089db672020-08-23 20:09:02 -0700227def GetCommitsBetween(start, end, parallel, src_path, pending_revisions,
228 skip_revisions):
Salud Lemus055838a2019-08-19 13:37:28 -0700229 """Determines the revisions between start and end."""
230
Jian Cai089db672020-08-23 20:09:02 -0700231 with get_llvm_hash.LLVMHash().CreateTempDirectory() as temp_dir:
232 # We have guaranteed contiguous revision numbers after this,
233 # and that guarnatee simplifies things considerably, so we don't
234 # support anything before it.
235 assert start >= git_llvm_rev.base_llvm_revision, f'{start} was too long ago'
Salud Lemus055838a2019-08-19 13:37:28 -0700236
Jian Caic16daa12020-04-15 17:53:41 -0700237 with get_llvm_hash.CreateTempLLVMRepo(temp_dir) as new_repo:
Salud Lemus42235702019-08-23 16:49:28 -0700238 if not src_path:
239 src_path = new_repo
Jian Cai089db672020-08-23 20:09:02 -0700240 index_step = (end - (start + 1)) // (parallel + 1)
241 if not index_step:
242 index_step = 1
243 revisions = [
244 rev for rev in range(start + 1, end, index_step)
245 if rev not in pending_revisions and rev not in skip_revisions
246 ]
Jian Caic16daa12020-04-15 17:53:41 -0700247 git_hashes = [
248 get_llvm_hash.GetGitHashFrom(src_path, rev) for rev in revisions
249 ]
Jian Cai089db672020-08-23 20:09:02 -0700250 return revisions, git_hashes
Salud Lemus055838a2019-08-19 13:37:28 -0700251
252
Jian Cai089db672020-08-23 20:09:02 -0700253def Bisect(revisions, git_hashes, bisect_state, last_tested, update_packages,
254 chroot_path, patch_metadata_file, extra_change_lists, options,
255 builder, verbose):
Salud Lemusc856b682019-08-29 11:46:49 -0700256 """Adds tryjobs and updates the status file with the new tryjobs."""
257
258 try:
259 for svn_revision, git_hash in zip(revisions, git_hashes):
Jian Cai089db672020-08-23 20:09:02 -0700260 tryjob_dict = modify_a_tryjob.AddTryjob(update_packages, git_hash,
261 svn_revision, chroot_path,
262 patch_metadata_file,
263 extra_change_lists, options,
264 builder, verbose, svn_revision)
Salud Lemusc856b682019-08-29 11:46:49 -0700265
Jian Cai089db672020-08-23 20:09:02 -0700266 bisect_state['jobs'].append(tryjob_dict)
Salud Lemusc856b682019-08-29 11:46:49 -0700267 finally:
268 # Do not want to lose progress if there is an exception.
269 if last_tested:
270 new_file = '%s.new' % last_tested
271 with open(new_file, 'w') as json_file:
Jian Cai089db672020-08-23 20:09:02 -0700272 json.dump(bisect_state, json_file, indent=4, separators=(',', ': '))
Salud Lemusc856b682019-08-29 11:46:49 -0700273
274 os.rename(new_file, last_tested)
275
276
Salud Lemus97c1e1c2019-09-10 16:28:47 -0700277def LoadStatusFile(last_tested, start, end):
278 """Loads the status file for bisection."""
279
280 try:
281 with open(last_tested) as f:
282 return json.load(f)
283 except IOError as err:
284 if err.errno != errno.ENOENT:
285 raise
286
287 return {'start': start, 'end': end, 'jobs': []}
288
289
Salud Lemusc856b682019-08-29 11:46:49 -0700290def main(args_output):
Jian Cai089db672020-08-23 20:09:02 -0700291 """Bisects LLVM commits.
Salud Lemus055838a2019-08-19 13:37:28 -0700292
293 Raises:
294 AssertionError: The script was run inside the chroot.
295 """
296
Jian Caic16daa12020-04-15 17:53:41 -0700297 chroot.VerifyOutsideChroot()
Salud Lemus055838a2019-08-19 13:37:28 -0700298 update_packages = [
299 'sys-devel/llvm', 'sys-libs/compiler-rt', 'sys-libs/libcxx',
300 'sys-libs/libcxxabi', 'sys-libs/llvm-libunwind'
301 ]
Salud Lemus055838a2019-08-19 13:37:28 -0700302 patch_metadata_file = 'PATCHES.json'
Salud Lemus055838a2019-08-19 13:37:28 -0700303 start = args_output.start_rev
304 end = args_output.end_rev
305
Jian Cai089db672020-08-23 20:09:02 -0700306 bisect_state = LoadStatusFile(args_output.last_tested, start, end)
307 if start != bisect_state['start'] or end != bisect_state['end']:
308 raise ValueError(f'The start {start} or the end {end} version provided is '
309 f'different than "start" {bisect_state["start"]} or "end" '
310 f'{bisect_state["end"]} in the .JSON file')
Salud Lemusc856b682019-08-29 11:46:49 -0700311
Salud Lemusffed65d2019-08-26 14:52:02 -0700312 # Pending and skipped revisions are between 'start_revision' and
313 # 'end_revision'.
314 start_revision, end_revision, pending_revisions, skip_revisions = \
Jian Cai089db672020-08-23 20:09:02 -0700315 GetRemainingRange(start, end, bisect_state['jobs'])
Salud Lemus055838a2019-08-19 13:37:28 -0700316
Jian Cai089db672020-08-23 20:09:02 -0700317 revisions, git_hashes = GetCommitsBetween(start_revision, end_revision,
318 args_output.parallel,
319 args_output.src_path,
320 pending_revisions, skip_revisions)
Salud Lemusffed65d2019-08-26 14:52:02 -0700321
Salud Lemusc856b682019-08-29 11:46:49 -0700322 # No more revisions between 'start_revision' and 'end_revision', so
323 # bisection is complete.
324 #
325 # This is determined by finding all valid revisions between 'start_revision'
326 # and 'end_revision' and that are NOT in the 'pending' and 'skipped' set.
Salud Lemusffed65d2019-08-26 14:52:02 -0700327 if not revisions:
Jian Cai089db672020-08-23 20:09:02 -0700328 if pending_revisions:
329 # Some tryjobs are not finished which may change the actual bad
330 # commit/revision when those tryjobs are finished.
331 no_revisions_message = (f'No revisions between start {start_revision} '
332 f'and end {end_revision} to create tryjobs\n')
333
334 if pending_revisions:
335 no_revisions_message += (
336 'The following tryjobs are pending:\n' +
337 '\n'.join(str(rev) for rev in pending_revisions) + '\n')
Salud Lemusffed65d2019-08-26 14:52:02 -0700338
Salud Lemusc856b682019-08-29 11:46:49 -0700339 if skip_revisions:
Jian Cai089db672020-08-23 20:09:02 -0700340 no_revisions_message += ('The following tryjobs were skipped:\n' +
341 '\n'.join(str(rev) for rev in skip_revisions) +
342 '\n')
Salud Lemusffed65d2019-08-26 14:52:02 -0700343
Jian Cai089db672020-08-23 20:09:02 -0700344 raise ValueError(no_revisions_message)
Salud Lemus055838a2019-08-19 13:37:28 -0700345
Jian Cai089db672020-08-23 20:09:02 -0700346 print(f'Finished bisecting for {args_output.last_tested}')
347 if args_output.src_path:
348 bad_llvm_hash = get_llvm_hash.GetGitHashFrom(args_output.src_path,
349 end_revision)
350 else:
351 bad_llvm_hash = get_llvm_hash.LLVMHash().GetLLVMHash(end_revision)
352 print(f'The bad revision is {end_revision} and its commit hash is '
353 f'{bad_llvm_hash}')
354 if skip_revisions:
355 skip_revisions_message = ('\nThe following revisions were skipped:\n' +
356 '\n'.join(str(rev) for rev in skip_revisions))
357 print(skip_revisions_message)
Salud Lemus055838a2019-08-19 13:37:28 -0700358
Jian Caib9a42992020-08-23 20:09:02 -0700359 if args_output.cleanup:
360 # Abondon all the CLs created for bisection
361 gerrit = os.path.join(args_output.chroot_path, 'chromite/bin/gerrit')
362 for build in bisect_state['jobs']:
363 try:
364 subprocess.check_output([gerrit, 'abandon', build['cl']],
365 stderr=subprocess.STDOUT,
366 encoding='utf-8')
367 except subprocess.CalledProcessError as err:
368 # the CL may have been abandoned
369 if 'chromite.lib.gob_util.GOBError' not in err.output:
370 raise
371
Jian Cai089db672020-08-23 20:09:02 -0700372 return BisectionExitStatus.BISECTION_COMPLETE.value
Salud Lemus055838a2019-08-19 13:37:28 -0700373
Jian Cai089db672020-08-23 20:09:02 -0700374 for rev in revisions:
375 if update_tryjob_status.FindTryjobIndex(rev,
376 bisect_state['jobs']) is not None:
377 raise ValueError(f'Revision {rev} exists already in "jobs"')
Salud Lemus055838a2019-08-19 13:37:28 -0700378
Jian Cai089db672020-08-23 20:09:02 -0700379 Bisect(revisions, git_hashes, bisect_state, args_output.last_tested,
380 update_packages, args_output.chroot_path, patch_metadata_file,
381 args_output.extra_change_lists, args_output.options,
382 args_output.builder, args_output.verbose)
Salud Lemus055838a2019-08-19 13:37:28 -0700383
384
385if __name__ == '__main__':
Jian Caic16daa12020-04-15 17:53:41 -0700386 sys.exit(main(GetCommandLineArgs()))