blob: 3c62afb99deba7c6d40656ca951a9e69eb50be79 [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
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000074def ParseXML(output):
75 try:
76 return xml.dom.minidom.parseString(output)
77 except xml.parsers.expat.ExpatError:
78 return None
79
80
81def GetNamedNodeText(node, node_name):
82 child_nodes = node.getElementsByTagName(node_name)
83 if not child_nodes:
84 return None
85 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
86 return child_nodes[0].firstChild.nodeValue
87
88
89def GetNodeNamedAttributeText(node, node_name, attribute_name):
90 child_nodes = node.getElementsByTagName(node_name)
91 if not child_nodes:
92 return None
93 assert len(child_nodes) == 1
94 return child_nodes[0].getAttribute(attribute_name)
95
96
97class Error(Exception):
98 """gclient exception class."""
99 pass
100
101
102class PrintableObject(object):
103 def __str__(self):
104 output = ''
105 for i in dir(self):
106 if i.startswith('__'):
107 continue
108 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
109 return output
110
111
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000112def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000113 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000114 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000115 try:
116 content = f.read()
117 finally:
118 f.close()
119 return content
120
121
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000122def FileWrite(filename, content, mode='w'):
123 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000124 try:
125 f.write(content)
126 finally:
127 f.close()
128
129
130def RemoveDirectory(*path):
131 """Recursively removes a directory, even if it's marked read-only.
132
133 Remove the directory located at *path, if it exists.
134
135 shutil.rmtree() doesn't work on Windows if any of the files or directories
136 are read-only, which svn repositories and some .svn files are. We need to
137 be able to force the files to be writable (i.e., deletable) as we traverse
138 the tree.
139
140 Even with all this, Windows still sometimes fails to delete a file, citing
141 a permission error (maybe something to do with antivirus scans or disk
142 indexing). The best suggestion any of the user forums had was to wait a
143 bit and try again, so we do that too. It's hand-waving, but sometimes it
144 works. :/
145
146 On POSIX systems, things are a little bit simpler. The modes of the files
147 to be deleted doesn't matter, only the modes of the directories containing
148 them are significant. As the directory tree is traversed, each directory
149 has its mode set appropriately before descending into it. This should
150 result in the entire tree being removed, with the possible exception of
151 *path itself, because nothing attempts to change the mode of its parent.
152 Doing so would be hazardous, as it's not a directory slated for removal.
153 In the ordinary case, this is not a problem: for our purposes, the user
154 will never lack write permission on *path's parent.
155 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000156 logging.debug(path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000157 file_path = os.path.join(*path)
158 if not os.path.exists(file_path):
159 return
160
161 if os.path.islink(file_path) or not os.path.isdir(file_path):
162 raise Error("RemoveDirectory asked to remove non-directory %s" % file_path)
163
164 has_win32api = False
165 if sys.platform == 'win32':
166 has_win32api = True
167 # Some people don't have the APIs installed. In that case we'll do without.
168 try:
169 win32api = __import__('win32api')
170 win32con = __import__('win32con')
171 except ImportError:
172 has_win32api = False
173 else:
174 # On POSIX systems, we need the x-bit set on the directory to access it,
175 # the r-bit to see its contents, and the w-bit to remove files from it.
176 # The actual modes of the files within the directory is irrelevant.
177 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
178 for fn in os.listdir(file_path):
179 fullpath = os.path.join(file_path, fn)
180
181 # If fullpath is a symbolic link that points to a directory, isdir will
182 # be True, but we don't want to descend into that as a directory, we just
183 # want to remove the link. Check islink and treat links as ordinary files
184 # would be treated regardless of what they reference.
185 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
186 if sys.platform == 'win32':
187 os.chmod(fullpath, stat.S_IWRITE)
188 if has_win32api:
189 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
190 try:
191 os.remove(fullpath)
192 except OSError, e:
193 if e.errno != errno.EACCES or sys.platform != 'win32':
194 raise
195 print 'Failed to delete %s: trying again' % fullpath
196 time.sleep(0.1)
197 os.remove(fullpath)
198 else:
199 RemoveDirectory(fullpath)
200
201 if sys.platform == 'win32':
202 os.chmod(file_path, stat.S_IWRITE)
203 if has_win32api:
204 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
205 try:
206 os.rmdir(file_path)
207 except OSError, e:
208 if e.errno != errno.EACCES or sys.platform != 'win32':
209 raise
210 print 'Failed to remove %s: trying again' % file_path
211 time.sleep(0.1)
212 os.rmdir(file_path)
213
214
215def SubprocessCall(command, in_directory, fail_status=None):
216 """Runs command, a list, in directory in_directory.
217
218 This function wraps SubprocessCallAndFilter, but does not perform the
219 filtering functions. See that function for a more complete usage
220 description.
221 """
222 # Call subprocess and capture nothing:
223 SubprocessCallAndFilter(command, in_directory, True, True, fail_status)
224
225
226def SubprocessCallAndFilter(command,
227 in_directory,
228 print_messages,
229 print_stdout,
230 fail_status=None, filter=None):
231 """Runs command, a list, in directory in_directory.
232
233 If print_messages is true, a message indicating what is being done
dpranke@google.com22e29d42009-10-28 00:48:26 +0000234 is printed to stdout. If print_messages is false, the message is printed
235 only if we actually need to print something else as well, so you can
236 get the context of the output. If print_messages is false and print_stdout
237 is false, no output at all is generated.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000238
239 Also, if print_stdout is true, the command's stdout is also forwarded
240 to stdout.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000241
242 If a filter function is specified, it is expected to take a single
243 string argument, and it will be called with each line of the
244 subprocess's output. Each line has had the trailing newline character
245 trimmed.
246
247 If the command fails, as indicated by a nonzero exit status, gclient will
248 exit with an exit status of fail_status. If fail_status is None (the
249 default), gclient will raise an Error exception.
250 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000251 logging.debug(command)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000252 if print_messages:
253 print("\n________ running \'%s\' in \'%s\'"
254 % (' '.join(command), in_directory))
255
256 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
257 # executable, but shell=True makes subprocess on Linux fail when it's called
258 # with a list because it only tries to execute the first item in the list.
259 kid = subprocess.Popen(command, bufsize=0, cwd=in_directory,
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000260 shell=(sys.platform == 'win32'), stdout=subprocess.PIPE,
dpranke@google.com5cc6c572009-11-06 20:04:56 +0000261 stderr=subprocess.STDOUT)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000262
263 # Also, we need to forward stdout to prevent weird re-ordering of output.
264 # This has to be done on a per byte basis to make sure it is not buffered:
265 # normally buffering is done for each line, but if svn requests input, no
266 # end-of-line character is output after the prompt and it would not show up.
267 in_byte = kid.stdout.read(1)
268 in_line = ""
269 while in_byte:
270 if in_byte != "\r":
271 if print_stdout:
dpranke@google.com22e29d42009-10-28 00:48:26 +0000272 if not print_messages:
273 print("\n________ running \'%s\' in \'%s\'"
274 % (' '.join(command), in_directory))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000275 print_messages = True
dpranke@google.com9e890f92009-10-28 01:32:29 +0000276 sys.stdout.write(in_byte)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000277 if in_byte != "\n":
278 in_line += in_byte
279 if in_byte == "\n" and filter:
280 filter(in_line)
281 in_line = ""
282 in_byte = kid.stdout.read(1)
283 rv = kid.wait()
284
285 if rv:
286 msg = "failed to run command: %s" % " ".join(command)
287
288 if fail_status != None:
289 print >>sys.stderr, msg
290 sys.exit(fail_status)
291
292 raise Error(msg)
293
294
295def IsUsingGit(root, paths):
296 """Returns True if we're using git to manage any of our checkouts.
297 |entries| is a list of paths to check."""
298 for path in paths:
299 if os.path.exists(os.path.join(root, path, '.git')):
300 return True
301 return False
maruel@chromium.orga9371762009-12-22 18:27:38 +0000302
303def FindGclientRoot(from_dir):
304 """Tries to find the gclient root."""
305 path = os.path.realpath(from_dir)
306 while not os.path.exists(os.path.join(path, '.gclient')):
307 next = os.path.split(path)
308 if not next[1]:
309 return None
310 path = next[0]
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000311 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000312 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000313
314def PathDifference(root, subpath):
315 """Returns the difference subpath minus root."""
316 root = os.path.realpath(root)
317 subpath = os.path.realpath(subpath)
318 if not subpath.startswith(root):
319 return None
320 # If the root does not have a trailing \ or /, we add it so the returned
321 # path starts immediately after the seperator regardless of whether it is
322 # provided.
323 root = os.path.join(root, '')
324 return subpath[len(root):]