blob: 1b11f96ae686f17ef6da35590d6eb9f2d5be01b0 [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
15import os
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +000016import stat
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000017import subprocess
18import sys
19import xml.dom.minidom
20
21## Generic utils
22
23
24def ParseXML(output):
25 try:
26 return xml.dom.minidom.parseString(output)
27 except xml.parsers.expat.ExpatError:
28 return None
29
30
31def GetNamedNodeText(node, node_name):
32 child_nodes = node.getElementsByTagName(node_name)
33 if not child_nodes:
34 return None
35 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
36 return child_nodes[0].firstChild.nodeValue
37
38
39def GetNodeNamedAttributeText(node, node_name, attribute_name):
40 child_nodes = node.getElementsByTagName(node_name)
41 if not child_nodes:
42 return None
43 assert len(child_nodes) == 1
44 return child_nodes[0].getAttribute(attribute_name)
45
46
47class Error(Exception):
48 """gclient exception class."""
49 pass
50
51
52class PrintableObject(object):
53 def __str__(self):
54 output = ''
55 for i in dir(self):
56 if i.startswith('__'):
57 continue
58 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
59 return output
60
61
62def FileRead(filename):
63 content = None
64 f = open(filename, "rU")
65 try:
66 content = f.read()
67 finally:
68 f.close()
69 return content
70
71
72def FileWrite(filename, content):
73 f = open(filename, "w")
74 try:
75 f.write(content)
76 finally:
77 f.close()
78
79
80def RemoveDirectory(*path):
81 """Recursively removes a directory, even if it's marked read-only.
82
83 Remove the directory located at *path, if it exists.
84
85 shutil.rmtree() doesn't work on Windows if any of the files or directories
86 are read-only, which svn repositories and some .svn files are. We need to
87 be able to force the files to be writable (i.e., deletable) as we traverse
88 the tree.
89
90 Even with all this, Windows still sometimes fails to delete a file, citing
91 a permission error (maybe something to do with antivirus scans or disk
92 indexing). The best suggestion any of the user forums had was to wait a
93 bit and try again, so we do that too. It's hand-waving, but sometimes it
94 works. :/
95
96 On POSIX systems, things are a little bit simpler. The modes of the files
97 to be deleted doesn't matter, only the modes of the directories containing
98 them are significant. As the directory tree is traversed, each directory
99 has its mode set appropriately before descending into it. This should
100 result in the entire tree being removed, with the possible exception of
101 *path itself, because nothing attempts to change the mode of its parent.
102 Doing so would be hazardous, as it's not a directory slated for removal.
103 In the ordinary case, this is not a problem: for our purposes, the user
104 will never lack write permission on *path's parent.
105 """
106 file_path = os.path.join(*path)
107 if not os.path.exists(file_path):
108 return
109
110 if os.path.islink(file_path) or not os.path.isdir(file_path):
111 raise Error("RemoveDirectory asked to remove non-directory %s" % file_path)
112
113 has_win32api = False
114 if sys.platform == 'win32':
115 has_win32api = True
116 # Some people don't have the APIs installed. In that case we'll do without.
117 try:
118 win32api = __import__('win32api')
119 win32con = __import__('win32con')
120 except ImportError:
121 has_win32api = False
122 else:
123 # On POSIX systems, we need the x-bit set on the directory to access it,
124 # the r-bit to see its contents, and the w-bit to remove files from it.
125 # The actual modes of the files within the directory is irrelevant.
126 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
127 for fn in os.listdir(file_path):
128 fullpath = os.path.join(file_path, fn)
129
130 # If fullpath is a symbolic link that points to a directory, isdir will
131 # be True, but we don't want to descend into that as a directory, we just
132 # want to remove the link. Check islink and treat links as ordinary files
133 # would be treated regardless of what they reference.
134 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
135 if sys.platform == 'win32':
136 os.chmod(fullpath, stat.S_IWRITE)
137 if has_win32api:
138 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
139 try:
140 os.remove(fullpath)
141 except OSError, e:
142 if e.errno != errno.EACCES or sys.platform != 'win32':
143 raise
144 print 'Failed to delete %s: trying again' % fullpath
145 time.sleep(0.1)
146 os.remove(fullpath)
147 else:
148 RemoveDirectory(fullpath)
149
150 if sys.platform == 'win32':
151 os.chmod(file_path, stat.S_IWRITE)
152 if has_win32api:
153 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
154 try:
155 os.rmdir(file_path)
156 except OSError, e:
157 if e.errno != errno.EACCES or sys.platform != 'win32':
158 raise
159 print 'Failed to remove %s: trying again' % file_path
160 time.sleep(0.1)
161 os.rmdir(file_path)
162
163
164def SubprocessCall(command, in_directory, fail_status=None):
165 """Runs command, a list, in directory in_directory.
166
167 This function wraps SubprocessCallAndFilter, but does not perform the
168 filtering functions. See that function for a more complete usage
169 description.
170 """
171 # Call subprocess and capture nothing:
172 SubprocessCallAndFilter(command, in_directory, True, True, fail_status)
173
174
175def SubprocessCallAndFilter(command,
176 in_directory,
177 print_messages,
178 print_stdout,
179 fail_status=None, filter=None):
180 """Runs command, a list, in directory in_directory.
181
182 If print_messages is true, a message indicating what is being done
183 is printed to stdout. If print_stdout is true, the command's stdout
184 is also forwarded to stdout.
185
186 If a filter function is specified, it is expected to take a single
187 string argument, and it will be called with each line of the
188 subprocess's output. Each line has had the trailing newline character
189 trimmed.
190
191 If the command fails, as indicated by a nonzero exit status, gclient will
192 exit with an exit status of fail_status. If fail_status is None (the
193 default), gclient will raise an Error exception.
194 """
195
196 if print_messages:
197 print("\n________ running \'%s\' in \'%s\'"
198 % (' '.join(command), in_directory))
199
200 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
201 # executable, but shell=True makes subprocess on Linux fail when it's called
202 # with a list because it only tries to execute the first item in the list.
203 kid = subprocess.Popen(command, bufsize=0, cwd=in_directory,
204 shell=(sys.platform == 'win32'), stdout=subprocess.PIPE)
205
206 # Also, we need to forward stdout to prevent weird re-ordering of output.
207 # This has to be done on a per byte basis to make sure it is not buffered:
208 # normally buffering is done for each line, but if svn requests input, no
209 # end-of-line character is output after the prompt and it would not show up.
210 in_byte = kid.stdout.read(1)
211 in_line = ""
212 while in_byte:
213 if in_byte != "\r":
214 if print_stdout:
215 sys.stdout.write(in_byte)
216 if in_byte != "\n":
217 in_line += in_byte
218 if in_byte == "\n" and filter:
219 filter(in_line)
220 in_line = ""
221 in_byte = kid.stdout.read(1)
222 rv = kid.wait()
223
224 if rv:
225 msg = "failed to run command: %s" % " ".join(command)
226
227 if fail_status != None:
228 print >>sys.stderr, msg
229 sys.exit(fail_status)
230
231 raise Error(msg)
232
233
234def IsUsingGit(root, paths):
235 """Returns True if we're using git to manage any of our checkouts.
236 |entries| is a list of paths to check."""
237 for path in paths:
238 if os.path.exists(os.path.join(root, path, '.git')):
239 return True
240 return False