blob: 5fd2ef27a7f9fc7da14ca6683792d233574c482c [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."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000031 def __init__(self, command, cwd, retcode, stdout, stderr=None):
32 OSError.__init__(self, command, cwd, retcode, stdout, stderr)
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000033 self.command = command
34 self.cwd = cwd
35 self.retcode = retcode
36 self.stdout = stdout
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000037 self.stderr = stderr
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000038
39
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000040def CheckCall(command, cwd=None, print_error=True):
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000041 """Like subprocess.check_call() but returns stdout.
42
43 Works on python 2.4
44 """
maruel@chromium.org116704f2010-06-11 17:34:38 +000045 logging.debug('%s, cwd=%s' % (str(command), str(cwd)))
maruel@chromium.org18111352009-12-20 17:21:28 +000046 try:
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000047 stderr = None
48 if not print_error:
49 stderr = subprocess.PIPE
maruel@chromium.org18111352009-12-20 17:21:28 +000050 process = subprocess.Popen(command, cwd=cwd,
51 shell=sys.platform.startswith('win'),
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000052 stdout=subprocess.PIPE,
53 stderr=stderr)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000054 std_out, std_err = process.communicate()
maruel@chromium.org18111352009-12-20 17:21:28 +000055 except OSError, e:
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +000056 raise CheckCallError(command, cwd, e.errno, None)
maruel@chromium.org18111352009-12-20 17:21:28 +000057 if process.returncode:
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000058 raise CheckCallError(command, cwd, process.returncode, std_out, std_err)
59 return std_out, std_err
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000060
61
msb@chromium.orgac915bb2009-11-13 17:03:01 +000062def SplitUrlRevision(url):
63 """Splits url and returns a two-tuple: url, rev"""
64 if url.startswith('ssh:'):
65 # Make sure ssh://test@example.com/test.git@stable works
maruel@chromium.org116704f2010-06-11 17:34:38 +000066 regex = r'(ssh://(?:[\w]+@)?[-\w:\.]+/[-\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000067 components = re.search(regex, url).groups()
68 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +000069 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000070 if len(components) == 1:
71 components += [None]
72 return tuple(components)
73
74
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000075def ParseXML(output):
76 try:
77 return xml.dom.minidom.parseString(output)
78 except xml.parsers.expat.ExpatError:
79 return None
80
81
82def GetNamedNodeText(node, node_name):
83 child_nodes = node.getElementsByTagName(node_name)
84 if not child_nodes:
85 return None
86 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
87 return child_nodes[0].firstChild.nodeValue
88
89
90def GetNodeNamedAttributeText(node, node_name, attribute_name):
91 child_nodes = node.getElementsByTagName(node_name)
92 if not child_nodes:
93 return None
94 assert len(child_nodes) == 1
95 return child_nodes[0].getAttribute(attribute_name)
96
97
98class Error(Exception):
99 """gclient exception class."""
100 pass
101
102
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000103def SyntaxErrorToError(filename, e):
104 """Raises a gclient_utils.Error exception with the human readable message"""
105 try:
106 # Try to construct a human readable error message
107 if filename:
108 error_message = 'There is a syntax error in %s\n' % filename
109 else:
110 error_message = 'There is a syntax error\n'
111 error_message += 'Line #%s, character %s: "%s"' % (
112 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
113 except:
114 # Something went wrong, re-raise the original exception
115 raise e
116 else:
117 raise Error(error_message)
118
119
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000120class PrintableObject(object):
121 def __str__(self):
122 output = ''
123 for i in dir(self):
124 if i.startswith('__'):
125 continue
126 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
127 return output
128
129
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000130def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000131 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000132 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000133 try:
134 content = f.read()
135 finally:
136 f.close()
137 return content
138
139
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000140def FileWrite(filename, content, mode='w'):
141 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000142 try:
143 f.write(content)
144 finally:
145 f.close()
146
147
148def RemoveDirectory(*path):
149 """Recursively removes a directory, even if it's marked read-only.
150
151 Remove the directory located at *path, if it exists.
152
153 shutil.rmtree() doesn't work on Windows if any of the files or directories
154 are read-only, which svn repositories and some .svn files are. We need to
155 be able to force the files to be writable (i.e., deletable) as we traverse
156 the tree.
157
158 Even with all this, Windows still sometimes fails to delete a file, citing
159 a permission error (maybe something to do with antivirus scans or disk
160 indexing). The best suggestion any of the user forums had was to wait a
161 bit and try again, so we do that too. It's hand-waving, but sometimes it
162 works. :/
163
164 On POSIX systems, things are a little bit simpler. The modes of the files
165 to be deleted doesn't matter, only the modes of the directories containing
166 them are significant. As the directory tree is traversed, each directory
167 has its mode set appropriately before descending into it. This should
168 result in the entire tree being removed, with the possible exception of
169 *path itself, because nothing attempts to change the mode of its parent.
170 Doing so would be hazardous, as it's not a directory slated for removal.
171 In the ordinary case, this is not a problem: for our purposes, the user
172 will never lack write permission on *path's parent.
173 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000174 logging.debug(path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000175 file_path = os.path.join(*path)
176 if not os.path.exists(file_path):
177 return
178
179 if os.path.islink(file_path) or not os.path.isdir(file_path):
maruel@chromium.org116704f2010-06-11 17:34:38 +0000180 raise Error('RemoveDirectory asked to remove non-directory %s' % file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000181
182 has_win32api = False
183 if sys.platform == 'win32':
184 has_win32api = True
185 # Some people don't have the APIs installed. In that case we'll do without.
186 try:
187 win32api = __import__('win32api')
188 win32con = __import__('win32con')
189 except ImportError:
190 has_win32api = False
191 else:
192 # On POSIX systems, we need the x-bit set on the directory to access it,
193 # the r-bit to see its contents, and the w-bit to remove files from it.
194 # The actual modes of the files within the directory is irrelevant.
195 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
196 for fn in os.listdir(file_path):
197 fullpath = os.path.join(file_path, fn)
198
199 # If fullpath is a symbolic link that points to a directory, isdir will
200 # be True, but we don't want to descend into that as a directory, we just
201 # want to remove the link. Check islink and treat links as ordinary files
202 # would be treated regardless of what they reference.
203 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
204 if sys.platform == 'win32':
205 os.chmod(fullpath, stat.S_IWRITE)
206 if has_win32api:
207 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
208 try:
209 os.remove(fullpath)
210 except OSError, e:
211 if e.errno != errno.EACCES or sys.platform != 'win32':
212 raise
213 print 'Failed to delete %s: trying again' % fullpath
214 time.sleep(0.1)
215 os.remove(fullpath)
216 else:
217 RemoveDirectory(fullpath)
218
219 if sys.platform == 'win32':
220 os.chmod(file_path, stat.S_IWRITE)
221 if has_win32api:
222 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
223 try:
224 os.rmdir(file_path)
225 except OSError, e:
226 if e.errno != errno.EACCES or sys.platform != 'win32':
227 raise
228 print 'Failed to remove %s: trying again' % file_path
229 time.sleep(0.1)
230 os.rmdir(file_path)
231
232
233def SubprocessCall(command, in_directory, fail_status=None):
234 """Runs command, a list, in directory in_directory.
235
236 This function wraps SubprocessCallAndFilter, but does not perform the
237 filtering functions. See that function for a more complete usage
238 description.
239 """
240 # Call subprocess and capture nothing:
241 SubprocessCallAndFilter(command, in_directory, True, True, fail_status)
242
243
244def SubprocessCallAndFilter(command,
245 in_directory,
246 print_messages,
247 print_stdout,
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000248 fail_status=None, filter_fn=None):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000249 """Runs command, a list, in directory in_directory.
250
251 If print_messages is true, a message indicating what is being done
dpranke@google.com22e29d42009-10-28 00:48:26 +0000252 is printed to stdout. If print_messages is false, the message is printed
253 only if we actually need to print something else as well, so you can
254 get the context of the output. If print_messages is false and print_stdout
255 is false, no output at all is generated.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000256
257 Also, if print_stdout is true, the command's stdout is also forwarded
258 to stdout.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000259
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000260 If a filter_fn function is specified, it is expected to take a single
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000261 string argument, and it will be called with each line of the
262 subprocess's output. Each line has had the trailing newline character
263 trimmed.
264
265 If the command fails, as indicated by a nonzero exit status, gclient will
266 exit with an exit status of fail_status. If fail_status is None (the
267 default), gclient will raise an Error exception.
268 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000269 logging.debug(command)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000270 if print_messages:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000271 print('\n________ running \'%s\' in \'%s\''
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000272 % (' '.join(command), in_directory))
273
274 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
275 # executable, but shell=True makes subprocess on Linux fail when it's called
276 # with a list because it only tries to execute the first item in the list.
277 kid = subprocess.Popen(command, bufsize=0, cwd=in_directory,
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000278 shell=(sys.platform == 'win32'), stdout=subprocess.PIPE,
dpranke@google.com5cc6c572009-11-06 20:04:56 +0000279 stderr=subprocess.STDOUT)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000280
281 # Also, we need to forward stdout to prevent weird re-ordering of output.
282 # This has to be done on a per byte basis to make sure it is not buffered:
283 # normally buffering is done for each line, but if svn requests input, no
284 # end-of-line character is output after the prompt and it would not show up.
285 in_byte = kid.stdout.read(1)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000286 in_line = ''
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000287 while in_byte:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000288 if in_byte != '\r':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000289 if print_stdout:
dpranke@google.com22e29d42009-10-28 00:48:26 +0000290 if not print_messages:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000291 print('\n________ running \'%s\' in \'%s\''
dpranke@google.com22e29d42009-10-28 00:48:26 +0000292 % (' '.join(command), in_directory))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000293 print_messages = True
dpranke@google.com9e890f92009-10-28 01:32:29 +0000294 sys.stdout.write(in_byte)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000295 if in_byte != '\n':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000296 in_line += in_byte
maruel@chromium.org116704f2010-06-11 17:34:38 +0000297 if in_byte == '\n' and filter_fn:
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000298 filter_fn(in_line)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000299 in_line = ''
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000300 in_byte = kid.stdout.read(1)
301 rv = kid.wait()
302
303 if rv:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000304 msg = 'failed to run command: %s' % ' '.join(command)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000305
306 if fail_status != None:
307 print >>sys.stderr, msg
308 sys.exit(fail_status)
309
310 raise Error(msg)
311
312
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000313def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000314 """Tries to find the gclient root."""
315 path = os.path.realpath(from_dir)
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000316 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000317 next = os.path.split(path)
318 if not next[1]:
319 return None
320 path = next[0]
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000321 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000322 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000323
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000324
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000325def 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):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000336
337
338def FindFileUpwards(filename, path=None):
339 """Search upwards from the a directory (default: current) to find a file."""
340 if not path:
341 path = os.getcwd()
342 path = os.path.realpath(path)
343 while True:
344 file_path = os.path.join(path, filename)
345 if os.path.isfile(file_path):
346 return file_path
347 (new_path, _) = os.path.split(path)
348 if new_path == path:
349 return None
350 path = new_path
351
352
353def GetGClientRootAndEntries(path=None):
354 """Returns the gclient root and the dict of entries."""
355 config_file = '.gclient_entries'
356 config_path = FindFileUpwards(config_file, path)
357
358 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000359 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000360 return None
361
362 env = {}
363 execfile(config_path, env)
364 config_dir = os.path.dirname(config_path)
365 return config_dir, env['entries']