blob: 0148efd2dd58e7a7ae07dd080c789724c441dfaf [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
Ryan Beltran52007282021-10-27 20:41:33 +000023import update_chromeos_llvm_hash
Jian Caic16daa12020-04-15 17:53:41 -070024import update_tryjob_status
Salud Lemus055838a2019-08-19 13:37:28 -070025
Ryan Beltrand0801982021-10-27 20:47:16 +000026
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 GetCommandLineArgs():
35 """Parses the command line for the command line arguments."""
36
37 # Default path to the chroot if a path is not specified.
38 cros_root = os.path.expanduser('~')
39 cros_root = os.path.join(cros_root, 'chromiumos')
40
41 # Create parser and add optional command-line arguments.
42 parser = argparse.ArgumentParser(
43 description='Bisects LLVM via tracking a JSON file.')
44
45 # Add argument for other change lists that want to run alongside the tryjob
46 # which has a change list of updating a package's git hash.
47 parser.add_argument(
48 '--parallel',
49 type=int,
50 default=3,
51 help='How many tryjobs to create between the last good version and '
52 'the first bad version (default: %(default)s)')
53
54 # Add argument for the good LLVM revision for bisection.
Ryan Beltrand0801982021-10-27 20:47:16 +000055 parser.add_argument('--start_rev',
56 required=True,
57 type=int,
58 help='The good revision for the bisection.')
Salud Lemus055838a2019-08-19 13:37:28 -070059
60 # Add argument for the bad LLVM revision for bisection.
Ryan Beltrand0801982021-10-27 20:47:16 +000061 parser.add_argument('--end_rev',
62 required=True,
63 type=int,
64 help='The bad revision for the bisection.')
Salud Lemus055838a2019-08-19 13:37:28 -070065
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.
Ryan Beltrand0801982021-10-27 20:47:16 +000090 parser.add_argument('--options',
91 required=False,
92 nargs='+',
93 help='options to use for the tryjob testing')
Salud Lemus055838a2019-08-19 13:37:28 -070094
95 # Add argument for the builder to use for the tryjob.
Ryan Beltrand0801982021-10-27 20:47:16 +000096 parser.add_argument('--builder',
97 required=True,
98 help='builder to use for the tryjob testing')
Salud Lemus055838a2019-08-19 13:37:28 -070099
100 # Add argument for the description of the tryjob.
Ryan Beltrand0801982021-10-27 20:47:16 +0000101 parser.add_argument('--description',
102 required=False,
103 nargs='+',
104 help='the description of the tryjob')
Salud Lemus055838a2019-08-19 13:37:28 -0700105
106 # Add argument for a specific chroot path.
Ryan Beltrand0801982021-10-27 20:47:16 +0000107 parser.add_argument('--chroot_path',
108 default=cros_root,
109 help='the path to the chroot (default: %(default)s)')
Salud Lemus055838a2019-08-19 13:37:28 -0700110
Salud Lemus6270ccc2019-09-04 18:15:35 -0700111 # Add argument for whether to display command contents to `stdout`.
Ryan Beltrand0801982021-10-27 20:47:16 +0000112 parser.add_argument('--verbose',
113 action='store_true',
114 help='display contents of a command to the terminal '
115 '(default: %(default)s)')
Salud Lemus055838a2019-08-19 13:37:28 -0700116
Jian Caib9a42992020-08-23 20:09:02 -0700117 # Add argument for whether to display command contents to `stdout`.
Ryan Beltrand0801982021-10-27 20:47:16 +0000118 parser.add_argument('--nocleanup',
119 action='store_false',
120 dest='cleanup',
121 help='Abandon CLs created for bisectoin')
Jian Caib9a42992020-08-23 20:09:02 -0700122
Salud Lemus055838a2019-08-19 13:37:28 -0700123 args_output = parser.parse_args()
124
125 assert args_output.start_rev < args_output.end_rev, (
Jian Cai089db672020-08-23 20:09:02 -0700126 'Start revision %d is >= end revision %d' %
127 (args_output.start_rev, args_output.end_rev))
Salud Lemus055838a2019-08-19 13:37:28 -0700128
129 if args_output.last_tested and not args_output.last_tested.endswith('.json'):
Jian Cai089db672020-08-23 20:09:02 -0700130 raise ValueError('Filed provided %s does not end in ".json"' %
131 args_output.last_tested)
Salud Lemus055838a2019-08-19 13:37:28 -0700132
133 return args_output
134
135
Jian Cai089db672020-08-23 20:09:02 -0700136def GetRemainingRange(start, end, tryjobs):
Salud Lemus055838a2019-08-19 13:37:28 -0700137 """Gets the start and end intervals in 'json_file'.
138
139 Args:
140 start: The start version of the bisection provided via the command line.
141 end: The end version of the bisection provided via the command line.
142 tryjobs: A list of tryjobs where each element is in the following format:
143 [
144 {[TRYJOB_INFORMATION]},
145 {[TRYJOB_INFORMATION]},
146 ...,
147 {[TRYJOB_INFORMATION]}
148 ]
149
150 Returns:
Salud Lemusffed65d2019-08-26 14:52:02 -0700151 The new start version and end version for bisection, a set of revisions
152 that are 'pending' and a set of revisions that are to be skipped.
Salud Lemus055838a2019-08-19 13:37:28 -0700153
154 Raises:
155 ValueError: The value for 'status' is missing or there is a mismatch
156 between 'start' and 'end' compared to the 'start' and 'end' in the JSON
157 file.
158 AssertionError: The new start version is >= than the new end version.
159 """
160
161 if not tryjobs:
Salud Lemus02630632019-08-28 15:27:59 -0700162 return start, end, {}, {}
Salud Lemus055838a2019-08-19 13:37:28 -0700163
164 # Verify that each tryjob has a value for the 'status' key.
165 for cur_tryjob_dict in tryjobs:
166 if not cur_tryjob_dict.get('status', None):
Salud Lemus6270ccc2019-09-04 18:15:35 -0700167 raise ValueError('"status" is missing or has no value, please '
Salud Lemus055838a2019-08-19 13:37:28 -0700168 'go to %s and update it' % cur_tryjob_dict['link'])
169
170 all_bad_revisions = [end]
Jian Caic16daa12020-04-15 17:53:41 -0700171 all_bad_revisions.extend(
Ryan Beltrand0801982021-10-27 20:47:16 +0000172 cur_tryjob['rev'] for cur_tryjob in tryjobs
Jian Caic16daa12020-04-15 17:53:41 -0700173 if cur_tryjob['status'] == update_tryjob_status.TryjobStatus.BAD.value)
Salud Lemus055838a2019-08-19 13:37:28 -0700174
175 # The minimum value for the 'bad' field in the tryjobs is the new end
176 # version.
177 bad_rev = min(all_bad_revisions)
178
179 all_good_revisions = [start]
Jian Caic16daa12020-04-15 17:53:41 -0700180 all_good_revisions.extend(
Ryan Beltrand0801982021-10-27 20:47:16 +0000181 cur_tryjob['rev'] for cur_tryjob in tryjobs
Jian Caic16daa12020-04-15 17:53:41 -0700182 if cur_tryjob['status'] == update_tryjob_status.TryjobStatus.GOOD.value)
Salud Lemus055838a2019-08-19 13:37:28 -0700183
184 # The maximum value for the 'good' field in the tryjobs is the new start
185 # version.
186 good_rev = max(all_good_revisions)
187
188 # The good version should always be strictly less than the bad version;
189 # otherwise, bisection is broken.
190 assert good_rev < bad_rev, ('Bisection is broken because %d (good) is >= '
191 '%d (bad)' % (good_rev, bad_rev))
192
193 # Find all revisions that are 'pending' within 'good_rev' and 'bad_rev'.
194 #
195 # NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev'
Salud Lemusffed65d2019-08-26 14:52:02 -0700196 # that have already been launched (this set is used when constructing the
Salud Lemus055838a2019-08-19 13:37:28 -0700197 # list of revisions to launch tryjobs for).
Salud Lemusffed65d2019-08-26 14:52:02 -0700198 pending_revisions = {
Salud Lemus055838a2019-08-19 13:37:28 -0700199 tryjob['rev']
200 for tryjob in tryjobs
Ryan Beltrand0801982021-10-27 20:47:16 +0000201 if tryjob['status'] == update_tryjob_status.TryjobStatus.PENDING.value
202 and good_rev < tryjob['rev'] < bad_rev
Salud Lemusffed65d2019-08-26 14:52:02 -0700203 }
Salud Lemus055838a2019-08-19 13:37:28 -0700204
Salud Lemusffed65d2019-08-26 14:52:02 -0700205 # Find all revisions that are to be skipped within 'good_rev' and 'bad_rev'.
206 #
207 # NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev'
208 # that have already been marked as 'skip' (this set is used when constructing
209 # the list of revisions to launch tryjobs for).
210 skip_revisions = {
211 tryjob['rev']
212 for tryjob in tryjobs
Ryan Beltrand0801982021-10-27 20:47:16 +0000213 if tryjob['status'] == update_tryjob_status.TryjobStatus.SKIP.value
214 and good_rev < tryjob['rev'] < bad_rev
Salud Lemusffed65d2019-08-26 14:52:02 -0700215 }
216
217 return good_rev, bad_rev, pending_revisions, skip_revisions
Salud Lemus055838a2019-08-19 13:37:28 -0700218
219
Jian Cai089db672020-08-23 20:09:02 -0700220def GetCommitsBetween(start, end, parallel, src_path, pending_revisions,
221 skip_revisions):
Salud Lemus055838a2019-08-19 13:37:28 -0700222 """Determines the revisions between start and end."""
223
Jian Cai089db672020-08-23 20:09:02 -0700224 with get_llvm_hash.LLVMHash().CreateTempDirectory() as temp_dir:
225 # We have guaranteed contiguous revision numbers after this,
226 # and that guarnatee simplifies things considerably, so we don't
227 # support anything before it.
228 assert start >= git_llvm_rev.base_llvm_revision, f'{start} was too long ago'
Salud Lemus055838a2019-08-19 13:37:28 -0700229
Jian Caic16daa12020-04-15 17:53:41 -0700230 with get_llvm_hash.CreateTempLLVMRepo(temp_dir) as new_repo:
Salud Lemus42235702019-08-23 16:49:28 -0700231 if not src_path:
232 src_path = new_repo
Jian Cai089db672020-08-23 20:09:02 -0700233 index_step = (end - (start + 1)) // (parallel + 1)
234 if not index_step:
235 index_step = 1
236 revisions = [
237 rev for rev in range(start + 1, end, index_step)
238 if rev not in pending_revisions and rev not in skip_revisions
239 ]
Jian Caic16daa12020-04-15 17:53:41 -0700240 git_hashes = [
241 get_llvm_hash.GetGitHashFrom(src_path, rev) for rev in revisions
242 ]
Jian Cai089db672020-08-23 20:09:02 -0700243 return revisions, git_hashes
Salud Lemus055838a2019-08-19 13:37:28 -0700244
245
Jian Cai089db672020-08-23 20:09:02 -0700246def Bisect(revisions, git_hashes, bisect_state, last_tested, update_packages,
247 chroot_path, patch_metadata_file, extra_change_lists, options,
248 builder, verbose):
Salud Lemusc856b682019-08-29 11:46:49 -0700249 """Adds tryjobs and updates the status file with the new tryjobs."""
250
251 try:
252 for svn_revision, git_hash in zip(revisions, git_hashes):
Jian Cai089db672020-08-23 20:09:02 -0700253 tryjob_dict = modify_a_tryjob.AddTryjob(update_packages, git_hash,
254 svn_revision, chroot_path,
255 patch_metadata_file,
256 extra_change_lists, options,
257 builder, verbose, svn_revision)
Salud Lemusc856b682019-08-29 11:46:49 -0700258
Jian Cai089db672020-08-23 20:09:02 -0700259 bisect_state['jobs'].append(tryjob_dict)
Salud Lemusc856b682019-08-29 11:46:49 -0700260 finally:
261 # Do not want to lose progress if there is an exception.
262 if last_tested:
263 new_file = '%s.new' % last_tested
264 with open(new_file, 'w') as json_file:
Jian Cai089db672020-08-23 20:09:02 -0700265 json.dump(bisect_state, json_file, indent=4, separators=(',', ': '))
Salud Lemusc856b682019-08-29 11:46:49 -0700266
267 os.rename(new_file, last_tested)
268
269
Salud Lemus97c1e1c2019-09-10 16:28:47 -0700270def LoadStatusFile(last_tested, start, end):
271 """Loads the status file for bisection."""
272
273 try:
274 with open(last_tested) as f:
275 return json.load(f)
276 except IOError as err:
277 if err.errno != errno.ENOENT:
278 raise
279
280 return {'start': start, 'end': end, 'jobs': []}
281
282
Salud Lemusc856b682019-08-29 11:46:49 -0700283def main(args_output):
Jian Cai089db672020-08-23 20:09:02 -0700284 """Bisects LLVM commits.
Salud Lemus055838a2019-08-19 13:37:28 -0700285
286 Raises:
287 AssertionError: The script was run inside the chroot.
288 """
289
Jian Caic16daa12020-04-15 17:53:41 -0700290 chroot.VerifyOutsideChroot()
Salud Lemus055838a2019-08-19 13:37:28 -0700291 patch_metadata_file = 'PATCHES.json'
Salud Lemus055838a2019-08-19 13:37:28 -0700292 start = args_output.start_rev
293 end = args_output.end_rev
294
Jian Cai089db672020-08-23 20:09:02 -0700295 bisect_state = LoadStatusFile(args_output.last_tested, start, end)
296 if start != bisect_state['start'] or end != bisect_state['end']:
Ryan Beltrand0801982021-10-27 20:47:16 +0000297 raise ValueError(
298 f'The start {start} or the end {end} version provided is '
299 f'different than "start" {bisect_state["start"]} or "end" '
300 f'{bisect_state["end"]} in the .JSON file')
Salud Lemusc856b682019-08-29 11:46:49 -0700301
Ryan Beltrand0801982021-10-27 20:47:16 +0000302 # Pending and skipped revisions are between 'start_rev' and 'end_rev'.
303 start_rev, end_rev, pending_revs, skip_revs = GetRemainingRange(
304 start, end, bisect_state['jobs'])
Salud Lemus055838a2019-08-19 13:37:28 -0700305
Ryan Beltrand0801982021-10-27 20:47:16 +0000306 revisions, git_hashes = GetCommitsBetween(start_rev, end_rev,
Jian Cai089db672020-08-23 20:09:02 -0700307 args_output.parallel,
Ryan Beltrand0801982021-10-27 20:47:16 +0000308 args_output.src_path, pending_revs,
309 skip_revs)
Salud Lemusffed65d2019-08-26 14:52:02 -0700310
Ryan Beltrand0801982021-10-27 20:47:16 +0000311 # No more revisions between 'start_rev' and 'end_rev', so
Salud Lemusc856b682019-08-29 11:46:49 -0700312 # bisection is complete.
313 #
Ryan Beltrand0801982021-10-27 20:47:16 +0000314 # This is determined by finding all valid revisions between 'start_rev'
315 # and 'end_rev' and that are NOT in the 'pending' and 'skipped' set.
Salud Lemusffed65d2019-08-26 14:52:02 -0700316 if not revisions:
Ryan Beltrand0801982021-10-27 20:47:16 +0000317 if pending_revs:
Jian Cai089db672020-08-23 20:09:02 -0700318 # Some tryjobs are not finished which may change the actual bad
319 # commit/revision when those tryjobs are finished.
Ryan Beltrand0801982021-10-27 20:47:16 +0000320 no_revisions_message = (f'No revisions between start {start_rev} '
321 f'and end {end_rev} to create tryjobs\n')
Jian Cai089db672020-08-23 20:09:02 -0700322
Ryan Beltrand0801982021-10-27 20:47:16 +0000323 if pending_revs:
324 no_revisions_message += ('The following tryjobs are pending:\n' +
325 '\n'.join(str(rev)
326 for rev in pending_revs) + '\n')
Salud Lemusffed65d2019-08-26 14:52:02 -0700327
Ryan Beltrand0801982021-10-27 20:47:16 +0000328 if skip_revs:
Jian Cai089db672020-08-23 20:09:02 -0700329 no_revisions_message += ('The following tryjobs were skipped:\n' +
Ryan Beltrand0801982021-10-27 20:47:16 +0000330 '\n'.join(str(rev)
331 for rev in skip_revs) + '\n')
Salud Lemusffed65d2019-08-26 14:52:02 -0700332
Jian Cai089db672020-08-23 20:09:02 -0700333 raise ValueError(no_revisions_message)
Salud Lemus055838a2019-08-19 13:37:28 -0700334
Jian Cai089db672020-08-23 20:09:02 -0700335 print(f'Finished bisecting for {args_output.last_tested}')
336 if args_output.src_path:
337 bad_llvm_hash = get_llvm_hash.GetGitHashFrom(args_output.src_path,
Ryan Beltrand0801982021-10-27 20:47:16 +0000338 end_rev)
Jian Cai089db672020-08-23 20:09:02 -0700339 else:
Ryan Beltrand0801982021-10-27 20:47:16 +0000340 bad_llvm_hash = get_llvm_hash.LLVMHash().GetLLVMHash(end_rev)
341 print(f'The bad revision is {end_rev} and its commit hash is '
Jian Cai089db672020-08-23 20:09:02 -0700342 f'{bad_llvm_hash}')
Ryan Beltrand0801982021-10-27 20:47:16 +0000343 if skip_revs:
344 skip_revs_message = ('\nThe following revisions were skipped:\n' +
345 '\n'.join(str(rev) for rev in skip_revs))
346 print(skip_revs_message)
Salud Lemus055838a2019-08-19 13:37:28 -0700347
Jian Caib9a42992020-08-23 20:09:02 -0700348 if args_output.cleanup:
Bob Haarman2c27f0a2020-11-20 15:30:19 +0000349 # Abandon all the CLs created for bisection
Jian Caib9a42992020-08-23 20:09:02 -0700350 gerrit = os.path.join(args_output.chroot_path, 'chromite/bin/gerrit')
351 for build in bisect_state['jobs']:
352 try:
Bob Haarman2c27f0a2020-11-20 15:30:19 +0000353 subprocess.check_output(
354 [gerrit, 'abandon', str(build['cl'])],
355 stderr=subprocess.STDOUT,
356 encoding='utf-8')
Jian Caib9a42992020-08-23 20:09:02 -0700357 except subprocess.CalledProcessError as err:
358 # the CL may have been abandoned
359 if 'chromite.lib.gob_util.GOBError' not in err.output:
360 raise
361
Jian Cai089db672020-08-23 20:09:02 -0700362 return BisectionExitStatus.BISECTION_COMPLETE.value
Salud Lemus055838a2019-08-19 13:37:28 -0700363
Jian Cai089db672020-08-23 20:09:02 -0700364 for rev in revisions:
365 if update_tryjob_status.FindTryjobIndex(rev,
366 bisect_state['jobs']) is not None:
367 raise ValueError(f'Revision {rev} exists already in "jobs"')
Salud Lemus055838a2019-08-19 13:37:28 -0700368
Jian Cai089db672020-08-23 20:09:02 -0700369 Bisect(revisions, git_hashes, bisect_state, args_output.last_tested,
Ryan Beltrand0801982021-10-27 20:47:16 +0000370 update_chromeos_llvm_hash.DEFAULT_PACKAGES, args_output.chroot_path,
371 patch_metadata_file, args_output.extra_change_lists,
372 args_output.options, args_output.builder, args_output.verbose)
Salud Lemus055838a2019-08-19 13:37:28 -0700373
374
375if __name__ == '__main__':
Jian Caic16daa12020-04-15 17:53:41 -0700376 sys.exit(main(GetCommandLineArgs()))