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