blob: 6ea091c12ce1624c2691a136bce0f06bf75a92ce [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.org5f3eee32009-09-17 00:34:30 +000018import os
msb@chromium.orgac915bb2009-11-13 17:03:01 +000019import re
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000020import stat
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000021import subprocess
22import sys
maruel@chromium.org167b9e62009-09-17 17:41:02 +000023import time
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000024import xml.dom.minidom
maruel@chromium.org167b9e62009-09-17 17:41:02 +000025import xml.parsers.expat
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000026
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000027
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000028class CheckCallError(OSError):
29 """CheckCall() returned non-0."""
30 def __init__(self, command, cwd, retcode, stdout):
31 OSError.__init__(self, command, cwd, retcode, stdout)
32 self.command = command
33 self.cwd = cwd
34 self.retcode = retcode
35 self.stdout = stdout
36
37
38def CheckCall(command, cwd=None):
39 """Like subprocess.check_call() but returns stdout.
40
41 Works on python 2.4
42 """
maruel@chromium.org18111352009-12-20 17:21:28 +000043 try:
44 process = subprocess.Popen(command, cwd=cwd,
45 shell=sys.platform.startswith('win'),
46 stdout=subprocess.PIPE)
47 output = process.communicate()[0]
48 except OSError, e:
49 raise CheckCallError(command, cwd, errno, None)
50 if process.returncode:
51 raise CheckCallError(command, cwd, process.returncode, output)
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000052 return output
53
54
msb@chromium.orgac915bb2009-11-13 17:03:01 +000055def SplitUrlRevision(url):
56 """Splits url and returns a two-tuple: url, rev"""
57 if url.startswith('ssh:'):
58 # Make sure ssh://test@example.com/test.git@stable works
msb@chromium.orgb9f2f622009-11-19 23:45:35 +000059 regex = r"(ssh://(?:[\w]+@)?[-\w:\.]+/[-\w\./]+)(?:@(.+))?"
msb@chromium.orgac915bb2009-11-13 17:03:01 +000060 components = re.search(regex, url).groups()
61 else:
62 components = url.split("@")
63 if len(components) == 1:
64 components += [None]
65 return tuple(components)
66
67
msb@chromium.orgc532e172009-12-15 17:18:32 +000068def FullUrlFromRelative(base_url, url):
69 # Find the forth '/' and strip from there. A bit hackish.
70 return '/'.join(base_url.split('/')[:4]) + url
71
72
73def FullUrlFromRelative2(base_url, url):
74 # Strip from last '/'
75 # Equivalent to unix basename
76 return base_url[:base_url.rfind('/')] + url
77
78
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000079def ParseXML(output):
80 try:
81 return xml.dom.minidom.parseString(output)
82 except xml.parsers.expat.ExpatError:
83 return None
84
85
86def GetNamedNodeText(node, node_name):
87 child_nodes = node.getElementsByTagName(node_name)
88 if not child_nodes:
89 return None
90 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
91 return child_nodes[0].firstChild.nodeValue
92
93
94def GetNodeNamedAttributeText(node, node_name, attribute_name):
95 child_nodes = node.getElementsByTagName(node_name)
96 if not child_nodes:
97 return None
98 assert len(child_nodes) == 1
99 return child_nodes[0].getAttribute(attribute_name)
100
101
102class Error(Exception):
103 """gclient exception class."""
104 pass
105
106
107class PrintableObject(object):
108 def __str__(self):
109 output = ''
110 for i in dir(self):
111 if i.startswith('__'):
112 continue
113 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
114 return output
115
116
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000117def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000118 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000119 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000120 try:
121 content = f.read()
122 finally:
123 f.close()
124 return content
125
126
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000127def FileWrite(filename, content, mode='w'):
128 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000129 try:
130 f.write(content)
131 finally:
132 f.close()
133
134
135def RemoveDirectory(*path):
136 """Recursively removes a directory, even if it's marked read-only.
137
138 Remove the directory located at *path, if it exists.
139
140 shutil.rmtree() doesn't work on Windows if any of the files or directories
141 are read-only, which svn repositories and some .svn files are. We need to
142 be able to force the files to be writable (i.e., deletable) as we traverse
143 the tree.
144
145 Even with all this, Windows still sometimes fails to delete a file, citing
146 a permission error (maybe something to do with antivirus scans or disk
147 indexing). The best suggestion any of the user forums had was to wait a
148 bit and try again, so we do that too. It's hand-waving, but sometimes it
149 works. :/
150
151 On POSIX systems, things are a little bit simpler. The modes of the files
152 to be deleted doesn't matter, only the modes of the directories containing
153 them are significant. As the directory tree is traversed, each directory
154 has its mode set appropriately before descending into it. This should
155 result in the entire tree being removed, with the possible exception of
156 *path itself, because nothing attempts to change the mode of its parent.
157 Doing so would be hazardous, as it's not a directory slated for removal.
158 In the ordinary case, this is not a problem: for our purposes, the user
159 will never lack write permission on *path's parent.
160 """
161 file_path = os.path.join(*path)
162 if not os.path.exists(file_path):
163 return
164
165 if os.path.islink(file_path) or not os.path.isdir(file_path):
166 raise Error("RemoveDirectory asked to remove non-directory %s" % file_path)
167
168 has_win32api = False
169 if sys.platform == 'win32':
170 has_win32api = True
171 # Some people don't have the APIs installed. In that case we'll do without.
172 try:
173 win32api = __import__('win32api')
174 win32con = __import__('win32con')
175 except ImportError:
176 has_win32api = False
177 else:
178 # On POSIX systems, we need the x-bit set on the directory to access it,
179 # the r-bit to see its contents, and the w-bit to remove files from it.
180 # The actual modes of the files within the directory is irrelevant.
181 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
182 for fn in os.listdir(file_path):
183 fullpath = os.path.join(file_path, fn)
184
185 # If fullpath is a symbolic link that points to a directory, isdir will
186 # be True, but we don't want to descend into that as a directory, we just
187 # want to remove the link. Check islink and treat links as ordinary files
188 # would be treated regardless of what they reference.
189 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
190 if sys.platform == 'win32':
191 os.chmod(fullpath, stat.S_IWRITE)
192 if has_win32api:
193 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
194 try:
195 os.remove(fullpath)
196 except OSError, e:
197 if e.errno != errno.EACCES or sys.platform != 'win32':
198 raise
199 print 'Failed to delete %s: trying again' % fullpath
200 time.sleep(0.1)
201 os.remove(fullpath)
202 else:
203 RemoveDirectory(fullpath)
204
205 if sys.platform == 'win32':
206 os.chmod(file_path, stat.S_IWRITE)
207 if has_win32api:
208 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
209 try:
210 os.rmdir(file_path)
211 except OSError, e:
212 if e.errno != errno.EACCES or sys.platform != 'win32':
213 raise
214 print 'Failed to remove %s: trying again' % file_path
215 time.sleep(0.1)
216 os.rmdir(file_path)
217
218
219def SubprocessCall(command, in_directory, fail_status=None):
220 """Runs command, a list, in directory in_directory.
221
222 This function wraps SubprocessCallAndFilter, but does not perform the
223 filtering functions. See that function for a more complete usage
224 description.
225 """
226 # Call subprocess and capture nothing:
227 SubprocessCallAndFilter(command, in_directory, True, True, fail_status)
228
229
230def SubprocessCallAndFilter(command,
231 in_directory,
232 print_messages,
233 print_stdout,
234 fail_status=None, filter=None):
235 """Runs command, a list, in directory in_directory.
236
237 If print_messages is true, a message indicating what is being done
dpranke@google.com22e29d42009-10-28 00:48:26 +0000238 is printed to stdout. If print_messages is false, the message is printed
239 only if we actually need to print something else as well, so you can
240 get the context of the output. If print_messages is false and print_stdout
241 is false, no output at all is generated.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000242
243 Also, if print_stdout is true, the command's stdout is also forwarded
244 to stdout.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000245
246 If a filter function is specified, it is expected to take a single
247 string argument, and it will be called with each line of the
248 subprocess's output. Each line has had the trailing newline character
249 trimmed.
250
251 If the command fails, as indicated by a nonzero exit status, gclient will
252 exit with an exit status of fail_status. If fail_status is None (the
253 default), gclient will raise an Error exception.
254 """
255
256 if print_messages:
257 print("\n________ running \'%s\' in \'%s\'"
258 % (' '.join(command), in_directory))
259
260 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
261 # executable, but shell=True makes subprocess on Linux fail when it's called
262 # with a list because it only tries to execute the first item in the list.
263 kid = subprocess.Popen(command, bufsize=0, cwd=in_directory,
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000264 shell=(sys.platform == 'win32'), stdout=subprocess.PIPE,
dpranke@google.com5cc6c572009-11-06 20:04:56 +0000265 stderr=subprocess.STDOUT)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000266
267 # Also, we need to forward stdout to prevent weird re-ordering of output.
268 # This has to be done on a per byte basis to make sure it is not buffered:
269 # normally buffering is done for each line, but if svn requests input, no
270 # end-of-line character is output after the prompt and it would not show up.
271 in_byte = kid.stdout.read(1)
272 in_line = ""
273 while in_byte:
274 if in_byte != "\r":
275 if print_stdout:
dpranke@google.com22e29d42009-10-28 00:48:26 +0000276 if not print_messages:
277 print("\n________ running \'%s\' in \'%s\'"
278 % (' '.join(command), in_directory))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000279 print_messages = True
dpranke@google.com9e890f92009-10-28 01:32:29 +0000280 sys.stdout.write(in_byte)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000281 if in_byte != "\n":
282 in_line += in_byte
283 if in_byte == "\n" and filter:
284 filter(in_line)
285 in_line = ""
286 in_byte = kid.stdout.read(1)
287 rv = kid.wait()
288
289 if rv:
290 msg = "failed to run command: %s" % " ".join(command)
291
292 if fail_status != None:
293 print >>sys.stderr, msg
294 sys.exit(fail_status)
295
296 raise Error(msg)
297
298
299def IsUsingGit(root, paths):
300 """Returns True if we're using git to manage any of our checkouts.
301 |entries| is a list of paths to check."""
302 for path in paths:
303 if os.path.exists(os.path.join(root, path, '.git')):
304 return True
305 return False