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.
19 DEFAULT_GCLIENT_CUSTOM_DEPS
= {
20 'src/data/page_cycler': 'https://chrome-internal.googlesource.com/'
21 'chrome/data/page_cycler/.git',
22 'src/data/dom_perf': 'https://chrome-internal.googlesource.com/'
23 'chrome/data/dom_perf/.git',
24 'src/data/mach_ports': 'https://chrome-internal.googlesource.com/'
25 'chrome/data/mach_ports/.git',
26 'src/tools/perf/data': 'https://chrome-internal.googlesource.com/'
27 'chrome/tools/perf/data/.git',
28 'src/third_party/adobe/flash/binaries/ppapi/linux':
29 'https://chrome-internal.googlesource.com/'
30 'chrome/deps/adobe/flash/binaries/ppapi/linux/.git',
31 'src/third_party/adobe/flash/binaries/ppapi/linux_x64':
32 'https://chrome-internal.googlesource.com/'
33 'chrome/deps/adobe/flash/binaries/ppapi/linux_x64/.git',
34 'src/third_party/adobe/flash/binaries/ppapi/mac':
35 'https://chrome-internal.googlesource.com/'
36 'chrome/deps/adobe/flash/binaries/ppapi/mac/.git',
37 'src/third_party/adobe/flash/binaries/ppapi/mac_64':
38 'https://chrome-internal.googlesource.com/'
39 'chrome/deps/adobe/flash/binaries/ppapi/mac_64/.git',
40 'src/third_party/adobe/flash/binaries/ppapi/win':
41 'https://chrome-internal.googlesource.com/'
42 'chrome/deps/adobe/flash/binaries/ppapi/win/.git',
43 'src/third_party/adobe/flash/binaries/ppapi/win_x64':
44 'https://chrome-internal.googlesource.com/'
45 'chrome/deps/adobe/flash/binaries/ppapi/win_x64/.git',
46 'src/chrome/tools/test/reference_build/chrome_win': None,
47 'src/chrome/tools/test/reference_build/chrome_mac': None,
48 'src/chrome/tools/test/reference_build/chrome_linux': None,
49 'src/third_party/WebKit/LayoutTests': None,
50 'src/tools/valgrind': None,
56 'url': 'https://chromium.googlesource.com/chromium/src.git',
57 'deps_file': '.DEPS.git',
63 GCLIENT_SPEC_ANDROID
= "\ntarget_os = ['android']"
64 GCLIENT_CUSTOM_DEPS_V8
= {'src/v8_bleeding_edge': 'git://github.com/v8/v8.git'}
65 FILE_DEPS_GIT
= '.DEPS.git'
68 REPO_SYNC_COMMAND
= ('git checkout -f $(git rev-list --max-count=1 '
69 '--before=%d remotes/m/master)')
71 # Paths to CrOS-related files.
72 # WARNING(qyearsley, 2014-08-15): These haven't been tested recently.
73 CROS_SDK_PATH
= os
.path
.join('..', 'cros', 'chromite', 'bin', 'cros_sdk')
74 CROS_TEST_KEY_PATH
= os
.path
.join(
75 '..', 'cros', 'chromite', 'ssh_keys', 'testing_rsa')
76 CROS_SCRIPT_KEY_PATH
= os
.path
.join(
77 '..', 'cros', 'src', 'scripts', 'mod_for_test_scripts', 'ssh_keys',
81 'https://chrome-internal.googlesource.com/chromeos/manifest-internal/',
83 'https://git.chromium.org/external/repo.git'
87 def OutputAnnotationStepStart(name
):
88 """Outputs annotation to signal the start of a step to a try bot.
91 name: The name of the step.
94 print '@@@SEED_STEP %s@@@' % name
95 print '@@@STEP_CURSOR %s@@@' % name
96 print '@@@STEP_STARTED@@@'
101 def OutputAnnotationStepClosed():
102 """Outputs annotation to signal the closing of a step to a try bot."""
104 print '@@@STEP_CLOSED@@@'
109 def OutputAnnotationStepLink(label
, url
):
110 """Outputs appropriate annotation to print a link.
113 label: The name to print.
114 url: The URL to print.
117 print '@@@STEP_LINK@%s@%s@@@' % (label
, url
)
122 def LoadExtraSrc(path_to_file
):
123 """Attempts to load an extra source file, and overrides global values.
125 If the extra source file is loaded successfully, then it will use the new
126 module to override some global values, such as gclient spec data.
129 path_to_file: File path.
132 The loaded module object, or None if none was imported.
135 global GCLIENT_SPEC_DATA
136 global GCLIENT_SPEC_ANDROID
137 extra_src
= imp
.load_source('data', path_to_file
)
138 GCLIENT_SPEC_DATA
= extra_src
.GetGClientSpec()
139 GCLIENT_SPEC_ANDROID
= extra_src
.GetGClientSpecExtraParams()
145 def IsTelemetryCommand(command
):
146 """Attempts to discern whether or not a given command is running telemetry."""
147 return ('tools/perf/run_' in command
or 'tools\\perf\\run_' in command
)
150 def _CreateAndChangeToSourceDirectory(working_directory
):
151 """Creates a directory 'bisect' as a subdirectory of |working_directory|.
153 If successful, the current working directory will be changed to the new
157 working_directory: The directory to create the new 'bisect' directory in.
160 True if the directory was successfully created (or already existed).
163 os
.chdir(working_directory
)
167 if e
.errno
!= errno
.EEXIST
: # EEXIST indicates that it already exists.
174 def _SubprocessCall(cmd
, cwd
=None):
175 """Runs a command in a subprocess.
178 cmd: The command to run.
179 cwd: Working directory to run from.
182 The return code of the call.
185 # "HOME" isn't normally defined on windows, but is needed
186 # for git to find the user's .netrc file.
187 if not os
.getenv('HOME'):
188 os
.environ
['HOME'] = os
.environ
['USERPROFILE']
189 shell
= os
.name
== 'nt'
190 return subprocess
.call(cmd
, shell
=shell
, cwd
=cwd
)
193 def RunGClient(params
, cwd
=None):
194 """Runs gclient with the specified parameters.
197 params: A list of parameters to pass to gclient.
198 cwd: Working directory to run from.
201 The return code of the call.
203 cmd
= ['gclient'] + params
204 return _SubprocessCall(cmd
, cwd
=cwd
)
208 """Sets up CrOS repo for bisecting ChromeOS.
211 True if successful, False otherwise.
217 if e
.errno
!= errno
.EEXIST
: # EEXIST means the directory already exists.
221 cmd
= ['init', '-u'] + REPO_PARAMS
225 if not _RunRepo(cmd
):
226 if not _RunRepo(['sync']):
233 def _RunRepo(params
):
234 """Runs CrOS repo command with specified parameters.
237 params: A list of parameters to pass to gclient.
240 The return code of the call (zero indicates success).
242 cmd
= ['repo'] + params
243 return _SubprocessCall(cmd
)
246 def RunRepoSyncAtTimestamp(timestamp
):
247 """Syncs all git depots to the timestamp specified using repo forall.
250 params: Unix timestamp to sync to.
253 The return code of the call.
255 cmd
= ['forall', '-c', REPO_SYNC_COMMAND
% timestamp
]
259 def RunGClientAndCreateConfig(opts
, custom_deps
=None, cwd
=None):
260 """Runs gclient and creates a config containing both src and src-internal.
263 opts: The options parsed from the command line through parse_args().
264 custom_deps: A dictionary of additional dependencies to add to .gclient.
265 cwd: Working directory to run from.
268 The return code of the call.
270 spec
= GCLIENT_SPEC_DATA
273 for k
, v
in custom_deps
.iteritems():
274 spec
[0]['custom_deps'][k
] = v
276 # Cannot have newlines in string on windows
277 spec
= 'solutions =' + str(spec
)
278 spec
= ''.join([l
for l
in spec
.splitlines()])
280 if 'android' in opts
.target_platform
:
281 spec
+= GCLIENT_SPEC_ANDROID
283 return_code
= RunGClient(
284 ['config', '--spec=%s' % spec
], cwd
=cwd
)
289 def OnAccessError(func
, path
, _
):
290 """Error handler for shutil.rmtree.
292 Source: http://goo.gl/DEYNCT
294 If the error is due to an access error (read only file), it attempts to add
295 write permissions, then retries.
297 If the error is for another reason it re-raises the error.
300 func: The function that raised the error.
301 path: The path name passed to func.
302 _: Exception information from sys.exc_info(). Not used.
304 if not os
.access(path
, os
.W_OK
):
305 os
.chmod(path
, stat
.S_IWUSR
)
311 def RemoveThirdPartyDirectory(dir_name
):
312 """Removes third_party directory from the source.
314 At some point, some of the third_parties were causing issues to changes in
315 the way they are synced. We remove such folder in order to avoid sync errors
319 True on success, otherwise False.
321 path_to_dir
= os
.path
.join(os
.getcwd(), 'third_party', dir_name
)
323 if os
.path
.exists(path_to_dir
):
324 shutil
.rmtree(path_to_dir
, onerror
=OnAccessError
)
326 print 'Error #%d while running shutil.rmtree(%s): %s' % (
327 e
.errno
, path_to_dir
, str(e
))
328 if e
.errno
!= errno
.ENOENT
:
333 def _CleanupPreviousGitRuns():
334 """Cleans up any leftover index.lock files after running git."""
335 # If a previous run of git crashed, or bot was reset, etc., then we might
336 # end up with leftover index.lock files.
337 for path
, _
, files
in os
.walk(os
.getcwd()):
338 for cur_file
in files
:
339 if cur_file
.endswith('index.lock'):
340 path_to_file
= os
.path
.join(path
, cur_file
)
341 os
.remove(path_to_file
)
344 def RunGClientAndSync(cwd
=None):
345 """Runs gclient and does a normal sync.
348 cwd: Working directory to run from.
351 The return code of the call.
353 params
= ['sync', '--verbose', '--nohooks', '--reset', '--force']
354 return RunGClient(params
, cwd
=cwd
)
357 def SetupGitDepot(opts
, custom_deps
):
358 """Sets up the depot for the bisection.
360 The depot will be located in a subdirectory called 'bisect'.
363 opts: The options parsed from the command line through parse_args().
364 custom_deps: A dictionary of additional dependencies to add to .gclient.
367 True if gclient successfully created the config file and did a sync, False
370 name
= 'Setting up Bisection Depot'
372 if opts
.output_buildbot_annotations
:
373 OutputAnnotationStepStart(name
)
377 if not RunGClientAndCreateConfig(opts
, custom_deps
):
378 passed_deps_check
= True
379 if os
.path
.isfile(os
.path
.join('src', FILE_DEPS_GIT
)):
382 passed_deps_check
= True
383 if passed_deps_check
:
384 passed_deps_check
= RemoveThirdPartyDirectory('libjingle')
385 if passed_deps_check
:
386 passed_deps_check
= RemoveThirdPartyDirectory('skia')
389 if passed_deps_check
:
390 _CleanupPreviousGitRuns()
392 RunGClient(['revert'])
393 if not RunGClientAndSync():
396 if opts
.output_buildbot_annotations
:
398 OutputAnnotationStepClosed()
403 def CheckIfBisectDepotExists(opts
):
404 """Checks if the bisect directory already exists.
407 opts: The options parsed from the command line through parse_args().
410 Returns True if it exists.
412 path_to_dir
= os
.path
.join(opts
.working_directory
, 'bisect', 'src')
413 return os
.path
.exists(path_to_dir
)
416 def CheckRunGit(command
, cwd
=None):
417 """Run a git subcommand, returning its output and return code. Asserts if
418 the return code of the call is non-zero.
421 command: A list containing the args to git.
424 A tuple of the output and return code.
426 (output
, return_code
) = RunGit(command
, cwd
=cwd
)
428 assert not return_code
, 'An error occurred while running'\
429 ' "git %s"' % ' '.join(command
)
433 def RunGit(command
, cwd
=None):
434 """Run a git subcommand, returning its output and return code.
437 command: A list containing the args to git.
438 cwd: A directory to change to while running the git command (optional).
441 A tuple of the output and return code.
443 command
= ['git'] + command
445 return RunProcessAndRetrieveOutput(command
, cwd
=cwd
)
448 def CreateBisectDirectoryAndSetupDepot(opts
, custom_deps
):
449 """Sets up a subdirectory 'bisect' and then retrieves a copy of the depot
453 opts: The options parsed from the command line through parse_args().
454 custom_deps: A dictionary of additional dependencies to add to .gclient.
456 if not _CreateAndChangeToSourceDirectory(opts
.working_directory
):
457 raise RuntimeError('Could not create bisect directory.')
459 if not SetupGitDepot(opts
, custom_deps
):
460 raise RuntimeError('Failed to grab source.')
463 def RunProcess(command
):
464 """Runs an arbitrary command.
466 If output from the call is needed, use RunProcessAndRetrieveOutput instead.
469 command: A list containing the command and args to execute.
472 The return code of the call.
474 # On Windows, use shell=True to get PATH interpretation.
475 shell
= IsWindowsHost()
476 return subprocess
.call(command
, shell
=shell
)
479 def RunProcessAndRetrieveOutput(command
, cwd
=None):
480 """Runs an arbitrary command, returning its output and return code.
482 Since output is collected via communicate(), there will be no output until
483 the call terminates. If you need output while the program runs (ie. so
484 that the buildbot doesn't terminate the script), consider RunProcess().
487 command: A list containing the command and args to execute.
488 cwd: A directory to change to while running the command. The command can be
489 relative to this directory. If this is None, the command will be run in
490 the current directory.
493 A tuple of the output and return code.
496 original_cwd
= os
.getcwd()
499 # On Windows, use shell=True to get PATH interpretation.
500 shell
= IsWindowsHost()
501 proc
= subprocess
.Popen(command
, shell
=shell
, stdout
=subprocess
.PIPE
)
502 (output
, _
) = proc
.communicate()
505 os
.chdir(original_cwd
)
507 return (output
, proc
.returncode
)
510 def IsStringInt(string_to_check
):
511 """Checks whether or not the given string can be converted to a integer.
514 string_to_check: Input string to check if it can be converted to an int.
517 True if the string can be converted to an int.
526 def IsStringFloat(string_to_check
):
527 """Checks whether or not the given string can be converted to a floating
531 string_to_check: Input string to check if it can be converted to a float.
534 True if the string can be converted to a float.
537 float(string_to_check
)
544 """Checks whether or not the script is running on Windows.
547 True if running on Windows.
549 return sys
.platform
== 'cygwin' or sys
.platform
.startswith('win')
552 def Is64BitWindows():
553 """Returns whether or not Windows is a 64-bit version.
556 True if Windows is 64-bit, False if 32-bit.
558 platform
= os
.environ
['PROCESSOR_ARCHITECTURE']
560 platform
= os
.environ
['PROCESSOR_ARCHITEW6432']
562 # Must not be running in WoW64, so PROCESSOR_ARCHITECTURE is correct
565 return platform
in ['AMD64', 'I64']
569 """Checks whether or not the script is running on Linux.
572 True if running on Linux.
574 return sys
.platform
.startswith('linux')
578 """Checks whether or not the script is running on Mac.
581 True if running on Mac.
583 return sys
.platform
.startswith('darwin')