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. 4 5"""Utility functions used by the bisect tool. 6 7This includes functions related to checking out the depot and outputting 8annotations for the Buildbot waterfall. 9""" 10 11import errno 12import imp 13import os 14import shutil 15import stat 16import subprocess 17import sys 18 19DEFAULT_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, 51} 52 53GCLIENT_SPEC_DATA = [ 54 { 55 'name': 'src', 56 'url': 'https://chromium.googlesource.com/chromium/src.git', 57 'deps_file': '.DEPS.git', 58 'managed': True, 59 'custom_deps': {}, 60 'safesync_url': '', 61 }, 62] 63GCLIENT_SPEC_ANDROID = "\ntarget_os = ['android']" 64GCLIENT_CUSTOM_DEPS_V8 = {'src/v8_bleeding_edge': 'git://github.com/v8/v8.git'} 65FILE_DEPS_GIT = '.DEPS.git' 66FILE_DEPS = 'DEPS' 67 68REPO_SYNC_COMMAND = ('git checkout -f $(git rev-list --max-count=1 ' 69 '--before=%d remotes/m/master)') 70 71# Paths to CrOS-related files. 72# WARNING(qyearsley, 2014-08-15): These haven't been tested recently. 73CROS_SDK_PATH = os.path.join('..', 'cros', 'chromite', 'bin', 'cros_sdk') 74CROS_TEST_KEY_PATH = os.path.join( 75 '..', 'cros', 'chromite', 'ssh_keys', 'testing_rsa') 76CROS_SCRIPT_KEY_PATH = os.path.join( 77 '..', 'cros', 'src', 'scripts', 'mod_for_test_scripts', 'ssh_keys', 78 'testing_rsa') 79 80REPO_PARAMS = [ 81 'https://chrome-internal.googlesource.com/chromeos/manifest-internal/', 82 '--repo-url', 83 'https://git.chromium.org/external/repo.git' 84] 85 86 87def OutputAnnotationStepStart(name): 88 """Outputs annotation to signal the start of a step to a try bot. 89 90 Args: 91 name: The name of the step. 92 """ 93 print 94 print '@@@SEED_STEP %s@@@' % name 95 print '@@@STEP_CURSOR %s@@@' % name 96 print '@@@STEP_STARTED@@@' 97 print 98 sys.stdout.flush() 99 100 101def OutputAnnotationStepClosed(): 102 """Outputs annotation to signal the closing of a step to a try bot.""" 103 print 104 print '@@@STEP_CLOSED@@@' 105 print 106 sys.stdout.flush() 107 108 109def OutputAnnotationStepLink(label, url): 110 """Outputs appropriate annotation to print a link. 111 112 Args: 113 label: The name to print. 114 url: The URL to print. 115 """ 116 print 117 print '@@@STEP_LINK@%s@%s@@@' % (label, url) 118 print 119 sys.stdout.flush() 120 121 122def LoadExtraSrc(path_to_file): 123 """Attempts to load an extra source file, and overrides global values. 124 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. 127 128 Args: 129 path_to_file: File path. 130 131 Returns: 132 The loaded module object, or None if none was imported. 133 """ 134 try: 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() 140 return extra_src 141 except ImportError: 142 return None 143 144 145def 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) 148 149 150def _CreateAndChangeToSourceDirectory(working_directory): 151 """Creates a directory 'bisect' as a subdirectory of |working_directory|. 152 153 If successful, the current working directory will be changed to the new 154 'bisect' directory. 155 156 Args: 157 working_directory: The directory to create the new 'bisect' directory in. 158 159 Returns: 160 True if the directory was successfully created (or already existed). 161 """ 162 cwd = os.getcwd() 163 os.chdir(working_directory) 164 try: 165 os.mkdir('bisect') 166 except OSError, e: 167 if e.errno != errno.EEXIST: # EEXIST indicates that it already exists. 168 os.chdir(cwd) 169 return False 170 os.chdir('bisect') 171 return True 172 173 174def _SubprocessCall(cmd, cwd=None): 175 """Runs a command in a subprocess. 176 177 Args: 178 cmd: The command to run. 179 cwd: Working directory to run from. 180 181 Returns: 182 The return code of the call. 183 """ 184 if os.name == 'nt': 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) 191 192 193def RunGClient(params, cwd=None): 194 """Runs gclient with the specified parameters. 195 196 Args: 197 params: A list of parameters to pass to gclient. 198 cwd: Working directory to run from. 199 200 Returns: 201 The return code of the call. 202 """ 203 cmd = ['gclient'] + params 204 return _SubprocessCall(cmd, cwd=cwd) 205 206 207def SetupCrosRepo(): 208 """Sets up CrOS repo for bisecting ChromeOS. 209 210 Returns: 211 True if successful, False otherwise. 212 """ 213 cwd = os.getcwd() 214 try: 215 os.mkdir('cros') 216 except OSError as e: 217 if e.errno != errno.EEXIST: # EEXIST means the directory already exists. 218 return False 219 os.chdir('cros') 220 221 cmd = ['init', '-u'] + REPO_PARAMS 222 223 passed = False 224 225 if not _RunRepo(cmd): 226 if not _RunRepo(['sync']): 227 passed = True 228 os.chdir(cwd) 229 230 return passed 231 232 233def _RunRepo(params): 234 """Runs CrOS repo command with specified parameters. 235 236 Args: 237 params: A list of parameters to pass to gclient. 238 239 Returns: 240 The return code of the call (zero indicates success). 241 """ 242 cmd = ['repo'] + params 243 return _SubprocessCall(cmd) 244 245 246def RunRepoSyncAtTimestamp(timestamp): 247 """Syncs all git depots to the timestamp specified using repo forall. 248 249 Args: 250 params: Unix timestamp to sync to. 251 252 Returns: 253 The return code of the call. 254 """ 255 cmd = ['forall', '-c', REPO_SYNC_COMMAND % timestamp] 256 return _RunRepo(cmd) 257 258 259def RunGClientAndCreateConfig(opts, custom_deps=None, cwd=None): 260 """Runs gclient and creates a config containing both src and src-internal. 261 262 Args: 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. 266 267 Returns: 268 The return code of the call. 269 """ 270 spec = GCLIENT_SPEC_DATA 271 272 if custom_deps: 273 for k, v in custom_deps.iteritems(): 274 spec[0]['custom_deps'][k] = v 275 276 # Cannot have newlines in string on windows 277 spec = 'solutions =' + str(spec) 278 spec = ''.join([l for l in spec.splitlines()]) 279 280 if 'android' in opts.target_platform: 281 spec += GCLIENT_SPEC_ANDROID 282 283 return_code = RunGClient( 284 ['config', '--spec=%s' % spec], cwd=cwd) 285 return return_code 286 287 288 289def OnAccessError(func, path, _): 290 """Error handler for shutil.rmtree. 291 292 Source: http://goo.gl/DEYNCT 293 294 If the error is due to an access error (read only file), it attempts to add 295 write permissions, then retries. 296 297 If the error is for another reason it re-raises the error. 298 299 Args: 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. 303 """ 304 if not os.access(path, os.W_OK): 305 os.chmod(path, stat.S_IWUSR) 306 func(path) 307 else: 308 raise 309 310 311def RemoveThirdPartyDirectory(dir_name): 312 """Removes third_party directory from the source. 313 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 316 while bisecting. 317 318 Returns: 319 True on success, otherwise False. 320 """ 321 path_to_dir = os.path.join(os.getcwd(), 'third_party', dir_name) 322 try: 323 if os.path.exists(path_to_dir): 324 shutil.rmtree(path_to_dir, onerror=OnAccessError) 325 except OSError, e: 326 print 'Error #%d while running shutil.rmtree(%s): %s' % ( 327 e.errno, path_to_dir, str(e)) 328 if e.errno != errno.ENOENT: 329 return False 330 return True 331 332 333def _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) 342 343 344def RunGClientAndSync(cwd=None): 345 """Runs gclient and does a normal sync. 346 347 Args: 348 cwd: Working directory to run from. 349 350 Returns: 351 The return code of the call. 352 """ 353 params = ['sync', '--verbose', '--nohooks', '--reset', '--force'] 354 return RunGClient(params, cwd=cwd) 355 356 357def SetupGitDepot(opts, custom_deps): 358 """Sets up the depot for the bisection. 359 360 The depot will be located in a subdirectory called 'bisect'. 361 362 Args: 363 opts: The options parsed from the command line through parse_args(). 364 custom_deps: A dictionary of additional dependencies to add to .gclient. 365 366 Returns: 367 True if gclient successfully created the config file and did a sync, False 368 otherwise. 369 """ 370 name = 'Setting up Bisection Depot' 371 372 if opts.output_buildbot_annotations: 373 OutputAnnotationStepStart(name) 374 375 passed = False 376 377 if not RunGClientAndCreateConfig(opts, custom_deps): 378 passed_deps_check = True 379 if os.path.isfile(os.path.join('src', FILE_DEPS_GIT)): 380 cwd = os.getcwd() 381 os.chdir('src') 382 if passed_deps_check: 383 passed_deps_check = RemoveThirdPartyDirectory('libjingle') 384 if passed_deps_check: 385 passed_deps_check = RemoveThirdPartyDirectory('skia') 386 os.chdir(cwd) 387 388 if passed_deps_check: 389 _CleanupPreviousGitRuns() 390 391 RunGClient(['revert']) 392 if not RunGClientAndSync(): 393 passed = True 394 395 if opts.output_buildbot_annotations: 396 print 397 OutputAnnotationStepClosed() 398 399 return passed 400 401 402def CheckIfBisectDepotExists(opts): 403 """Checks if the bisect directory already exists. 404 405 Args: 406 opts: The options parsed from the command line through parse_args(). 407 408 Returns: 409 Returns True if it exists. 410 """ 411 path_to_dir = os.path.join(opts.working_directory, 'bisect', 'src') 412 return os.path.exists(path_to_dir) 413 414 415def CheckRunGit(command, cwd=None): 416 """Run a git subcommand, returning its output and return code. Asserts if 417 the return code of the call is non-zero. 418 419 Args: 420 command: A list containing the args to git. 421 422 Returns: 423 A tuple of the output and return code. 424 """ 425 (output, return_code) = RunGit(command, cwd=cwd) 426 427 assert not return_code, 'An error occurred while running'\ 428 ' "git %s"' % ' '.join(command) 429 return output 430 431 432def RunGit(command, cwd=None): 433 """Run a git subcommand, returning its output and return code. 434 435 Args: 436 command: A list containing the args to git. 437 cwd: A directory to change to while running the git command (optional). 438 439 Returns: 440 A tuple of the output and return code. 441 """ 442 command = ['git'] + command 443 return RunProcessAndRetrieveOutput(command, cwd=cwd) 444 445 446def CreateBisectDirectoryAndSetupDepot(opts, custom_deps): 447 """Sets up a subdirectory 'bisect' and then retrieves a copy of the depot 448 there using gclient. 449 450 Args: 451 opts: The options parsed from the command line through parse_args(). 452 custom_deps: A dictionary of additional dependencies to add to .gclient. 453 """ 454 if not _CreateAndChangeToSourceDirectory(opts.working_directory): 455 raise RuntimeError('Could not create bisect directory.') 456 457 if not SetupGitDepot(opts, custom_deps): 458 raise RuntimeError('Failed to grab source.') 459 460 461def RunProcess(command): 462 """Runs an arbitrary command. 463 464 If output from the call is needed, use RunProcessAndRetrieveOutput instead. 465 466 Args: 467 command: A list containing the command and args to execute. 468 469 Returns: 470 The return code of the call. 471 """ 472 # On Windows, use shell=True to get PATH interpretation. 473 shell = IsWindowsHost() 474 return subprocess.call(command, shell=shell) 475 476 477def RunProcessAndRetrieveOutput(command, cwd=None): 478 """Runs an arbitrary command, returning its output and return code. 479 480 Since output is collected via communicate(), there will be no output until 481 the call terminates. If you need output while the program runs (ie. so 482 that the buildbot doesn't terminate the script), consider RunProcess(). 483 484 Args: 485 command: A list containing the command and args to execute. 486 cwd: A directory to change to while running the command. The command can be 487 relative to this directory. If this is None, the command will be run in 488 the current directory. 489 490 Returns: 491 A tuple of the output and return code. 492 """ 493 if cwd: 494 original_cwd = os.getcwd() 495 os.chdir(cwd) 496 497 # On Windows, use shell=True to get PATH interpretation. 498 shell = IsWindowsHost() 499 proc = subprocess.Popen(command, shell=shell, stdout=subprocess.PIPE) 500 (output, _) = proc.communicate() 501 502 if cwd: 503 os.chdir(original_cwd) 504 505 return (output, proc.returncode) 506 507 508def IsStringInt(string_to_check): 509 """Checks whether or not the given string can be converted to a integer. 510 511 Args: 512 string_to_check: Input string to check if it can be converted to an int. 513 514 Returns: 515 True if the string can be converted to an int. 516 """ 517 try: 518 int(string_to_check) 519 return True 520 except ValueError: 521 return False 522 523 524def IsStringFloat(string_to_check): 525 """Checks whether or not the given string can be converted to a floating 526 point number. 527 528 Args: 529 string_to_check: Input string to check if it can be converted to a float. 530 531 Returns: 532 True if the string can be converted to a float. 533 """ 534 try: 535 float(string_to_check) 536 return True 537 except ValueError: 538 return False 539 540 541def IsWindowsHost(): 542 """Checks whether or not the script is running on Windows. 543 544 Returns: 545 True if running on Windows. 546 """ 547 return sys.platform == 'cygwin' or sys.platform.startswith('win') 548 549 550def Is64BitWindows(): 551 """Returns whether or not Windows is a 64-bit version. 552 553 Returns: 554 True if Windows is 64-bit, False if 32-bit. 555 """ 556 platform = os.environ['PROCESSOR_ARCHITECTURE'] 557 try: 558 platform = os.environ['PROCESSOR_ARCHITEW6432'] 559 except KeyError: 560 # Must not be running in WoW64, so PROCESSOR_ARCHITECTURE is correct 561 pass 562 563 return platform in ['AMD64', 'I64'] 564 565 566def IsLinuxHost(): 567 """Checks whether or not the script is running on Linux. 568 569 Returns: 570 True if running on Linux. 571 """ 572 return sys.platform.startswith('linux') 573 574 575def IsMacHost(): 576 """Checks whether or not the script is running on Mac. 577 578 Returns: 579 True if running on Mac. 580 """ 581 return sys.platform.startswith('darwin') 582