]>
Commit | Line | Data |
---|---|---|
85245f48 RD |
1 | #!/usr/bin/env python |
2 | """ | |
3 | This script will search for installed versions of wxPython on OSX and | |
4 | allow the user to choose one to uninstall. It then will use the | |
5 | metadata stored about the installed package to remove all the files | |
6 | associated with that install. | |
7 | ||
8 | Only the files installed by the main Installer Package will be | |
9 | removed. This includes the Python modules and the wxWidgets shared | |
10 | libraries. If you also installed the demo or docs by dragging them out | |
11 | of the disk image, then you will need to drag them to the Trash | |
12 | yourself. | |
13 | """ | |
14 | ||
15 | import sys, os, glob | |
45b32352 | 16 | from fnmatch import fnmatchcase |
85245f48 RD |
17 | import cPickle, urllib |
18 | ||
19 | RCPTDIR = "/Library/Receipts" | |
20 | RSRCDIR = "Contents/Resources" | |
21 | ||
45b32352 RD |
22 | # Only completly clean out dirs that have one of these as a prefix. |
23 | # We do this because the file list returned from lsbom will include /, | |
24 | # /usr, /usr/local, etc. | |
85245f48 RD |
25 | PREFIXES = [ '/Library/Python/2.3/', |
26 | '/Library/Python/2.4/', | |
f343983a | 27 | '/Library/Python/2.5/', |
767d6c83 RD |
28 | '/Library/Frameworks/Python.framework/Versions/2.3/lib/python2.3/site-packages/', |
29 | '/Library/Frameworks/Python.framework/Versions/2.4/lib/python2.4/site-packages/', | |
f343983a | 30 | '/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/site-packages/', |
85245f48 RD |
31 | '/usr/local/lib/', |
32 | ] | |
33 | ||
45b32352 RD |
34 | # The files that match one of the items in this list will only be |
35 | # removed if the last installation of wxPython on the system is being | |
36 | # uninstalled. | |
37 | COMMON_FILES = [ '/usr/local/bin/*', | |
38 | 'wx.pth', | |
39 | 'wxversion.py', | |
40 | ] | |
41 | ||
42 | ||
85245f48 RD |
43 | |
44 | class AccessError(Exception): | |
45 | pass | |
46 | ||
156fa22b RD |
47 | class ReceiptError(Exception): |
48 | pass | |
49 | ||
85245f48 RD |
50 | |
51 | class InstalledReceipt(object): | |
52 | def __init__(self, rcptPath): | |
53 | self.rcptPath = rcptPath | |
54 | self.rsrcPath = os.path.join(rcptPath, RSRCDIR) | |
156fa22b RD |
55 | bf = glob.glob(os.path.join(self.rsrcPath, "*.bom")) |
56 | if bf: | |
57 | self.bomFile = bf[0] | |
58 | else: | |
59 | print "WARNING: Unable to find %s/*.bom" % self.rsrcPath | |
60 | raise ReceiptError | |
85245f48 RD |
61 | self.findMetaData() |
62 | ||
63 | ||
64 | def findMetaData(self): | |
65 | # TODO: Make this be able to also look at Info.plist files | |
156fa22b RD |
66 | infoFiles = glob.glob(os.path.join(self.rsrcPath, "*.info")) |
67 | if infoFiles: | |
68 | # there should be only one | |
69 | infoFile = infoFiles[0] | |
70 | self.mdata = {} | |
71 | for line in open(infoFile, "r").readlines(): | |
72 | line = line.strip() | |
73 | if line and line[0] != '#': | |
74 | ls = line.split() | |
75 | self.mdata[ls[0]] = line[len(ls[0])+1:] | |
76 | else: | |
77 | print "WARNING: Unable to find %s/*.info" % self.rsrcPath | |
78 | raise ReceiptError | |
79 | ||
85245f48 RD |
80 | |
81 | def getFileList(self): | |
82 | p = os.popen("lsbom -s %s" % self.bomFile, "r") | |
83 | data = p.read() | |
84 | data.strip() | |
85 | data = filter(lambda s: s!='' and s!='.', data.split('\n')) | |
86 | loc = self.mdata['DefaultLocation'] | |
87 | return [loc+item for item in data] | |
88 | ||
89 | ||
90 | def walkFiles(self, handleFile, handleDir): | |
91 | dirs = [] | |
92 | names = self.getFileList() | |
93 | ||
94 | # the plain files | |
95 | for name in names: | |
96 | name = os.path.abspath(name) | |
97 | if os.path.isdir(name): | |
98 | dirs.append(name) | |
99 | else: | |
100 | handleFile(name) | |
101 | ||
102 | # the directories | |
103 | dirs.reverse() | |
104 | for dir in dirs: | |
105 | for prefix in PREFIXES: | |
106 | if dir.startswith(prefix): | |
107 | handleDir(dir) | |
108 | break | |
109 | ||
110 | # Finally, remove the Receipts package, bottom-up | |
111 | for dirpath, dirname, filenames in os.walk(self.rcptPath, False): | |
112 | for name in filenames: | |
113 | name = os.path.join(dirpath, name) | |
114 | handleFile(name) | |
115 | handleDir(dirpath) | |
116 | ||
ccb9a407 KO |
117 | # wxaddons should be always kept as the user may have installed |
118 | # third-party modules seperate from wxpython. | |
119 | def testWxaddons(self, name): | |
120 | for prefix in PREFIXES: | |
121 | if name.startswith(prefix + "wxaddons"): | |
122 | return True | |
123 | return False | |
85245f48 | 124 | |
45b32352 RD |
125 | def testCommon(self, name): |
126 | for cmn in COMMON_FILES: | |
127 | if fnmatchcase(name, cmn) or fnmatchcase(os.path.basename(name), cmn): | |
128 | return True | |
129 | return False | |
130 | ||
85245f48 RD |
131 | |
132 | def showFiles(self): | |
133 | def show(name): | |
134 | if os.path.exists(name): | |
45b32352 RD |
135 | if not self.lastInstall and self.testCommon(name): |
136 | return | |
ccb9a407 KO |
137 | if self.testWxaddons(name): |
138 | return | |
85245f48 RD |
139 | print "Will remove:", name |
140 | self.walkFiles(show, show) | |
141 | ||
142 | ||
143 | def testUninstallAccess(self): | |
144 | def testFile(name): | |
145 | if os.path.exists(name): | |
45b32352 RD |
146 | if not self.lastInstall and self.testCommon(name): |
147 | return | |
85245f48 RD |
148 | if not os.access(name, os.W_OK): |
149 | raise AccessError(name) | |
150 | self.walkFiles(testFile, testFile) | |
151 | ||
152 | ||
153 | def doUninstall(self): | |
154 | def removeFile(name): | |
155 | if os.path.exists(name): | |
45b32352 RD |
156 | if not self.lastInstall and self.testCommon(name): |
157 | return | |
ccb9a407 KO |
158 | if self.testWxaddons(name): |
159 | return | |
85245f48 RD |
160 | print "Removing:", name |
161 | os.unlink(name) | |
162 | def removeDir(name): | |
163 | print "Removing:", name | |
ccb9a407 | 164 | if os.path.exists(name) and not self.testWxaddons(name): |
85245f48 | 165 | hasFiles = os.listdir(name) |
45b32352 | 166 | if hasFiles: # perhaps some stale symlinks, or .pyc files |
85245f48 RD |
167 | for file in hasFiles: |
168 | os.unlink(os.path.join(name, file)) | |
169 | os.rmdir(name) | |
170 | ||
171 | try: | |
172 | self.testUninstallAccess() | |
173 | except AccessError, e: | |
174 | print "UNABLE TO UNINSTALL!\nNo permission to remove: ", e.args[0] | |
175 | sys.exit() | |
176 | ||
177 | self.walkFiles(removeFile, removeDir) | |
178 | ||
179 | ||
180 | ||
181 | ||
182 | def findInstalled(): | |
183 | installed = [] | |
184 | for name in glob.glob(os.path.join(RCPTDIR, "wxPython*")): | |
156fa22b RD |
185 | try: |
186 | ir = InstalledReceipt(name) | |
187 | installed.append(ir) | |
188 | except ReceiptError: | |
189 | pass # just skip it... | |
85245f48 RD |
190 | |
191 | return installed | |
192 | ||
193 | ||
194 | # Just in case a Python < 2.3 is used to run this | |
195 | try: | |
196 | enumerate | |
197 | except NameError: | |
198 | def enumerate(sequence): | |
199 | return zip(range(len(sequence)), sequence) | |
200 | ||
201 | ||
202 | def main(): | |
203 | if len(sys.argv) > 1 and sys.argv[1] == "-doit": | |
204 | inst = cPickle.loads(urllib.unquote(sys.argv[2])) | |
205 | inst.doUninstall() | |
206 | sys.exit() | |
207 | ||
208 | print __doc__ | |
209 | installed = findInstalled() | |
210 | ||
211 | if not installed: | |
212 | print "*** No wxPython installations found! ***" | |
213 | raw_input("Press RETURN...") | |
214 | sys.exit() | |
215 | ||
216 | for i, inst in enumerate(installed): | |
dad65e8a | 217 | print " %2d. %-40s %s" % (i+1, inst.mdata["Title"], inst.mdata["Version"]) |
85245f48 RD |
218 | |
219 | ||
220 | ans = raw_input("Enter the number of the install to examine or 'Q' to quit: ") | |
221 | if ans in ['Q', 'q']: | |
222 | sys.exit() | |
223 | inst = installed[int(ans) - 1] | |
45b32352 | 224 | inst.lastInstall = len(installed) == 1 |
85245f48 RD |
225 | |
226 | while True: | |
227 | ||
228 | print """ | |
229 | Title: %(Title)s | |
230 | Version: %(Version)s | |
231 | Description: %(Description)s | |
232 | """ % inst.mdata | |
233 | ||
234 | ans = raw_input("(U)ninstall, (S)how what will be removed, or (Q)uit? [u,s,q] ") | |
235 | if ans in ['Q', 'q']: | |
236 | sys.exit() | |
237 | ||
238 | elif ans in ['S', 's']: | |
239 | inst.showFiles() | |
240 | ||
241 | elif ans in ['U', 'u']: | |
242 | ||
243 | print "Launching uninstaller with sudo, please enter your password if prompted:" | |
244 | os.system("sudo %s -doit %s" % | |
245 | (sys.argv[0], | |
246 | urllib.quote(cPickle.dumps(inst)))) | |
247 | sys.exit() | |
248 | ||
249 | ||
250 | if __name__ == '__main__': | |
251 | main() |