Make callers of CommandLine use it via the base:: namespace.
[chromium-blink-merge.git] / tools / auto_bisect / bisect_utils.py
blob5f80d4543c3e1ab4d5481f9953a28fd342bccdcf
1 # Copyright 2014 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """Utility functions used by the bisect tool.
7 This includes functions related to checking out the depot and outputting
8 annotations for the Buildbot waterfall.
9 """
11 import errno
12 import imp
13 import os
14 import stat
15 import subprocess
16 import sys
18 DEFAULT_GCLIENT_CUSTOM_DEPS = {
19 'src/data/page_cycler': 'https://chrome-internal.googlesource.com/'
20 'chrome/data/page_cycler/.git',
21 'src/data/dom_perf': 'https://chrome-internal.googlesource.com/'
22 'chrome/data/dom_perf/.git',
23 'src/data/mach_ports': 'https://chrome-internal.googlesource.com/'
24 'chrome/data/mach_ports/.git',
25 'src/tools/perf/data': 'https://chrome-internal.googlesource.com/'
26 'chrome/tools/perf/data/.git',
27 'src/third_party/adobe/flash/binaries/ppapi/linux':
28 'https://chrome-internal.googlesource.com/'
29 'chrome/deps/adobe/flash/binaries/ppapi/linux/.git',
30 'src/third_party/adobe/flash/binaries/ppapi/linux_x64':
31 'https://chrome-internal.googlesource.com/'
32 'chrome/deps/adobe/flash/binaries/ppapi/linux_x64/.git',
33 'src/third_party/adobe/flash/binaries/ppapi/mac':
34 'https://chrome-internal.googlesource.com/'
35 'chrome/deps/adobe/flash/binaries/ppapi/mac/.git',
36 'src/third_party/adobe/flash/binaries/ppapi/mac_64':
37 'https://chrome-internal.googlesource.com/'
38 'chrome/deps/adobe/flash/binaries/ppapi/mac_64/.git',
39 'src/third_party/adobe/flash/binaries/ppapi/win':
40 'https://chrome-internal.googlesource.com/'
41 'chrome/deps/adobe/flash/binaries/ppapi/win/.git',
42 'src/third_party/adobe/flash/binaries/ppapi/win_x64':
43 'https://chrome-internal.googlesource.com/'
44 'chrome/deps/adobe/flash/binaries/ppapi/win_x64/.git',
45 'src/chrome/tools/test/reference_build/chrome_win': None,
46 'src/chrome/tools/test/reference_build/chrome_mac': None,
47 'src/chrome/tools/test/reference_build/chrome_linux': None,
48 'src/third_party/WebKit/LayoutTests': None,
49 'src/tools/valgrind': None,
52 GCLIENT_SPEC_DATA = [
54 'name': 'src',
55 'url': 'https://chromium.googlesource.com/chromium/src.git',
56 'deps_file': '.DEPS.git',
57 'managed': True,
58 'custom_deps': {},
59 'safesync_url': '',
62 GCLIENT_SPEC_ANDROID = "\ntarget_os = ['android']"
63 GCLIENT_CUSTOM_DEPS_V8 = {
64 'src/v8_bleeding_edge': 'https://chromium.googlesource.com/v8/v8.git'
66 FILE_DEPS_GIT = '.DEPS.git'
67 FILE_DEPS = 'DEPS'
69 # Bisect working directory.
70 BISECT_DIR = 'bisect'
72 # The percentage at which confidence is considered high.
73 HIGH_CONFIDENCE = 95
75 # Below is the map of "depot" names to information about each depot. Each depot
76 # is a repository, and in the process of bisecting, revision ranges in these
77 # repositories may also be bisected.
79 # Each depot information dictionary may contain:
80 # src: Path to the working directory.
81 # recurse: True if this repository will get bisected.
82 # svn: URL of SVN repository. Needed for git workflow to resolve hashes to
83 # SVN revisions.
84 # from: Parent depot that must be bisected before this is bisected.
85 # deps_var: Key name in vars variable in DEPS file that has revision
86 # information.
87 DEPOT_DEPS_NAME = {
88 'chromium': {
89 'src': 'src',
90 'recurse': True,
91 'from': ['android-chrome'],
92 'viewvc': 'https://chromium.googlesource.com/chromium/src/+/',
93 'deps_var': 'chromium_rev'
95 'webkit': {
96 'src': 'src/third_party/WebKit',
97 'recurse': True,
98 'from': ['chromium'],
99 'viewvc': 'https://chromium.googlesource.com/chromium/blink/+/',
100 'deps_var': 'webkit_revision'
102 'angle': {
103 'src': 'src/third_party/angle',
104 'src_old': 'src/third_party/angle_dx11',
105 'recurse': True,
106 'from': ['chromium'],
107 'platform': 'nt',
108 'viewvc': 'https://chromium.googlesource.com/angle/angle/+/',
109 'deps_var': 'angle_revision'
111 'v8': {
112 'src': 'src/v8',
113 'recurse': True,
114 'from': ['chromium'],
115 'custom_deps': GCLIENT_CUSTOM_DEPS_V8,
116 'viewvc': 'https://chromium.googlesource.com/v8/v8.git/+/',
117 'deps_var': 'v8_revision'
119 'v8_bleeding_edge': {
120 'src': 'src/v8_bleeding_edge',
121 'recurse': True,
122 'svn': 'https://v8.googlecode.com/svn/branches/bleeding_edge',
123 'from': ['v8'],
124 'viewvc': 'https://chromium.googlesource.com/v8/v8.git/+/',
125 'deps_var': 'v8_revision'
127 'skia/src': {
128 'src': 'src/third_party/skia/src',
129 'recurse': True,
130 'from': ['chromium'],
131 'viewvc': 'https://chromium.googlesource.com/skia/+/',
132 'deps_var': 'skia_revision'
136 DEPOT_NAMES = DEPOT_DEPS_NAME.keys()
138 # The possible values of the --bisect_mode flag, which determines what to
139 # use when classifying a revision as "good" or "bad".
140 BISECT_MODE_MEAN = 'mean'
141 BISECT_MODE_STD_DEV = 'std_dev'
142 BISECT_MODE_RETURN_CODE = 'return_code'
145 def AddAdditionalDepotInfo(depot_info):
146 """Adds additional depot info to the global depot variables."""
147 global DEPOT_DEPS_NAME
148 global DEPOT_NAMES
149 DEPOT_DEPS_NAME = dict(DEPOT_DEPS_NAME.items() + depot_info.items())
150 DEPOT_NAMES = DEPOT_DEPS_NAME.keys()
153 def OutputAnnotationStepStart(name):
154 """Outputs annotation to signal the start of a step to a try bot.
156 Args:
157 name: The name of the step.
159 print
160 print '@@@SEED_STEP %s@@@' % name
161 print '@@@STEP_CURSOR %s@@@' % name
162 print '@@@STEP_STARTED@@@'
163 print
164 sys.stdout.flush()
167 def OutputAnnotationStepClosed():
168 """Outputs annotation to signal the closing of a step to a try bot."""
169 print
170 print '@@@STEP_CLOSED@@@'
171 print
172 sys.stdout.flush()
175 def OutputAnnotationStepLink(label, url):
176 """Outputs appropriate annotation to print a link.
178 Args:
179 label: The name to print.
180 url: The URL to print.
182 print
183 print '@@@STEP_LINK@%s@%s@@@' % (label, url)
184 print
185 sys.stdout.flush()
188 def LoadExtraSrc(path_to_file):
189 """Attempts to load an extra source file, and overrides global values.
191 If the extra source file is loaded successfully, then it will use the new
192 module to override some global values, such as gclient spec data.
194 Args:
195 path_to_file: File path.
197 Returns:
198 The loaded module object, or None if none was imported.
200 try:
201 global GCLIENT_SPEC_DATA
202 global GCLIENT_SPEC_ANDROID
203 extra_src = imp.load_source('data', path_to_file)
204 GCLIENT_SPEC_DATA = extra_src.GetGClientSpec()
205 GCLIENT_SPEC_ANDROID = extra_src.GetGClientSpecExtraParams()
206 return extra_src
207 except ImportError:
208 return None
211 def IsTelemetryCommand(command):
212 """Attempts to discern whether or not a given command is running telemetry."""
213 return 'tools/perf/run_' in command or 'tools\\perf\\run_' in command
216 def _CreateAndChangeToSourceDirectory(working_directory):
217 """Creates a directory 'bisect' as a subdirectory of |working_directory|.
219 If successful, the current working directory will be changed to the new
220 'bisect' directory.
222 Args:
223 working_directory: The directory to create the new 'bisect' directory in.
225 Returns:
226 True if the directory was successfully created (or already existed).
228 cwd = os.getcwd()
229 os.chdir(working_directory)
230 try:
231 os.mkdir(BISECT_DIR)
232 except OSError, e:
233 if e.errno != errno.EEXIST: # EEXIST indicates that it already exists.
234 os.chdir(cwd)
235 return False
236 os.chdir(BISECT_DIR)
237 return True
240 def _SubprocessCall(cmd, cwd=None):
241 """Runs a command in a subprocess.
243 Args:
244 cmd: The command to run.
245 cwd: Working directory to run from.
247 Returns:
248 The return code of the call.
250 if os.name == 'nt':
251 # "HOME" isn't normally defined on windows, but is needed
252 # for git to find the user's .netrc file.
253 if not os.getenv('HOME'):
254 os.environ['HOME'] = os.environ['USERPROFILE']
255 shell = os.name == 'nt'
256 return subprocess.call(cmd, shell=shell, cwd=cwd)
259 def RunGClient(params, cwd=None):
260 """Runs gclient with the specified parameters.
262 Args:
263 params: A list of parameters to pass to gclient.
264 cwd: Working directory to run from.
266 Returns:
267 The return code of the call.
269 cmd = ['gclient'] + params
270 return _SubprocessCall(cmd, cwd=cwd)
273 def RunGClientAndCreateConfig(opts, custom_deps=None, cwd=None):
274 """Runs gclient and creates a config containing both src and src-internal.
276 Args:
277 opts: The options parsed from the command line through parse_args().
278 custom_deps: A dictionary of additional dependencies to add to .gclient.
279 cwd: Working directory to run from.
281 Returns:
282 The return code of the call.
284 spec = GCLIENT_SPEC_DATA
286 if custom_deps:
287 for k, v in custom_deps.iteritems():
288 spec[0]['custom_deps'][k] = v
290 # Cannot have newlines in string on windows
291 spec = 'solutions =' + str(spec)
292 spec = ''.join([l for l in spec.splitlines()])
294 if 'android' in opts.target_platform:
295 spec += GCLIENT_SPEC_ANDROID
297 return_code = RunGClient(
298 ['config', '--spec=%s' % spec], cwd=cwd)
299 return return_code
303 def OnAccessError(func, path, _):
304 """Error handler for shutil.rmtree.
306 Source: http://goo.gl/DEYNCT
308 If the error is due to an access error (read only file), it attempts to add
309 write permissions, then retries.
311 If the error is for another reason it re-raises the error.
313 Args:
314 func: The function that raised the error.
315 path: The path name passed to func.
316 _: Exception information from sys.exc_info(). Not used.
318 if not os.access(path, os.W_OK):
319 os.chmod(path, stat.S_IWUSR)
320 func(path)
321 else:
322 raise
325 def _CleanupPreviousGitRuns(cwd=os.getcwd()):
326 """Cleans up any leftover index.lock files after running git."""
327 # If a previous run of git crashed, or bot was reset, etc., then we might
328 # end up with leftover index.lock files.
329 for path, _, files in os.walk(cwd):
330 for cur_file in files:
331 if cur_file.endswith('index.lock'):
332 path_to_file = os.path.join(path, cur_file)
333 os.remove(path_to_file)
336 def RunGClientAndSync(revision=None, cwd=None):
337 """Runs gclient and does a normal sync.
339 Args:
340 revision: Revision that need to be synced.
341 cwd: Working directory to run from.
343 Returns:
344 The return code of the call.
346 params = ['sync', '--verbose', '--nohooks', '--force',
347 '--delete_unversioned_trees']
348 if revision:
349 params.extend(['--revision', revision])
350 return RunGClient(params, cwd=cwd)
353 def SetupGitDepot(opts, custom_deps):
354 """Sets up the depot for the bisection.
356 The depot will be located in a subdirectory called 'bisect'.
358 Args:
359 opts: The options parsed from the command line through parse_args().
360 custom_deps: A dictionary of additional dependencies to add to .gclient.
362 Returns:
363 True if gclient successfully created the config file and did a sync, False
364 otherwise.
366 name = 'Setting up Bisection Depot'
367 try:
368 if opts.output_buildbot_annotations:
369 OutputAnnotationStepStart(name)
371 if RunGClientAndCreateConfig(opts, custom_deps):
372 return False
374 _CleanupPreviousGitRuns()
375 RunGClient(['revert'])
376 return not RunGClientAndSync()
377 finally:
378 if opts.output_buildbot_annotations:
379 OutputAnnotationStepClosed()
382 def CheckIfBisectDepotExists(opts):
383 """Checks if the bisect directory already exists.
385 Args:
386 opts: The options parsed from the command line through parse_args().
388 Returns:
389 Returns True if it exists.
391 path_to_dir = os.path.join(opts.working_directory, BISECT_DIR, 'src')
392 return os.path.exists(path_to_dir)
395 def CheckRunGit(command, cwd=None):
396 """Run a git subcommand, returning its output and return code. Asserts if
397 the return code of the call is non-zero.
399 Args:
400 command: A list containing the args to git.
402 Returns:
403 A tuple of the output and return code.
405 (output, return_code) = RunGit(command, cwd=cwd)
407 assert not return_code, 'An error occurred while running'\
408 ' "git %s"' % ' '.join(command)
409 return output
412 def RunGit(command, cwd=None):
413 """Run a git subcommand, returning its output and return code.
415 Args:
416 command: A list containing the args to git.
417 cwd: A directory to change to while running the git command (optional).
419 Returns:
420 A tuple of the output and return code.
422 command = ['git'] + command
423 return RunProcessAndRetrieveOutput(command, cwd=cwd)
426 def CreateBisectDirectoryAndSetupDepot(opts, custom_deps):
427 """Sets up a subdirectory 'bisect' and then retrieves a copy of the depot
428 there using gclient.
430 Args:
431 opts: The options parsed from the command line through parse_args().
432 custom_deps: A dictionary of additional dependencies to add to .gclient.
434 if CheckIfBisectDepotExists(opts):
435 path_to_dir = os.path.join(os.path.abspath(opts.working_directory),
436 BISECT_DIR, 'src')
437 (output, _) = RunGit(['rev-parse', '--is-inside-work-tree'],
438 cwd=path_to_dir)
439 if output.strip() == 'true':
440 # Before checking out master, cleanup up any leftover index.lock files.
441 _CleanupPreviousGitRuns(path_to_dir)
442 # Checks out the master branch, throws an exception if git command fails.
443 CheckRunGit(['checkout', '-f', 'master'], cwd=path_to_dir)
444 if not _CreateAndChangeToSourceDirectory(opts.working_directory):
445 raise RuntimeError('Could not create bisect directory.')
447 if not SetupGitDepot(opts, custom_deps):
448 raise RuntimeError('Failed to grab source.')
451 def RunProcess(command):
452 """Runs an arbitrary command.
454 If output from the call is needed, use RunProcessAndRetrieveOutput instead.
456 Args:
457 command: A list containing the command and args to execute.
459 Returns:
460 The return code of the call.
462 # On Windows, use shell=True to get PATH interpretation.
463 shell = IsWindowsHost()
464 return subprocess.call(command, shell=shell)
467 def RunProcessAndRetrieveOutput(command, cwd=None):
468 """Runs an arbitrary command, returning its output and return code.
470 Since output is collected via communicate(), there will be no output until
471 the call terminates. If you need output while the program runs (ie. so
472 that the buildbot doesn't terminate the script), consider RunProcess().
474 Args:
475 command: A list containing the command and args to execute.
476 cwd: A directory to change to while running the command. The command can be
477 relative to this directory. If this is None, the command will be run in
478 the current directory.
480 Returns:
481 A tuple of the output and return code.
483 if cwd:
484 original_cwd = os.getcwd()
485 os.chdir(cwd)
487 # On Windows, use shell=True to get PATH interpretation.
488 shell = IsWindowsHost()
489 proc = subprocess.Popen(command, shell=shell, stdout=subprocess.PIPE,
490 stderr=subprocess.STDOUT)
491 (output, _) = proc.communicate()
493 if cwd:
494 os.chdir(original_cwd)
496 return (output, proc.returncode)
499 def IsStringInt(string_to_check):
500 """Checks whether or not the given string can be converted to an int."""
501 try:
502 int(string_to_check)
503 return True
504 except ValueError:
505 return False
508 def IsStringFloat(string_to_check):
509 """Checks whether or not the given string can be converted to a float."""
510 try:
511 float(string_to_check)
512 return True
513 except ValueError:
514 return False
517 def IsWindowsHost():
518 return sys.platform == 'cygwin' or sys.platform.startswith('win')
521 def Is64BitWindows():
522 """Checks whether or not Windows is a 64-bit version."""
523 platform = os.environ.get('PROCESSOR_ARCHITEW6432')
524 if not platform:
525 # Must not be running in WoW64, so PROCESSOR_ARCHITECTURE is correct.
526 platform = os.environ.get('PROCESSOR_ARCHITECTURE')
527 return platform and platform in ['AMD64', 'I64']
530 def IsLinuxHost():
531 return sys.platform.startswith('linux')
534 def IsMacHost():
535 return sys.platform.startswith('darwin')