PluginPyInstaller.py

Sat, 03 Feb 2018 17:09:46 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 03 Feb 2018 17:09:46 +0100
changeset 16
11732bdf5b26
parent 12
1492fbafd273
child 18
4693332eb716
permissions
-rw-r--r--

Prepared release of RC1.

# -*- 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 = "0.9.9"
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 = []
    
    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:
            # 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)
            
            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

eric ide

mercurial