|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2007 - 2009 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the Plugin installation dialog. |
|
8 """ |
|
9 |
|
10 import os |
|
11 import sys |
|
12 import shutil |
|
13 import zipfile |
|
14 import compileall |
|
15 import urlparse |
|
16 |
|
17 from PyQt4.QtGui import * |
|
18 from PyQt4.QtCore import * |
|
19 |
|
20 from E4Gui.E4Completers import E4FileCompleter |
|
21 |
|
22 from PluginManager import PluginManager |
|
23 from Ui_PluginInstallDialog import Ui_PluginInstallDialog |
|
24 |
|
25 import Utilities |
|
26 import Preferences |
|
27 |
|
28 from Utilities.uic import compileUiFiles |
|
29 |
|
30 class PluginInstallWidget(QWidget, Ui_PluginInstallDialog): |
|
31 """ |
|
32 Class implementing the Plugin installation dialog. |
|
33 """ |
|
34 def __init__(self, pluginManager, pluginFileNames, parent = None): |
|
35 """ |
|
36 Constructor |
|
37 |
|
38 @param pluginManager reference to the plugin manager object |
|
39 @param pluginFileNames list of plugin files suggested for |
|
40 installation (list of strings) |
|
41 @param parent parent of this dialog (QWidget) |
|
42 """ |
|
43 QWidget.__init__(self, parent) |
|
44 self.setupUi(self) |
|
45 |
|
46 if pluginManager is None: |
|
47 # started as external plugin installer |
|
48 self.__pluginManager = PluginManager(doLoadPlugins = False) |
|
49 self.__external = True |
|
50 else: |
|
51 self.__pluginManager = pluginManager |
|
52 self.__external = False |
|
53 |
|
54 self.__backButton = \ |
|
55 self.buttonBox.addButton(self.trUtf8("< Back"), QDialogButtonBox.ActionRole) |
|
56 self.__nextButton = \ |
|
57 self.buttonBox.addButton(self.trUtf8("Next >"), QDialogButtonBox.ActionRole) |
|
58 self.__finishButton = \ |
|
59 self.buttonBox.addButton(self.trUtf8("Install"), QDialogButtonBox.ActionRole) |
|
60 |
|
61 self.__closeButton = self.buttonBox.button(QDialogButtonBox.Close) |
|
62 self.__cancelButton = self.buttonBox.button(QDialogButtonBox.Cancel) |
|
63 |
|
64 userDir = self.__pluginManager.getPluginDir("user") |
|
65 if userDir is not None: |
|
66 self.destinationCombo.addItem(self.trUtf8("User plugins directory"), |
|
67 QVariant(userDir)) |
|
68 |
|
69 globalDir = self.__pluginManager.getPluginDir("global") |
|
70 if globalDir is not None and os.access(globalDir, os.W_OK): |
|
71 self.destinationCombo.addItem(self.trUtf8("Global plugins directory"), |
|
72 QVariant(globalDir)) |
|
73 |
|
74 self.__installedDirs = [] |
|
75 self.__installedFiles = [] |
|
76 |
|
77 self.__restartNeeded = False |
|
78 |
|
79 downloadDir = QDir(Preferences.getPluginManager("DownloadPath")) |
|
80 for pluginFileName in pluginFileNames: |
|
81 fi = QFileInfo(pluginFileName) |
|
82 if fi.isRelative(): |
|
83 pluginFileName = QFileInfo(downloadDir, fi.fileName()).absoluteFilePath() |
|
84 self.archivesList.addItem(pluginFileName) |
|
85 self.archivesList.sortItems() |
|
86 |
|
87 self.__currentIndex = 0 |
|
88 self.__selectPage() |
|
89 |
|
90 def restartNeeded(self): |
|
91 """ |
|
92 Public method to check, if a restart of the IDE is required. |
|
93 |
|
94 @return flag indicating a restart is required (boolean) |
|
95 """ |
|
96 return self.__restartNeeded |
|
97 |
|
98 def __createArchivesList(self): |
|
99 """ |
|
100 Private method to create a list of plugin archive names. |
|
101 |
|
102 @return list of plugin archive names (list of strings) |
|
103 """ |
|
104 archivesList = [] |
|
105 for row in range(self.archivesList.count()): |
|
106 archivesList.append(self.archivesList.item(row).text()) |
|
107 return archivesList |
|
108 |
|
109 def __selectPage(self): |
|
110 """ |
|
111 Private method to show the right wizard page. |
|
112 """ |
|
113 self.wizard.setCurrentIndex(self.__currentIndex) |
|
114 if self.__currentIndex == 0: |
|
115 self.__backButton.setEnabled(False) |
|
116 self.__nextButton.setEnabled(self.archivesList.count() > 0) |
|
117 self.__finishButton.setEnabled(False) |
|
118 self.__closeButton.hide() |
|
119 self.__cancelButton.show() |
|
120 elif self.__currentIndex == 1: |
|
121 self.__backButton.setEnabled(True) |
|
122 self.__nextButton.setEnabled(self.destinationCombo.count() > 0) |
|
123 self.__finishButton.setEnabled(False) |
|
124 self.__closeButton.hide() |
|
125 self.__cancelButton.show() |
|
126 else: |
|
127 self.__backButton.setEnabled(True) |
|
128 self.__nextButton.setEnabled(False) |
|
129 self.__finishButton.setEnabled(True) |
|
130 self.__closeButton.hide() |
|
131 self.__cancelButton.show() |
|
132 |
|
133 msg = self.trUtf8("Plugin ZIP-Archives:\n{0}\n\nDestination:\n{1} ({2})")\ |
|
134 .format("\n".join(self.__createArchivesList()), |
|
135 self.destinationCombo.currentText(), |
|
136 self.destinationCombo.itemData( |
|
137 self.destinationCombo.currentIndex()).toString() |
|
138 ) |
|
139 self.summaryEdit.setPlainText(msg) |
|
140 |
|
141 @pyqtSlot() |
|
142 def on_addArchivesButton_clicked(self): |
|
143 """ |
|
144 Private slot to select plugin ZIP-archives via a file selection dialog. |
|
145 """ |
|
146 dn = Preferences.getPluginManager("DownloadPath") |
|
147 archives = QFileDialog.getOpenFileNames( |
|
148 self, |
|
149 self.trUtf8("Select plugin ZIP-archives"), |
|
150 dn, |
|
151 self.trUtf8("Plugin archive (*.zip)")) |
|
152 |
|
153 if archives: |
|
154 matchflags = Qt.MatchFixedString |
|
155 if not Utilities.isWindowsPlatform(): |
|
156 matchflags |= Qt.MatchCaseSensitive |
|
157 for archive in archives: |
|
158 if len(self.archivesList.findItems(archive, matchflags)) == 0: |
|
159 # entry not in list already |
|
160 self.archivesList.addItem(archive) |
|
161 self.archivesList.sortItems() |
|
162 |
|
163 self.__nextButton.setEnabled(self.archivesList.count() > 0) |
|
164 |
|
165 @pyqtSlot() |
|
166 def on_archivesList_itemSelectionChanged(self): |
|
167 """ |
|
168 Private slot called, when the selection of the archives list changes. |
|
169 """ |
|
170 self.removeArchivesButton.setEnabled(len(self.archivesList.selectedItems()) > 0) |
|
171 |
|
172 @pyqtSlot() |
|
173 def on_removeArchivesButton_clicked(self): |
|
174 """ |
|
175 Private slot to remove archives from the list. |
|
176 """ |
|
177 for archiveItem in self.archivesList.selectedItems(): |
|
178 itm = self.archivesList.takeItem(self.archivesList.row(archiveItem)) |
|
179 del itm |
|
180 |
|
181 self.__nextButton.setEnabled(self.archivesList.count() > 0) |
|
182 |
|
183 @pyqtSlot(QAbstractButton) |
|
184 def on_buttonBox_clicked(self, button): |
|
185 """ |
|
186 Private slot to handle the click of a button of the button box. |
|
187 """ |
|
188 if button == self.__backButton: |
|
189 self.__currentIndex -= 1 |
|
190 self.__selectPage() |
|
191 elif button == self.__nextButton: |
|
192 self.__currentIndex += 1 |
|
193 self.__selectPage() |
|
194 elif button == self.__finishButton: |
|
195 self.__installPlugins() |
|
196 self.__finishButton.setEnabled(False) |
|
197 self.__closeButton.show() |
|
198 self.__cancelButton.hide() |
|
199 |
|
200 def __installPlugins(self): |
|
201 """ |
|
202 Private method to install the selected plugin archives. |
|
203 |
|
204 @return flag indicating success (boolean) |
|
205 """ |
|
206 res = True |
|
207 self.summaryEdit.clear() |
|
208 for archive in self.__createArchivesList(): |
|
209 self.summaryEdit.append(self.trUtf8("Installing {0} ...").format(archive)) |
|
210 ok, msg, restart = self.__installPlugin(archive) |
|
211 res = res and ok |
|
212 if ok: |
|
213 self.summaryEdit.append(self.trUtf8(" ok")) |
|
214 else: |
|
215 self.summaryEdit.append(msg) |
|
216 if restart: |
|
217 self.__restartNeeded = True |
|
218 self.summaryEdit.append("\n") |
|
219 if res: |
|
220 self.summaryEdit.append(self.trUtf8(\ |
|
221 """The plugins were installed successfully.""")) |
|
222 else: |
|
223 self.summaryEdit.append(self.trUtf8(\ |
|
224 """Some plugins could not be installed.""")) |
|
225 |
|
226 return res |
|
227 |
|
228 def __installPlugin(self, archiveFilename): |
|
229 """ |
|
230 Private slot to install the selected plugin. |
|
231 |
|
232 @param archiveFilename name of the plugin archive |
|
233 file (string) |
|
234 @return flag indicating success (boolean), error message |
|
235 upon failure (string) and flag indicating a restart |
|
236 of the IDE is required (boolean) |
|
237 """ |
|
238 installedPluginName = "" |
|
239 |
|
240 archive = archiveFilename |
|
241 destination = \ |
|
242 self.destinationCombo.itemData(self.destinationCombo.currentIndex())\ |
|
243 .toString() |
|
244 |
|
245 # check if archive is a local url |
|
246 url = urlparse.urlparse(archive) |
|
247 if url[0].lower() == 'file': |
|
248 archive = url[2] |
|
249 |
|
250 # check, if the archive exists |
|
251 if not os.path.exists(archive): |
|
252 return False, \ |
|
253 self.trUtf8("""<p>The archive file <b>{0}</b> does not exist. """ |
|
254 """Aborting...</p>""").format(archive), \ |
|
255 False |
|
256 |
|
257 # check, if the archive is a valid zip file |
|
258 if not zipfile.is_zipfile(archive): |
|
259 return False, \ |
|
260 self.trUtf8("""<p>The file <b>{0}</b> is not a valid plugin """ |
|
261 """ZIP-archive. Aborting...</p>""").format(archive), \ |
|
262 False |
|
263 |
|
264 # check, if the destination is writeable |
|
265 if not os.access(destination, os.W_OK): |
|
266 return False, \ |
|
267 self.trUtf8("""<p>The destination directory <b>{0}</b> is not """ |
|
268 """writeable. Aborting...</p>""").format(destination), \ |
|
269 False |
|
270 |
|
271 zip = zipfile.ZipFile(archive, "r") |
|
272 |
|
273 # check, if the archive contains a valid plugin |
|
274 pluginFound = False |
|
275 pluginFileName = "" |
|
276 for name in zip.namelist(): |
|
277 if self.__pluginManager.isValidPluginName(name): |
|
278 installedPluginName = name[:-3] |
|
279 pluginFound = True |
|
280 pluginFileName = name |
|
281 break |
|
282 |
|
283 if not pluginFound: |
|
284 return False, \ |
|
285 self.trUtf8("""<p>The file <b>{0}</b> is not a valid plugin """ |
|
286 """ZIP-archive. Aborting...</p>""").format(archive), \ |
|
287 False |
|
288 |
|
289 # parse the plugin module's plugin header |
|
290 pluginSource = zip.read(pluginFileName) |
|
291 packageName = "" |
|
292 internalPackages = [] |
|
293 needsRestart = False |
|
294 pyqtApi = 0 |
|
295 for line in pluginSource.splitlines(): |
|
296 if line.startswith("packageName"): |
|
297 tokens = line.split("=") |
|
298 if tokens[0].strip() == "packageName" and \ |
|
299 tokens[1].strip()[1:-1] != "__core__": |
|
300 if tokens[1].strip()[0] in ['"', "'"]: |
|
301 packageName = tokens[1].strip()[1:-1] |
|
302 else: |
|
303 if tokens[1].strip() == "None": |
|
304 packageName = "None" |
|
305 elif line.startswith("internalPackages"): |
|
306 tokens = line.split("=") |
|
307 token = tokens[1].strip()[1:-1] # it is a comma separated string |
|
308 internalPackages = [p.strip() for p in token.split(",")] |
|
309 elif line.startswith("needsRestart"): |
|
310 tokens = line.split("=") |
|
311 needsRestart = tokens[1].strip() == "True" |
|
312 elif line.startswith("pyqtApi"): |
|
313 tokens = line.split("=") |
|
314 try: |
|
315 pyqtApi = int(tokens[1].strip()) |
|
316 except ValueError: |
|
317 pass |
|
318 elif line.startswith("# End-Of-Header"): |
|
319 break |
|
320 |
|
321 if not packageName: |
|
322 return False, \ |
|
323 self.trUtf8("""<p>The plugin module <b>{0}</b> does not contain """ |
|
324 """a 'packageName' attribute. Aborting...</p>""")\ |
|
325 .format(pluginFileName), \ |
|
326 False |
|
327 |
|
328 if pyqtApi < 2: |
|
329 return False, \ |
|
330 self.trUtf8("""<p>The plugin module <b>{0}</b> does not conform""" |
|
331 """ with the PyQt v2 API. Aborting...</p>""")\ |
|
332 .format(pluginFileName), \ |
|
333 False |
|
334 |
|
335 # check, if it is a plugin, that collides with others |
|
336 if not os.path.exists(os.path.join(destination, pluginFileName)) and \ |
|
337 packageName != "None" and \ |
|
338 os.path.exists(os.path.join(destination, packageName)): |
|
339 return False, \ |
|
340 self.trUtf8("""<p>The plugin package <b>{0}</b> exists. """ |
|
341 """Aborting...</p>""")\ |
|
342 .format(os.path.join(destination, packageName)), \ |
|
343 False |
|
344 |
|
345 if os.path.exists(os.path.join(destination, pluginFileName)) and \ |
|
346 packageName != "None" and \ |
|
347 not os.path.exists(os.path.join(destination, packageName)): |
|
348 return False, \ |
|
349 self.trUtf8("""<p>The plugin module <b>{0}</b> exists. """ |
|
350 """Aborting...</p>""")\ |
|
351 .format(os.path.join(destination, pluginFileName)), \ |
|
352 False |
|
353 |
|
354 activatePlugin = False |
|
355 if not self.__external: |
|
356 activatePlugin = \ |
|
357 not self.__pluginManager.isPluginLoaded(installedPluginName) or \ |
|
358 (self.__pluginManager.isPluginLoaded(installedPluginName) and \ |
|
359 self.__pluginManager.isPluginActive(installedPluginName)) |
|
360 # try to unload a plugin with the same name |
|
361 self.__pluginManager.unloadPlugin(installedPluginName, destination) |
|
362 |
|
363 # uninstall existing plugin first to get clean conditions |
|
364 self.__uninstallPackage(destination, pluginFileName, packageName) |
|
365 |
|
366 # clean sys.modules |
|
367 reload_ = self.__pluginManager.removePluginFromSysModules( |
|
368 installedPluginName, packageName, internalPackages) |
|
369 |
|
370 # now do the installation |
|
371 self.__installedDirs = [] |
|
372 self.__installedFiles = [] |
|
373 try: |
|
374 if packageName != "None": |
|
375 packageDirs = ["%s/" % packageName, "%s\\" % packageName] |
|
376 namelist = zip.namelist() |
|
377 namelist.sort() |
|
378 tot = len(namelist) |
|
379 prog = 0 |
|
380 self.progress.setMaximum(tot) |
|
381 QApplication.processEvents() |
|
382 for name in namelist: |
|
383 self.progress.setValue(prog) |
|
384 QApplication.processEvents() |
|
385 prog += 1 |
|
386 if name == pluginFileName or \ |
|
387 name.startswith("%s/" % packageName) or \ |
|
388 name.startswith("%s\\" % packageName): |
|
389 outname = name.replace("/", os.sep) |
|
390 outname = os.path.join(destination, outname) |
|
391 if outname.endswith("/") or outname.endswith("\\"): |
|
392 # it is a directory entry |
|
393 outname = outname[:-1] |
|
394 if not os.path.exists(outname): |
|
395 self.__makedirs(outname) |
|
396 else: |
|
397 # it is a file |
|
398 d = os.path.dirname(outname) |
|
399 if not os.path.exists(d): |
|
400 self.__makedirs(d) |
|
401 f = open(outname, "wb") |
|
402 f.write(zip.read(name)) |
|
403 f.close() |
|
404 self.__installedFiles.append(outname) |
|
405 self.progress.setValue(tot) |
|
406 # now compile user interface files |
|
407 compileUiFiles(os.path.join(destination, packageName), True) |
|
408 else: |
|
409 outname = os.path.join(destination, pluginFileName) |
|
410 f = open(outname, "wb") |
|
411 f.write(pluginSource) |
|
412 f.close() |
|
413 self.__installedFiles.append(outname) |
|
414 except os.error, why: |
|
415 self.__rollback() |
|
416 return False, \ |
|
417 self.trUtf8("Error installing plugin. Reason: {0}").format(unicode(why)), \ |
|
418 False |
|
419 except IOError, why: |
|
420 self.__rollback() |
|
421 return False, \ |
|
422 self.trUtf8("Error installing plugin. Reason: {0}").format(unicode(why)), \ |
|
423 False |
|
424 except OSError, why: |
|
425 self.__rollback() |
|
426 return False, \ |
|
427 self.trUtf8("Error installing plugin. Reason: {0}").format(unicode(why)), \ |
|
428 False |
|
429 except: |
|
430 print >>sys.stderr, "Unspecific exception installing plugin." |
|
431 self.__rollback() |
|
432 return False, \ |
|
433 self.trUtf8("Unspecific exception installing plugin."), \ |
|
434 False |
|
435 |
|
436 # now compile the plugins |
|
437 compileall.compile_dir(destination, quiet = True) |
|
438 |
|
439 if not self.__external: |
|
440 # now load and activate the plugin |
|
441 self.__pluginManager.loadPlugin(installedPluginName, destination, reload_) |
|
442 if activatePlugin: |
|
443 self.__pluginManager.activatePlugin(installedPluginName) |
|
444 |
|
445 return True, "", needsRestart |
|
446 |
|
447 def __rollback(self): |
|
448 """ |
|
449 Private method to rollback a failed installation. |
|
450 """ |
|
451 for fname in self.__installedFiles: |
|
452 if os.path.exists(fname): |
|
453 os.remove(fname) |
|
454 for dname in self.__installedDirs: |
|
455 if os.path.exists(dname): |
|
456 shutil.rmtree(dname) |
|
457 |
|
458 def __makedirs(self, name, mode = 0777): |
|
459 """ |
|
460 Private method to create a directory and all intermediate ones. |
|
461 |
|
462 This is an extended version of the Python one in order to |
|
463 record the created directories. |
|
464 |
|
465 @param name name of the directory to create (string) |
|
466 @param mode permission to set for the new directory (integer) |
|
467 """ |
|
468 head, tail = os.path.split(name) |
|
469 if not tail: |
|
470 head, tail = os.path.split(head) |
|
471 if head and tail and not os.path.exists(head): |
|
472 self.__makedirs(head, mode) |
|
473 if tail == os.curdir: # xxx/newdir/. exists if xxx/newdir exists |
|
474 return |
|
475 os.mkdir(name, mode) |
|
476 self.__installedDirs.append(name) |
|
477 |
|
478 def __uninstallPackage(self, destination, pluginFileName, packageName): |
|
479 """ |
|
480 Private method to uninstall an already installed plugin to prepare |
|
481 the update. |
|
482 |
|
483 @param destination name of the plugin directory (string) |
|
484 @param pluginFileName name of the plugin file (string) |
|
485 @param packageName name of the plugin package (string) |
|
486 """ |
|
487 if packageName == "" or packageName == "None": |
|
488 packageDir = None |
|
489 else: |
|
490 packageDir = os.path.join(destination, packageName) |
|
491 pluginFile = os.path.join(destination, pluginFileName) |
|
492 |
|
493 try: |
|
494 if packageDir and os.path.exists(packageDir): |
|
495 shutil.rmtree(packageDir) |
|
496 |
|
497 fnameo = "%so" % pluginFile |
|
498 if os.path.exists(fnameo): |
|
499 os.remove(fnameo) |
|
500 |
|
501 fnamec = "%sc" % pluginFile |
|
502 if os.path.exists(fnamec): |
|
503 os.remove(fnamec) |
|
504 |
|
505 os.remove(pluginFile) |
|
506 except (IOError, OSError, os.error): |
|
507 # ignore some exceptions |
|
508 pass |
|
509 |
|
510 class PluginInstallDialog(QDialog): |
|
511 """ |
|
512 Class for the dialog variant. |
|
513 """ |
|
514 def __init__(self, pluginManager, pluginFileNames, parent = None): |
|
515 """ |
|
516 Constructor |
|
517 |
|
518 @param pluginManager reference to the plugin manager object |
|
519 @param pluginFileNames list of plugin files suggested for |
|
520 installation (list of strings) |
|
521 @param parent reference to the parent widget (QWidget) |
|
522 """ |
|
523 QDialog.__init__(self, parent) |
|
524 self.setSizeGripEnabled(True) |
|
525 |
|
526 self.__layout = QVBoxLayout(self) |
|
527 self.__layout.setMargin(0) |
|
528 self.setLayout(self.__layout) |
|
529 |
|
530 self.cw = PluginInstallWidget(pluginManager, pluginFileNames, self) |
|
531 size = self.cw.size() |
|
532 self.__layout.addWidget(self.cw) |
|
533 self.resize(size) |
|
534 |
|
535 self.connect(self.cw.buttonBox, SIGNAL("accepted()"), self.accept) |
|
536 self.connect(self.cw.buttonBox, SIGNAL("rejected()"), self.reject) |
|
537 |
|
538 def restartNeeded(self): |
|
539 """ |
|
540 Public method to check, if a restart of the IDE is required. |
|
541 |
|
542 @return flag indicating a restart is required (boolean) |
|
543 """ |
|
544 return self.cw.restartNeeded() |
|
545 |
|
546 class PluginInstallWindow(QMainWindow): |
|
547 """ |
|
548 Main window class for the standalone dialog. |
|
549 """ |
|
550 def __init__(self, pluginFileNames, parent = None): |
|
551 """ |
|
552 Constructor |
|
553 |
|
554 @param pluginFileNames list of plugin files suggested for |
|
555 installation (list of strings) |
|
556 @param parent reference to the parent widget (QWidget) |
|
557 """ |
|
558 QMainWindow.__init__(self, parent) |
|
559 self.cw = PluginInstallWidget(None, pluginFileNames, self) |
|
560 size = self.cw.size() |
|
561 self.setCentralWidget(self.cw) |
|
562 self.resize(size) |
|
563 |
|
564 self.connect(self.cw.buttonBox, SIGNAL("accepted()"), self.close) |
|
565 self.connect(self.cw.buttonBox, SIGNAL("rejected()"), self.close) |