diff -r f99d60d6b59b -r 2602857055c5 eric6/PluginManager/PluginInstallDialog.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/PluginManager/PluginInstallDialog.py Sun Apr 14 15:09:21 2019 +0200 @@ -0,0 +1,637 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2007 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the Plugin installation dialog. +""" + +from __future__ import unicode_literals + +import os +import sys +import shutil +import zipfile +import compileall +import glob +try: # Py3 + import urllib.parse as parse +except (ImportError): + import urlparse as parse # __IGNORE_WARNING__ + +from PyQt5.QtCore import pyqtSlot, Qt, QDir, QFileInfo +from PyQt5.QtWidgets import QWidget, QDialogButtonBox, QAbstractButton, \ + QApplication, QDialog, QVBoxLayout + +from E5Gui import E5FileDialog +from E5Gui.E5MainWindow import E5MainWindow + +from .Ui_PluginInstallDialog import Ui_PluginInstallDialog + +import Utilities +import Preferences + +from Utilities.uic import compileUiFiles + + +class PluginInstallWidget(QWidget, Ui_PluginInstallDialog): + """ + Class implementing the Plugin installation dialog. + """ + def __init__(self, pluginManager, pluginFileNames, parent=None): + """ + Constructor + + @param pluginManager reference to the plugin manager object + @param pluginFileNames list of plugin files suggested for + installation (list of strings) + @param parent parent of this dialog (QWidget) + """ + super(PluginInstallWidget, self).__init__(parent) + self.setupUi(self) + + if pluginManager is None: + # started as external plugin installer + from .PluginManager import PluginManager + self.__pluginManager = PluginManager(doLoadPlugins=False) + self.__external = True + else: + self.__pluginManager = pluginManager + self.__external = False + + self.__backButton = self.buttonBox.addButton( + self.tr("< Back"), QDialogButtonBox.ActionRole) + self.__nextButton = self.buttonBox.addButton( + self.tr("Next >"), QDialogButtonBox.ActionRole) + self.__finishButton = self.buttonBox.addButton( + self.tr("Install"), QDialogButtonBox.ActionRole) + + self.__closeButton = self.buttonBox.button(QDialogButtonBox.Close) + self.__cancelButton = self.buttonBox.button(QDialogButtonBox.Cancel) + + userDir = self.__pluginManager.getPluginDir("user") + if userDir is not None: + self.destinationCombo.addItem( + self.tr("User plugins directory"), + userDir) + + globalDir = self.__pluginManager.getPluginDir("global") + if globalDir is not None and os.access(globalDir, os.W_OK): + self.destinationCombo.addItem( + self.tr("Global plugins directory"), + globalDir) + + self.__installedDirs = [] + self.__installedFiles = [] + + self.__restartNeeded = False + + downloadDir = QDir(Preferences.getPluginManager("DownloadPath")) + for pluginFileName in pluginFileNames: + fi = QFileInfo(pluginFileName) + if fi.isRelative(): + pluginFileName = QFileInfo( + downloadDir, fi.fileName()).absoluteFilePath() + self.archivesList.addItem(pluginFileName) + self.archivesList.sortItems() + + self.__currentIndex = 0 + self.__selectPage() + + def restartNeeded(self): + """ + Public method to check, if a restart of the IDE is required. + + @return flag indicating a restart is required (boolean) + """ + return self.__restartNeeded + + def __createArchivesList(self): + """ + Private method to create a list of plugin archive names. + + @return list of plugin archive names (list of strings) + """ + archivesList = [] + for row in range(self.archivesList.count()): + archivesList.append(self.archivesList.item(row).text()) + return archivesList + + def __selectPage(self): + """ + Private method to show the right wizard page. + """ + self.wizard.setCurrentIndex(self.__currentIndex) + if self.__currentIndex == 0: + self.__backButton.setEnabled(False) + self.__nextButton.setEnabled(self.archivesList.count() > 0) + self.__finishButton.setEnabled(False) + self.__closeButton.hide() + self.__cancelButton.show() + elif self.__currentIndex == 1: + self.__backButton.setEnabled(True) + self.__nextButton.setEnabled(self.destinationCombo.count() > 0) + self.__finishButton.setEnabled(False) + self.__closeButton.hide() + self.__cancelButton.show() + else: + self.__backButton.setEnabled(True) + self.__nextButton.setEnabled(False) + self.__finishButton.setEnabled(True) + self.__closeButton.hide() + self.__cancelButton.show() + + msg = self.tr( + "Plugin ZIP-Archives:\n{0}\n\nDestination:\n{1} ({2})")\ + .format("\n".join(self.__createArchivesList()), + self.destinationCombo.currentText(), + self.destinationCombo.itemData( + self.destinationCombo.currentIndex()) + ) + self.summaryEdit.setPlainText(msg) + + @pyqtSlot() + def on_addArchivesButton_clicked(self): + """ + Private slot to select plugin ZIP-archives via a file selection dialog. + """ + dn = Preferences.getPluginManager("DownloadPath") + archives = E5FileDialog.getOpenFileNames( + self, + self.tr("Select plugin ZIP-archives"), + dn, + self.tr("Plugin archive (*.zip)")) + + if archives: + matchflags = Qt.MatchFixedString + if not Utilities.isWindowsPlatform(): + matchflags |= Qt.MatchCaseSensitive + for archive in archives: + if len(self.archivesList.findItems(archive, matchflags)) == 0: + # entry not in list already + self.archivesList.addItem(archive) + self.archivesList.sortItems() + + self.__nextButton.setEnabled(self.archivesList.count() > 0) + + @pyqtSlot() + def on_archivesList_itemSelectionChanged(self): + """ + Private slot called, when the selection of the archives list changes. + """ + self.removeArchivesButton.setEnabled( + len(self.archivesList.selectedItems()) > 0) + + @pyqtSlot() + def on_removeArchivesButton_clicked(self): + """ + Private slot to remove archives from the list. + """ + for archiveItem in self.archivesList.selectedItems(): + itm = self.archivesList.takeItem( + self.archivesList.row(archiveItem)) + del itm + + self.__nextButton.setEnabled(self.archivesList.count() > 0) + + @pyqtSlot(QAbstractButton) + def on_buttonBox_clicked(self, button): + """ + Private slot to handle the click of a button of the button box. + + @param button reference to the button pressed (QAbstractButton) + """ + if button == self.__backButton: + self.__currentIndex -= 1 + self.__selectPage() + elif button == self.__nextButton: + self.__currentIndex += 1 + self.__selectPage() + elif button == self.__finishButton: + self.__finishButton.setEnabled(False) + self.__installPlugins() + if not Preferences.getPluginManager("ActivateExternal"): + Preferences.setPluginManager("ActivateExternal", True) + self.__restartNeeded = True + self.__closeButton.show() + self.__cancelButton.hide() + + def __installPlugins(self): + """ + Private method to install the selected plugin archives. + + @return flag indicating success (boolean) + """ + res = True + self.summaryEdit.clear() + for archive in self.__createArchivesList(): + self.summaryEdit.append( + self.tr("Installing {0} ...").format(archive)) + ok, msg, restart = self.__installPlugin(archive) + res = res and ok + if ok: + self.summaryEdit.append(self.tr(" ok")) + else: + self.summaryEdit.append(msg) + if restart: + self.__restartNeeded = True + self.summaryEdit.append("\n") + if res: + self.summaryEdit.append(self.tr( + """The plugins were installed successfully.""")) + else: + self.summaryEdit.append(self.tr( + """Some plugins could not be installed.""")) + + return res + + def __installPlugin(self, archiveFilename): + """ + Private slot to install the selected plugin. + + @param archiveFilename name of the plugin archive + file (string) + @return flag indicating success (boolean), error message + upon failure (string) and flag indicating a restart + of the IDE is required (boolean) + """ + installedPluginName = "" + + archive = archiveFilename + destination = self.destinationCombo.itemData( + self.destinationCombo.currentIndex()) + + # check if archive is a local url + url = parse.urlparse(archive) + if url[0].lower() == 'file': + archive = url[2] + + # check, if the archive exists + if not os.path.exists(archive): + return False, \ + self.tr( + """<p>The archive file <b>{0}</b> does not exist. """ + """Aborting...</p>""").format(archive), \ + False + + # check, if the archive is a valid zip file + if not zipfile.is_zipfile(archive): + return False, \ + self.tr( + """<p>The file <b>{0}</b> is not a valid plugin """ + """ZIP-archive. Aborting...</p>""").format(archive), \ + False + + # check, if the destination is writeable + if not os.access(destination, os.W_OK): + return False, \ + self.tr( + """<p>The destination directory <b>{0}</b> is not """ + """writeable. Aborting...</p>""").format(destination), \ + False + + zipFile = zipfile.ZipFile(archive, "r") + + # check, if the archive contains a valid plugin + pluginFound = False + pluginFileName = "" + for name in zipFile.namelist(): + if self.__pluginManager.isValidPluginName(name): + installedPluginName = name[:-3] + pluginFound = True + pluginFileName = name + break + + if not pluginFound: + return False, \ + self.tr( + """<p>The file <b>{0}</b> is not a valid plugin """ + """ZIP-archive. Aborting...</p>""").format(archive), \ + False + + # parse the plugin module's plugin header + pluginSource = Utilities.decode(zipFile.read(pluginFileName))[0] + packageName = "" + internalPackages = [] + needsRestart = False + pyqtApi = 0 + doCompile = True + for line in pluginSource.splitlines(): + if line.startswith("packageName"): + tokens = line.split("=") + if tokens[0].strip() == "packageName" and \ + tokens[1].strip()[1:-1] != "__core__": + if tokens[1].strip()[0] in ['"', "'"]: + packageName = tokens[1].strip()[1:-1] + else: + if tokens[1].strip() == "None": + packageName = "None" + elif line.startswith("internalPackages"): + tokens = line.split("=") + token = tokens[1].strip()[1:-1] + # it is a comma separated string + internalPackages = [p.strip() for p in token.split(",")] + elif line.startswith("needsRestart"): + tokens = line.split("=") + needsRestart = tokens[1].strip() == "True" + elif line.startswith("pyqtApi"): + tokens = line.split("=") + try: + pyqtApi = int(tokens[1].strip()) + except ValueError: + pass + elif line.startswith("doNotCompile"): + tokens = line.split("=") + if tokens[1].strip() == "True": + doCompile = False + elif line.startswith("# End-Of-Header"): + break + + if not packageName: + return False, \ + self.tr( + """<p>The plugin module <b>{0}</b> does not contain """ + """a 'packageName' attribute. Aborting...</p>""")\ + .format(pluginFileName), \ + False + + if pyqtApi < 2: + return False, \ + self.tr( + """<p>The plugin module <b>{0}</b> does not conform""" + """ with the PyQt v2 API. Aborting...</p>""")\ + .format(pluginFileName), \ + False + + # check, if it is a plugin, that collides with others + if not os.path.exists(os.path.join(destination, pluginFileName)) and \ + packageName != "None" and \ + os.path.exists(os.path.join(destination, packageName)): + return False, \ + self.tr("""<p>The plugin package <b>{0}</b> exists. """ + """Aborting...</p>""")\ + .format(os.path.join(destination, packageName)), \ + False + + if os.path.exists(os.path.join(destination, pluginFileName)) and \ + packageName != "None" and \ + not os.path.exists(os.path.join(destination, packageName)): + return False, \ + self.tr("""<p>The plugin module <b>{0}</b> exists. """ + """Aborting...</p>""")\ + .format(os.path.join(destination, pluginFileName)), \ + False + + activatePlugin = False + if not self.__external: + activatePlugin = \ + not self.__pluginManager.isPluginLoaded( + installedPluginName) or \ + (self.__pluginManager.isPluginLoaded(installedPluginName) and + self.__pluginManager.isPluginActive(installedPluginName)) + # try to unload a plugin with the same name + self.__pluginManager.unloadPlugin(installedPluginName) + + # uninstall existing plug-in first to get clean conditions + if packageName != "None" and \ + not os.path.exists( + os.path.join(destination, packageName, "__init__.py")): + # package directory contains just data, don't delete it + self.__uninstallPackage(destination, pluginFileName, "") + else: + self.__uninstallPackage(destination, pluginFileName, packageName) + + # clean sys.modules + reload_ = self.__pluginManager.removePluginFromSysModules( + installedPluginName, packageName, internalPackages) + + # now do the installation + self.__installedDirs = [] + self.__installedFiles = [] + try: + if packageName != "None": + namelist = sorted(zipFile.namelist()) + tot = len(namelist) + prog = 0 + self.progress.setMaximum(tot) + QApplication.processEvents() + for name in namelist: + self.progress.setValue(prog) + QApplication.processEvents() + prog += 1 + if name == pluginFileName or \ + name.startswith("{0}/".format(packageName)) or \ + name.startswith("{0}\\".format(packageName)): + outname = name.replace("/", os.sep) + outname = os.path.join(destination, outname) + if outname.endswith("/") or outname.endswith("\\"): + # it is a directory entry + outname = outname[:-1] + if not os.path.exists(outname): + self.__makedirs(outname) + else: + # it is a file + d = os.path.dirname(outname) + if not os.path.exists(d): + self.__makedirs(d) + f = open(outname, "wb") + f.write(zipFile.read(name)) + f.close() + self.__installedFiles.append(outname) + self.progress.setValue(tot) + # now compile user interface files + compileUiFiles(os.path.join(destination, packageName), True) + else: + outname = os.path.join(destination, pluginFileName) + f = open(outname, "w", encoding="utf-8") + f.write(pluginSource) + f.close() + self.__installedFiles.append(outname) + except os.error as why: + self.__rollback() + return False, \ + self.tr( + "Error installing plugin. Reason: {0}").format(str(why)), \ + False + except IOError as why: + self.__rollback() + return False, \ + self.tr( + "Error installing plugin. Reason: {0}").format(str(why)), \ + False + except OSError as why: + self.__rollback() + return False, \ + self.tr( + "Error installing plugin. Reason: {0}").format(str(why)), \ + False + except Exception: + sys.stderr.write("Unspecific exception installing plugin.\n") + self.__rollback() + return False, \ + self.tr("Unspecific exception installing plugin."), \ + False + + # now compile the plugins + if doCompile: + dirName = os.path.join(destination, packageName) + files = os.path.join(destination, pluginFileName) + if sys.version_info[0] == 2: + dirName = dirName.encode(sys.getfilesystemencoding()) + files = files.encode(sys.getfilesystemencoding()) + os.path.join_unicode = False + compileall.compile_dir(dirName, quiet=True) + compileall.compile_file(files, quiet=True) + os.path.join_unicode = True + + if not self.__external: + # now load and activate the plugin + self.__pluginManager.loadPlugin(installedPluginName, destination, + reload_) + if activatePlugin: + self.__pluginManager.activatePlugin(installedPluginName) + + return True, "", needsRestart + + def __rollback(self): + """ + Private method to rollback a failed installation. + """ + for fname in self.__installedFiles: + if os.path.exists(fname): + os.remove(fname) + for dname in self.__installedDirs: + if os.path.exists(dname): + shutil.rmtree(dname) + + def __makedirs(self, name, mode=0o777): + """ + Private method to create a directory and all intermediate ones. + + This is an extended version of the Python one in order to + record the created directories. + + @param name name of the directory to create (string) + @param mode permission to set for the new directory (integer) + """ + head, tail = os.path.split(name) + if not tail: + head, tail = os.path.split(head) + if head and tail and not os.path.exists(head): + self.__makedirs(head, mode) + if tail == os.curdir: + # xxx/newdir/. exists if xxx/newdir exists + return + os.mkdir(name, mode) + self.__installedDirs.append(name) + + def __uninstallPackage(self, destination, pluginFileName, packageName): + """ + Private method to uninstall an already installed plugin to prepare + the update. + + @param destination name of the plugin directory (string) + @param pluginFileName name of the plugin file (string) + @param packageName name of the plugin package (string) + """ + if packageName == "" or packageName == "None": + packageDir = None + else: + packageDir = os.path.join(destination, packageName) + pluginFile = os.path.join(destination, pluginFileName) + + try: + if packageDir and os.path.exists(packageDir): + shutil.rmtree(packageDir) + + fnameo = "{0}o".format(pluginFile) + if os.path.exists(fnameo): + os.remove(fnameo) + + fnamec = "{0}c".format(pluginFile) + if os.path.exists(fnamec): + os.remove(fnamec) + + pluginDirCache = os.path.join( + os.path.dirname(pluginFile), "__pycache__") + if os.path.exists(pluginDirCache): + pluginFileName = os.path.splitext( + os.path.basename(pluginFile))[0] + for fnameo in glob.glob( + os.path.join(pluginDirCache, + "{0}*.pyo".format(pluginFileName))): + os.remove(fnameo) + for fnamec in glob.glob( + os.path.join(pluginDirCache, + "{0}*.pyc".format(pluginFileName))): + os.remove(fnamec) + + os.remove(pluginFile) + except (IOError, OSError, os.error): + # ignore some exceptions + pass + + +class PluginInstallDialog(QDialog): + """ + Class for the dialog variant. + """ + def __init__(self, pluginManager, pluginFileNames, parent=None): + """ + Constructor + + @param pluginManager reference to the plugin manager object + @param pluginFileNames list of plugin files suggested for + installation (list of strings) + @param parent reference to the parent widget (QWidget) + """ + super(PluginInstallDialog, self).__init__(parent) + self.setSizeGripEnabled(True) + + self.__layout = QVBoxLayout(self) + self.__layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(self.__layout) + + self.cw = PluginInstallWidget(pluginManager, pluginFileNames, self) + size = self.cw.size() + self.__layout.addWidget(self.cw) + self.resize(size) + self.setWindowTitle(self.cw.windowTitle()) + + self.cw.buttonBox.accepted.connect(self.accept) + self.cw.buttonBox.rejected.connect(self.reject) + + def restartNeeded(self): + """ + Public method to check, if a restart of the IDE is required. + + @return flag indicating a restart is required (boolean) + """ + return self.cw.restartNeeded() + + +class PluginInstallWindow(E5MainWindow): + """ + Main window class for the standalone dialog. + """ + def __init__(self, pluginFileNames, parent=None): + """ + Constructor + + @param pluginFileNames list of plugin files suggested for + installation (list of strings) + @param parent reference to the parent widget (QWidget) + """ + super(PluginInstallWindow, self).__init__(parent) + self.cw = PluginInstallWidget(None, pluginFileNames, self) + size = self.cw.size() + self.setCentralWidget(self.cw) + self.resize(size) + self.setWindowTitle(self.cw.windowTitle()) + + self.setStyle(Preferences.getUI("Style"), + Preferences.getUI("StyleSheet")) + + self.cw.buttonBox.accepted.connect(self.close) + self.cw.buttonBox.rejected.connect(self.close)