Home | History | Annotate | Download | only in auto_bisect
      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 
      7 This includes functions related to checking out the depot and outputting
      8 annotations for the Buildbot waterfall.
      9 """
     10 
     11 import errno
     12 import imp
     13 import os
     14 import shutil
     15 import stat
     16 import subprocess
     17 import sys
     18 
     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,
     51 }
     52 
     53 GCLIENT_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 ]
     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'
     66 FILE_DEPS = 'DEPS'
     67 
     68 REPO_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.
     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',
     78     'testing_rsa')
     79 
     80 REPO_PARAMS = [
     81     'https://chrome-internal.googlesource.com/chromeos/manifest-internal/',
     82     '--repo-url',
     83     'https://git.chromium.org/external/repo.git'
     84 ]
     85 
     86 
     87 def 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 
    101 def 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 
    109 def 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 
    122 def 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 
    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)
    148 
    149 
    150 def _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 
    174 def _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 
    193 def 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 
    207 def 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 
    233 def _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 
    246 def 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 
    259 def 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 
    289 def 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 
    311 def 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 
    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)
    342 
    343 
    344 def 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 
    357 def 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 
    402 def 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 
    415 def 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 
    432 def 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 
    446 def 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 
    461 def 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 
    477 def 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 
    508 def 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 
    524 def 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 
    541 def 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 
    550 def 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 
    566 def 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 
    575 def 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