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