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