CxFreeze/CxfreezeConfigDialog.py

Tue, 10 Dec 2024 15:48:55 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Tue, 10 Dec 2024 15:48:55 +0100
branch
eric7
changeset 149
8f00ab5d72a2
parent 145
c423d46df27e
permissions
-rw-r--r--

Updated copyright for 2025.

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

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

"""
Module implementing a dialog to enter the parameters for cxfreeze.
"""

import contextlib
import copy
import os
import sys

from PyQt6.QtCore import QDir, QProcess, pyqtSlot
from PyQt6.QtWidgets import QDialog, QListWidgetItem

try:
    from eric7.EricGui import EricPixmapCache
except ImportError:
    from UI import PixmapCache as EricPixmapCache

from eric7.EricWidgets.EricDirFileDialog import EricDirFileDialog
from eric7.EricWidgets.EricPathPicker import EricPathPickerModes

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

from .Ui_CxfreezeConfigDialog import Ui_CxfreezeConfigDialog


class CxfreezeConfigDialog(QDialog, Ui_CxfreezeConfigDialog):
    """
    Class implementing a dialog to enter the parameters for cxfreeze.
    """

    def __init__(self, project, exe, parms=None, parent=None):
        """
        Constructor

        @param project reference to the project object
        @type Project
        @param exe name of the cxfreeze executable
        @type str
        @param parms parameters to set in the dialog
        @type dict
        @param parent parent widget of this dialog
        @type QWidget
        """
        QDialog.__init__(self, parent)
        self.setupUi(self)

        self.selectFileOrFolderButton.setIcon(EricPixmapCache.getIcon("open.png"))

        self.targetDirPicker.setMode(EricPathPickerModes.DIRECTORY_MODE)
        self.targetDirPicker.setWindowTitle(self.tr("Select target directory"))

        iconsI18N = self.tr("Icons")
        allFilesI18N = self.tr("All files")
        if isWindowsPlatform():
            iconFilter = "{0} (*.ico);;{1} (*.*)".format(iconsI18N, allFilesI18N)
        elif isMacPlatform():
            iconFilter = "{0} (*.icns *.png);;{1} (*.*)".format(iconsI18N, allFilesI18N)
        else:
            iconFilter = "{0} (*.png);;{1} (*.*)".format(iconsI18N, allFilesI18N)
        self.applicationIconPicker.setMode(EricPathPickerModes.OPEN_FILE_MODE)
        self.applicationIconPicker.setWindowTitle(
            self.tr("Select the application icon")
        )
        self.applicationIconPicker.setFilters(iconFilter)

        self.extListFilePicker.setMode(EricPathPickerModes.OPEN_FILE_MODE)
        self.extListFilePicker.setWindowTitle(self.tr("Select external list file"))

        self.__project = project
        self.__initializeDefaults()

        # get a copy of the defaults to store the user settings
        self.__parameters = copy.deepcopy(self.defaults)

        # combine it with the values of parms
        if parms is not None:
            self.__parameters.update(parms)

        self.cxfreezeExecCombo.addItems(exe)
        # try to set the saved script path
        with contextlib.suppress(ValueError):
            idx = exe.index(self.__parameters["script"])
            self.cxfreezeExecCombo.setCurrentIndex(idx)

        # initialize general tab
        self.targetDirPicker.setText(self.__parameters["targetDirectory"])
        self.targetNameEdit.setText(self.__parameters["targetName"])
        self.basenameCombo.setEditText(self.__parameters["baseName"])
        self.initscriptCombo.setEditText(self.__parameters["initScript"])
        self.applicationIconPicker.setText(self.__parameters["applicationIcon"])
        self.keeppathCheckBox.setChecked(self.__parameters["keepPath"])
        self.compressCheckBox.setChecked(self.__parameters["compress"])
        if self.__parameters["optimize"] == 0:
            self.nooptimizeRadioButton.setChecked(True)
        elif self.__parameters["optimize"] == 1:
            self.optimizeRadioButton.setChecked(True)
        else:
            self.optimizeDocRadioButton.setChecked(True)

        # initialize advanced tab
        self.defaultPathEdit.setText(os.pathsep.join(self.__parameters["defaultPath"]))
        self.includePathEdit.setText(os.pathsep.join(self.__parameters["includePath"]))
        self.replacePathsEdit.setText(
            os.pathsep.join(self.__parameters["replacePaths"])
        )
        self.includeModulesEdit.setText(",".join(self.__parameters["includeModules"]))
        self.excludeModulesEdit.setText(",".join(self.__parameters["excludeModules"]))
        self.extListFilePicker.setText(self.__parameters["extListFile"])

        # initialize additional files tab
        self.fileOrFolderList.addItems(self.__parameters["additionalFiles"])

    def __initializeDefaults(self):
        """
        Private method to set the default values.

        These are needed later on to generate the command line parameters.
        """
        self.defaults = {
            # general options
            "targetDirectory": "",
            "targetName": "",
            "baseName": "Console",
            "initScript": "Console",
            "applicationIcon": "",
            "script": "",
            "keepPath": False,
            "compress": False,
            "optimize": 0,  # 0, 1 or 2
            # advanced options
            "defaultPath": [],
            "includePath": [],
            "replacePaths": [],
            "includeModules": [],
            "excludeModules": [],
            "extListFile": "",
            # additional files tab
            "additionalFiles": [],
        }
        # overwrite 'baseName' if OS is Windows
        if sys.platform == "win32":
            self.defaults["baseName"] = "Win32GUI"
        # overwrite 'initScript' if version 3 interpreter
        if self.__project.getProjectLanguage() == "Python3":
            self.defaults["initScript"] = "Console3"

    def generateParameters(self):
        """
        Public method that generates the command line parameters.

        It generates a list of strings to be used to set the QProcess arguments
        for the cxfreeze call and a list containing the non default parameters.
        The second list can be passed back upon object generation to overwrite
        the default settings.

        @return a tuple of the command line parameters and non default
            parameters
        @rtype tuple of (list of str, dict)
        """
        parms = {}
        args = []

        # 1. the program name
        args.append(self.cxfreezeExecCombo.currentText())

        # 2. the commandline options
        # 2.1 general options
        if self.__parameters["targetDirectory"] != self.defaults["targetDirectory"]:
            parms["targetDirectory"] = self.__parameters["targetDirectory"]
            args.append("--target-dir={0}".format(self.__parameters["targetDirectory"]))
        if self.__parameters["targetName"] != self.defaults["targetName"]:
            parms["targetName"] = self.__parameters["targetName"][:]
            args.append("--target-name={0}".format(self.__parameters["targetName"]))
        parms["baseName"] = self.__parameters["baseName"][:]
        if self.__parameters["baseName"] != "":
            args.append("--base-name={0}".format(self.__parameters["baseName"]))
        parms["initScript"] = self.__parameters["initScript"][:]
        if self.__parameters["initScript"] != "":
            args.append("--init-script={0}".format(self.__parameters["initScript"]))
        parms["applicationIcon"] = self.__parameters["applicationIcon"][:]
        if self.__parameters["applicationIcon"] != self.defaults["applicationIcon"]:
            args.append("--icon={0}".format(self.__parameters["applicationIcon"]))
        parms["script"] = self.__parameters["script"][:]
        if self.__parameters["keepPath"] != self.defaults["keepPath"]:
            parms["keepPath"] = self.__parameters["keepPath"]
            args.append("--no-copy-deps")
        if self.__parameters["compress"] != self.defaults["compress"]:
            parms["compress"] = self.__parameters["compress"]
            args.append("--compress")
        if self.__parameters["optimize"] != self.defaults["optimize"]:
            parms["optimize"] = self.__parameters["optimize"]
            if self.__parameters["optimize"] == 1:
                args.append("-O")
            elif self.__parameters["optimize"] == 2:
                args.append("-OO")

        # 2.2 advanced options
        if self.__parameters["defaultPath"] != self.defaults["defaultPath"]:
            parms["defaultPath"] = self.__parameters["defaultPath"][:]
            args.append(
                "--default-path={0}".format(
                    os.pathsep.join(self.__parameters["defaultPath"])
                )
            )
        if self.__parameters["includePath"] != self.defaults["includePath"]:
            parms["includePath"] = self.__parameters["includePath"][:]
            args.append(
                "--include-path={0}".format(
                    os.pathsep.join(self.__parameters["includePath"])
                )
            )
        if self.__parameters["replacePaths"] != self.defaults["replacePaths"]:
            parms["replacePaths"] = self.__parameters["replacePaths"][:]
            args.append(
                "--replace-paths={0}".format(
                    os.pathsep.join(self.__parameters["replacePaths"])
                )
            )
        if self.__parameters["includeModules"] != self.defaults["includeModules"]:
            parms["includeModules"] = self.__parameters["includeModules"][:]
            args.append(
                "--include-modules={0}".format(
                    ",".join(self.__parameters["includeModules"])
                )
            )
        if self.__parameters["excludeModules"] != self.defaults["excludeModules"]:
            parms["excludeModules"] = self.__parameters["excludeModules"][:]
            args.append(
                "--exclude-modules={0}".format(
                    ",".join(self.__parameters["excludeModules"])
                )
            )
        if self.__parameters["extListFile"] != self.defaults["extListFile"]:
            parms["extListFile"] = self.__parameters["extListFile"]
            args.append("--ext-list-file={0}".format(self.__parameters["extListFile"]))

        # 2.3 additional files tab
        if self.__parameters["additionalFiles"] != []:
            parms["additionalFiles"] = self.__parameters["additionalFiles"][:]

        return (args, parms)

    @pyqtSlot(str)
    def on_cxfreezeExecCombo_currentIndexChanged(self, text):
        """
        Private slot to handle the selection of a cxfreeze executable.

        @param text selected cxfreeze executable
        @type str
        """
        # version specific setup
        if isWindowsPlatform():
            # remove "\Scripts\cx_Freeze.bat" from path
            dirname = os.path.dirname(text)
            dirname = os.path.dirname(dirname)

            # first try the fast way
            modpath = os.path.join(dirname, "Lib", "site-packages", "cx_Freeze")
            if not os.path.exists(modpath):
                # but if it failed search in the whole directory tree
                modpath = None
                for dirpath, dirnames, _ in os.walk(dirname):
                    if "cx_Freeze" in dirnames:
                        modpath = os.path.join(dirpath, "cx_Freeze")
                        break
        else:
            with open(text, "r") as f:
                args = f.readline()
            if not args:
                return

            args = args.strip("!#\n").split(" ")
            program = args.pop(0)

            script = os.path.join(
                os.path.dirname(os.path.abspath(__file__)), "CxfreezeFindPath.py"
            )
            if not os.path.exists(script):
                return

            args.append(script)
            process = QProcess()
            process.start(program, args)
            process.waitForFinished(5000)
            # get a QByteArray of the output
            cxPath = process.readAllStandardOutput()
            modpath = str(cxPath, encoding="utf-8").strip("\n\r")
            if not modpath.endswith("cx_Freeze"):
                return

        # populate combo boxes
        if modpath:
            d = QDir(os.path.join(modpath, "bases"))
            basesList = d.entryList(QDir.Filter.Files)
            if isWindowsPlatform():
                # strip the final '.exe' from the bases
                tmpBasesList = basesList[:]
                basesList = []
                for b in tmpBasesList:
                    base, ext = os.path.splitext(b)
                    if ext == ".exe":
                        basesList.append(base)
                    else:
                        basesList.append(b)

            basesList.insert(0, "")
            currentText = self.basenameCombo.currentText()
            self.basenameCombo.clear()
            self.basenameCombo.addItems(basesList)
            self.basenameCombo.setEditText(currentText)

            d = QDir(os.path.join(modpath, "initscripts"))
            initList = d.entryList(["*.py"])
            initList.insert(0, "")
            currentText = self.initscriptCombo.currentText()
            self.initscriptCombo.clear()
            self.initscriptCombo.addItems([os.path.splitext(i)[0] for i in initList])
            self.initscriptCombo.setEditText(currentText)

    @pyqtSlot(int)
    def on_fileOrFolderList_currentRowChanged(self, row):
        """
        Private slot to handle the currentRowChanged signal of the
        fileOrFolderList.

        @param row the current row
        @type int
        """
        self.deleteSelectedButton.setEnabled(row != -1)
        if row != -1:
            self.fileOrFolderList.setCurrentRow(row)

    @pyqtSlot(QListWidgetItem)
    def on_fileOrFolderList_itemDoubleClicked(self, itm):
        """
        Private slot to handle the itemDoubleClicked signal of the
        fileOrFolderList.

        @param itm the selected row
        @type QListWidgetItem
        """
        self.fileOrFolderEdit.setText(itm.text())
        row = self.fileOrFolderList.currentRow()
        itm = self.fileOrFolderList.takeItem(row)
        del itm

    @pyqtSlot()
    def on_addFileOrFolderButton_clicked(self):
        """
        Private slot to add the entered file or directory to the list view.
        """
        txt = self.fileOrFolderEdit.text()
        if txt:
            self.fileOrFolderList.addItem(txt)
            self.fileOrFolderEdit.clear()
        row = self.fileOrFolderList.currentRow()
        self.on_fileOrFolderList_currentRowChanged(row)

    @pyqtSlot(str)
    def on_fileOrFolderEdit_textChanged(self, txt):
        """
        Private slot to handle the textChanged signal of the directory edit.

        @param txt the text of the directory edit
        @type str
        """
        self.addFileOrFolderButton.setEnabled(txt != "")

    @pyqtSlot()
    def on_deleteSelectedButton_clicked(self):
        """
        Private slot to delete the selected entry from the list view.
        """
        row = self.fileOrFolderList.currentRow()
        itm = self.fileOrFolderList.takeItem(row)
        del itm
        row = self.fileOrFolderList.currentRow()
        self.on_fileOrFolderList_currentRowChanged(row)

    @pyqtSlot()
    def on_selectFileOrFolderButton_clicked(self):
        """
        Private slot to select files or folders.

        It displays a file and directory selection dialog to
        select the files and directories which should be copied
        into the distribution folder.
        """
        items = EricDirFileDialog.getOpenFileAndDirNames(
            self, self.tr("Select files and folders"), self.__project.getProjectPath()
        )

        for itm in items:
            itm = self.__project.getRelativePath(itm)
            self.fileOrFolderList.addItem(toNativeSeparators(itm))
        row = self.fileOrFolderList.currentRow()
        self.on_fileOrFolderList_currentRowChanged(row)

    def accept(self):
        """
        Public method called by the Ok button.

        It saves the values in the parameters dictionary.
        """
        # get data of general tab
        self.__parameters["targetDirectory"] = self.__project.getRelativePath(
            self.targetDirPicker.text()
        )
        self.__parameters["targetName"] = self.targetNameEdit.text()
        self.__parameters["baseName"] = self.basenameCombo.currentText()
        self.__parameters["initScript"] = self.initscriptCombo.currentText()
        self.__parameters["applicationIcon"] = self.__project.getRelativePath(
            self.applicationIconPicker.text()
        )
        self.__parameters["script"] = self.cxfreezeExecCombo.currentText()
        self.__parameters["keepPath"] = self.keeppathCheckBox.isChecked()
        self.__parameters["compress"] = self.compressCheckBox.isChecked()
        if self.nooptimizeRadioButton.isChecked():
            self.__parameters["optimize"] = 0
        elif self.optimizeRadioButton.isChecked():
            self.__parameters["optimize"] = 1
        else:
            self.__parameters["optimize"] = 2

        # get data of advanced tab
        self.__parameters["defaultPath"] = self.__splitIt(
            self.defaultPathEdit.text(), os.pathsep
        )
        self.__parameters["includePath"] = self.__splitIt(
            self.includePathEdit.text(), os.pathsep
        )
        self.__parameters["replacePaths"] = self.__splitIt(
            self.replacePathsEdit.text(), os.pathsep
        )
        self.__parameters["includeModules"] = self.__splitIt(
            self.includeModulesEdit.text(), ","
        )
        self.__parameters["excludeModules"] = self.__splitIt(
            self.excludeModulesEdit.text(), ","
        )
        self.__parameters["extListFile"] = self.__project.getRelativePath(
            self.extListFilePicker.text()
        )

        # get data of the additional files tab
        additionalFiles = [
            self.fileOrFolderList.item(x).text()
            for x in range(self.fileOrFolderList.count())
        ]
        self.__parameters["additionalFiles"] = additionalFiles

        # call the accept slot of the base class
        QDialog.accept(self)

    def __splitIt(self, s, sep):
        """
        Private method to split a string observing various conditions.

        @param s string to split
        @type str
        @param sep separator string
        @type str
        @return list of split values
        @rtype list of str
        """
        if s == "" or s is None:
            return []

        if s.endswith(sep):
            s = s[:-1]

        return s.split(sep)

eric ide

mercurial