PluginPyInstaller.py

Thu, 27 May 2021 19:23:58 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Thu, 27 May 2021 19:23:58 +0200
branch
eric7
changeset 37
9ecfea29a47c
parent 35
d9b3cadaf707
child 38
fc9ef9dcd51a
permissions
-rw-r--r--

Created new branch <eric7>.

# -*- coding: utf-8 -*-

# Copyright (c) 2018 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the PyInstaller interface plug-in.
"""

import contextlib
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 = "2.2.0"
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
# 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(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 = []
            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 (boolean)
    """
    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 (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() == "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"))]
            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"))]
            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

eric ide

mercurial