Repository: marcelotduarte/cx_Freeze (python)

Issue:
Replace _PyMem_RawStrdup with strdup
Per https://github.com/python/cpython/issues/127991#issuecomment-2547810583

Fixes #2568 


Discussion:

Patch:
diff --git a/cx_Freeze/_compat.py b/cx_Freeze/_compat.py
--- a/cx_Freeze/_compat.py
+++ b/cx_Freeze/_compat.py
@@ -7,6 +7,7 @@
 from pathlib import Path
 
 __all__ = [
+    "ABI_THREAD",
     "BUILD_EXE_DIR",
     "EXE_SUFFIX",
     "EXT_SUFFIX",
@@ -21,8 +22,9 @@
 
 PLATFORM = sysconfig.get_platform()
 PYTHON_VERSION = sysconfig.get_python_version()
+ABI_THREAD = sysconfig.get_config_var("abi_thread") or ""
 
-BUILD_EXE_DIR = Path(f"build/exe.{PLATFORM}-{PYTHON_VERSION}")
+BUILD_EXE_DIR = Path(f"build/exe.{PLATFORM}-{PYTHON_VERSION}{ABI_THREAD}")
 EXE_SUFFIX = sysconfig.get_config_var("EXE")
 EXT_SUFFIX = sysconfig.get_config_var("EXT_SUFFIX")
 
diff --git a/cx_Freeze/executable.py b/cx_Freeze/executable.py
--- a/cx_Freeze/executable.py
+++ b/cx_Freeze/executable.py
@@ -11,6 +11,7 @@
 from typing import TYPE_CHECKING
 
 from cx_Freeze._compat import (
+    ABI_THREAD,
     EXE_SUFFIX,
     IS_MACOS,
     IS_MINGW,
@@ -74,12 +75,17 @@ def base(self) -> Path:
 
     @base.setter
     def base(self, name: str | Path | None) -> None:
-        # The default base is the legacy console, except for
+        # The default base is the legacy console, except for Python 3.13t and
         # Python 3.13 on macOS, that supports only the new console
-        if IS_MACOS and sys.version_info[:2] >= (3, 13):
-            name = name or "console"
-        else:
+        version = sys.version_info[:2]
+        if (
+            version <= (3, 13)
+            and ABI_THREAD == ""
+            and not (IS_MACOS and version == (3, 13))
+        ):
             name = name or "console_legacy"
+        else:
+            name = name or "console"
         # silently ignore gui and service on non-windows systems
         if not (IS_WINDOWS or IS_MINGW) and name in ("gui", "service"):
             name = "console"
diff --git a/cx_Freeze/freezer.py b/cx_Freeze/freezer.py
--- a/cx_Freeze/freezer.py
+++ b/cx_Freeze/freezer.py
@@ -22,11 +22,13 @@
 from setuptools import Distribution
 
 from cx_Freeze._compat import (
+    ABI_THREAD,
     BUILD_EXE_DIR,
     IS_CONDA,
     IS_MACOS,
     IS_MINGW,
     IS_WINDOWS,
+    PYTHON_VERSION,
 )
 from cx_Freeze.common import get_resource_file_path, process_path_specs
 from cx_Freeze.exception import FileError, OptionError
@@ -1035,9 +1037,10 @@ def _default_bin_includes(self) -> list[str]:
             # MSYS2 python returns a static library.
             names = [name.replace(".dll.a", ".dll")]
         else:
+            py_version = f"{PYTHON_VERSION}{ABI_THREAD}"
             names = [
                 f"python{sys.version_info[0]}.dll",
-                f"python{sys.version_info[0]}{sys.version_info[1]}.dll",
+                f"python{py_version.replace('.','')}.dll",
             ]
         python_shared_libs: list[Path] = []
         for name in names:
@@ -1113,7 +1116,7 @@ def _default_bin_excludes(self) -> list[str]:
     def _default_bin_includes(self) -> list[str]:
         python_shared_libs: list[Path] = []
         # Check for distributed "cx_Freeze/bases/lib/Python"
-        name = "Python"
+        name = f"Python{ABI_THREAD.upper()}"
         for bin_path in self._default_bin_path_includes():
             fullname = Path(bin_path, name).resolve()
             if fullname.is_file():
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -97,7 +97,8 @@ def build_extension(self, ext) -> None:
             library_dirs.append(get_config_var("LIBPL"))
             if not ENABLE_SHARED or IS_CONDA:
                 library_dirs.append(get_config_var("LIBDIR"))
-            libraries.append(f"python{get_python_version()}")
+            abi_thread = get_config_var("abi_thread") or ""
+            libraries.append(f"python{get_python_version()}{abi_thread}")
             if get_config_var("LIBS"):
                 extra_args.extend(get_config_var("LIBS").split())
             if get_config_var("LIBM"):
@@ -275,38 +276,48 @@ def get_extensions() -> list[Extension]:
         os.environ.get("CI", "") != "true"
         or os.environ.get("CIBUILDWHEEL", "0") != "1"
     )
+    abi_thread = get_config_var("abi_thread") or ""
+    version = sys.version_info[:2]
     extensions = [
         Extension(
             "cx_Freeze.bases.console",
             ["source/bases/console.c", "source/bases/_common.c"],
             optional=optional,
-        ),
-        Extension(
-            "cx_Freeze.bases.console_legacy",
-            ["source/legacy/console.c"],
-            depends=["source/legacy/common.c"],
-            optional=optional
-            or (sys.version_info[:2] >= (3, 13) and IS_MACOS),
-        ),
+        )
     ]
