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