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.
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,
55 'url': 'https://chromium.googlesource.com/chromium/src.git',
56 'deps_file': '.DEPS.git',
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'
69 # Bisect working directory.
72 # The percentage at which confidence is considered high.
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 # depends: A list of other repositories that are actually part of the same
83 # repository in svn. If the repository has any dependent repositories
84 # (e.g. skia/src needs skia/include and skia/gyp to be updated), then
85 # they are specified here.
86 # svn: URL of SVN repository. Needed for git workflow to resolve hashes to
88 # from: Parent depot that must be bisected before this is bisected.
89 # deps_var: Key name in vars variable in DEPS file that has revision
96 'from': ['android-chrome'],
98 'http://src.chromium.org/viewvc/chrome?view=revision&revision=',
99 'deps_var': 'chromium_rev'
102 'src': 'src/third_party/WebKit',
105 'from': ['chromium'],
107 'http://src.chromium.org/viewvc/blink?view=revision&revision=',
108 'deps_var': 'webkit_revision'
111 'src': 'src/third_party/angle',
112 'src_old': 'src/third_party/angle_dx11',
115 'from': ['chromium'],
117 'deps_var': 'angle_revision'
123 'from': ['chromium'],
124 'custom_deps': GCLIENT_CUSTOM_DEPS_V8
,
125 'viewvc': 'https://code.google.com/p/v8/source/detail?r=',
126 'deps_var': 'v8_revision'
128 'v8_bleeding_edge': {
129 'src': 'src/v8_bleeding_edge',
132 'svn': 'https://v8.googlecode.com/svn/branches/bleeding_edge',
134 'viewvc': 'https://code.google.com/p/v8/source/detail?r=',
135 'deps_var': 'v8_revision'
138 'src': 'src/third_party/skia/src',
140 'svn': 'http://skia.googlecode.com/svn/trunk/src',
141 'depends': ['skia/include', 'skia/gyp'],
142 'from': ['chromium'],
143 'viewvc': 'https://code.google.com/p/skia/source/detail?r=',
144 'deps_var': 'skia_revision'
147 'src': 'src/third_party/skia/include',
149 'svn': 'http://skia.googlecode.com/svn/trunk/include',
151 'from': ['chromium'],
152 'viewvc': 'https://code.google.com/p/skia/source/detail?r=',
156 'src': 'src/third_party/skia/gyp',
158 'svn': 'http://skia.googlecode.com/svn/trunk/gyp',
160 'from': ['chromium'],
161 'viewvc': 'https://code.google.com/p/skia/source/detail?r=',
166 DEPOT_NAMES
= DEPOT_DEPS_NAME
.keys()
168 # The possible values of the --bisect_mode flag, which determines what to
169 # use when classifying a revision as "good" or "bad".
170 BISECT_MODE_MEAN
= 'mean'
171 BISECT_MODE_STD_DEV
= 'std_dev'
172 BISECT_MODE_RETURN_CODE
= 'return_code'
175 def AddAdditionalDepotInfo(depot_info
):
176 """Adds additional depot info to the global depot variables."""
177 global DEPOT_DEPS_NAME
179 DEPOT_DEPS_NAME
= dict(DEPOT_DEPS_NAME
.items() + depot_info
.items())
180 DEPOT_NAMES
= DEPOT_DEPS_NAME
.keys()
183 def OutputAnnotationStepStart(name
):
184 """Outputs annotation to signal the start of a step to a try bot.
187 name: The name of the step.
190 print '@@@SEED_STEP %s@@@' % name
191 print '@@@STEP_CURSOR %s@@@' % name
192 print '@@@STEP_STARTED@@@'
197 def OutputAnnotationStepClosed():
198 """Outputs annotation to signal the closing of a step to a try bot."""
200 print '@@@STEP_CLOSED@@@'
205 def OutputAnnotationStepLink(label
, url
):
206 """Outputs appropriate annotation to print a link.
209 label: The name to print.
210 url: The URL to print.
213 print '@@@STEP_LINK@%s@%s@@@' % (label
, url
)
218 def LoadExtraSrc(path_to_file
):
219 """Attempts to load an extra source file, and overrides global values.
221 If the extra source file is loaded successfully, then it will use the new
222 module to override some global values, such as gclient spec data.
225 path_to_file: File path.
228 The loaded module object, or None if none was imported.
231 global GCLIENT_SPEC_DATA
232 global GCLIENT_SPEC_ANDROID
233 extra_src
= imp
.load_source('data', path_to_file
)
234 GCLIENT_SPEC_DATA
= extra_src
.GetGClientSpec()
235 GCLIENT_SPEC_ANDROID
= extra_src
.GetGClientSpecExtraParams()
241 def IsTelemetryCommand(command
):
242 """Attempts to discern whether or not a given command is running telemetry."""
243 return 'tools/perf/run_' in command
or 'tools\\perf\\run_' in command
246 def _CreateAndChangeToSourceDirectory(working_directory
):
247 """Creates a directory 'bisect' as a subdirectory of |working_directory|.
249 If successful, the current working directory will be changed to the new
253 working_directory: The directory to create the new 'bisect' directory in.
256 True if the directory was successfully created (or already existed).
259 os
.chdir(working_directory
)
263 if e
.errno
!= errno
.EEXIST
: # EEXIST indicates that it already exists.
270 def _SubprocessCall(cmd
, cwd
=None):
271 """Runs a command in a subprocess.
274 cmd: The command to run.
275 cwd: Working directory to run from.
278 The return code of the call.
281 # "HOME" isn't normally defined on windows, but is needed
282 # for git to find the user's .netrc file.
283 if not os
.getenv('HOME'):
284 os
.environ
['HOME'] = os
.environ
['USERPROFILE']
285 shell
= os
.name
== 'nt'
286 return subprocess
.call(cmd
, shell
=shell
, cwd
=cwd
)
289 def RunGClient(params
, cwd
=None):
290 """Runs gclient with the specified parameters.
293 params: A list of parameters to pass to gclient.
294 cwd: Working directory to run from.
297 The return code of the call.
299 cmd
= ['gclient'] + params
300 return _SubprocessCall(cmd
, cwd
=cwd
)
303 def RunGClientAndCreateConfig(opts
, custom_deps
=None, cwd
=None):
304 """Runs gclient and creates a config containing both src and src-internal.
307 opts: The options parsed from the command line through parse_args().
308 custom_deps: A dictionary of additional dependencies to add to .gclient.
309 cwd: Working directory to run from.
312 The return code of the call.
314 spec
= GCLIENT_SPEC_DATA
317 for k
, v
in custom_deps
.iteritems():
318 spec
[0]['custom_deps'][k
] = v
320 # Cannot have newlines in string on windows
321 spec
= 'solutions =' + str(spec
)
322 spec
= ''.join([l
for l
in spec
.splitlines()])
324 if 'android' in opts
.target_platform
:
325 spec
+= GCLIENT_SPEC_ANDROID
327 return_code
= RunGClient(
328 ['config', '--spec=%s' % spec
], cwd
=cwd
)
333 def OnAccessError(func
, path
, _
):
334 """Error handler for shutil.rmtree.
336 Source: http://goo.gl/DEYNCT
338 If the error is due to an access error (read only file), it attempts to add
339 write permissions, then retries.
341 If the error is for another reason it re-raises the error.
344 func: The function that raised the error.
345 path: The path name passed to func.
346 _: Exception information from sys.exc_info(). Not used.
348 if not os
.access(path
, os
.W_OK
):
349 os
.chmod(path
, stat
.S_IWUSR
)
355 def _CleanupPreviousGitRuns(cwd
=os
.getcwd()):
356 """Cleans up any leftover index.lock files after running git."""
357 # If a previous run of git crashed, or bot was reset, etc., then we might
358 # end up with leftover index.lock files.
359 for path
, _
, files
in os
.walk(cwd
):
360 for cur_file
in files
:
361 if cur_file
.endswith('index.lock'):
362 path_to_file
= os
.path
.join(path
, cur_file
)
363 os
.remove(path_to_file
)
366 def RunGClientAndSync(cwd
=None):
367 """Runs gclient and does a normal sync.
370 cwd: Working directory to run from.
373 The return code of the call.
375 params
= ['sync', '--verbose', '--nohooks', '--reset', '--force',
376 '--delete_unversioned_trees']
377 return RunGClient(params
, cwd
=cwd
)
380 def SetupGitDepot(opts
, custom_deps
):
381 """Sets up the depot for the bisection.
383 The depot will be located in a subdirectory called 'bisect'.
386 opts: The options parsed from the command line through parse_args().
387 custom_deps: A dictionary of additional dependencies to add to .gclient.
390 True if gclient successfully created the config file and did a sync, False
393 name
= 'Setting up Bisection Depot'
395 if opts
.output_buildbot_annotations
:
396 OutputAnnotationStepStart(name
)
398 if RunGClientAndCreateConfig(opts
, custom_deps
):
401 _CleanupPreviousGitRuns()
402 RunGClient(['revert'])
403 return not RunGClientAndSync()
405 if opts
.output_buildbot_annotations
:
406 OutputAnnotationStepClosed()
409 def CheckIfBisectDepotExists(opts
):
410 """Checks if the bisect directory already exists.
413 opts: The options parsed from the command line through parse_args().
416 Returns True if it exists.
418 path_to_dir
= os
.path
.join(opts
.working_directory
, BISECT_DIR
, 'src')
419 return os
.path
.exists(path_to_dir
)
422 def CheckRunGit(command
, cwd
=None):
423 """Run a git subcommand, returning its output and return code. Asserts if
424 the return code of the call is non-zero.
427 command: A list containing the args to git.
430 A tuple of the output and return code.
432 (output
, return_code
) = RunGit(command
, cwd
=cwd
)
434 assert not return_code
, 'An error occurred while running'\
435 ' "git %s"' % ' '.join(command
)
439 def RunGit(command
, cwd
=None):
440 """Run a git subcommand, returning its output and return code.
443 command: A list containing the args to git.
444 cwd: A directory to change to while running the git command (optional).
447 A tuple of the output and return code.
449 command
= ['git'] + command
450 return RunProcessAndRetrieveOutput(command
, cwd
=cwd
)
453 def CreateBisectDirectoryAndSetupDepot(opts
, custom_deps
):
454 """Sets up a subdirectory 'bisect' and then retrieves a copy of the depot
458 opts: The options parsed from the command line through parse_args().
459 custom_deps: A dictionary of additional dependencies to add to .gclient.
461 if CheckIfBisectDepotExists(opts
):
462 path_to_dir
= os
.path
.join(os
.path
.abspath(opts
.working_directory
),
464 (output
, _
) = RunGit(['rev-parse', '--is-inside-work-tree'],
466 if output
.strip() == 'true':
467 # Before checking out master, cleanup up any leftover index.lock files.
468 _CleanupPreviousGitRuns(path_to_dir
)
469 # Checks out the master branch, throws an exception if git command fails.
470 CheckRunGit(['checkout', '-f', 'master'], cwd
=path_to_dir
)
471 if not _CreateAndChangeToSourceDirectory(opts
.working_directory
):
472 raise RuntimeError('Could not create bisect directory.')
474 if not SetupGitDepot(opts
, custom_deps
):
475 raise RuntimeError('Failed to grab source.')
478 def RunProcess(command
):
479 """Runs an arbitrary command.
481 If output from the call is needed, use RunProcessAndRetrieveOutput instead.
484 command: A list containing the command and args to execute.
487 The return code of the call.
489 # On Windows, use shell=True to get PATH interpretation.
490 shell
= IsWindowsHost()
491 return subprocess
.call(command
, shell
=shell
)
494 def RunProcessAndRetrieveOutput(command
, cwd
=None):
495 """Runs an arbitrary command, returning its output and return code.
497 Since output is collected via communicate(), there will be no output until
498 the call terminates. If you need output while the program runs (ie. so
499 that the buildbot doesn't terminate the script), consider RunProcess().
502 command: A list containing the command and args to execute.
503 cwd: A directory to change to while running the command. The command can be
504 relative to this directory. If this is None, the command will be run in
505 the current directory.
508 A tuple of the output and return code.
511 original_cwd
= os
.getcwd()
514 # On Windows, use shell=True to get PATH interpretation.
515 shell
= IsWindowsHost()
516 proc
= subprocess
.Popen(command
, shell
=shell
, stdout
=subprocess
.PIPE
)
517 (output
, _
) = proc
.communicate()
520 os
.chdir(original_cwd
)
522 return (output
, proc
.returncode
)
525 def IsStringInt(string_to_check
):
526 """Checks whether or not the given string can be converted to an int."""
534 def IsStringFloat(string_to_check
):
535 """Checks whether or not the given string can be converted to a float."""
537 float(string_to_check
)
544 return sys
.platform
== 'cygwin' or sys
.platform
.startswith('win')
547 def Is64BitWindows():
548 """Checks whether or not Windows is a 64-bit version."""
549 platform
= os
.environ
.get('PROCESSOR_ARCHITEW6432')
551 # Must not be running in WoW64, so PROCESSOR_ARCHITECTURE is correct.
552 platform
= os
.environ
.get('PROCESSOR_ARCHITECTURE')
553 return platform
and platform
in ['AMD64', 'I64']
557 return sys
.platform
.startswith('linux')
561 return sys
.platform
.startswith('darwin')