blob: 5068563e980627b4a02059ebb199fe7890fa9653 [file] [log] [blame]
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00001# Copyright 2009 Google Inc. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000015"""Generic utils."""
16
maruel@chromium.org167b9e62009-09-17 17:41:02 +000017import errno
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +000018import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000019import os
msb@chromium.orgac915bb2009-11-13 17:03:01 +000020import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000021import stat
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000022import subprocess
23import sys
maruel@chromium.org167b9e62009-09-17 17:41:02 +000024import time
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000025import xml.dom.minidom
maruel@chromium.org167b9e62009-09-17 17:41:02 +000026import xml.parsers.expat
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000027
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000028
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000029class CheckCallError(OSError):
30 """CheckCall() returned non-0."""
31 def __init__(self, command, cwd, retcode, stdout):
32 OSError.__init__(self, command, cwd, retcode, stdout)
33 self.command = command
34 self.cwd = cwd
35 self.retcode = retcode
36 self.stdout = stdout
37
38
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000039def CheckCall(command, cwd=None, print_error=True):
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000040 """Like subprocess.check_call() but returns stdout.
41
42 Works on python 2.4
43 """
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +000044 logging.debug("%s, cwd=%s" % (str(command), str(cwd)))
maruel@chromium.org18111352009-12-20 17:21:28 +000045 try:
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000046 stderr = None
47 if not print_error:
48 stderr = subprocess.PIPE
maruel@chromium.org18111352009-12-20 17:21:28 +000049 process = subprocess.Popen(command, cwd=cwd,
50 shell=sys.platform.startswith('win'),
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000051 stdout=subprocess.PIPE,
52 stderr=stderr)
maruel@chromium.org18111352009-12-20 17:21:28 +000053 output = process.communicate()[0]
54 except OSError, e:
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +000055 raise CheckCallError(command, cwd, e.errno, None)
maruel@chromium.org18111352009-12-20 17:21:28 +000056 if process.returncode:
57 raise CheckCallError(command, cwd, process.returncode, output)
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000058 return output
59
60
msb@chromium.orgac915bb2009-11-13 17:03:01 +000061def SplitUrlRevision(url):
62 """Splits url and returns a two-tuple: url, rev"""
63 if url.startswith('ssh:'):
64 # Make sure ssh://test@example.com/test.git@stable works
msb@chromium.orgb9f2f622009-11-19 23:45:35 +000065 regex = r"(ssh://(?:[\w]+@)?[-\w:\.]+/[-\w\./]+)(?:@(.+))?"
msb@chromium.orgac915bb2009-11-13 17:03:01 +000066 components = re.search(regex, url).groups()
67 else:
68 components = url.split("@")
69 if len(components) == 1:
70 components += [None]
71 return tuple(components)
72
73
msb@chromium.orgc532e172009-12-15 17:18:32 +000074def FullUrlFromRelative(base_url, url):
75 # Find the forth '/' and strip from there. A bit hackish.
76 return '/'.join(base_url.split('/')[:4]) + url
77
78
79def FullUrlFromRelative2(base_url, url):
80 # Strip from last '/'
81 # Equivalent to unix basename
82 return base_url[:base_url.rfind('/')] + url
83
84
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000085def ParseXML(output):
86 try:
87 return xml.dom.minidom.parseString(output)
88 except xml.parsers.expat.ExpatError:
89 return None
90
91
92def GetNamedNodeText(node, node_name):
93 child_nodes = node.getElementsByTagName(node_name)
94 if not child_nodes:
95 return None
96 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
97 return child_nodes[0].firstChild.nodeValue
98
99
100def GetNodeNamedAttributeText(node, node_name, attribute_name):
101 child_nodes = node.getElementsByTagName(node_name)
102 if not child_nodes:
103 return None
104 assert len(child_nodes) == 1
105 return child_nodes[0].getAttribute(attribute_name)
106
107
108class Error(Exception):
109 """gclient exception class."""
110 pass
111
112
113class PrintableObject(object):
114 def __str__(self):
115 output = ''
116 for i in dir(self):
117 if i.startswith('__'):
118 continue
119 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
120 return output
121
122
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000123def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000124 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000125 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000126 try:
127 content = f.read()
128 finally:
129 f.close()
130 return content
131
132
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000133def FileWrite(filename, content, mode='w'):
134 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000135 try:
136 f.write(content)
137 finally:
138 f.close()
139
140
141def RemoveDirectory(*path):
142 """Recursively removes a directory, even if it's marked read-only.
143
144 Remove the directory located at *path, if it exists.
145
146 shutil.rmtree() doesn't work on Windows if any of the files or directories
147 are read-only, which svn repositories and some .svn files are. We need to
148 be able to force the files to be writable (i.e., deletable) as we traverse
149 the tree.
150
151 Even with all this, Windows still sometimes fails to delete a file, citing
152 a permission error (maybe something to do with antivirus scans or disk
153 indexing). The best suggestion any of the user forums had was to wait a
154 bit and try again, so we do that too. It's hand-waving, but sometimes it
155 works. :/
156
157 On POSIX systems, things are a little bit simpler. The modes of the files
158 to be deleted doesn't matter, only the modes of the directories containing
159 them are significant. As the directory tree is traversed, each directory
160 has its mode set appropriately before descending into it. This should
161 result in the entire tree being removed, with the possible exception of
162 *path itself, because nothing attempts to change the mode of its parent.
163 Doing so would be hazardous, as it's not a directory slated for removal.
164 In the ordinary case, this is not a problem: for our purposes, the user
165 will never lack write permission on *path's parent.
166 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000167 logging.debug(path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000168 file_path = os.path.join(*path)
169 if not os.path.exists(file_path):
170 return
171
172 if os.path.islink(file_path) or not os.path.isdir(file_path):
173 raise Error("RemoveDirectory asked to remove non-directory %s" % file_path)
174
175 has_win32api = False
176 if sys.platform == 'win32':
177 has_win32api = True
178 # Some people don't have the APIs installed. In that case we'll do without.
179 try:
180 win32api = __import__('win32api')
181 win32con = __import__('win32con')
182 except ImportError:
183 has_win32api = False
184 else:
185 # On POSIX systems, we need the x-bit set on the directory to access it,
186 # the r-bit to see its contents, and the w-bit to remove files from it.
187 # The actual modes of the files within the directory is irrelevant.
188 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
189 for fn in os.listdir(file_path):
190 fullpath = os.path.join(file_path, fn)
191
192 # If fullpath is a symbolic link that points to a directory, isdir will
193 # be True, but we don't want to descend into that as a directory, we just
194 # want to remove the link. Check islink and treat links as ordinary files
195 # would be treated regardless of what they reference.
196 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
197 if sys.platform == 'win32':
198 os.chmod(fullpath, stat.S_IWRITE)
199 if has_win32api:
200 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
201 try:
202 os.remove(fullpath)
203 except OSError, e:
204 if e.errno != errno.EACCES or sys.platform != 'win32':
205 raise
206 print 'Failed to delete %s: trying again' % fullpath
207 time.sleep(0.1)
208 os.remove(fullpath)
209 else:
210 RemoveDirectory(fullpath)
211
212 if sys.platform == 'win32':
213 os.chmod(file_path, stat.S_IWRITE)
214 if has_win32api:
215 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
216 try:
217 os.rmdir(file_path)
218 except OSError, e:
219 if e.errno != errno.EACCES or sys.platform != 'win32':
220 raise
221 print 'Failed to remove %s: trying again' % file_path
222 time.sleep(0.1)
223 os.rmdir(file_path)
224
225
226def SubprocessCall(command, in_directory, fail_status=None):
227 """Runs command, a list, in directory in_directory.
228
229 This function wraps SubprocessCallAndFilter, but does not perform the
230 filtering functions. See that function for a more complete usage
231 description.
232 """
233 # Call subprocess and capture nothing:
234 SubprocessCallAndFilter(command, in_directory, True, True, fail_status)
235
236
237def SubprocessCallAndFilter(command,
238 in_directory,
239 print_messages,
240 print_stdout,
241 fail_status=None, filter=None):
242 """Runs command, a list, in directory in_directory.
243
244 If print_messages is true, a message indicating what is being done
dpranke@google.com22e29d42009-10-28 00:48:26 +0000245 is printed to stdout. If print_messages is false, the message is printed
246 only if we actually need to print something else as well, so you can
247 get the context of the output. If print_messages is false and print_stdout
248 is false, no output at all is generated.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000249
250 Also, if print_stdout is true, the command's stdout is also forwarded
251 to stdout.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000252
253 If a filter function is specified, it is expected to take a single
254 string argument, and it will be called with each line of the
255 subprocess's output. Each line has had the trailing newline character
256 trimmed.
257
258 If the command fails, as indicated by a nonzero exit status, gclient will
259 exit with an exit status of fail_status. If fail_status is None (the
260 default), gclient will raise an Error exception.
261 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000262 logging.debug(command)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000263 if print_messages:
264 print("\n________ running \'%s\' in \'%s\'"
265 % (' '.join(command), in_directory))
266
267 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
268 # executable, but shell=True makes subprocess on Linux fail when it's called
269 # with a list because it only tries to execute the first item in the list.
270 kid = subprocess.Popen(command, bufsize=0, cwd=in_directory,
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000271 shell=(sys.platform == 'win32'), stdout=subprocess.PIPE,
dpranke@google.com5cc6c572009-11-06 20:04:56 +0000272 stderr=subprocess.STDOUT)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000273
274 # Also, we need to forward stdout to prevent weird re-ordering of output.
275 # This has to be done on a per byte basis to make sure it is not buffered:
276 # normally buffering is done for each line, but if svn requests input, no
277 # end-of-line character is output after the prompt and it would not show up.
278 in_byte = kid.stdout.read(1)
279 in_line = ""
280 while in_byte:
281 if in_byte != "\r":
282 if print_stdout:
dpranke@google.com22e29d42009-10-28 00:48:26 +0000283 if not print_messages:
284 print("\n________ running \'%s\' in \'%s\'"
285 % (' '.join(command), in_directory))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000286 print_messages = True
dpranke@google.com9e890f92009-10-28 01:32:29 +0000287 sys.stdout.write(in_byte)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000288 if in_byte != "\n":
289 in_line += in_byte
290 if in_byte == "\n" and filter:
291 filter(in_line)
292 in_line = ""
293 in_byte = kid.stdout.read(1)
294 rv = kid.wait()
295
296 if rv:
297 msg = "failed to run command: %s" % " ".join(command)
298
299 if fail_status != None:
300 print >>sys.stderr, msg
301 sys.exit(fail_status)
302
303 raise Error(msg)
304
305
306def IsUsingGit(root, paths):
307 """Returns True if we're using git to manage any of our checkouts.
308 |entries| is a list of paths to check."""
309 for path in paths:
310 if os.path.exists(os.path.join(root, path, '.git')):
311 return True
312 return False
maruel@chromium.orga9371762009-12-22 18:27:38 +0000313
314def FindGclientRoot(from_dir):
315 """Tries to find the gclient root."""
316 path = os.path.realpath(from_dir)
317 while not os.path.exists(os.path.join(path, '.gclient')):
318 next = os.path.split(path)
319 if not next[1]:
320 return None
321 path = next[0]
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000322 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000323 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000324
325def PathDifference(root, subpath):
326 """Returns the difference subpath minus root."""
327 root = os.path.realpath(root)
328 subpath = os.path.realpath(subpath)
329 if not subpath.startswith(root):
330 return None
331 # If the root does not have a trailing \ or /, we add it so the returned
332 # path starts immediately after the seperator regardless of whether it is
333 # provided.
334 root = os.path.join(root, '')
335 return subpath[len(root):]