blob: c530f4cb4cd79078635de9fe26a0912023e426a0 [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.org80cbe8b2010-08-13 13:53:07 +000025import threading
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000026import xml.dom.minidom
maruel@chromium.org167b9e62009-09-17 17:41:02 +000027import xml.parsers.expat
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000028
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000029
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000030class CheckCallError(OSError):
31 """CheckCall() returned non-0."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000032 def __init__(self, command, cwd, retcode, stdout, stderr=None):
33 OSError.__init__(self, command, cwd, retcode, stdout, stderr)
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000034 self.command = command
35 self.cwd = cwd
36 self.retcode = retcode
37 self.stdout = stdout
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000038 self.stderr = stderr
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000039
40
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000041def CheckCall(command, cwd=None, print_error=True):
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000042 """Like subprocess.check_call() but returns stdout.
43
44 Works on python 2.4
45 """
maruel@chromium.org116704f2010-06-11 17:34:38 +000046 logging.debug('%s, cwd=%s' % (str(command), str(cwd)))
maruel@chromium.org18111352009-12-20 17:21:28 +000047 try:
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000048 stderr = None
49 if not print_error:
50 stderr = subprocess.PIPE
maruel@chromium.org18111352009-12-20 17:21:28 +000051 process = subprocess.Popen(command, cwd=cwd,
52 shell=sys.platform.startswith('win'),
maruel@chromium.orgea8c1a92009-12-20 17:21:59 +000053 stdout=subprocess.PIPE,
54 stderr=stderr)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000055 std_out, std_err = process.communicate()
maruel@chromium.org18111352009-12-20 17:21:28 +000056 except OSError, e:
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +000057 raise CheckCallError(command, cwd, e.errno, None)
maruel@chromium.org18111352009-12-20 17:21:28 +000058 if process.returncode:
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000059 raise CheckCallError(command, cwd, process.returncode, std_out, std_err)
60 return std_out, std_err
maruel@chromium.org9a2f37e2009-12-19 16:03:28 +000061
62
msb@chromium.orgac915bb2009-11-13 17:03:01 +000063def SplitUrlRevision(url):
64 """Splits url and returns a two-tuple: url, rev"""
65 if url.startswith('ssh:'):
66 # Make sure ssh://test@example.com/test.git@stable works
maruel@chromium.org116704f2010-06-11 17:34:38 +000067 regex = r'(ssh://(?:[\w]+@)?[-\w:\.]+/[-\w\./]+)(?:@(.+))?'
msb@chromium.orgac915bb2009-11-13 17:03:01 +000068 components = re.search(regex, url).groups()
69 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +000070 components = url.split('@', 1)
msb@chromium.orgac915bb2009-11-13 17:03:01 +000071 if len(components) == 1:
72 components += [None]
73 return tuple(components)
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
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000104def SyntaxErrorToError(filename, e):
105 """Raises a gclient_utils.Error exception with the human readable message"""
106 try:
107 # Try to construct a human readable error message
108 if filename:
109 error_message = 'There is a syntax error in %s\n' % filename
110 else:
111 error_message = 'There is a syntax error\n'
112 error_message += 'Line #%s, character %s: "%s"' % (
113 e.lineno, e.offset, re.sub(r'[\r\n]*$', '', e.text))
114 except:
115 # Something went wrong, re-raise the original exception
116 raise e
117 else:
118 raise Error(error_message)
119
120
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000121class PrintableObject(object):
122 def __str__(self):
123 output = ''
124 for i in dir(self):
125 if i.startswith('__'):
126 continue
127 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
128 return output
129
130
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000131def FileRead(filename, mode='rU'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000132 content = None
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000133 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000134 try:
135 content = f.read()
136 finally:
137 f.close()
138 return content
139
140
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000141def FileWrite(filename, content, mode='w'):
142 f = open(filename, mode)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000143 try:
144 f.write(content)
145 finally:
146 f.close()
147
148
149def RemoveDirectory(*path):
150 """Recursively removes a directory, even if it's marked read-only.
151
152 Remove the directory located at *path, if it exists.
153
154 shutil.rmtree() doesn't work on Windows if any of the files or directories
155 are read-only, which svn repositories and some .svn files are. We need to
156 be able to force the files to be writable (i.e., deletable) as we traverse
157 the tree.
158
159 Even with all this, Windows still sometimes fails to delete a file, citing
160 a permission error (maybe something to do with antivirus scans or disk
161 indexing). The best suggestion any of the user forums had was to wait a
162 bit and try again, so we do that too. It's hand-waving, but sometimes it
163 works. :/
164
165 On POSIX systems, things are a little bit simpler. The modes of the files
166 to be deleted doesn't matter, only the modes of the directories containing
167 them are significant. As the directory tree is traversed, each directory
168 has its mode set appropriately before descending into it. This should
169 result in the entire tree being removed, with the possible exception of
170 *path itself, because nothing attempts to change the mode of its parent.
171 Doing so would be hazardous, as it's not a directory slated for removal.
172 In the ordinary case, this is not a problem: for our purposes, the user
173 will never lack write permission on *path's parent.
174 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000175 logging.debug(path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000176 file_path = os.path.join(*path)
177 if not os.path.exists(file_path):
178 return
179
180 if os.path.islink(file_path) or not os.path.isdir(file_path):
maruel@chromium.org116704f2010-06-11 17:34:38 +0000181 raise Error('RemoveDirectory asked to remove non-directory %s' % file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000182
183 has_win32api = False
184 if sys.platform == 'win32':
185 has_win32api = True
186 # Some people don't have the APIs installed. In that case we'll do without.
187 try:
188 win32api = __import__('win32api')
189 win32con = __import__('win32con')
190 except ImportError:
191 has_win32api = False
192 else:
193 # On POSIX systems, we need the x-bit set on the directory to access it,
194 # the r-bit to see its contents, and the w-bit to remove files from it.
195 # The actual modes of the files within the directory is irrelevant.
196 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
197 for fn in os.listdir(file_path):
198 fullpath = os.path.join(file_path, fn)
199
200 # If fullpath is a symbolic link that points to a directory, isdir will
201 # be True, but we don't want to descend into that as a directory, we just
202 # want to remove the link. Check islink and treat links as ordinary files
203 # would be treated regardless of what they reference.
204 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
205 if sys.platform == 'win32':
206 os.chmod(fullpath, stat.S_IWRITE)
207 if has_win32api:
208 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
209 try:
210 os.remove(fullpath)
211 except OSError, e:
212 if e.errno != errno.EACCES or sys.platform != 'win32':
213 raise
214 print 'Failed to delete %s: trying again' % fullpath
215 time.sleep(0.1)
216 os.remove(fullpath)
217 else:
218 RemoveDirectory(fullpath)
219
220 if sys.platform == 'win32':
221 os.chmod(file_path, stat.S_IWRITE)
222 if has_win32api:
223 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
224 try:
225 os.rmdir(file_path)
226 except OSError, e:
227 if e.errno != errno.EACCES or sys.platform != 'win32':
228 raise
229 print 'Failed to remove %s: trying again' % file_path
230 time.sleep(0.1)
231 os.rmdir(file_path)
232
233
234def SubprocessCall(command, in_directory, fail_status=None):
235 """Runs command, a list, in directory in_directory.
236
237 This function wraps SubprocessCallAndFilter, but does not perform the
238 filtering functions. See that function for a more complete usage
239 description.
240 """
241 # Call subprocess and capture nothing:
242 SubprocessCallAndFilter(command, in_directory, True, True, fail_status)
243
244
245def SubprocessCallAndFilter(command,
246 in_directory,
247 print_messages,
248 print_stdout,
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000249 fail_status=None, filter_fn=None):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000250 """Runs command, a list, in directory in_directory.
251
252 If print_messages is true, a message indicating what is being done
dpranke@google.com22e29d42009-10-28 00:48:26 +0000253 is printed to stdout. If print_messages is false, the message is printed
254 only if we actually need to print something else as well, so you can
255 get the context of the output. If print_messages is false and print_stdout
256 is false, no output at all is generated.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000257
258 Also, if print_stdout is true, the command's stdout is also forwarded
259 to stdout.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000260
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000261 If a filter_fn function is specified, it is expected to take a single
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000262 string argument, and it will be called with each line of the
263 subprocess's output. Each line has had the trailing newline character
264 trimmed.
265
266 If the command fails, as indicated by a nonzero exit status, gclient will
267 exit with an exit status of fail_status. If fail_status is None (the
268 default), gclient will raise an Error exception.
269 """
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000270 logging.debug(command)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000271 if print_messages:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000272 print('\n________ running \'%s\' in \'%s\''
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000273 % (' '.join(command), in_directory))
274
275 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
276 # executable, but shell=True makes subprocess on Linux fail when it's called
277 # with a list because it only tries to execute the first item in the list.
278 kid = subprocess.Popen(command, bufsize=0, cwd=in_directory,
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000279 shell=(sys.platform == 'win32'), stdout=subprocess.PIPE,
dpranke@google.com5cc6c572009-11-06 20:04:56 +0000280 stderr=subprocess.STDOUT)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000281
282 # Also, we need to forward stdout to prevent weird re-ordering of output.
283 # This has to be done on a per byte basis to make sure it is not buffered:
284 # normally buffering is done for each line, but if svn requests input, no
285 # end-of-line character is output after the prompt and it would not show up.
286 in_byte = kid.stdout.read(1)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000287 in_line = ''
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000288 while in_byte:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000289 if in_byte != '\r':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000290 if print_stdout:
dpranke@google.com22e29d42009-10-28 00:48:26 +0000291 if not print_messages:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000292 print('\n________ running \'%s\' in \'%s\''
dpranke@google.com22e29d42009-10-28 00:48:26 +0000293 % (' '.join(command), in_directory))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000294 print_messages = True
dpranke@google.com9e890f92009-10-28 01:32:29 +0000295 sys.stdout.write(in_byte)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000296 if in_byte != '\n':
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000297 in_line += in_byte
maruel@chromium.org116704f2010-06-11 17:34:38 +0000298 if in_byte == '\n' and filter_fn:
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000299 filter_fn(in_line)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000300 in_line = ''
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000301 in_byte = kid.stdout.read(1)
302 rv = kid.wait()
303
304 if rv:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000305 msg = 'failed to run command: %s' % ' '.join(command)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000306
307 if fail_status != None:
308 print >>sys.stderr, msg
309 sys.exit(fail_status)
310
311 raise Error(msg)
312
313
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000314def FindGclientRoot(from_dir, filename='.gclient'):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000315 """Tries to find the gclient root."""
316 path = os.path.realpath(from_dir)
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000317 while not os.path.exists(os.path.join(path, filename)):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000318 next = os.path.split(path)
319 if not next[1]:
320 return None
321 path = next[0]
maruel@chromium.orgd9141bf2009-12-23 16:13:32 +0000322 logging.info('Found gclient root at ' + path)
maruel@chromium.orga9371762009-12-22 18:27:38 +0000323 return path
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000324
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000325
maruel@chromium.org3ccbf7e2009-12-22 20:46:42 +0000326def PathDifference(root, subpath):
327 """Returns the difference subpath minus root."""
328 root = os.path.realpath(root)
329 subpath = os.path.realpath(subpath)
330 if not subpath.startswith(root):
331 return None
332 # If the root does not have a trailing \ or /, we add it so the returned
333 # path starts immediately after the seperator regardless of whether it is
334 # provided.
335 root = os.path.join(root, '')
336 return subpath[len(root):]
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000337
338
339def FindFileUpwards(filename, path=None):
340 """Search upwards from the a directory (default: current) to find a file."""
341 if not path:
342 path = os.getcwd()
343 path = os.path.realpath(path)
344 while True:
345 file_path = os.path.join(path, filename)
346 if os.path.isfile(file_path):
347 return file_path
348 (new_path, _) = os.path.split(path)
349 if new_path == path:
350 return None
351 path = new_path
352
353
354def GetGClientRootAndEntries(path=None):
355 """Returns the gclient root and the dict of entries."""
356 config_file = '.gclient_entries'
357 config_path = FindFileUpwards(config_file, path)
358
359 if not config_path:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000360 print "Can't find %s" % config_file
piman@chromium.orgf43d0192010-04-15 02:36:04 +0000361 return None
362
363 env = {}
364 execfile(config_path, env)
365 config_dir = os.path.dirname(config_path)
366 return config_dir, env['entries']
maruel@chromium.org80cbe8b2010-08-13 13:53:07 +0000367
368
369class WorkItem(object):
370 """One work item."""
371 # A list of string, each being a WorkItem name.
372 requirements = []
373 # A unique string representing this work item.
374 name = None
375
376 def run(self):
377 pass
378
379
380class ExecutionQueue(object):
381 """Dependencies sometime needs to be run out of order due to From() keyword.
382
383 This class manages that all the required dependencies are run before running
384 each one.
385
386 Methods of this class are multithread safe.
387 """
388 def __init__(self, progress):
389 self.lock = threading.Lock()
390 # List of WorkItem, Dependency inherits from WorkItem.
391 self.queued = []
392 # List of strings representing each Dependency.name that was run.
393 self.ran = []
394 # List of items currently running.
395 self.running = []
396 self.progress = progress
397 if self.progress:
398 self.progress.update()
399
400 def enqueue(self, d):
401 """Enqueue one Dependency to be executed later once its requirements are
402 satisfied.
403 """
404 assert isinstance(d, WorkItem)
405 try:
406 self.lock.acquire()
407 self.queued.append(d)
408 total = len(self.queued) + len(self.ran) + len(self.running)
409 finally:
410 self.lock.release()
411 if self.progress:
412 self.progress._total = total + 1
413 self.progress.update(0)
414
415 def flush(self, *args, **kwargs):
416 """Runs all enqueued items until all are executed."""
417 while self._run_one_item(*args, **kwargs):
418 pass
419 queued = []
420 running = []
421 try:
422 self.lock.acquire()
423 if self.queued:
424 queued = self.queued
425 self.queued = []
426 if self.running:
427 running = self.running
428 self.running = []
429 finally:
430 self.lock.release()
431 if self.progress:
432 self.progress.end()
433 if queued:
434 raise gclient_utils.Error('Entries still queued: %s' % str(queued))
435 if running:
436 raise gclient_utils.Error('Entries still queued: %s' % str(running))
437
438 def _run_one_item(self, *args, **kwargs):
439 """Removes one item from the queue that has all its requirements completed
440 and execute it.
441
442 Returns False if no item could be run.
443 """
444 i = 0
445 d = None
446 try:
447 self.lock.acquire()
448 while i != len(self.queued) and not d:
449 d = self.queued.pop(i)
450 for r in d.requirements:
451 if not r in self.ran:
452 self.queued.insert(i, d)
453 d = None
454 break
455 i += 1
456 if not d:
457 return False
458 self.running.append(d)
459 finally:
460 self.lock.release()
461 d.run(*args, **kwargs)
462 try:
463 self.lock.acquire()
464 assert not d.name in self.ran
465 if not d.name in self.ran:
466 self.ran.append(d.name)
467 self.running.remove(d)
468 if self.progress:
469 self.progress.update(1)
470 finally:
471 self.lock.release()
472 return True