PluginPyInstaller.py

Sat, 23 Dec 2023 16:30:39 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 23 Dec 2023 16:30:39 +0100
branch
eric7
changeset 55
3794f1ca53af
parent 54
359e2d772474
child 56
02709629940d
permissions
-rw-r--r--

Corrected some code style issues and converted some source code documentation to the new style.

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

# Copyright (c) 2018 - 2024 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 QCoreApplication, QObject, QProcess, QTranslator, pyqtSlot
from PyQt6.QtWidgets import QDialog

from eric7.EricGui.EricAction import EricAction
from eric7.EricWidgets import EricMessageBox
from eric7.EricWidgets.EricApplication import ericApp

try:
    from eric7.SystemUtilities.OSUtilities import getEnvironmentEntry, isWindowsPlatform
except ImportError:
    # imports for eric < 23.1
    from eric7.Globals import isWindowsPlatform
    from eric7.Utilities import getEnvironmentEntry

# Start-Of-Header
name = "PyInstaller Plugin"
author = "Detlev Offenbach <detlev@die-offenbachs.de>"
autoactivate = True
deactivateable = True
version = "10.2.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 isWindowsPlatform():
        #
        # Windows
        #
        try:
            import winreg  # noqa: I101, I103
        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 = 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 = 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 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):  # noqa: U100
        """
        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.
        """
        from PyInstallerInterface.PyInstallerConfigDialog import PyInstallerConfigDialog
        from PyInstallerInterface.PyInstallerExecDialog import PyInstallerExecDialog

        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

            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
                dia = PyInstallerExecDialog("pyinstaller")
                dia.show()
                res = dia.start(args, 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.
        """
        from PyInstallerInterface.PyInstallerConfigDialog import PyInstallerConfigDialog
        from PyInstallerInterface.PyInstallerExecDialog import PyInstallerExecDialog

        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

            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
                dia = PyInstallerExecDialog("pyinstaller")
                dia.show()
                res = dia.start(args, project, script)
                if res:
                    dia.exec()

    @pyqtSlot()
    def __pyinstallerCleanup(self):
        """
        Private slot to remove the directories created by pyinstaller.
        """
        from PyInstallerInterface.PyInstallerCleanupDialog import (
            PyInstallerCleanupDialog,
        )

        project = ericApp().getObject("Project")

        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, U200, I102

eric ide

mercurial