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