-
-    if IS_MINGW or IS_WINDOWS:
+    if (
+        version <= (3, 13)
+        and abi_thread == ""
+        and not (IS_MACOS and version == (3, 13))
+    ):
         extensions += [
             Extension(
-                "cx_Freeze.bases.Win32GUI",
-                ["source/legacy/Win32GUI.c"],
+                "cx_Freeze.bases.console_legacy",
+                ["source/legacy/console.c"],
                 depends=["source/legacy/common.c"],
-                libraries=["user32"],
                 optional=optional,
-            ),
-            Extension(
-                "cx_Freeze.bases.Win32Service",
-                ["source/legacy/Win32Service.c"],
-                depends=["source/legacy/common.c"],
-                extra_link_args=["/DELAYLOAD:cx_Logging"],
-                libraries=["advapi32"],
-                optional=optional,
-            ),
+            )
+        ]
+    if IS_MINGW or IS_WINDOWS:
+        if version <= (3, 13) and abi_thread == "":
+            extensions += [
+                Extension(
+                    "cx_Freeze.bases.Win32GUI",
+                    ["source/legacy/Win32GUI.c"],
+                    depends=["source/legacy/common.c"],
+                    libraries=["user32"],
+                    optional=optional,
+                ),
+                Extension(
+                    "cx_Freeze.bases.Win32Service",
+                    ["source/legacy/Win32Service.c"],
+                    depends=["source/legacy/common.c"],
+                    extra_link_args=["/DELAYLOAD:cx_Logging"],
+                    libraries=["advapi32"],
+                    optional=optional,
+                ),
+            ]
+        extensions += [
             Extension(
                 "cx_Freeze.bases.gui",
                 ["source/bases/Win32GUI.c", "source/bases/_common.c"],


Test patch:
diff --git a/tests/test_executables.py b/tests/test_executables.py
--- a/tests/test_executables.py
+++ b/tests/test_executables.py
@@ -12,6 +12,7 @@
 
 from cx_Freeze import Executable
 from cx_Freeze._compat import (
+    ABI_THREAD,
     BUILD_EXE_DIR,
     EXE_SUFFIX,
     IS_MACOS,
@@ -241,14 +242,18 @@ def test_executables(
         ("icon.ico", "icon.icns", "icon.png", "icon.svg"),
     ),
 ]
-if IS_MACOS and sys.version_info[:2] >= (3, 13):
+if (
+    sys.version_info[:2] <= (3, 13)
+    and ABI_THREAD == ""
+    and not (IS_MACOS and sys.version_info[:2] == (3, 13))
+):
     TEST_VALID_PARAMETERS += [
-        ("base", None, "console-"),
+        ("base", None, "console_legacy-"),
+        ("base", "console_legacy", "console_legacy-"),
     ]
 else:
     TEST_VALID_PARAMETERS += [
-        ("base", None, "console_legacy-"),
-        ("base", "console_legacy", "console_legacy-"),
+        ("base", None, "console-"),
     ]
 if IS_WINDOWS or IS_MINGW:
     TEST_VALID_PARAMETERS += [
diff --git a/tests/test_freezer.py b/tests/test_freezer.py
--- a/tests/test_freezer.py
+++ b/tests/test_freezer.py
@@ -12,6 +12,7 @@
 
 from cx_Freeze import Freezer
 from cx_Freeze._compat import (
+    ABI_THREAD,
     BUILD_EXE_DIR,
     EXE_SUFFIX,
     IS_CONDA,
@@ -99,19 +100,20 @@ def test_freezer_default_bin_includes(tmp_path: Path, monkeypatch) -> None:
     monkeypatch.chdir(tmp_path)
 
     freezer = Freezer(executables=["hello.py"])
+    py_version = f"{PYTHON_VERSION}{ABI_THREAD}"
     if IS_MINGW:
-        expected = f"libpython{PYTHON_VERSION}.dll"
+        expected = f"libpython{py_version}.dll"
     elif IS_WINDOWS:
-        expected = f"python{PYTHON_VERSION.replace('.','')}.dll"
+        expected = f"python{py_version.replace('.','')}.dll"
     elif IS_CONDA:  # macOS or Linux
         if IS_MACOS:
-            expected = f"libpython{PYTHON_VERSION}.dylib"
+            expected = f"libpython{py_version}.dylib"
         else:
-            expected = f"libpython{PYTHON_VERSION}.so*"
+            expected = f"libpython{py_version}.so*"
     elif IS_MACOS:
-        expected = "Python"
+        expected = f"Python{ABI_THREAD.upper()}"
     elif ENABLE_SHARED:  # Linux
-        expected = f"libpython{PYTHON_VERSION}.so*"
+        expected = f"libpython{py_version}.so*"
     else:
         assert freezer.default_bin_includes == []
         return