Home | History | Annotate | Download | only in bots
      1 #!/usr/bin/env python
      2 #
      3 # Copyright 2016 Google Inc.
      4 #
      5 # Use of this source code is governed by a BSD-style license that can be
      6 # found in the LICENSE file.
      7 
      8 
      9 import datetime
     10 import errno
     11 import os
     12 import shutil
     13 import sys
     14 import subprocess
     15 import tempfile
     16 import time
     17 import uuid
     18 
     19 
     20 GCLIENT = 'gclient.bat' if sys.platform == 'win32' else 'gclient'
     21 GIT = 'git.bat' if sys.platform == 'win32' else 'git'
     22 WHICH = 'where' if sys.platform == 'win32' else 'which'
     23 
     24 
     25 class print_timings(object):
     26   def __init__(self):
     27     self._start = None
     28 
     29   def __enter__(self):
     30     self._start = datetime.datetime.utcnow()
     31     print 'Task started at %s GMT' % str(self._start)
     32 
     33   def __exit__(self, t, v, tb):
     34     finish = datetime.datetime.utcnow()
     35     duration = (finish-self._start).total_seconds()
     36     print 'Task finished at %s GMT (%f seconds)' % (str(finish), duration)
     37 
     38 
     39 class tmp_dir(object):
     40   """Helper class used for creating a temporary directory and working in it."""
     41   def __init__(self):
     42     self._orig_dir = None
     43     self._tmp_dir = None
     44 
     45   def __enter__(self):
     46     self._orig_dir = os.getcwd()
     47     self._tmp_dir = tempfile.mkdtemp()
     48     os.chdir(self._tmp_dir)
     49     return self
     50 
     51   def __exit__(self, t, v, tb):
     52     os.chdir(self._orig_dir)
     53     RemoveDirectory(self._tmp_dir)
     54 
     55   @property
     56   def name(self):
     57     return self._tmp_dir
     58 
     59 
     60 class chdir(object):
     61   """Helper class used for changing into and out of a directory."""
     62   def __init__(self, d):
     63     self._dir = d
     64     self._orig_dir = None
     65 
     66   def __enter__(self):
     67     self._orig_dir = os.getcwd()
     68     os.chdir(self._dir)
     69     return self
     70 
     71   def __exit__(self, t, v, tb):
     72     os.chdir(self._orig_dir)
     73 
     74 
     75 def git_clone(repo_url, dest_dir):
     76   """Clone the given repo into the given destination directory."""
     77   subprocess.check_call([GIT, 'clone', repo_url, dest_dir])
     78 
     79 
     80 class git_branch(object):
     81   """Check out a temporary git branch.
     82 
     83   On exit, deletes the branch and attempts to restore the original state.
     84   """
     85   def __init__(self):
     86     self._branch = None
     87     self._orig_branch = None
     88     self._stashed = False
     89 
     90   def __enter__(self):
     91     output = subprocess.check_output([GIT, 'stash'])
     92     self._stashed = 'No local changes' not in output
     93 
     94     # Get the original branch name or commit hash.
     95     self._orig_branch = subprocess.check_output([
     96         GIT, 'rev-parse', '--abbrev-ref', 'HEAD']).rstrip()
     97     if self._orig_branch == 'HEAD':
     98       self._orig_branch = subprocess.check_output([
     99           GIT, 'rev-parse', 'HEAD']).rstrip()
    100 
    101     # Check out a new branch, based at updated origin/master.
    102     subprocess.check_call([GIT, 'fetch', 'origin'])
    103     self._branch = '_tmp_%s' % uuid.uuid4()
    104     subprocess.check_call([GIT, 'checkout', '-b', self._branch,
    105                            '-t', 'origin/master'])
    106     return self
    107 
    108   def __exit__(self, exc_type, _value, _traceback):
    109     subprocess.check_call([GIT, 'reset', '--hard', 'HEAD'])
    110     subprocess.check_call([GIT, 'checkout', self._orig_branch])
    111     if self._stashed:
    112       subprocess.check_call([GIT, 'stash', 'pop'])
    113     subprocess.check_call([GIT, 'branch', '-D', self._branch])
    114 
    115 
    116 def RemoveDirectory(*path):
    117   """Recursively removes a directory, even if it's marked read-only.
    118 
    119   This was copied from:
    120   https://chromium.googlesource.com/chromium/tools/build/+/f3e7ff03613cd59a463b2ccc49773c3813e77404/scripts/common/chromium_utils.py#491
    121 
    122   Remove the directory located at *path, if it exists.
    123 
    124   shutil.rmtree() doesn't work on Windows if any of the files or directories
    125   are read-only, which svn repositories and some .svn files are.  We need to
    126   be able to force the files to be writable (i.e., deletable) as we traverse
    127   the tree.
    128 
    129   Even with all this, Windows still sometimes fails to delete a file, citing
    130   a permission error (maybe something to do with antivirus scans or disk
    131   indexing).  The best suggestion any of the user forums had was to wait a
    132   bit and try again, so we do that too.  It's hand-waving, but sometimes it
    133   works. :/
    134   """
    135   file_path = os.path.join(*path)
    136   if not os.path.exists(file_path):
    137     return
    138 
    139   if sys.platform == 'win32':
    140     # Give up and use cmd.exe's rd command.
    141     file_path = os.path.normcase(file_path)
    142     for _ in xrange(3):
    143       print 'RemoveDirectory running %s' % (' '.join(
    144           ['cmd.exe', '/c', 'rd', '/q', '/s', file_path]))
    145       if not subprocess.call(['cmd.exe', '/c', 'rd', '/q', '/s', file_path]):
    146         break
    147       print '  Failed'
    148       time.sleep(3)
    149     return
    150 
    151   def RemoveWithRetry_non_win(rmfunc, path):
    152     if os.path.islink(path):
    153       return os.remove(path)
    154     else:
    155       return rmfunc(path)
    156 
    157   remove_with_retry = RemoveWithRetry_non_win
    158 
    159   def RmTreeOnError(function, path, excinfo):
    160     r"""This works around a problem whereby python 2.x on Windows has no ability
    161     to check for symbolic links.  os.path.islink always returns False.  But
    162     shutil.rmtree will fail if invoked on a symbolic link whose target was
    163     deleted before the link.  E.g., reproduce like this:
    164     > mkdir test
    165     > mkdir test\1
    166     > mklink /D test\current test\1
    167     > python -c "import chromium_utils; chromium_utils.RemoveDirectory('test')"
    168     To avoid this issue, we pass this error-handling function to rmtree.  If
    169     we see the exact sort of failure, we ignore it.  All other failures we re-
    170     raise.
    171     """
    172 
    173     exception_type = excinfo[0]
    174     exception_value = excinfo[1]
    175     # If shutil.rmtree encounters a symbolic link on Windows, os.listdir will
    176     # fail with a WindowsError exception with an ENOENT errno (i.e., file not
    177     # found).  We'll ignore that error.  Note that WindowsError is not defined
    178     # for non-Windows platforms, so we use OSError (of which it is a subclass)
    179     # to avoid lint complaints about an undefined global on non-Windows
    180     # platforms.
    181     if (function is os.listdir) and issubclass(exception_type, OSError):
    182       if exception_value.errno == errno.ENOENT:
    183         # File does not exist, and we're trying to delete, so we can ignore the
    184         # failure.
    185         print 'WARNING:  Failed to list %s during rmtree.  Ignoring.\n' % path
    186       else:
    187         raise
    188     else:
    189       raise
    190 
    191   for root, dirs, files in os.walk(file_path, topdown=False):
    192     # For POSIX:  making the directory writable guarantees removability.
    193     # Windows will ignore the non-read-only bits in the chmod value.
    194     os.chmod(root, 0770)
    195     for name in files:
    196       remove_with_retry(os.remove, os.path.join(root, name))
    197     for name in dirs:
    198       remove_with_retry(lambda p: shutil.rmtree(p, onerror=RmTreeOnError),
    199                         os.path.join(root, name))
    200 
    201   remove_with_retry(os.rmdir, file_path)
    202