diff '--color=auto' -Naur cx_freeze-7.2.4/cx_Freeze/command/bdist_msi.py cx_freeze-7.2.4.new/cx_Freeze/command/bdist_msi.py --- cx_freeze-7.2.4/cx_Freeze/command/bdist_msi.py 2024-10-29 04:24:54.000000000 +0100 +++ cx_freeze-7.2.4.new/cx_Freeze/command/bdist_msi.py 2024-11-02 17:24:46.307791598 +0100 @@ -7,6 +7,7 @@ import os import re import shutil +import sys import warnings from sysconfig import get_platform from typing import ClassVar, ContextManager @@ -15,11 +16,11 @@ from setuptools import Command from cx_Freeze._compat import IS_MINGW, IS_WINDOWS -from cx_Freeze.exception import OptionError +from cx_Freeze.exception import OptionError, PlatformError __all__ = ["bdist_msi"] -if IS_MINGW or IS_WINDOWS: +if (IS_MINGW or IS_WINDOWS) and sys.version_info[:2] < (3, 13): @contextlib.contextmanager def suppress_known_deprecation() -> ContextManager: @@ -48,9 +49,9 @@ from cx_Freeze.command._pydialog import PyDialog - # force the remove existing products action to happen first since Windows - # installer appears to be braindead and doesn't handle files shared between - # different "products" very well + # force the remove existing products action to happen first since + # Windows installer appears to be braindead and doesn't handle files + # shared between different "products" very well install_execute_sequence = sequence.InstallExecuteSequence for index, info in enumerate(install_execute_sequence): if info[0] == "RemoveExistingProducts": @@ -980,6 +981,15 @@ self.license_file = None def finalize_options(self) -> None: + major = sys.version_info.major + minor = sys.version_info.minor + if (major, minor) >= (3, 13): + msg = ( + f"bdist_msi is not supported on Python {major}.{minor} yet.\n" + " Please check a pinned issue at " + "https://github.com/marcelotduarte/cx_Freeze/issues" + ) + raise PlatformError(msg) self.set_undefined_options("bdist", ("skip_build", "skip_build")) if self.bdist_dir is None: diff '--color=auto' -Naur cx_freeze-7.2.4/cx_Freeze/command/bdist_msi.py~ cx_freeze-7.2.4.new/cx_Freeze/command/bdist_msi.py~ --- cx_freeze-7.2.4/cx_Freeze/command/bdist_msi.py~ 1970-01-01 01:00:00.000000000 +0100 +++ cx_freeze-7.2.4.new/cx_Freeze/command/bdist_msi.py~ 2024-11-02 17:24:09.126523289 +0100 @@ -0,0 +1,1208 @@ +"""Implements the 'bdist_msi' command (create Windows installer packages).""" + +from __future__ import annotations + +import contextlib +import logging +import os +import re +import shutil +import sys +import warnings +from sysconfig import get_platform +from typing import ClassVar, ContextManager + +from packaging.version import Version +from setuptools import Command + +from cx_Freeze._compat import IS_MINGW, IS_WINDOWS +from cx_Freeze.exception import OptionError, PlatformError + +__all__ = ["bdist_msi"] + +if IS_MINGW or IS_WINDOWS: + + @contextlib.contextmanager + def suppress_known_deprecation() -> ContextManager: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "'msilib' is deprecated") + yield + + with suppress_known_deprecation(): + from msilib import ( # pylint: disable=deprecated-module + CAB, + PID_AUTHOR, + PID_COMMENTS, + PID_KEYWORDS, + Binary, + Dialog, + Directory, + Feature, + add_data, + add_tables, + gen_uuid, + init_database, + make_id, + schema, + sequence, + ) + + from cx_Freeze.command._pydialog import PyDialog + + # force the remove existing products action to happen first since + # Windows installer appears to be braindead and doesn't handle files + # shared between different "products" very well + install_execute_sequence = sequence.InstallExecuteSequence + for index, info in enumerate(install_execute_sequence): + if info[0] == "RemoveExistingProducts": + install_execute_sequence[index] = (info[0], info[1], 1450) + + +class bdist_msi(Command): + """Create a Microsoft Installer (.msi) binary distribution.""" + + description = __doc__ + + user_options: ClassVar[list[tuple[str, str | None, str]]] = [ + ( + "bdist-dir=", + None, + "temporary directory for creating the distribution", + ), + ("dist-dir=", "d", "directory to put final built distributions in"), + ( + "install-script=", + None, + "basename of installation script to be run after " + "installation or before deinstallation", + ), + ( + "keep-temp", + "k", + "keep the pseudo-installation tree around after " + "creating the distribution archive", + ), + ( + "pre-install-script=", + None, + "Fully qualified filename of a script to be run before " + "any files are installed. This script need not be in the " + "distribution", + ), + ( + "skip-build", + None, + "skip rebuilding everything (for testing/debugging)", + ), + # cx_Freeze specific + ("add-to-path=", None, "add target dir to PATH environment variable"), + ("all-users=", None, "installation for all users (or just me)"), + ( + "data=", + None, + "dictionary of data indexed by table name, and each value is a " + "tuple to include in table", + ), + ("directories=", None, "list of 3-tuples of directories to create"), + ("environment-variables=", None, "list of environment variables"), + ("extensions=", None, "Extensions for which to register Verbs"), + ("initial-target-dir=", None, "initial target directory"), + ("install-icon=", None, "icon path to add/remove programs "), + ("product-code=", None, "product code to use"), + ( + "summary-data=", + None, + "Dictionary of data to include in msi summary data stream. " + 'Allowed keys are "author", "comments", "keywords".', + ), + ("target-name=", None, "name of the file to create"), + ("target-version=", None, "version of the file to create"), + ("upgrade-code=", None, "upgrade code to use"), + ( + "license-file=", + None, + "rft formatted license file to include in the installer", + ), + ] + + boolean_options: ClassVar[list[str]] = [ + "keep-temp", + "skip-build", + ] + + x = y = 50 + width = 370 + height = 300 + title = "[ProductName] Setup" + modeless = 1 + modal = 3 + _binary_columns: ClassVar[dict[str, int]] = { + "Binary": 1, + "Icon": 1, + "Patch": 4, + "SFPCatalog": 1, + "MsiDigitalCertificate": 1, + "MsiPatchHeaders": 1, + } + + def add_config(self) -> None: + if self.add_to_path: + path = "Path" + if self.all_users: + path = "=-*" + path + add_data( + self.db, + "Environment", + [("E_PATH", path, r"[~];[TARGETDIR]", "TARGETDIR")], + ) + if self.directories: + add_data(self.db, "Directory", self.directories) + if self.environment_variables: + add_data(self.db, "Environment", self.environment_variables) + # This is needed in case the AlwaysInstallElevated policy is set. + # Otherwise installation will not end up in TARGETDIR. + add_data( + self.db, + "Property", + [("SecureCustomProperties", "TARGETDIR;REINSTALLMODE")], + ) + add_data( + self.db, + "CustomAction", + [ + ( + "A_SET_TARGET_DIR", + 256 + 51, + "TARGETDIR", + self.initial_target_dir, + ), + ( + "A_SET_REINSTALL_MODE", + 256 + 51, + "REINSTALLMODE", + "amus", + ), + ], + ) + add_data( + self.db, + "InstallExecuteSequence", + [ + ("A_SET_TARGET_DIR", 'TARGETDIR=""', 401), + ("A_SET_REINSTALL_MODE", 'REINSTALLMODE=""', 402), + ], + ) + add_data( + self.db, + "InstallUISequence", + [ + ("PrepareDlg", None, 140), + ("A_SET_TARGET_DIR", 'TARGETDIR=""', 401), + ("A_SET_REINSTALL_MODE", 'REINSTALLMODE=""', 402), + ("SelectDirectoryDlg", "not Installed", 1230), + ("LicenseAgreementDlg", "not Installed", 1240), + ( + "MaintenanceTypeDlg", + "Installed and not Resume and not Preselected", + 1250, + ), + ("ProgressDlg", None, 1280), + ], + ) + for idx, executable in enumerate(self.distribution.executables): + if ( + executable.shortcut_name is not None + and executable.shortcut_dir is not None + ): + base_name = os.path.basename(executable.target_name) + add_data( + self.db, + "Shortcut", + [ + ( + f"S_APP_{idx}", + os.fspath(executable.shortcut_dir), + executable.shortcut_name, + "TARGETDIR", + f"[TARGETDIR]{base_name}", + None, + None, + None, + None, + None, + None, + "TARGETDIR", + ) + ], + ) + for table_name, table_data in self.data.items(): + col = self._binary_columns.get(table_name) + if col is not None: + data = [ + (*row[:col], Binary(row[col]), *row[col + 1 :]) + for row in table_data + ] + else: + data = table_data + add_data(self.db, table_name, data) + + # If provided, add data to MSI's summary information stream + if len(self.summary_data) > 0: + for k in self.summary_data: + if k not in ["author", "comments", "keywords"]: + msg = f"Unknown key provided in summary-data: {k!r}" + raise OptionError(msg) + + summary_info = self.db.GetSummaryInformation(5) + if "author" in self.summary_data: + summary_info.SetProperty( + PID_AUTHOR, self.summary_data["author"] + ) + if "comments" in self.summary_data: + summary_info.SetProperty( + PID_COMMENTS, self.summary_data["comments"] + ) + if "keywords" in self.summary_data: + summary_info.SetProperty( + PID_KEYWORDS, self.summary_data["keywords"] + ) + summary_info.Persist() + + def add_cancel_dialog(self) -> None: + dialog = Dialog( + self.db, + "CancelDlg", + 50, + 10, + 260, + 85, + 3, + self.title, + "No", + "No", + "No", + ) + dialog.text( + "Text", + 48, + 15, + 194, + 30, + 3, + "Are you sure you want to cancel [ProductName] installation?", + ) + button = dialog.pushbutton("Yes", 72, 57, 56, 17, 3, "Yes", "No") + button.event("EndDialog", "Exit") + button = dialog.pushbutton("No", 132, 57, 56, 17, 3, "No", "Yes") + button.event("EndDialog", "Return") + + def add_error_dialog(self) -> None: + dialog = Dialog( + self.db, + "ErrorDlg", + 50, + 10, + 330, + 101, + 65543, + self.title, + "ErrorText", + None, + None, + ) + dialog.text("ErrorText", 50, 9, 280, 48, 3, "") + for text, pos in [ + ("No", 120), + ("Yes", 240), + ("Abort", 0), + ("Cancel", 42), + ("Ignore", 81), + ("Ok", 159), + ("Retry", 198), + ]: + button = dialog.pushbutton(text[0], pos, 72, 81, 21, 3, text, None) + button.event("EndDialog", f"Error{text}") + + def add_exit_dialog(self) -> None: + dialog = PyDialog( + self.db, + "ExitDialog", + self.x, + self.y, + self.width, + self.height, + self.modal, + self.title, + "Finish", + "Finish", + "Finish", + ) + dialog.title("Completing the [ProductName] installer") + dialog.backbutton("< Back", "Finish", active=False) + dialog.cancelbutton("Cancel", "Back", active=False) + dialog.text( + "Description", + 15, + 235, + 320, + 20, + 0x30003, + "Click the Finish button to exit the installer.", + ) + button = dialog.nextbutton("Finish", "Cancel", name="Finish") + button.event("EndDialog", "Return") + + def add_fatal_error_dialog(self) -> None: + dialog = PyDialog( + self.db, + "FatalError", + self.x, + self.y, + self.width, + self.height, + self.modal, + self.title, + "Finish", + "Finish", + "Finish", + ) + dialog.title("[ProductName] installer ended prematurely") + dialog.backbutton("< Back", "Finish", active=False) + dialog.cancelbutton("Cancel", "Back", active=False) + dialog.text( + "Description1", + 15, + 70, + 320, + 80, + 0x30003, + "[ProductName] setup ended prematurely because of an error. " + "Your system has not been modified. To install this program " + "at a later time, please run the installation again.", + ) + dialog.text( + "Description2", + 15, + 155, + 320, + 20, + 0x30003, + "Click the Finish button to exit the installer.", + ) + button = dialog.nextbutton("Finish", "Cancel", name="Finish") + button.event("EndDialog", "Exit") + + def add_files(self) -> None: + database = self.db + cab = CAB("distfiles") + feature = Feature( + database, + "default", + "Default Feature", + "Everything", + 1, + directory="TARGETDIR", + ) + feature.set_current() + rootdir = os.path.abspath(self.bdist_dir) + root = Directory( + database, cab, None, rootdir, "TARGETDIR", "SourceDir" + ) + database.Commit() + todo = [root] + while todo: + directory = todo.pop() + for file in os.listdir(directory.absolute): + sep_comp = self.separate_components.get( + os.path.relpath( + os.path.join(directory.absolute, file), + self.bdist_dir, + ) + ) + if sep_comp is not None: + restore_component = directory.component + directory.start_component( + component=sep_comp, + flags=0, + feature=feature, + keyfile=file, + ) + directory.add_file(file) + directory.component = restore_component + elif os.path.isdir(os.path.join(directory.absolute, file)): + sfile = directory.make_short(file) + new_dir = Directory( + database, cab, directory, file, file, f"{sfile}|{file}" + ) + todo.append(new_dir) + else: + directory.add_file(file) + cab.commit(database) + + def add_files_in_use_dialog(self) -> None: + dialog = PyDialog( + self.db, + "FilesInUse", + self.x, + self.y, + self.width, + self.height, + 19, + self.title, + "Retry", + "Retry", + "Retry", + bitmap=False, + ) + dialog.text( + "Title", 15, 6, 200, 15, 0x30003, r"{\DlgFontBold8}Files in Use" + ) + dialog.text( + "Description", + 20, + 23, + 280, + 20, + 0x30003, + "Some files that need to be updated are currently in use.", + ) + dialog.text( + "Text", + 20, + 55, + 330, + 50, + 3, + "The following applications are using files that need to be " + "updated by this setup. Close these applications and then " + "click Retry to continue the installation or Cancel to exit it.", + ) + dialog.control( + "List", + "ListBox", + 20, + 107, + 330, + 130, + 7, + "FileInUseProcess", + None, + None, + None, + ) + button = dialog.backbutton("Exit", "Ignore", name="Exit") + button.event("EndDialog", "Exit") + button = dialog.nextbutton("Ignore", "Retry", name="Ignore") + button.event("EndDialog", "Ignore") + button = dialog.cancelbutton("Retry", "Exit", name="Retry") + button.event("EndDialog", "Retry") + + def add_maintenance_type_dialog(self) -> None: + dialog = PyDialog( + self.db, + "MaintenanceTypeDlg", + self.x, + self.y, + self.width, + self.height, + self.modal, + self.title, + "Next", + "Next", + "Cancel", + ) + dialog.title("Welcome to the [ProductName] Setup Wizard") + dialog.text( + "BodyText", + 15, + 63, + 330, + 42, + 3, + "Select whether you want to repair or remove [ProductName].", + ) + group = dialog.radiogroup( + "RepairRadioGroup", + 15, + 108, + 330, + 60, + 3, + "MaintenanceForm_Action", + "", + "Next", + ) + group.add("Repair", 0, 18, 300, 17, "&Repair [ProductName]") + group.add("Remove", 0, 36, 300, 17, "Re&move [ProductName]") + dialog.backbutton("< Back", None, active=False) + button = dialog.nextbutton("Finish", "Cancel") + button.event( + "[REINSTALL]", "ALL", 'MaintenanceForm_Action="Repair"', 5 + ) + button.event( + "[Progress1]", "Repairing", 'MaintenanceForm_Action="Repair"', 6 + ) + button.event( + "[Progress2]", "repairs", 'MaintenanceForm_Action="Repair"', 7 + ) + button.event("Reinstall", "ALL", 'MaintenanceForm_Action="Repair"', 8) + button.event("[REMOVE]", "ALL", 'MaintenanceForm_Action="Remove"', 11) + button.event( + "[Progress1]", "Removing", 'MaintenanceForm_Action="Remove"', 12 + ) + button.event( + "[Progress2]", "removes", 'MaintenanceForm_Action="Remove"', 13 + ) + button.event("Remove", "ALL", 'MaintenanceForm_Action="Remove"', 14) + button.event( + "EndDialog", "Return", 'MaintenanceForm_Action<>"Change"', 20 + ) + button = dialog.cancelbutton("Cancel", "RepairRadioGroup") + button.event("SpawnDialog", "CancelDlg") + + def add_prepare_dialog(self) -> None: + dialog = PyDialog( + self.db, + "PrepareDlg", + self.x, + self.y, + self.width, + self.height, + self.modeless, + self.title, + "Cancel", + "Cancel", + "Cancel", + ) + dialog.text( + "Description", + 15, + 70, + 320, + 40, + 0x30003, + "Please wait while the installer prepares to guide you through " + "the installation.", + ) + dialog.title("Welcome to the [ProductName] installer") + text = dialog.text( + "ActionText", 15, 110, 320, 20, 0x30003, "Pondering..." + ) + text.mapping("ActionText", "Text") + text = dialog.text("ActionData", 15, 135, 320, 30, 0x30003, None) + text.mapping("ActionData", "Text") + dialog.backbutton("Back", None, active=False) + dialog.nextbutton("Next", None, active=False) + button = dialog.cancelbutton("Cancel", None) + button.event("SpawnDialog", "CancelDlg") + + def add_progress_dialog(self) -> None: + dialog = PyDialog( + self.db, + "ProgressDlg", + self.x, + self.y, + self.width, + self.height, + self.modeless, + self.title, + "Cancel", + "Cancel", + "Cancel", + bitmap=False, + ) + dialog.text( + "Title", + 20, + 15, + 200, + 15, + 0x30003, + r"{\DlgFontBold8}[Progress1] [ProductName]", + ) + dialog.text( + "Text", + 35, + 65, + 300, + 30, + 3, + "Please wait while the installer [Progress2] [ProductName].", + ) + dialog.text("StatusLabel", 35, 100, 35, 20, 3, "Status:") + text = dialog.text( + "ActionText", 70, 100, self.width - 70, 20, 3, "Pondering..." + ) + text.mapping("ActionText", "Text") + control = dialog.control( + "ProgressBar", + "ProgressBar", + 35, + 120, + 300, + 10, + 65537, + None, + "Progress done", + None, + None, + ) + control.mapping("SetProgress", "Progress") + dialog.backbutton("< Back", "Next", active=False) + dialog.nextbutton("Next >", "Cancel", active=False) + button = dialog.cancelbutton("Cancel", "Back") + button.event("SpawnDialog", "CancelDlg") + + def add_properties(self) -> None: + metadata = self.distribution.metadata + props = [ + ("DistVersion", metadata.get_version()), + ("DefaultUIFont", "DlgFont8"), + ("ErrorDialog", "ErrorDlg"), + ("Progress1", "Install"), + ("Progress2", "installs"), + ("MaintenanceForm_Action", "Repair"), + ("ALLUSERS", "2"), + ] + + if not self.all_users: + props.append(("MSIINSTALLPERUSER", "1")) + email = metadata.author_email or metadata.maintainer_email + if email: + props.append(("ARPCONTACT", email)) + if metadata.url: + props.append(("ARPURLINFOABOUT", metadata.url)) + if self.upgrade_code is not None: + if not _is_valid_guid(self.upgrade_code): + msg = "upgrade-code must be in valid GUID format" + raise ValueError(msg) + props.append(("UpgradeCode", self.upgrade_code.upper())) + if self.install_icon: + props.append(("ARPPRODUCTICON", "InstallIcon")) + add_data(self.db, "Property", props) + if self.install_icon: + add_data( + self.db, + "Icon", + [("InstallIcon", Binary(self.install_icon))], + ) + + def add_select_directory_dialog(self) -> None: + dialog = PyDialog( + self.db, + "SelectDirectoryDlg", + self.x, + self.y, + self.width, + self.height, + self.modal, + self.title, + "Next", + "Next", + "Cancel", + ) + dialog.title("Select destination directory") + dialog.backbutton("< Back", None, active=False) + button = dialog.nextbutton("Next >", "Cancel") + button.event("SetTargetPath", "TARGETDIR", ordering=1) + button.event("SpawnWaitDialog", "WaitForCostingDlg", ordering=2) + button.event("EndDialog", "Return", ordering=3) + button = dialog.cancelbutton("Cancel", "DirectoryCombo") + button.event("SpawnDialog", "CancelDlg") + dialog.control( + "DirectoryCombo", + "DirectoryCombo", + 15, + 70, + 272, + 80, + 393219, + "TARGETDIR", + None, + "DirectoryList", + None, + ) + dialog.control( + "DirectoryList", + "DirectoryList", + 15, + 90, + 308, + 136, + 3, + "TARGETDIR", + None, + "PathEdit", + None, + ) + dialog.control( + "PathEdit", + "PathEdit", + 15, + 230, + 306, + 16, + 3, + "TARGETDIR", + None, + "Next", + None, + ) + button = dialog.pushbutton("Up", 306, 70, 18, 18, 3, "Up", None) + button.event("DirectoryListUp", "0") + button = dialog.pushbutton("NewDir", 324, 70, 30, 18, 3, "New", None) + button.event("DirectoryListNew", "0") + + def add_text_styles(self) -> None: + add_data( + self.db, + "TextStyle", + [ + ("DlgFont8", "Tahoma", 9, None, 0), + ("DlgFontBold8", "Tahoma", 8, None, 1), + ("VerdanaBold10", "Verdana", 10, None, 1), + ("VerdanaRed9", "Verdana", 9, 255, 0), + ], + ) + + def add_ui(self) -> None: + self.add_text_styles() + self.add_error_dialog() + self.add_fatal_error_dialog() + self.add_cancel_dialog() + self.add_exit_dialog() + self.add_user_exit_dialog() + self.add_files_in_use_dialog() + self.add_wait_for_costing_dialog() + self.add_prepare_dialog() + self.add_license_dialog() + self.add_select_directory_dialog() + self.add_progress_dialog() + self.add_maintenance_type_dialog() + + def add_upgrade_config(self, sversion) -> None: + if self.upgrade_code is not None: + add_data( + self.db, + "Upgrade", + [ + ( + self.upgrade_code, + None, + sversion, + None, + 513, + None, + "REMOVEOLDVERSION", + ), + ( + self.upgrade_code, + sversion, + None, + None, + 257, + None, + "REMOVENEWVERSION", + ), + ], + ) + + def add_user_exit_dialog(self) -> None: + dialog = PyDialog( + self.db, + "UserExit", + self.x, + self.y, + self.width, + self.height, + self.modal, + self.title, + "Finish", + "Finish", + "Finish", + ) + dialog.title("[ProductName] installer was interrupted") + dialog.backbutton("< Back", "Finish", active=False) + dialog.cancelbutton("Cancel", "Back", active=False) + dialog.text( + "Description1", + 15, + 70, + 320, + 80, + 0x30003, + "[ProductName] setup was interrupted. Your system has not " + "been modified. To install this program at a later time, " + "please run the installation again.", + ) + dialog.text( + "Description2", + 15, + 155, + 320, + 20, + 0x30003, + "Click the Finish button to exit the installer.", + ) + button = dialog.nextbutton("Finish", "Cancel", name="Finish") + button.event("EndDialog", "Exit") + + def add_license_dialog(self) -> None: + if self.license_file: + with open(self.license_file, encoding="utf-8") as file: + license_text = file.read() + + dialog = PyDialog( + self.db, + "LicenseAgreementDlg", + self.x, + self.y, + self.width, + self.height, + self.modal, + self.title, + "Next", + "Next", + "Cancel", + ) + dialog.title("License Agreement") + dialog.backbutton("< Back", None, active=False) + dialog.control( + name="Text", + type="ScrollableText", + x=15, + y=30, + w=self.width - 30, + h=self.height - 100, + attr=3, + text=license_text, + prop=None, + next=None, + help=None, + ) + + button = dialog.nextbutton( + name="Next", + title="Accept", + tabnext="Cancel", + active=0, + ) + button.event("SpawnWaitDialog", "SelectDirectoryDlg", ordering=1) + button.event("EndDialog", "Return", ordering=2) + + button = dialog.cancelbutton("Cancel", tabnext="Next") + button.event("SpawnDialog", "CancelDlg") + + checkbox = dialog.checkbox( + "LicenseAcceptedCheckbox", + 15, + 230, + 300, + 20, + 3, + "CheckboxProp", + "I &accept the terms in the License Agreement", + None, + ) + # [proptery], value + checkbox.event("[LicenseAcceptedClicked]", "1") + + add_data( + self.db, + "ControlCondition", + # Dialog, Control , Action, Condition + [ + ( + "LicenseAgreementDlg", + "Next", + "Enable", + "LicenseAcceptedClicked", + ) + ], + ) + + def add_wait_for_costing_dialog(self) -> None: + dialog = Dialog( + self.db, + "WaitForCostingDlg", + 50, + 10, + 260, + 85, + self.modal, + self.title, + "Return", + "Return", + "Return", + ) + dialog.text( + "Text", + 48, + 15, + 194, + 30, + 3, + "Please wait while the installer finishes determining your " + "disk space requirements.", + ) + button = dialog.pushbutton( + "Return", 102, 57, 56, 17, 3, "Return", None + ) + button.event("EndDialog", "Exit") + + def _append_to_data(self, table, *line) -> None: + rows = self.data.setdefault(table, []) + line = tuple(line) + if line not in rows: + rows.append(line) + + def initialize_options(self) -> None: + self.bdist_dir = None + self.keep_temp = 0 + self.dist_dir = None + self.skip_build = None + self.install_script = None + self.pre_install_script = None + # cx_Freeze specific + self.upgrade_code = None + self.product_code = None + self.add_to_path = None + self.initial_target_dir = None + self.target_name = None + self.target_version = None + self.fullname = None + self.directories = None + self.environment_variables = None + self.data = None + self.summary_data = None + self.install_icon = None + self.all_users = False + self.extensions = None + self.license_file = None + + def finalize_options(self) -> None: + major = sys.version_info.major + minor = sys.version_info.minor + if (major, minor) >= (3, 13): + msg = ( + f"bdist_msi is not supported on Python {major}.{minor} yet.\n" + " Please check a pinned issue at " + "https://github.com/marcelotduarte/cx_Freeze/issues" + ) + raise PlatformError(msg) + self.set_undefined_options("bdist", ("skip_build", "skip_build")) + + if self.bdist_dir is None: + bdist_base = self.get_finalized_command("bdist").bdist_base + self.bdist_dir = os.path.join(bdist_base, "msi") + + self.set_undefined_options("bdist", ("dist_dir", "dist_dir")) + + if self.pre_install_script: + msg = "the pre-install-script feature is not yet implemented" + raise OptionError(msg) + + if self.install_script: + for script in self.distribution.scripts: + if self.install_script == os.path.basename(script): + break + else: + msg = ( + f"install_script '{self.install_script}' not found in " + "scripts" + ) + raise OptionError(msg) + self.install_script_key = None + + # cx_Freeze specific + if self.target_name is None: + self.target_name = self.distribution.get_name() + if self.target_version is None and self.distribution.metadata.version: + self.target_version = self.distribution.metadata.version + name = self.target_name + version = self.target_version or self.distribution.get_version() + self.fullname = f"{name}-{version}" + platform = get_platform().replace("win-amd64", "win64") + if self.initial_target_dir is None: + if platform == "win64" or platform.startswith("mingw_x86_64"): + program_files_folder = "ProgramFiles64Folder" + else: + program_files_folder = "ProgramFilesFolder" + self.initial_target_dir = rf"[{program_files_folder}]\{name}" + if self.add_to_path is None: + self.add_to_path = False + if self.directories is None: + self.directories = [] + if self.environment_variables is None: + self.environment_variables = [] + if self.license_file is not None: + self.license_file = os.path.abspath(self.license_file) + if self.data is None: + self.data = {} + if not isinstance(self.summary_data, dict): + self.summary_data = {} + self.separate_components = {} + for idx, executable in enumerate(self.distribution.executables): + base_name = os.path.basename(executable.target_name) + # Trying to make these names unique from any directory name + self.separate_components[base_name] = make_id( + f"_cx_executable{idx}_{executable}" + ) + if self.extensions is None: + self.extensions = [] + for extension in self.extensions: + try: + ext = extension["extension"] + verb = extension["verb"] + executable = extension["executable"] + except KeyError: + msg = ( + "Each extension must have at least extension, verb, " + "and executable" + ) + raise ValueError(msg) from None + try: + component = self.separate_components[executable] + except KeyError: + msg = ( + "Executable must be the base target name of one of the " + "distribution's executables" + ) + raise ValueError(msg) from None + stem = os.path.splitext(executable)[0] + progid = make_id(f"{name}.{stem}.{version}") + mime = extension.get("mime", None) + # "%1" a better default for argument? + argument = extension.get("argument", None) + context = extension.get("context", f"{self.fullname} {verb}") + # Add rows via self.data to safely ignore duplicates + self._append_to_data( + "ProgId", + progid, + None, + None, + self.distribution.get_description() or "UNKNOWN", + None, + None, + ) + self._append_to_data( + "Extension", ext, component, progid, mime, "default" + ) + self._append_to_data("Verb", ext, verb, 0, context, argument) + if mime is not None: + self._append_to_data("MIME", mime, ext, "None") + # Registry entries that allow proper display of the app in menu + self._append_to_data( + "Registry", + f"{progid}-name", + -1, + rf"Software\Classes\{progid}", + "FriendlyAppName", + name, + component, + ) + self._append_to_data( + "Registry", + f"{progid}-verb-{verb}", + -1, + rf"Software\Classes\{progid}\shell\{verb}", + "FriendlyAppName", + name, + component, + ) + self._append_to_data( + "Registry", + f"{progid}-author", + -1, + rf"Software\Classes\{progid}\Application", + "ApplicationCompany", + self.distribution.get_author() or "UNKNOWN", + component, + ) + + def run(self) -> None: + if not self.skip_build: + self.run_command("build") + + # install everything from build directory in a new prefix + install_dir = self.bdist_dir + install = self.reinitialize_command("install", reinit_subcommands=1) + install.prefix = install_dir + install.skip_build = self.skip_build + install.warn_dir = 0 + logging.info("installing to %s", install_dir) + install.ensure_finalized() + install.run() + + # make msi (by default in dist directory) + self.mkpath(self.dist_dir) + platform = get_platform().replace("win-amd64", "win64") + + msi_name: str + if os.path.splitext(self.target_name)[1].lower() == ".msi": + msi_name = self.target_name + elif self.target_version: + msi_name = f"{self.fullname}-{platform}.msi" + else: + msi_name = f"{self.target_name}-{platform}.msi" + installer_name = os.path.join(self.dist_dir, msi_name) + installer_name = os.path.abspath(installer_name) + if os.path.exists(installer_name): + os.unlink(installer_name) + + author = self.distribution.metadata.get_contact() or "UNKNOWN" + version = self.target_version or self.distribution.get_version() + # ProductVersion must be strictly numeric + base_version = Version(version).base_version + + # msilib is reloaded in order to reset the "_directories" global member + # in that module. That member is used by msilib to prevent any two + # directories from having the same logical name. _directories might + # already have contents due to msilib having been previously used in + # the current instance of the python interpreter -- if so, it could + # prevent the root from getting the logical name TARGETDIR, breaking + # the MSI. + # importlib.reload(msilib) + + if self.product_code is None: + self.product_code = gen_uuid() + self.db = init_database( + installer_name, + schema, + self.target_name, + self.product_code, + base_version, + author, + ) + add_tables(self.db, sequence) + self.add_properties() + self.add_config() + self.add_upgrade_config(base_version) + self.add_ui() + self.add_files() + self.db.Commit() + self.distribution.dist_files.append( + ("bdist_msi", base_version or "any", self.target_name) + ) + + if not self.keep_temp: + logging.info( + "removing '%s' (and everything under it)", install_dir + ) + if not self.dry_run: + try: + shutil.rmtree(install_dir) + except OSError as exc: + logging.warning("error removing %s: %s", install_dir, exc) + + # Cause the MSI file to be released. Without this, then if bdist_msi + # is run programmatically from within a larger script, subsequent + # editting of the MSI is blocked. + self.db = None + + +def _is_valid_guid(code) -> bool: + pattern = re.compile( + r"^\{[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}\}$", re.IGNORECASE + ) + return isinstance(code, str) and pattern.match(code) is not None diff '--color=auto' -Naur cx_freeze-7.2.4/cx_Freeze/finder.py cx_freeze-7.2.4.new/cx_Freeze/finder.py --- cx_freeze-7.2.4/cx_Freeze/finder.py 2024-10-29 04:24:54.000000000 +0100 +++ cx_freeze-7.2.4.new/cx_Freeze/finder.py 2024-11-02 17:23:02.949045748 +0100 @@ -39,6 +39,7 @@ CALL_FUNCTION = opcode.opmap.get("CALL_FUNCTION") CALL = opcode.opmap.get("CALL") PRECALL = opcode.opmap.get("PRECALL") +PUSH_NULL = opcode.opmap.get("PUSH_NULL") EXTENDED_ARG = opcode.opmap["EXTENDED_ARG"] LOAD_CONST = opcode.opmap["LOAD_CONST"] @@ -624,14 +625,21 @@ if opc == LOAD_NAME: name = code.co_names[arg] continue + if PUSH_NULL and opc == PUSH_NULL: + continue if name and name == "__import__" and len(arguments) == 1: # Try to identify a __import__ call + # Python 3.13 bytecode: + # 1 2 LOAD_NAME 0 (__import__) + # 4 PUSH_NULL + # 6 LOAD_CONST 0 ('pkgutil') + # 8 CALL 1 # Python 3.12 bytecode: # 20 2 PUSH_NULL # 4 LOAD_NAME 0 (__import__) # 6 LOAD_CONST 0 ('pkgutil') # 8 CALL 1 - # Python 3.6 to 3.10 uses CALL_FUNCTION instead fo CALL + # Python 3.6 to 3.10 uses CALL_FUNCTION instead of CALL # Python 3.11 uses PRECALL then CALL if CALL_FUNCTION and (opc, arg) == (CALL_FUNCTION, 1): import_call = 1 diff '--color=auto' -Naur cx_freeze-7.2.4/doc/src/bdist_msi.rst cx_freeze-7.2.4.new/doc/src/bdist_msi.rst --- cx_freeze-7.2.4/doc/src/bdist_msi.rst 2024-10-29 04:24:54.000000000 +0100 +++ cx_freeze-7.2.4.new/doc/src/bdist_msi.rst 2024-11-02 17:23:02.949045748 +0100 @@ -1,8 +1,13 @@ bdist_msi ========= -This command is a standard command in Python 2.5 and higher which has been -modified by cx_Freeze to handle installing executables and their dependencies. +This command is implemented by cx_Freeze to handle installing executables and +their dependencies creating Windows installer packages. + +.. warning:: + + This command is not supported in Python 3.13. + The following options were added to the standard set of options for the command: diff '--color=auto' -Naur cx_freeze-7.2.4/doc/src/index.rst cx_freeze-7.2.4.new/doc/src/index.rst --- cx_freeze-7.2.4/doc/src/index.rst 2024-10-29 04:24:54.000000000 +0100 +++ cx_freeze-7.2.4.new/doc/src/index.rst 2024-11-02 17:23:02.949045748 +0100 @@ -8,6 +8,10 @@ The current version of **cx_Freeze** is 7.2 and supports Python 3.8 to 3.12. +.. warning:: + + cx_Freeze does not support Python 3.13 in macOS yet. + **cx_Freeze** is distributed under an open-source :ref:`PSF license `. Contents: diff '--color=auto' -Naur cx_freeze-7.2.4/pyproject.toml cx_freeze-7.2.4.new/pyproject.toml --- cx_freeze-7.2.4/pyproject.toml 2024-10-29 04:24:54.000000000 +0100 +++ cx_freeze-7.2.4.new/pyproject.toml 2024-11-02 17:23:02.949045748 +0100 @@ -29,6 +29,7 @@ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Software Distribution", @@ -154,8 +155,8 @@ build-verbosity = 1 manylinux-x86_64-image = "quay.io/pypa/manylinux2014_x86_64:latest" skip = [ - "cp3{8,9}-{many,musl}linux_{aarch64,ppc64le}", - "cp3{8,9,10}-macosx_{universal2,arm64}", + "cp3{9,10}-{many,musl}linux_{aarch64,ppc64le}", + "cp3{9,10,13}-macosx_{universal2,arm64}", ] [tool.cibuildwheel.linux] diff '--color=auto' -Naur cx_freeze-7.2.4/tests/test_command_bdist_msi.py cx_freeze-7.2.4.new/tests/test_command_bdist_msi.py --- cx_freeze-7.2.4/tests/test_command_bdist_msi.py 2024-10-29 04:24:54.000000000 +0100 +++ cx_freeze-7.2.4.new/tests/test_command_bdist_msi.py 2024-11-02 17:23:02.949045748 +0100 @@ -19,6 +19,12 @@ if sys.platform != "win32": pytest.skip(reason="Windows tests", allow_module_level=True) +if sys.version_info[:2] >= (3, 13): + pytest.skip( + reason="bdist_msi is not supported on Python 3.13 yet.", + allow_module_level=True, + ) + DIST_ATTRS = { "name": "foo", "version": "0.0",