From 711a1a68805ba709e387f3e541a932acd9e108c2 Mon Sep 17 00:00:00 2001 From: Marcelo Duarte Date: Wed, 8 Jan 2025 01:31:11 -0300 Subject: [PATCH] chore: enable experimental free-threaded python 3.13 on unix --- .github/workflows/ci.yml | 68 +++++++++++++++++++++++++++++++++++++-- ci/build-wheel.sh | 9 +++--- cx_Freeze/_compat.py | 4 ++- cx_Freeze/executable.py | 14 +++++--- cx_Freeze/freezer.py | 7 ++-- pyproject.toml | 4 ++- requirements-dev.txt | 2 +- setup.py | 57 +++++++++++++++++++------------- tests/test_executables.py | 13 +++++--- tests/test_freezer.py | 14 ++++---- 10 files changed, 144 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43dcea7e3..e40268871 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,8 +75,13 @@ jobs: shell: bash env: UV_SYSTEM_PYTHON: true + UV_NO_PROGRESS: true steps: + - name: Install dependencies (Linux) + if: runner.os == 'Linux' + run: sudo apt-get install -qy alien fakeroot rpm + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -98,19 +103,78 @@ jobs: pattern: cx-freeze-whl-${{ matrix.os }}* path: wheelhouse + - name: Install dependencies to test + run: | + uv pip install -r requirements.txt -r tests/requirements.txt + uv pip install cx_Freeze --no-index --no-deps -f wheelhouse --reinstall + + - name: Generate coverage report + env: + COVERAGE_FILE: ".coverage.${{ matrix.python-version }}.${{ matrix.os }}" + run: pytest -nauto --cov="cx_Freeze" + + - name: Upload coverage reports + uses: actions/upload-artifact@v4 + with: + name: cov-${{ matrix.python-version }}.${{ matrix.os }} + path: .coverage.* + include-hidden-files: true + + test_free_threaded: + needs: + - build_wheel + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-14] + python-version: ['3.13t'] + fail-fast: false + defaults: + run: + shell: bash + env: + UV_NO_PROGRESS: true + steps: + - name: Install dependencies (Linux) if: runner.os == 'Linux' run: sudo apt-get install -qy alien fakeroot rpm + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + repository: marcelotduarte/cx_Freeze + + - uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: | + requirements.txt + tests/requirements.txt + python-version: ${{ matrix.python-version }} + + - name: Env + run: env | sort + + - name: Sysconfig + run: uv run --no-project -m sysconfig + + - name: Download the wheel + uses: actions/download-artifact@v4 + with: + merge-multiple: true + pattern: cx-freeze-whl-${{ matrix.os }}* + path: wheelhouse + - name: Install dependencies to test run: | - uv pip install -r requirements.txt -r tests/requirements.txt + uv sync --no-install-project --extra tests uv pip install cx_Freeze --no-index --no-deps -f wheelhouse --reinstall - name: Generate coverage report env: COVERAGE_FILE: ".coverage.${{ matrix.python-version }}.${{ matrix.os }}" - run: pytest -nauto --cov="cx_Freeze" + run: uv run --no-project pytest -nauto --cov="cx_Freeze" - name: Upload coverage reports uses: actions/upload-artifact@v4 diff --git a/ci/build-wheel.sh b/ci/build-wheel.sh index 355c74c23..62d20af3a 100755 --- a/ci/build-wheel.sh +++ b/ci/build-wheel.sh @@ -22,8 +22,9 @@ PY_PLATFORM=$($PYTHON -c "import sysconfig; print(sysconfig.get_platform(), end= PY_VERSION=$($PYTHON -c "import sysconfig; print(sysconfig.get_python_version(), end='')") PY_VERSION_FULL=$($PYTHON -c "import sysconfig; print(sysconfig.get_config_var('py_version'), end='')") PY_VERSION_NODOT=$($PYTHON -c "import sysconfig; print(sysconfig.get_config_var('py_version_nodot'), end='')") +PY_ABI_THREAD=$($PYTHON -c "import sysconfig; print(sysconfig.get_config_var('abi_thread') or '', end='')") -PYTHON_TAG=cp$PY_VERSION_NODOT +PYTHON_TAG=cp$PY_VERSION_NODOT$PY_ABI_THREAD if [[ $PY_PLATFORM == linux* ]]; then PLATFORM_TAG=many$(echo $PY_PLATFORM | sed 's/\-/_/') PLATFORM_TAG_MASK="$(echo $PLATFORM_TAG | sed 's/_/*_/')" @@ -81,8 +82,8 @@ _bump_my_version () { _cibuildwheel () { local args=$* - # Use python >= 3.11 - local py_version=$(_vergte $PY_VERSION 3.11) + # Use python >= 3.11 (python 3.13t is supported) + local py_version=$(_vergte $PY_VERSION 3.11)$PY_ABI_THREAD # Do not export UV_* to avoid conflict with uv in cibuildwheel macOS/Windows unset UV_SYSTEM_PYTHON uvx -p $py_version cibuildwheel $args @@ -101,7 +102,7 @@ echo "::endgroup::" mkdir -p wheelhouse >/dev/null if [[ $PY_PLATFORM == linux* ]]; then echo "::group::Build sdist" - uv build -p $PY_VERSION --no-build-isolation --sdist -o wheelhouse + uv build -p $PY_VERSION$PY_ABI_THREAD --sdist -o wheelhouse echo "::endgroup::" fi echo "::group::Build wheel(s)" diff --git a/cx_Freeze/_compat.py b/cx_Freeze/_compat.py index a5581e282..e712a32b1 100644 --- 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 index 1f00319ee..b19b50555 100644 --- 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 index e877d15d4..d3052ef14 100644 --- 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/pyproject.toml b/pyproject.toml index 08315b507..5db1c16ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ dynamic = ["version"] [project.optional-dependencies] dev = [ - "bump-my-version==0.29.0", + "bump-my-version==0.29.0 ;python_version < '3.13'", "cibuildwheel==2.22.0", "pre-commit==4.0.1", # python_version >= 3.9 ] @@ -164,9 +164,11 @@ optional_value = "final" before-build = "uv pip install -r requirements.txt" build-frontend = "build[uv]" build-verbosity = 1 +enable = ["cpython-freethreading"] skip = [ "cp3{9,10,13}-musllinux_*", "cp3{9,10,13}-manylinux_ppc64le", + "cp313t-win*", ] [tool.cibuildwheel.linux] diff --git a/requirements-dev.txt b/requirements-dev.txt index e366973e1..48d4793ef 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,3 @@ -bump-my-version==0.29.0 +bump-my-version==0.29.0 ;python_version < '3.13' cibuildwheel==2.22.0 pre-commit==4.0.1 diff --git a/setup.py b/setup.py index aab1b7a5b..7185a255a 100644 --- 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"], diff --git a/tests/test_executables.py b/tests/test_executables.py index b9b5c2ad4..bc93d8188 100644 --- 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 index f297e0feb..eb91ef24c 100644 --- 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