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