PluginManager/PluginInstallDialog.py

changeset 0
de9c2efb9d02
child 7
c679fb30c8f3
equal deleted inserted replaced
-1:000000000000 0:de9c2efb9d02
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)

eric ide

mercurial