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 <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",
