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