blob: da26a5402f5f22e859345845adeb750cb5734302 [file] [log] [blame]
Salud Lemus055838a2019-08-19 13:37:28 -07001#!/usr/bin/env python2
2# -*- 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
9from __future__ import division
10from __future__ import print_function
11
12import argparse
13import errno
14import json
15import os
16
17from assert_not_in_chroot import VerifyOutsideChroot
Salud Lemus42235702019-08-23 16:49:28 -070018from get_llvm_hash import CreateTempLLVMRepo
Salud Lemus055838a2019-08-19 13:37:28 -070019from get_llvm_hash import LLVMHash
20from modify_a_tryjob import AddTryjob
21from patch_manager import _ConvertToASCII
22from update_tryjob_status import FindTryjobIndex
23from update_tryjob_status import TryjobStatus
24
25
26def is_file_and_json(json_file):
27 """Validates that the file exists and is a JSON file."""
28 return os.path.isfile(json_file) and json_file.endswith('.json')
29
30
31def GetCommandLineArgs():
32 """Parses the command line for the command line arguments."""
33
34 # Default path to the chroot if a path is not specified.
35 cros_root = os.path.expanduser('~')
36 cros_root = os.path.join(cros_root, 'chromiumos')
37
38 # Create parser and add optional command-line arguments.
39 parser = argparse.ArgumentParser(
40 description='Bisects LLVM via tracking a JSON file.')
41
42 # Add argument for other change lists that want to run alongside the tryjob
43 # which has a change list of updating a package's git hash.
44 parser.add_argument(
45 '--parallel',
46 type=int,
47 default=3,
48 help='How many tryjobs to create between the last good version and '
49 'the first bad version (default: %(default)s)')
50
51 # Add argument for the good LLVM revision for bisection.
52 parser.add_argument(
53 '--start_rev',
54 required=True,
55 type=int,
56 help='The good revision for the bisection.')
57
58 # Add argument for the bad LLVM revision for bisection.
59 parser.add_argument(
60 '--end_rev',
61 required=True,
62 type=int,
63 help='The bad revision for the bisection.')
64
65 # Add argument for the absolute path to the file that contains information on
66 # the previous tested svn version.
67 parser.add_argument(
68 '--last_tested',
69 required=True,
70 help='the absolute path to the file that contains the tryjobs')
71
72 # Add argument for the absolute path to the LLVM source tree.
73 parser.add_argument(
74 '--src_path',
75 help='the path to the LLVM source tree to use (used for retrieving the '
76 'git hash of each version between the last good version and first bad '
77 'version)')
78
79 # Add argument for other change lists that want to run alongside the tryjob
80 # which has a change list of updating a package's git hash.
81 parser.add_argument(
82 '--extra_change_lists',
83 type=int,
84 nargs='+',
85 help='change lists that would like to be run alongside the change list '
86 'of updating the packages')
87
88 # Add argument for custom options for the tryjob.
89 parser.add_argument(
90 '--options',
91 required=False,
92 nargs='+',
93 help='options to use for the tryjob testing')
94
95 # Add argument for the builder to use for the tryjob.
96 parser.add_argument(
97 '--builder', required=True, help='builder to use for the tryjob testing')
98
99 # Add argument for the description of the tryjob.
100 parser.add_argument(
101 '--description',
102 required=False,
103 nargs='+',
104 help='the description of the tryjob')
105
106 # Add argument for a specific chroot path.
107 parser.add_argument(
108 '--chroot_path',
109 default=cros_root,
110 help='the path to the chroot (default: %(default)s)')
111
112 # Add argument for the log level.
113 parser.add_argument(
114 '--log_level',
115 default='none',
116 choices=['none', 'quiet', 'average', 'verbose'],
117 help='the level for the logs (default: %(default)s)')
118
119 args_output = parser.parse_args()
120
121 assert args_output.start_rev < args_output.end_rev, (
122 'Start revision %d is >= end revision %d' % (args_output.start_rev,
123 args_output.end_rev))
124
125 if args_output.last_tested and not args_output.last_tested.endswith('.json'):
126 raise ValueError(
127 'Filed provided %s does not end in \'.json\'' % args_output.last_tested)
128
129 return args_output
130
131
132def _ValidateStartAndEndAgainstJSONStartAndEnd(start, end, json_start,
133 json_end):
134 """Valides that the command line arguments are the same as the JSON."""
135
136 if start != json_start or end != json_end:
137 raise ValueError('The start %d or the end %d version provided is '
138 'different than \'start\' %d or \'end\' %d in the .JSON '
139 'file' % (start, end, json_start, json_end))
140
141
142def GetStartAndEndRevision(start, end, tryjobs):
143 """Gets the start and end intervals in 'json_file'.
144
145 Args:
146 start: The start version of the bisection provided via the command line.
147 end: The end version of the bisection provided via the command line.
148 tryjobs: A list of tryjobs where each element is in the following format:
149 [
150 {[TRYJOB_INFORMATION]},
151 {[TRYJOB_INFORMATION]},
152 ...,
153 {[TRYJOB_INFORMATION]}
154 ]
155
156 Returns:
Salud Lemusffed65d2019-08-26 14:52:02 -0700157 The new start version and end version for bisection, a set of revisions
158 that are 'pending' and a set of revisions that are to be skipped.
Salud Lemus055838a2019-08-19 13:37:28 -0700159
160 Raises:
161 ValueError: The value for 'status' is missing or there is a mismatch
162 between 'start' and 'end' compared to the 'start' and 'end' in the JSON
163 file.
164 AssertionError: The new start version is >= than the new end version.
165 """
166
167 if not tryjobs:
Salud Lemus02630632019-08-28 15:27:59 -0700168 return start, end, {}, {}
Salud Lemus055838a2019-08-19 13:37:28 -0700169
170 # Verify that each tryjob has a value for the 'status' key.
171 for cur_tryjob_dict in tryjobs:
172 if not cur_tryjob_dict.get('status', None):
173 raise ValueError('\'status\' is missing or has no value, please '
174 'go to %s and update it' % cur_tryjob_dict['link'])
175
176 all_bad_revisions = [end]
177 all_bad_revisions.extend(cur_tryjob['rev']
178 for cur_tryjob in tryjobs
179 if cur_tryjob['status'] == TryjobStatus.BAD.value)
180
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]
186 all_good_revisions.extend(cur_tryjob['rev']
187 for cur_tryjob in tryjobs
188 if cur_tryjob['status'] == TryjobStatus.GOOD.value)
189
190 # The maximum value for the 'good' field in the tryjobs is the new start
191 # version.
192 good_rev = max(all_good_revisions)
193
194 # The good version should always be strictly less than the bad version;
195 # otherwise, bisection is broken.
196 assert good_rev < bad_rev, ('Bisection is broken because %d (good) is >= '
197 '%d (bad)' % (good_rev, bad_rev))
198
199 # Find all revisions that are 'pending' within 'good_rev' and 'bad_rev'.
200 #
201 # NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev'
Salud Lemusffed65d2019-08-26 14:52:02 -0700202 # that have already been launched (this set is used when constructing the
Salud Lemus055838a2019-08-19 13:37:28 -0700203 # list of revisions to launch tryjobs for).
Salud Lemusffed65d2019-08-26 14:52:02 -0700204 pending_revisions = {
Salud Lemus055838a2019-08-19 13:37:28 -0700205 tryjob['rev']
206 for tryjob in tryjobs
207 if tryjob['status'] == TryjobStatus.PENDING.value and
208 good_rev < tryjob['rev'] < bad_rev
Salud Lemusffed65d2019-08-26 14:52:02 -0700209 }
Salud Lemus055838a2019-08-19 13:37:28 -0700210
Salud Lemusffed65d2019-08-26 14:52:02 -0700211 # Find all revisions that are to be skipped within 'good_rev' and 'bad_rev'.
212 #
213 # NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev'
214 # that have already been marked as 'skip' (this set is used when constructing
215 # the list of revisions to launch tryjobs for).
216 skip_revisions = {
217 tryjob['rev']
218 for tryjob in tryjobs
219 if tryjob['status'] == TryjobStatus.SKIP.value and
220 good_rev < tryjob['rev'] < bad_rev
221 }
222
223 return good_rev, bad_rev, pending_revisions, skip_revisions
Salud Lemus055838a2019-08-19 13:37:28 -0700224
225
226def GetRevisionsBetweenBisection(start, end, parallel, src_path,
Salud Lemusffed65d2019-08-26 14:52:02 -0700227 pending_revisions, skip_revisions):
Salud Lemus055838a2019-08-19 13:37:28 -0700228 """Gets the revisions between 'start' and 'end'.
229
230 Sometimes, the LLVM source tree's revisions do not increment by 1 (there is
231 a jump), so need to construct a list of all revisions that are NOT missing
232 between 'start' and 'end'. Then, the step amount (i.e. length of the list
233 divided by ('parallel' + 1)) will be used for indexing into the list.
234
235 Args:
236 start: The start revision.
237 end: The end revision.
238 parallel: The number of tryjobs to create between 'start' and 'end'.
239 src_path: The absolute path to the LLVM source tree to use.
Salud Lemusffed65d2019-08-26 14:52:02 -0700240 pending_revisions: A set containing 'pending' revisions that are between
241 'start' and 'end'.
242 skip_revisions: A set containing revisions between 'start' and 'end' that
243 are to be skipped.
Salud Lemus055838a2019-08-19 13:37:28 -0700244
245 Returns:
246 A list of revisions between 'start' and 'end'.
247 """
248
249 new_llvm = LLVMHash()
250
251 valid_revisions = []
252
253 # Start at ('start' + 1) because 'start' is the good revision.
254 #
255 # FIXME: Searching for each revision from ('start' + 1) up to 'end' in the
256 # LLVM source tree is a quadratic algorithm. It's a good idea to optimize
257 # this.
258 for cur_revision in range(start + 1, end):
259 try:
Salud Lemusffed65d2019-08-26 14:52:02 -0700260 if cur_revision not in pending_revisions and \
261 cur_revision not in skip_revisions:
Salud Lemus055838a2019-08-19 13:37:28 -0700262 # Verify that the current revision exists by finding its corresponding
263 # git hash in the LLVM source tree.
264 new_llvm.GetGitHashForVersion(src_path, cur_revision)
265 valid_revisions.append(cur_revision)
266 except ValueError:
267 # Could not find the git hash for the current revision.
268 continue
269
270 # ('parallel' + 1) so that the last revision in the list is not close to
271 # 'end' (have a bit more coverage).
272 index_step = len(valid_revisions) // (parallel + 1)
273
274 if not index_step:
275 index_step = 1
276
Salud Lemus055838a2019-08-19 13:37:28 -0700277 result = [valid_revisions[index] \
Salud Lemusffed65d2019-08-26 14:52:02 -0700278 for index in range(0, len(valid_revisions), index_step)]
Salud Lemus055838a2019-08-19 13:37:28 -0700279
280 return result
281
282
283def GetRevisionsListAndHashList(start, end, parallel, src_path,
Salud Lemusffed65d2019-08-26 14:52:02 -0700284 pending_revisions, skip_revisions):
Salud Lemus055838a2019-08-19 13:37:28 -0700285 """Determines the revisions between start and end."""
286
287 new_llvm = LLVMHash()
288
289 with new_llvm.CreateTempDirectory() as temp_dir:
Salud Lemus42235702019-08-23 16:49:28 -0700290 with CreateTempLLVMRepo(temp_dir) as new_repo:
291 if not src_path:
292 src_path = new_repo
Salud Lemus055838a2019-08-19 13:37:28 -0700293
Salud Lemus42235702019-08-23 16:49:28 -0700294 # Get a list of revisions between start and end.
Salud Lemusffed65d2019-08-26 14:52:02 -0700295 revisions = GetRevisionsBetweenBisection(
296 start, end, parallel, src_path, pending_revisions, skip_revisions)
Salud Lemus055838a2019-08-19 13:37:28 -0700297
Salud Lemus42235702019-08-23 16:49:28 -0700298 git_hashes = [
299 new_llvm.GetGitHashForVersion(src_path, rev) for rev in revisions
300 ]
Salud Lemus055838a2019-08-19 13:37:28 -0700301
Salud Lemus055838a2019-08-19 13:37:28 -0700302 return revisions, git_hashes
303
304
305def main():
306 """Bisects LLVM based off of a .JSON file.
307
308 Raises:
309 AssertionError: The script was run inside the chroot.
310 """
311
312 VerifyOutsideChroot()
313
314 args_output = GetCommandLineArgs()
315
316 update_packages = [
317 'sys-devel/llvm', 'sys-libs/compiler-rt', 'sys-libs/libcxx',
318 'sys-libs/libcxxabi', 'sys-libs/llvm-libunwind'
319 ]
320
321 patch_metadata_file = 'PATCHES.json'
322
323 start = args_output.start_rev
324 end = args_output.end_rev
325
326 try:
327 with open(args_output.last_tested) as f:
328 bisect_contents = _ConvertToASCII(json.load(f))
329 except IOError as err:
330 if err.errno != errno.ENOENT:
331 raise
332 bisect_contents = {'start': start, 'end': end, 'jobs': []}
333
334 _ValidateStartAndEndAgainstJSONStartAndEnd(
335 start, end, bisect_contents['start'], bisect_contents['end'])
Salud Lemus02630632019-08-28 15:27:59 -0700336 print(start, end)
337 print(bisect_contents['jobs'])
Salud Lemusffed65d2019-08-26 14:52:02 -0700338 # Pending and skipped revisions are between 'start_revision' and
339 # 'end_revision'.
340 start_revision, end_revision, pending_revisions, skip_revisions = \
341 GetStartAndEndRevision(start, end, bisect_contents['jobs'])
Salud Lemus055838a2019-08-19 13:37:28 -0700342
343 revisions, git_hashes = GetRevisionsListAndHashList(
344 start_revision, end_revision, args_output.parallel, args_output.src_path,
Salud Lemusffed65d2019-08-26 14:52:02 -0700345 pending_revisions, skip_revisions)
346
347 if not revisions:
348 no_revisions_message = (
349 'No revisions between start %d and end '
350 '%d to create tryjobs' % (start_revision, end_revision))
351
352 if skip_revisions:
353 no_revisions_message += '\nThe following tryjobs were skipped:\n' \
354 + '\n'.join(str(rev) for rev in skip_revisions)
355
356 raise ValueError(no_revisions_message)
Salud Lemus055838a2019-08-19 13:37:28 -0700357
358 # Check if any revisions that are going to be added as a tryjob exist already
359 # in the 'jobs' list.
360 for rev in revisions:
361 if FindTryjobIndex(rev, bisect_contents['jobs']) is not None:
362 raise ValueError('Revision %d exists already in \'jobs\'' % rev)
363
364 try:
365 for svn_revision, git_hash in zip(revisions, git_hashes):
366 tryjob_dict = AddTryjob(update_packages, git_hash, svn_revision,
367 args_output.chroot_path, patch_metadata_file,
368 args_output.extra_change_lists,
369 args_output.options, args_output.builder,
370 args_output.log_level, svn_revision)
371
372 bisect_contents['jobs'].append(tryjob_dict)
373 finally:
374 # Do not want to lose progress if there is an exception.
375 if args_output.last_tested:
376 new_file = '%s.new' % args_output.last_tested
377 with open(new_file, 'w') as json_file:
378 json.dump(bisect_contents, json_file, indent=4, separators=(',', ': '))
379
380 os.rename(new_file, args_output.last_tested)
381
382
383if __name__ == '__main__':
384 main()