Thu, 30 Dec 2021 11:19:59 +0100
Updated copyright for 2022.
# -*- coding: utf-8 -*- # Copyright (c) 2018 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the PyInstaller interface plug-in. """ import contextlib import os import platform import shutil from PyQt6.QtCore import ( pyqtSlot, QObject, QCoreApplication, QTranslator, QProcess ) from PyQt6.QtWidgets import QDialog from EricWidgets import EricMessageBox from EricGui.EricAction import EricAction from EricWidgets.EricApplication import ericApp import Utilities # Start-Of-Header name = "PyInstaller Plugin" author = "Detlev Offenbach <detlev@die-offenbachs.de>" autoactivate = True deactivateable = True version = "1.0.0" className = "PyInstallerPlugin" packageName = "PyInstallerInterface" 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 # End-Of-Header error = "" 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 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(16) 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 = [] with contextlib.suppress(WindowsError, OSError): 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) 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) if not executables and majorVersion >= 3: # check the PATH environment variable if nothing was found # Python 3 only path = Utilities.getEnvironmentEntry('PATH') if path: dirs = path.split(os.pathsep) for directory in dirs: for prog in ("pyinstaller.exe", "pyi-makespec.exe"): exe = os.path.join(directory, prog) if os.access(exe, os.X_OK): executables.add(exe) 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 _exePy3 = set() versionArgs = ["-c", "import sys; print(sys.version_info[0])"] for exe in exes: with open(exe, "r") as f: 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) executables = _exePy3 # 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 @rtype bool """ global error, exePy3 exePy3 = _findExecutable(3) if not 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().__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 @rtype tuple of (None, bool) """ 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 = ericApp().getObject("Project") menu = project.getMenu("Packagers") if menu: self.__projectSeparator = menu.addSeparator() # Execute PyInstaller act = EricAction( 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 = EricAction( 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 = EricAction( 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.addEricActions(self.__projectActs) project.showMenu.connect(self.__projectShowMenu) return None, True def deactivate(self): """ Public method to deactivate this plug-in. """ menu = ericApp().getObject("Project").getMenu("Packagers") if menu: for act in self.__projectActs: menu.removeAction(act) if self.__projectSeparator: menu.removeAction(self.__projectSeparator) ericApp().getObject("Project").removeEricActions( 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 = ( ericApp().getObject("Project").getProjectLanguage() == "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__), "PyInstallerInterface", "i18n") translation = "pyinstaller_{0}".format(loc) translator = QTranslator(None) loaded = translator.load(translation, locale_dir) if loaded: self.__translator = translator ericApp().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 = ericApp().getObject("Project") majorVersionStr = project.getProjectLanguage() if majorVersionStr == "Python3": executables = [f for f in exePy3 if f.endswith(("pyinstaller", "pyinstaller.exe"))] if not executables: EricMessageBox.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 PyInstallerInterface.PyInstallerConfigDialog import ( PyInstallerConfigDialog ) params = project.getData('PACKAGERSPARMS', "PYINSTALLER") dlg = PyInstallerConfigDialog(project, executables, params, mode="installer") if dlg.exec() == QDialog.DialogCode.Accepted: args, params, script = dlg.generateParameters() project.setData('PACKAGERSPARMS', "PYINSTALLER", params) # now do the call from PyInstallerInterface.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 = ericApp().getObject("Project") majorVersionStr = project.getProjectLanguage() if majorVersionStr == "Python3": executables = [f for f in exePy3 if f.endswith(("pyi-makespec", "pyi-makespec.exe"))] if not executables: EricMessageBox.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 PyInstallerInterface.PyInstallerConfigDialog import ( PyInstallerConfigDialog ) params = project.getData('PACKAGERSPARMS', "PYINSTALLER") dlg = PyInstallerConfigDialog(project, executables, params, mode="spec") if dlg.exec() == QDialog.DialogCode.Accepted: args, params, script = dlg.generateParameters() project.setData('PACKAGERSPARMS', "PYINSTALLER", params) # now do the call from PyInstallerInterface.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 = ericApp().getObject("Project") from PyInstallerInterface.PyInstallerCleanupDialog import ( PyInstallerCleanupDialog ) dlg = PyInstallerCleanupDialog() if dlg.exec() == QDialog.DialogCode.Accepted: removeDirs = dlg.getDirectories() for directory in removeDirs: rd = os.path.join(project.getProjectPath(), directory) shutil.rmtree(rd, ignore_errors=True) def installDependencies(pipInstall): """ Function to install dependencies of this plug-in. @param pipInstall function to be called with a list of package names. @type function """ try: import PyInstaller # __IGNORE_WARNING__ except ImportError: pipInstall(["pyinstaller"]) # # eflag: noqa = M801