Sun, 22 Apr 2018 14:34:47 +0200
Added a separator to the project packagers menu.
# -*- coding: utf-8 -*- # Copyright (c) 2018 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the PyInstaller interface plug-in. """ from __future__ import unicode_literals try: str = unicode except NameError: pass import os import platform import shutil from PyQt5.QtCore import pyqtSlot, QObject, QCoreApplication, QTranslator, \ QProcess from PyQt5.QtWidgets import QDialog from E5Gui import E5MessageBox from E5Gui.E5Action import E5Action from E5Gui.E5Application import e5App import Utilities # Start-Of-Header name = "PyInstaller Plugin" author = "Detlev Offenbach <detlev@die-offenbachs.de>" autoactivate = True deactivateable = True version = "1.0.1" className = "PyInstallerPlugin" packageName = "PyInstaller" shortDescription = "Show dialogs to configure and execute PyInstaller." longDescription = ( """This plug-in implements dialogs to configure and execute PyInstaller""" """ for an eric project. PyInstaller must be available or must be""" """ installed via 'pip install PyInstaller'.""" ) needsRestart = False pyqtApi = 2 python2Compatible = True # End-Of-Header error = "" exePy2 = [] exePy3 = [] def exeDisplayDataList(): """ Module function to support the display of some executable info. @return list of dictionaries containing the data to query the presence of the executable @rtype list of dict """ dataList = [] data = { "programEntry": True, "header": QCoreApplication.translate( "PyInstallerPlugin", "Packagers - PyInstaller"), "exe": "dummyExe", "versionCommand": "--version", "versionStartsWith": "dummyExe", "versionPosition": -1, "version": "", "versionCleanup": None, "versionRe": "^\d", } if _checkProgram(): for exePath in (exePy2 + exePy3): data["exe"] = exePath data["versionStartsWith"] = "" dataList.append(data.copy()) else: dataList.append(data) return dataList def _findExecutable(majorVersion): """ Restricted function to determine the names of the executables. @param majorVersion major python version @type int @return names of the executables @rtype list of str """ # Determine Python Version if majorVersion == 3: minorVersions = range(10) elif majorVersion == 2: minorVersions = [7] # PyInstaller supports just Python 2.7 else: return [] executables = set() if Utilities.isWindowsPlatform(): # # Windows # try: import winreg except ImportError: import _winreg as winreg # __IGNORE_WARNING__ def getExePath(branch, access, versionStr): exes = [] try: software = winreg.OpenKey(branch, 'Software', 0, access) python = winreg.OpenKey(software, 'Python', 0, access) pcore = winreg.OpenKey(python, 'PythonCore', 0, access) version = winreg.OpenKey(pcore, versionStr, 0, access) installpath = winreg.QueryValue(version, 'InstallPath') # Look for pyinstaller.exe exe = os.path.join(installpath, 'Scripts', 'pyinstaller.exe') if os.access(exe, os.X_OK): exes.append(exe) # Look for pyi-makespec.exe exe = os.path.join(installpath, 'Scripts', 'pyi-makespec.exe') if os.access(exe, os.X_OK): exes.append(exe) except (WindowsError, OSError): # __IGNORE_WARNING__ pass return exes versionSuffixes = ["", "-32", "-64"] for minorVersion in minorVersions: for versionSuffix in versionSuffixes: versionStr = '{0}.{1}{2}'.format(majorVersion, minorVersion, versionSuffix) exePaths = getExePath( winreg.HKEY_CURRENT_USER, winreg.KEY_WOW64_32KEY | winreg.KEY_READ, versionStr) for exePath in exePaths: executables.add(exePath) exePaths = getExePath( winreg.HKEY_LOCAL_MACHINE, winreg.KEY_WOW64_32KEY | winreg.KEY_READ, versionStr) for exePath in exePaths: executables.add(exePath) # Even on Intel 64-bit machines it's 'AMD64' if platform.machine() == 'AMD64': exePaths = getExePath( winreg.HKEY_CURRENT_USER, winreg.KEY_WOW64_64KEY | winreg.KEY_READ, versionStr) for exePath in exePaths: executables.add(exePath) exePaths = getExePath( winreg.HKEY_LOCAL_MACHINE, winreg.KEY_WOW64_64KEY | winreg.KEY_READ, versionStr) for exePath in exePaths: executables.add(exePath) else: # # Linux, Unix ... pyinstallerScripts = ['pyinstaller', 'pyi-makespec'] # There could be multiple pyinstaller executables in the path # e.g. for different python variants path = Utilities.getEnvironmentEntry('PATH') # environment variable not defined if path is None: return [] # step 1: determine possible candidates exes = [] dirs = path.split(os.pathsep) for directory in dirs: for pyinstallerScript in pyinstallerScripts: exe = os.path.join(directory, pyinstallerScript) if os.access(exe, os.X_OK): exes.append(exe) # step 2: determine the Python variant _exePy2 = set() _exePy3 = set() versionArgs = ["-c", "import sys; print(sys.version_info[0])"] for exe in exes: try: f = open(exe, "r") line0 = f.readline() program = line0.replace("#!", "").strip() process = QProcess() process.start(program, versionArgs) process.waitForFinished(5000) # get a QByteArray of the output versionBytes = process.readAllStandardOutput() versionStr = str(versionBytes, encoding='utf-8').strip() if versionStr == "3": _exePy3.add(exe) elif versionStr == "2": _exePy2.add(exe) finally: f.close() executables = _exePy3 if majorVersion == 3 else _exePy2 # sort items, the probably newest topmost executables = list(executables) executables.sort(reverse=True) return executables def _checkProgram(): """ Restricted function to check the availability of pyinstaller. @return flag indicating availability (boolean) """ global error, exePy2, exePy3 exePy2 = _findExecutable(2) exePy3 = _findExecutable(3) if (exePy2 + exePy3) == []: if Utilities.isWindowsPlatform(): error = QCoreApplication.translate( "PyInstallerPlugin", "The pyinstaller.exe executable could not be found." ) else: error = QCoreApplication.translate( "PyInstallerPlugin", "The pyinstaller executable could not be found." ) return False else: return True class PyInstallerPlugin(QObject): """ Class implementing the PyInstaller interface plug-in. """ def __init__(self, ui): """ Constructor @param ui reference to the user interface object @type UI.UserInterface """ super(PyInstallerPlugin, self).__init__(ui) self.__ui = ui self.__initialize() _checkProgram() self.__translator = None self.__loadTranslator() def __initialize(self): """ Private slot to (re)initialize the plug-in. """ self.__projectActs = [] self.__projectSeparator = None def activate(self): """ Public method to activate this plug-in. @return tuple of None and activation status (boolean) """ global error # There is already an error, don't activate if error: return None, False # pyinstaller interface is only activated if it is available if not _checkProgram(): return None, False # clear previous error error = "" project = e5App().getObject("Project") menu = project.getMenu("Packagers") if menu: self.__projectSeparator = menu.addSeparator() # Execute PyInstaller act = E5Action( self.tr('Execute PyInstaller'), self.tr('Execute Py&Installer'), 0, 0, self, 'packagers_pyinstaller_run') act.setStatusTip( self.tr('Generate a distribution package using PyInstaller')) act.setWhatsThis(self.tr( """<b>Execute PyInstaller</b>""" """<p>Generate a distribution package using PyInstaller.""" """ The command is executed in the project path. All""" """ files and directories must be given as absolute paths or""" """ as paths relative to the project path.</p>""" )) act.triggered.connect(self.__pyinstaller) menu.addAction(act) self.__projectActs.append(act) # Execute pyi-makespec act = E5Action( self.tr('Make PyInstaller Spec File'), self.tr('Make PyInstaller &Spec File'), 0, 0, self, 'packagers_pyinstaller_spec') act.setStatusTip( self.tr('Generate a spec file to be used by PyInstaller')) act.setWhatsThis(self.tr( """<b>Make PyInstaller Spec File</b>""" """<p>Generate a spec file to be used by PyInstaller.""" """ The command is executed in the project path. All""" """ files and directories must be given as absolute paths or""" """ as paths relative to the project path.</p>""" )) act.triggered.connect(self.__pyiMakeSpec) menu.addAction(act) self.__projectActs.append(act) # clean the pyinstaller created directories act = E5Action( self.tr('Clean PyInstaller'), self.tr('&Clean PyInstaller'), 0, 0, self, 'packagers_pyinstaller_clean') act.setStatusTip( self.tr('Remove the PyInstaller created directories')) act.setWhatsThis(self.tr( """<b>Clean PyInstaller</b>""" """<p>Remove the PyInstaller created directories (dist and""" """ build). These are subdirectories within the project""" """ path.</p>""" )) act.triggered.connect(self.__pyinstallerCleanup) menu.addAction(act) self.__projectActs.append(act) project.addE5Actions(self.__projectActs) project.showMenu.connect(self.__projectShowMenu) return None, True def deactivate(self): """ Public method to deactivate this plug-in. """ menu = e5App().getObject("Project").getMenu("Packagers") if menu: for act in self.__projectActs: menu.removeAction(act) if self.__projectSeparator: menu.removeAction(self.__projectSeparator) e5App().getObject("Project").removeE5Actions( self.__projectActs) self.__initialize() def __projectShowMenu(self, menuName, menu): """ Private slot called, when the the project menu or a submenu is about to be shown. @param menuName name of the menu to be shown @type str @param menu reference to the menu @type QMenu """ if menuName == "Packagers": enable = e5App().getObject("Project").getProjectLanguage() in \ ["Python", "Python2", "Python3"] for act in self.__projectActs: act.setEnabled(enable) def __loadTranslator(self): """ Private method to load the translation file. """ if self.__ui is not None: loc = self.__ui.getLocale() if loc and loc != "C": locale_dir = \ os.path.join(os.path.dirname(__file__), "PyInstaller", "i18n") translation = "pyinstaller_{0}".format(loc) translator = QTranslator(None) loaded = translator.load(translation, locale_dir) if loaded: self.__translator = translator e5App().installTranslator(self.__translator) else: print("Warning: translation file '{0}' could not be" " loaded.".format(translation)) print("Using default.") @pyqtSlot() def __pyinstaller(self): """ Private slot to execute the pyinstaller command for the current project. """ project = e5App().getObject("Project") majorVersionStr = project.getProjectLanguage() if majorVersionStr == "Python3": executables = [f for f in exePy3 if f.endswith(("pyinstaller", "pyinstaller.exe"))] else: executables = [f for f in exePy2 if f.endswith(("pyinstaller", "pyinstaller.exe"))] if not executables: E5MessageBox.critical( self.__ui, self.tr("pyinstaller"), self.tr("""The pyinstaller executable could not be found.""")) return # check if all files saved and errorfree before continue if not project.checkAllScriptsDirty(reportSyntaxErrors=True): return from PyInstaller.PyInstallerConfigDialog import PyInstallerConfigDialog params = project.getData('PACKAGERSPARMS', "PYINSTALLER") dlg = PyInstallerConfigDialog(project, executables, params, mode="installer") if dlg.exec_() == QDialog.Accepted: args, params, script = dlg.generateParameters() project.setData('PACKAGERSPARMS', "PYINSTALLER", params) # now do the call from PyInstaller.PyInstallerExecDialog import PyInstallerExecDialog dia = PyInstallerExecDialog("pyinstaller") dia.show() res = dia.start(args, params, project, script) if res: dia.exec_() @pyqtSlot() def __pyiMakeSpec(self): """ Private slot to execute the pyi-makespec command for the current project to generate a spec file to be used by pyinstaller. """ project = e5App().getObject("Project") majorVersionStr = project.getProjectLanguage() if majorVersionStr == "Python3": executables = [f for f in exePy3 if f.endswith(("pyi-makespec", "pyi-makespec.exe"))] else: executables = [f for f in exePy2 if f.endswith(("pyi-makespec", "pyi-makespec.exe"))] if not executables: E5MessageBox.critical( self.__ui, self.tr("pyi-makespec"), self.tr("""The pyi-makespec executable could not be found.""")) return # check if all files saved and errorfree before continue if not project.checkAllScriptsDirty(reportSyntaxErrors=True): return from PyInstaller.PyInstallerConfigDialog import PyInstallerConfigDialog params = project.getData('PACKAGERSPARMS', "PYINSTALLER") dlg = PyInstallerConfigDialog(project, executables, params, mode="spec") if dlg.exec_() == QDialog.Accepted: args, params, script = dlg.generateParameters() project.setData('PACKAGERSPARMS', "PYINSTALLER", params) # now do the call from PyInstaller.PyInstallerExecDialog import PyInstallerExecDialog dia = PyInstallerExecDialog("pyinstaller") dia.show() res = dia.start(args, params, project, script) if res: dia.exec_() @pyqtSlot() def __pyinstallerCleanup(self): """ Private slot to remove the directories created by pyinstaller. """ project = e5App().getObject("Project") from PyInstaller.PyInstallerCleanupDialog import \ PyInstallerCleanupDialog dlg = PyInstallerCleanupDialog() if dlg.exec_() == QDialog.Accepted: removeDirs = dlg.getDirectories() for directory in removeDirs: rd = os.path.join(project.getProjectPath(), directory) shutil.rmtree(rd, ignore_errors=True) # # eflag: noqa = M801