From f738654240e4395eaf3219dbaa62ebbaba1064c3 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sat, 18 Jul 2026 11:47:20 -0400 Subject: [PATCH 1/7] Prepare 22.0.0 release --- .github/scripts/install-macos-build-deps.sh | 16 +- .github/workflows/benchmark.yml | 27 ++ .github/workflows/build.yml | 293 ++++++++++++-------- .github/workflows/docs.yml | 6 +- .github/workflows/verify_conda_build.yml | 39 +-- .github/workflows/verify_shared_build.yml | 116 ++++---- CMakeLists.txt | 2 +- LICENSE-cffi | 21 ++ cm_library_c_binding/build.py | 2 +- cm_python_module/CMakeLists.txt | 16 +- docs/history.md | 11 +- docs/install.md | 14 +- hatch.toml | 3 + hatch_build.py | 37 +-- mkdocs.yml | 2 +- pyproject.toml | 16 +- scripts/check_dist.py | 86 ++++++ scripts/check_prerelease_python.py | 30 ++ src/coincurve/__init__.py | 2 +- src/coincurve/der.py | 2 +- tests/test_freethreading.py | 30 ++ 21 files changed, 499 insertions(+), 272 deletions(-) create mode 100644 .github/workflows/benchmark.yml create mode 100644 LICENSE-cffi create mode 100644 scripts/check_dist.py create mode 100644 scripts/check_prerelease_python.py create mode 100644 tests/test_freethreading.py diff --git a/.github/scripts/install-macos-build-deps.sh b/.github/scripts/install-macos-build-deps.sh index ec3029b27..5820e4c34 100755 --- a/.github/scripts/install-macos-build-deps.sh +++ b/.github/scripts/install-macos-build-deps.sh @@ -1,14 +1,6 @@ #!/bin/bash -set -ex +set -euxo pipefail -# update brew -brew update - -# Update openssl if necessary -brew outdated openssl || brew upgrade openssl - -# Install packages needed to build lib-secp256k1 -for pkg in pkg-config; do - brew list $pkg > /dev/null || brew install $pkg - brew outdated --quiet $pkg || brew upgrade $pkg -done +# CMake uses pkg-config to detect system libraries. Avoid updating the entire +# Homebrew installation in every CI job; the hosted runner image is immutable. +brew list pkg-config > /dev/null 2>&1 || brew install pkg-config diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 000000000..e0343cc7f --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,27 @@ +name: benchmark + +on: + workflow_dispatch: + +jobs: + benchmark: + name: Run benchmarks + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-python@v6 + with: + python-version: '3.14' + + - name: Install GMP headers + run: | + sudo apt-get update + sudo apt-get install --yes libgmp-dev + + - name: Install uv + uses: astral-sh/setup-uv@v8 + + - name: Run benchmarks + run: uv run --python-preference system scripts/bench.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aa62bbf55..56caf1610 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,11 +11,11 @@ on: - master concurrency: - group: build-${{ github.head_ref }} - cancel-in-progress: true + group: build-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }} env: - PYTHON_VERSION: '3.13' + PYTHON_VERSION: '3.14' COINCURVE_IGNORE_SYSTEM_LIB: 'ON' COINCURVE_SECP256K1_STATIC: 'ON' COINCURVE_CROSS_HOST: '' @@ -33,10 +33,28 @@ env: assert a.ecdh(b.public_key.format())==b.ecdh(a.public_key.format()) " && python -m pytest {project} - CIBW_SKIP: > - pp* jobs: + quality: + name: Static analysis and types + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install Hatch + uses: pypa/hatch@install + + - name: Run static analysis + run: hatch fmt --check + + - name: Check types + run: hatch run types:check + test: name: Test Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} runs-on: ${{ matrix.os }} @@ -44,14 +62,12 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] steps: - - name: Checkout code - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} @@ -62,88 +78,161 @@ jobs: - name: Install Hatch uses: pypa/hatch@install - - name: Run static analysis - run: hatch fmt --check + - name: Run tests + run: hatch test --python ${{ matrix.python-version }} --randomize - - name: Check types - run: hatch run types:check + coverage: + name: Upload coverage + runs-on: ubuntu-latest - - name: Run tests - run: hatch test --python ${{ matrix.python-version }} --cover-quiet --randomize + permissions: + contents: read + id-token: write - - name: Create coverage report - run: hatch run hatch-test.py${{ matrix.python-version }}:coverage xml + steps: + - uses: actions/checkout@v7 - - name: Upload coverage data - uses: actions/upload-artifact@v4 + - uses: actions/setup-python@v6 with: - name: coverage-${{ matrix.os }}-${{ matrix.python-version }} - path: coverage.xml + python-version: ${{ env.PYTHON_VERSION }} - - name: Install uv - uses: astral-sh/setup-uv@v5 + - name: Install Hatch + uses: pypa/hatch@install - - name: Benchmark - run: uv run --python-preference system scripts/bench.py + - name: Run tests with coverage + run: hatch test --python ${{ env.PYTHON_VERSION }} --cover-quiet --randomize - coverage: - name: Upload coverage + - name: Create coverage report + run: hatch run hatch-test.py${{ env.PYTHON_VERSION }}:coverage xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v7 + with: + files: coverage.xml + use_oidc: true + + sdist: + name: Build source distribution needs: + - quality - test runs-on: ubuntu-latest - permissions: - contents: read - id-token: write + steps: + - uses: actions/checkout@v7 + + - name: Install build dependencies + run: python -m pip install build + + - name: Build source distribution + run: python -m build --sdist + + - uses: actions/upload-artifact@v7 + with: + name: artifact-sdist + path: dist/* + if-no-files-found: error + + prerelease-source: + name: Test sdist on Python ${{ matrix.python.version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} + needs: + - sdist + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python: + - version: 3.15-dev + free-threaded: false + - version: 3.15t-dev + free-threaded: true steps: - - name: Download coverage data - uses: actions/download-artifact@v5 + - uses: actions/checkout@v7 + + - uses: actions/setup-python@v6 with: - pattern: coverage-* - path: coverage_data + python-version: ${{ matrix.python.version }} + check-latest: true - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 + - name: Verify prerelease interpreter + env: + EXPECTED_FREE_THREADED: ${{ matrix.python.free-threaded }} + run: python scripts/check_prerelease_python.py + + - uses: actions/download-artifact@v8 with: - directory: coverage_data - use_oidc: true + name: artifact-sdist + path: dist + + - name: Install source distribution + run: > + python -m pip install pytest pytest-randomly + dist/coincurve-22.0.0.tar.gz + + - name: Run tests + run: python -m pytest tests linux-wheels-x86_64: - name: Build Linux wheels for x86-64 + name: Build Linux wheels for x86-64 and i686 needs: + - quality - test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Build wheels - uses: pypa/cibuildwheel@v2.23 + uses: pypa/cibuildwheel@v4.1.0 + env: + CIBW_ARCHS_LINUX: x86_64 i686 - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: - name: artifact-linux-wheels-x86_64 + name: artifact-linux-wheels-x86_64-i686 + path: wheelhouse/*.whl + if-no-files-found: error + + linux-wheels-arm64: + name: Build Linux wheels for ARM64 + needs: + - quality + - test + runs-on: ubuntu-24.04-arm + + steps: + - uses: actions/checkout@v7 + + - name: Build wheels + uses: pypa/cibuildwheel@v4.1.0 + env: + CIBW_ARCHS_LINUX: aarch64 + + - uses: actions/upload-artifact@v7 + with: + name: artifact-linux-wheels-arm64 path: wheelhouse/*.whl if-no-files-found: error macos-wheels-x86_64: name: Build macOS wheels for x86-64 needs: + - quality - test runs-on: macos-15-intel steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Build wheels - uses: pypa/cibuildwheel@v3.2.0 + uses: pypa/cibuildwheel@v4.1.0 env: CIBW_ARCHS_MACOS: x86_64 - CIBW_SKIP: "cp314t-*" - MACOSX_DEPLOYMENT_TARGET: 10.13 + MACOSX_DEPLOYMENT_TARGET: '10.15' - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: artifact-macos-wheels-x86_64 path: wheelhouse/*.whl @@ -152,16 +241,19 @@ jobs: macos-wheels-arm64: name: Build macOS wheels for ARM64 needs: + - quality - test runs-on: macos-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Build wheels - uses: pypa/cibuildwheel@v2.23 + uses: pypa/cibuildwheel@v4.1.0 + env: + CIBW_ARCHS_MACOS: arm64 - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: artifact-macos-wheels-arm64 path: wheelhouse/*.whl @@ -170,23 +262,20 @@ jobs: windows-wheels-x86_64: name: Build Windows wheels for x86-64 needs: + - quality - test runs-on: windows-latest steps: - - uses: actions/checkout@v5 - - - uses: actions/setup-python@v6 - with: - python-version: ${{ env.PYTHON_VERSION }} + - uses: actions/checkout@v7 - name: Build wheels - uses: pypa/cibuildwheel@v2.23 + uses: pypa/cibuildwheel@v4.1.0 env: - CIBW_ARCHS_WINDOWS: 'AMD64' + CIBW_ARCHS_WINDOWS: AMD64 CIBW_BEFORE_ALL: choco install -y --no-progress --no-color cmake>=3.28 - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: artifact-windows-wheels-x86_64 path: wheelhouse/*.whl @@ -195,82 +284,61 @@ jobs: windows-wheels-arm64: name: Build Windows wheels for ARM64 needs: + - quality - test - runs-on: windows-latest + runs-on: windows-11-arm steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Build wheels - uses: pypa/cibuildwheel@v2.23 + uses: pypa/cibuildwheel@v4.1.0 env: - COINCURVE_CROSS_HOST: 'arm64' - CIBW_ARCHS_WINDOWS: 'ARM64' + CIBW_ARCHS_WINDOWS: ARM64 CIBW_BEFORE_ALL: choco install -y --no-progress --no-color cmake>=3.28 - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: artifact-windows-wheels-arm64 path: wheelhouse/*.whl if-no-files-found: error - sdist: - name: Build source distribution + validate-distribution: + name: Validate release artifacts needs: - - test + - prerelease-source + - linux-wheels-x86_64 + - linux-wheels-arm64 + - macos-wheels-x86_64 + - macos-wheels-arm64 + - windows-wheels-x86_64 + - windows-wheels-arm64 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - - name: Install build dependencies - run: python -m pip install build - - - name: Build source distribution - run: python -m build --sdist - - - uses: actions/upload-artifact@v4 + - uses: actions/download-artifact@v8 with: - name: artifact-sdist - path: dist/* - if-no-files-found: error - - linux-wheels-arm64: - name: Build Linux wheels for ARM64 - needs: - - test - runs-on: ubuntu-latest - if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.event.ref, 'refs/tags')) - - steps: - - uses: actions/checkout@v5 + pattern: artifact-* + merge-multiple: true + path: dist - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: arm64 + - name: Install validation tools + run: python -m pip install check-wheel-contents twine - - name: Build wheels - uses: pypa/cibuildwheel@v2.23 - env: - CIBW_ARCHS_LINUX: aarch64 + - name: Validate metadata and wheel contents + run: | + python -m twine check dist/* + check-wheel-contents dist/*.whl - - uses: actions/upload-artifact@v4 - with: - name: artifact-linux-wheels-arm64 - path: wheelhouse/*.whl - if-no-files-found: error + - name: Validate artifact inventory + run: python scripts/check_dist.py dist 22.0.0 publish: name: Publish release needs: - - linux-wheels-x86_64 - - macos-wheels-x86_64 - - macos-wheels-arm64 - - windows-wheels-x86_64 - - windows-wheels-arm64 - - sdist - - linux-wheels-arm64 + - validate-distribution runs-on: ubuntu-latest if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') @@ -278,14 +346,13 @@ jobs: id-token: write steps: - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v8 with: pattern: artifact-* merge-multiple: true path: dist - - run: ls -l dist - name: Push build artifacts to PyPI - uses: pypa/gh-action-pypi-publish@v1.13.0 + uses: pypa/gh-action-pypi-publish@v1.14.0 with: skip-existing: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 2827c288d..7a687e901 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 with: # Fetch all history for applying timestamps to every page fetch-depth: 0 @@ -32,7 +32,7 @@ jobs: - name: Build documentation run: hatch run docs:build - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: documentation path: site @@ -45,7 +45,7 @@ jobs: - build steps: - - uses: actions/download-artifact@v5 + - uses: actions/download-artifact@v8 with: name: documentation path: site diff --git a/.github/workflows/verify_conda_build.yml b/.github/workflows/verify_conda_build.yml index 3f4c415ef..fd69a7220 100644 --- a/.github/workflows/verify_conda_build.yml +++ b/.github/workflows/verify_conda_build.yml @@ -11,8 +11,8 @@ on: - master concurrency: - group: build_conda-${{ github.head_ref }} - cancel-in-progress: true + group: conda-build-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }} env: PYTHON_VERSION: '3.12' @@ -20,23 +20,6 @@ env: COINCURVE_IGNORE_SYSTEM_LIB: 'OFF' COINCURVE_SECP256K1_STATIC: 'OFF' COINCURVE_CROSS_HOST: '' - CIBW_ENVIRONMENT_PASS_LINUX: > - COINCURVE_UPSTREAM_REF - COINCURVE_IGNORE_SYSTEM_LIB - COINCURVE_SECP256K1_STATIC - COINCURVE_CROSS_HOST - CIBW_BEFORE_ALL_MACOS: ./.github/scripts/install-macos-build-deps.sh - CIBW_TEST_REQUIRES: pytest - CIBW_TEST_COMMAND: > - python -c - "from coincurve import PrivateKey; - a=PrivateKey(); - b=PrivateKey(); - assert a.ecdh(b.public_key.format())==b.ecdh(a.public_key.format()) - " && - python -m pytest {project} - CIBW_SKIP: > - pp* jobs: test: @@ -47,7 +30,7 @@ jobs: shell: bash -el {0} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Install Miniconda uses: conda-incubator/setup-miniconda@v3 @@ -60,20 +43,8 @@ jobs: - name: Install Hatch uses: pypa/hatch@install - - name: Run static analysis - run: hatch fmt --check - - - name: Check types - run: hatch run types:check - - name: Run tests - run: LD_LIBRARY_PATH=$CONDA_PREFIX/lib hatch test - - - name: Install uv - uses: astral-sh/setup-uv@v5 - - - name: Benchmark - run: LD_LIBRARY_PATH=$CONDA_PREFIX/lib uv run --python-preference system scripts/bench.py + run: LD_LIBRARY_PATH="$CONDA_PREFIX/lib" hatch test --python "$PYTHON_VERSION" --randomize linux-wheels-x86_64: name: Build Linux wheels @@ -85,7 +56,7 @@ jobs: shell: bash -el {0} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Install Miniconda uses: conda-incubator/setup-miniconda@v3 diff --git a/.github/workflows/verify_shared_build.yml b/.github/workflows/verify_shared_build.yml index bce2f1719..c3eb37627 100644 --- a/.github/workflows/verify_shared_build.yml +++ b/.github/workflows/verify_shared_build.yml @@ -11,17 +11,19 @@ on: - master concurrency: - group: build_shared-${{ github.head_ref }} - cancel-in-progress: true + group: shared-build-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }} env: - PYTHON_VERSION: '3.12' - COINCURVE_IGNORE_SYSTEM_LIB: '1' - # Only 'SHARED' is recognized, any other string means 'not SHARED' - COINCURVE_SECP256K1_BUILD: 'SHARED' + PYTHON_VERSION: '3.14' + COINCURVE_IGNORE_SYSTEM_LIB: 'ON' + COINCURVE_SECP256K1_STATIC: 'OFF' + COINCURVE_CROSS_HOST: '' + CIBW_BUILD: 'cp314-*' CIBW_ENVIRONMENT_PASS_LINUX: > COINCURVE_IGNORE_SYSTEM_LIB - COINCURVE_SECP256K1_BUILD + COINCURVE_SECP256K1_STATIC + COINCURVE_CROSS_HOST CIBW_BEFORE_ALL_MACOS: ./.github/scripts/install-macos-build-deps.sh CIBW_TEST_REQUIRES: pytest CIBW_TEST_COMMAND: > @@ -32,116 +34,108 @@ env: assert a.ecdh(b.public_key.format())==b.ecdh(a.public_key.format()) " && python -m pytest {project} - CIBW_TEST_SKIP: "*-macosx_arm64" - CIBW_SKIP: > - pp* jobs: test: - name: Test latest Python + name: Test shared libsecp256k1 build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v6 + - uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} - - name: Install system dependencies - if: runner.os == 'macOS' - run: ./.github/scripts/install-macos-build-deps.sh - - name: Install Hatch uses: pypa/hatch@install - - name: Run static analysis - run: hatch fmt --check - - - name: Check types - run: hatch run types:check - - name: Run tests - run: hatch test - - - name: Install uv - uses: astral-sh/setup-uv@v5 - - - name: Benchmark - run: uv run --python-preference system scripts/bench.py + run: hatch test --python ${{ env.PYTHON_VERSION }} --randomize linux-wheels-x86_64: - name: Build Linux wheels for x86-64 + name: Build shared Linux wheel for x86-64 needs: - test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 + + - name: Build wheel + uses: pypa/cibuildwheel@v4.1.0 + env: + CIBW_ARCHS_LINUX: x86_64 + + linux-wheels-arm64: + name: Build shared Linux wheel for ARM64 + needs: + - test + runs-on: ubuntu-24.04-arm + + steps: + - uses: actions/checkout@v7 - - name: Build wheels - uses: pypa/cibuildwheel@v2.23 + - name: Build wheel + uses: pypa/cibuildwheel@v4.1.0 + env: + CIBW_ARCHS_LINUX: aarch64 macos-wheels-x86_64: - name: Build macOS wheels for x86-64 + name: Build shared macOS wheel for x86-64 needs: - test runs-on: macos-15-intel steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - - name: Build wheels - uses: pypa/cibuildwheel@v3.2.0 + - name: Build wheel + uses: pypa/cibuildwheel@v4.1.0 env: CIBW_ARCHS_MACOS: x86_64 - CIBW_SKIP: "cp314t-*" - MACOSX_DEPLOYMENT_TARGET: 10.13 + MACOSX_DEPLOYMENT_TARGET: '10.15' macos-wheels-arm64: - name: Build macOS wheels for ARM64 + name: Build shared macOS wheel for ARM64 needs: - test runs-on: macos-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - - name: Build wheels - uses: pypa/cibuildwheel@v2.23 + - name: Build wheel + uses: pypa/cibuildwheel@v4.1.0 + env: + CIBW_ARCHS_MACOS: arm64 windows-wheels-x86_64: - name: Build Windows wheels for x86-64 + name: Build shared Windows wheel for x86-64 needs: - test runs-on: windows-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - - uses: actions/setup-python@v6 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Build wheels - uses: pypa/cibuildwheel@v2.23 + - name: Build wheel + uses: pypa/cibuildwheel@v4.1.0 env: - CIBW_ARCHS_WINDOWS: 'AMD64' + CIBW_ARCHS_WINDOWS: AMD64 CIBW_BEFORE_ALL: choco install -y --no-progress --no-color cmake>=3.28 windows-wheels-arm64: - name: Build Windows wheels for ARM64 + name: Build shared Windows wheel for ARM64 needs: - test - runs-on: windows-latest + runs-on: windows-11-arm steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - - name: Build wheels - uses: pypa/cibuildwheel@v2.23 + - name: Build wheel + uses: pypa/cibuildwheel@v4.1.0 env: - COINCURVE_CROSS_HOST: 'arm64' - CIBW_ARCHS_WINDOWS: 'ARM64' + CIBW_ARCHS_WINDOWS: ARM64 CIBW_BEFORE_ALL: choco install -y --no-progress --no-color cmake>=3.28 diff --git a/CMakeLists.txt b/CMakeLists.txt index ea135d03c..c900518f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,7 +23,7 @@ set(CFFI_HEADERS_DIR ${PROJECT_BINARY_DIR}/_gen_cffi_headers) set(CFFI_OUTPUT_LIBRARY "_${VENDORED_LIBRARY_PKG_CONFIG}") # Setting python for the host system (before change in CMAKE_SYSTEM_PROCESSOR) -find_package(Python 3 REQUIRED COMPONENTS Interpreter Development.Module Development.SABIModule) +find_package(Python 3 REQUIRED COMPONENTS Interpreter Development.Module) include(SetCrossCompilerGithubActions) SetCrossCompilerGithubActions() diff --git a/LICENSE-cffi b/LICENSE-cffi new file mode 100644 index 000000000..a19e1eea1 --- /dev/null +++ b/LICENSE-cffi @@ -0,0 +1,21 @@ +Except when otherwise stated (look for LICENSE files in directories or +information at the beginning of each file) all software and +documentation is licensed as follows: + + MIT No Attribution + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the + Software is furnished to do so. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. diff --git a/cm_library_c_binding/build.py b/cm_library_c_binding/build.py index 47505a4fc..f056bbdc6 100644 --- a/cm_library_c_binding/build.py +++ b/cm_library_c_binding/build.py @@ -66,7 +66,7 @@ def mk_ffi( code.append(source.include) code.append("#define PY_USE_BUNDLED") - _ffi.set_source(name, "\n".join(code)) + _ffi.set_source(name, "\n".join(code), py_limited_api=False) return _ffi diff --git a/cm_python_module/CMakeLists.txt b/cm_python_module/CMakeLists.txt index c73cc6371..98ab8a730 100644 --- a/cm_python_module/CMakeLists.txt +++ b/cm_python_module/CMakeLists.txt @@ -1,11 +1,10 @@ -# Create the shared library from the CFFI binding and the static library from ${CFFI_INPUT_LIBRARY} -if (CMAKE_SYSTEM_NAME STREQUAL "Windows") - Python_add_library(${CFFI_OUTPUT_LIBRARY} MODULE USE_SABI 3.8 "${CFFI_C_CODE_DIR}/${CFFI_C_CODE}") -else() - set(Python_SOABI ${SKBUILD_SOABI}) - Python_add_library(${CFFI_OUTPUT_LIBRARY} MODULE WITH_SOABI "${CFFI_C_CODE_DIR}/${CFFI_C_CODE}") - target_compile_definitions(${CFFI_OUTPUT_LIBRARY} PRIVATE Py_LIMITED_API) -endif() +# Create an interpreter-specific shared library from the CFFI binding. +# The bundled _cffi_backend is interpreter-specific as well, so the limited API +# cannot make these wheels abi3. Free-threaded CPython 3.14 also requires the +# extension to be built without Py_LIMITED_API. +set(Python_SOABI ${SKBUILD_SOABI}) +Python_add_library(${CFFI_OUTPUT_LIBRARY} MODULE WITH_SOABI "${CFFI_C_CODE_DIR}/${CFFI_C_CODE}") +target_compile_definitions(${CFFI_OUTPUT_LIBRARY} PRIVATE _CFFI_NO_LIMITED_API) set_source_files_properties("${CFFI_C_CODE_DIR}/${CFFI_C_CODE}" PROPERTIES GENERATED 1) @@ -20,7 +19,6 @@ if (PROJECT_IGNORE_SYSTEM_LIB OR NOT VENDORED_AS_SYSTEM_LIB_FOUND) target_include_directories(${CFFI_OUTPUT_LIBRARY} PUBLIC ${VENDORED_HEADERS_DIR}) # Link the vendored library to the output library - # https://docs.python.org/3/c-api/stable.html#limited-c-api target_link_libraries(${CFFI_OUTPUT_LIBRARY} PRIVATE ${CFFI_INPUT_LIBRARY}) elseif(VENDORED_AS_SYSTEM_LIB_FOUND) message(STATUS "Vendored system library found: ${VENDORED_AS_SYSTEM_LIB_LIBRARIES}") diff --git a/docs/history.md b/docs/history.md index 5d47dac92..7012143fb 100644 --- a/docs/history.md +++ b/docs/history.md @@ -4,7 +4,16 @@ Important changes are emphasized. -## Unreleased +## 22.0.0 + +- Add binary wheels for Python 3.14 +- Add binary wheels for free-threaded Python 3.14 +- Add source-build support for Python 3.15, including free-threaded builds +- Fix source builds when the installed CFFI distribution does not include its license metadata +- Fix `COINCURVE_VENDOR_CFFI` so disabling it also excludes the bundled `_cffi_backend` module +- Build Windows ARM64 and Linux AArch64 wheels on native GitHub Actions runners +- Require macOS 10.15 or later for x86-64 wheels +- Improve release artifact validation and reduce duplicated CI work ## 21.0.0 diff --git a/docs/install.md b/docs/install.md index d321bdb61..2a1d52db2 100644 --- a/docs/install.md +++ b/docs/install.md @@ -10,7 +10,8 @@ pip install coincurve ## Wheel -Binary wheels are available for most platforms and require at least version `19.3` of pip to install. +Binary wheels are available for most platforms. An up-to-date version of pip is recommended, +particularly when installing free-threaded wheels. | | | | | | | --- | --- | --- | --- | --- | @@ -20,6 +21,12 @@ Binary wheels are available for most platforms and require at least version `19. | CPython 3.11 | | | | | | CPython 3.12 | | | | | | CPython 3.13 | | | | | +| CPython 3.14 | | | | | +| CPython 3.14t (free-threaded) | | | | | + +The x86-64 macOS wheels require macOS 10.15 or later. Python 3.15 prereleases, including +free-threaded builds, are supported through source installation; binary wheels will be added +after Python 3.15 reaches release candidate status. ## Source @@ -28,8 +35,9 @@ If you are on a platform without support for pre-compiled wheels, you will need A few environment variables influence the build: - `COINCURVE_UPSTREAM_REF` - This is the Git reference of [libsecp256k1][] to use rather than the (frequently updated) default. -- `COINCURVE_IGNORE_SYSTEM_LIB` - The presence of this will force fetching of [libsecp256k1][] even if it's already detected at the system level. -- `COINCURVE_VENDOR_CFFI` - Setting this to anything other than `1` (the default) prevents vendoring of the `_cffi_backend` module. Re-distributors should make sure to require `cffi` as a runtime dependency when disabling this. +- `COINCURVE_IGNORE_SYSTEM_LIB` - Setting this to `ON` forces fetching of [libsecp256k1][] even if it is detected at the system level. +- `COINCURVE_SECP256K1_STATIC` - Setting this to `OFF` builds the vendored [libsecp256k1][] as a shared library. +- `COINCURVE_VENDOR_CFFI` - Setting this to anything other than `1` (the default) prevents vendoring of the `_cffi_backend` module and its license. Re-distributors must add `cffi` as a runtime dependency when disabling this. !!! tip To avoid installing the binary wheels on compatible distributions, use the `--no-binary` option. diff --git a/hatch.toml b/hatch.toml index 5531e94f9..cfc065162 100644 --- a/hatch.toml +++ b/hatch.toml @@ -9,6 +9,9 @@ dependencies = ["ruff==0.13.0"] [envs.hatch-test] dev-mode = false +[[envs.hatch-test.matrix]] +python = ["3.14t", "3.14", "3.13", "3.12", "3.11", "3.10", "3.9"] + [envs.types] dependencies = [ "mypy", diff --git a/hatch_build.py b/hatch_build.py index 012e6501f..05cd7383f 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -1,9 +1,6 @@ from __future__ import annotations import os -import shutil -from functools import cached_property -from importlib.metadata import PackagePath, distribution from typing import Any import _cffi_backend # noqa: PLC2701 @@ -18,35 +15,15 @@ class CustomBuildHook(BuildHookInterface): LICENSE_NAME = "LICENSE-cffi" - @cached_property - def local_cffi_license(self) -> str: - return os.path.join(self.root, self.LICENSE_NAME) - - @staticmethod - def get_cffi_distribution_license_files() -> list[PackagePath]: - license_files = [] - - dist_files = distribution("cffi").files or [] - for f in dist_files: - if f.name == "LICENSE" and f.parts[0].endswith(".dist-info"): - license_files.append(f) - break - - return license_files - def initialize(self, version: str, build_data: dict[str, Any]) -> None: # noqa: ARG002 - cffi_shared_lib = _cffi_backend.__file__ - relative_path = f"coincurve/{os.path.basename(cffi_shared_lib)}" - build_data["force_include"][cffi_shared_lib] = relative_path + if os.environ.get("COINCURVE_VENDOR_CFFI", "1") != "1": + return - license_files = self.get_cffi_distribution_license_files() - if len(license_files) != 1: - message = f"Expected exactly one LICENSE file in cffi distribution, got {len(license_files)}" + cffi_shared_lib = _cffi_backend.__file__ + if cffi_shared_lib is None: + message = "Could not locate the _cffi_backend extension module" raise RuntimeError(message) - license_file = license_files[0] - shutil.copy2(license_file.locate(), self.local_cffi_license) + relative_path = f"coincurve/{os.path.basename(cffi_shared_lib)}" + build_data["force_include"][cffi_shared_lib] = relative_path self.metadata.core.license_files.append(self.LICENSE_NAME) - - def finalize(self, version: str, build_data: dict[str, Any], artifact: str) -> None: # noqa: ARG002 - os.remove(self.local_cffi_license) diff --git a/mkdocs.yml b/mkdocs.yml index f8e481f24..95619384c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -83,7 +83,7 @@ plugins: signature_crossrefs: true # Other show_bases: false - import: + inventories: - https://docs.python.org/3/objects.inv markdown_extensions: diff --git a/pyproject.toml b/pyproject.toml index 3a3ad643b..058118a1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,9 @@ build-backend = "hatchling.build" requires = [ "hatchling>=1.27.0", - "cffi", + "cffi>=1.17.1; python_version < '3.14'", + "cffi>=2.0.0; python_version == '3.14'", + "cffi>=2.1.0; python_version >= '3.15'", "scikit-build-core>=0.9.0", "pkgconf; sys_platform == 'win32'", ] @@ -98,6 +100,18 @@ VENDORED_OPTION_EXPERIMENTAL = "ON" # VENDORED_CMAKE__ = VENDORED_LIBRARY_STATIC_BUILD = { env = "COINCURVE_SECP256K1_STATIC", default = "ON" } +# --- cibuildwheel --- +[tool.cibuildwheel] +build = [ + "cp39-*", + "cp310-*", + "cp311-*", + "cp312-*", + "cp313-*", + "cp314-*", + "cp314t-*", +] + # --- Coverage --- [tool.coverage.run] source_pkgs = ["coincurve", "tests"] diff --git a/scripts/check_dist.py b/scripts/check_dist.py new file mode 100644 index 000000000..21dd98f04 --- /dev/null +++ b/scripts/check_dist.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import argparse +from pathlib import Path + +PYTHON_TAGS = {"cp39", "cp310", "cp311", "cp312", "cp313", "cp314", "cp314t"} +PLATFORM_TAGS = { + "macos-arm64", + "macos-x86_64", + "manylinux-aarch64", + "manylinux-i686", + "manylinux-x86_64", + "musllinux-aarch64", + "musllinux-i686", + "musllinux-x86_64", + "windows-amd64", + "windows-arm64", +} + + +def classify_platform(platform: str) -> str: + if platform.startswith("macosx_"): + family = "macos" + elif "manylinux" in platform: + family = "manylinux" + elif "musllinux" in platform: + family = "musllinux" + elif platform == "win_amd64": + return "windows-amd64" + elif platform == "win_arm64": + return "windows-arm64" + else: + message = f"Unsupported wheel platform tag: {platform}" + raise ValueError(message) + + for architecture in ("aarch64", "arm64", "i686", "x86_64"): + if platform.endswith(f"_{architecture}"): + return f"{family}-{architecture}" + + message = f"Unsupported wheel architecture: {platform}" + raise ValueError(message) + + +def classify_wheel(wheel: Path) -> tuple[str, str]: + _, python_tag, abi_tag, platform = wheel.stem.rsplit("-", 3) + build_tag = abi_tag if abi_tag.endswith("t") else python_tag + if build_tag.startswith("cp315"): + message = f"Python 3.15 wheels are source-only for this release: {wheel.name}" + raise ValueError(message) + if build_tag not in PYTHON_TAGS: + message = f"Unexpected Python tag in {wheel.name}: {build_tag}" + raise ValueError(message) + return build_tag, classify_platform(platform) + + +def check_distribution(directory: Path, version: str) -> None: + expected_sdist = directory / f"coincurve-{version}.tar.gz" + sdists = sorted(directory.glob("*.tar.gz")) + if sdists != [expected_sdist]: + message = f"Expected only {expected_sdist.name}, found: {[path.name for path in sdists]}" + raise RuntimeError(message) + + wheels = sorted(directory.glob("*.whl")) + actual = {classify_wheel(wheel) for wheel in wheels} + expected = {(python, platform) for python in PYTHON_TAGS for platform in PLATFORM_TAGS} + + missing = sorted(expected - actual) + unexpected = sorted(actual - expected) + if missing or unexpected or len(wheels) != len(expected): + message = ( + f"Invalid wheel set: expected {len(expected)}, found {len(wheels)}; " + f"missing={missing}; unexpected={unexpected}" + ) + raise RuntimeError(message) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Verify coincurve release artifacts") + parser.add_argument("directory", type=Path) + parser.add_argument("version") + args = parser.parse_args() + check_distribution(args.directory, args.version) + + +if __name__ == "__main__": + main() diff --git a/scripts/check_prerelease_python.py b/scripts/check_prerelease_python.py new file mode 100644 index 000000000..247cdcfe7 --- /dev/null +++ b/scripts/check_prerelease_python.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import os +import sys +import sysconfig + +MINIMUM_BETA_SERIAL = 4 + + +def main() -> None: + version = sys.version_info + if version[:2] != (3, 15): + message = f"Expected Python 3.15, found {sys.version}" + raise RuntimeError(message) + if version.releaselevel == "alpha" or (version.releaselevel == "beta" and version.serial < MINIMUM_BETA_SERIAL): + message = f"Expected Python 3.15.0b4 or newer, found {sys.version}" + raise RuntimeError(message) + + expected_free_threaded = os.environ["EXPECTED_FREE_THREADED"] == "true" + free_threaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) + if free_threaded != expected_free_threaded: + message = f"Expected free-threaded={expected_free_threaded}, found free-threaded={free_threaded}" + raise RuntimeError(message) + + print(sys.version) + print(f"free-threaded={free_threaded}") + + +if __name__ == "__main__": + main() diff --git a/src/coincurve/__init__.py b/src/coincurve/__init__.py index e3e5c6226..e51c2ed54 100644 --- a/src/coincurve/__init__.py +++ b/src/coincurve/__init__.py @@ -2,7 +2,7 @@ from coincurve.keys import PrivateKey, PublicKey, PublicKeyXOnly from coincurve.utils import verify_signature -__version__ = "21.0.0" +__version__ = "22.0.0" __all__ = [ "GLOBAL_CONTEXT", "Context", diff --git a/src/coincurve/der.py b/src/coincurve/der.py index b72c8d0e5..fa1e0477e 100644 --- a/src/coincurve/der.py +++ b/src/coincurve/der.py @@ -130,7 +130,7 @@ def encode_der(private_key: bytes, public_key: bytes | None = None) -> bytes: ec_key_seq.extend(ec_key_buffer) # Wrap in octet string for outer structure - ec_key_os = encode_octet_string(ec_key_seq) + ec_key_os = encode_octet_string(bytes(ec_key_seq)) # Build the outer PKCS#8 structure result = bytearray([SEQUENCE_TAG]) diff --git a/tests/test_freethreading.py b/tests/test_freethreading.py new file mode 100644 index 000000000..093d9cf56 --- /dev/null +++ b/tests/test_freethreading.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import sys +import sysconfig +from concurrent.futures import ThreadPoolExecutor + +import pytest + +from coincurve import PrivateKey + +pytestmark = pytest.mark.skipif( + not sysconfig.get_config_var("Py_GIL_DISABLED"), + reason="requires free-threaded CPython", +) + + +def sign_and_verify(seed: int) -> bool: + private_key = PrivateKey(seed.to_bytes(32, "big")) + message = f"message-{seed}".encode() + signature = private_key.sign(message) + return private_key.public_key.verify(signature, message) + + +def test_import_does_not_enable_gil(): + assert not sys._is_gil_enabled() # noqa: SLF001 + + +def test_concurrent_operations_with_shared_context(): + with ThreadPoolExecutor(max_workers=8) as executor: + assert all(executor.map(sign_and_verify, range(1, 129))) From a3ca3782f9598fb01ab9d8b40e2630bf359f8d9f Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sat, 18 Jul 2026 11:52:49 -0400 Subject: [PATCH 2/7] Fix release CI test setup --- .github/workflows/build.yml | 8 ++++--- .github/workflows/verify_shared_build.yml | 29 ----------------------- 2 files changed, 5 insertions(+), 32 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 56caf1610..811d78364 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,15 +75,17 @@ jobs: if: runner.os == 'macOS' run: ./.github/scripts/install-macos-build-deps.sh - - name: Install Hatch - uses: pypa/hatch@install + - name: Install project and test dependencies + run: python -m pip install . pytest pytest-randomly - name: Run tests - run: hatch test --python ${{ matrix.python-version }} --randomize + run: python -m pytest tests coverage: name: Upload coverage runs-on: ubuntu-latest + env: + PYTHON_VERSION: '3.14t' permissions: contents: read diff --git a/.github/workflows/verify_shared_build.yml b/.github/workflows/verify_shared_build.yml index c3eb37627..c5987088a 100644 --- a/.github/workflows/verify_shared_build.yml +++ b/.github/workflows/verify_shared_build.yml @@ -36,27 +36,8 @@ env: python -m pytest {project} jobs: - test: - name: Test shared libsecp256k1 build - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v7 - - - uses: actions/setup-python@v6 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install Hatch - uses: pypa/hatch@install - - - name: Run tests - run: hatch test --python ${{ env.PYTHON_VERSION }} --randomize - linux-wheels-x86_64: name: Build shared Linux wheel for x86-64 - needs: - - test runs-on: ubuntu-latest steps: @@ -69,8 +50,6 @@ jobs: linux-wheels-arm64: name: Build shared Linux wheel for ARM64 - needs: - - test runs-on: ubuntu-24.04-arm steps: @@ -83,8 +62,6 @@ jobs: macos-wheels-x86_64: name: Build shared macOS wheel for x86-64 - needs: - - test runs-on: macos-15-intel steps: @@ -98,8 +75,6 @@ jobs: macos-wheels-arm64: name: Build shared macOS wheel for ARM64 - needs: - - test runs-on: macos-latest steps: @@ -112,8 +87,6 @@ jobs: windows-wheels-x86_64: name: Build shared Windows wheel for x86-64 - needs: - - test runs-on: windows-latest steps: @@ -127,8 +100,6 @@ jobs: windows-wheels-arm64: name: Build shared Windows wheel for ARM64 - needs: - - test runs-on: windows-11-arm steps: From 114830fec2c8be2743d4fededaf05581bca272fd Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sat, 18 Jul 2026 12:05:15 -0400 Subject: [PATCH 3/7] Drop Python 3.9 and package shared builds --- .github/workflows/build.yml | 2 +- CMakeLists.txt | 22 ++++++++++++++++++++-- docs/history.md | 1 + docs/install.md | 1 - hatch.toml | 2 +- pyproject.toml | 3 +-- scripts/check_dist.py | 2 +- 7 files changed, 25 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 811d78364..46692ba38 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,7 +62,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] steps: - uses: actions/checkout@v7 diff --git a/CMakeLists.txt b/CMakeLists.txt index c900518f5..c4bd6b730 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,5 +43,23 @@ add_subdirectory(cm_library_cffi_headers) add_subdirectory(cm_library_c_binding) add_subdirectory(cm_python_module) -# Configure installation of the shared library ${CFFI_OUTPUT_LIBRARY} in the package -install(TARGETS ${CFFI_OUTPUT_LIBRARY} LIBRARY DESTINATION ${SKBUILD_PLATLIB_DIR}/${SKBUILD_PROJECT_NAME}) +# Configure installation of the extension in the package. +set(PROJECT_PACKAGE_DIR ${SKBUILD_PLATLIB_DIR}/${SKBUILD_PROJECT_NAME}) +install(TARGETS ${CFFI_OUTPUT_LIBRARY} LIBRARY DESTINATION ${PROJECT_PACKAGE_DIR}) + +# A dynamically linked vendored libsecp256k1 must survive the temporary build +# directory so wheel repair tools can find it. Keep it beside the extension and +# use a loader-relative runtime path; Windows already searches the module's +# directory for dependent DLLs. +if ((PROJECT_IGNORE_SYSTEM_LIB OR NOT VENDORED_AS_SYSTEM_LIB_FOUND) AND NOT VENDORED_LIBRARY_STATIC_BUILD) + if (APPLE) + set_target_properties(${CFFI_OUTPUT_LIBRARY} PROPERTIES INSTALL_RPATH "@loader_path") + elseif (UNIX) + set_target_properties(${CFFI_OUTPUT_LIBRARY} PROPERTIES INSTALL_RPATH "$ORIGIN") + endif() + install( + TARGETS ${CFFI_INPUT_LIBRARY} + LIBRARY DESTINATION ${PROJECT_PACKAGE_DIR} + RUNTIME DESTINATION ${PROJECT_PACKAGE_DIR} + ) +endif() diff --git a/docs/history.md b/docs/history.md index 7012143fb..d47b58da6 100644 --- a/docs/history.md +++ b/docs/history.md @@ -6,6 +6,7 @@ Important changes are emphasized. ## 22.0.0 +- **Breaking:** Drop support for Python 3.9 - Add binary wheels for Python 3.14 - Add binary wheels for free-threaded Python 3.14 - Add source-build support for Python 3.15, including free-threaded builds diff --git a/docs/install.md b/docs/install.md index 2a1d52db2..4e4b013b5 100644 --- a/docs/install.md +++ b/docs/install.md @@ -16,7 +16,6 @@ particularly when installing free-threaded wheels. | | | | | | | --- | --- | --- | --- | --- | | | macOS | Windows | Linux (glibc) | Linux (musl) | -| CPython 3.9 |
  • x86_64
  • ARM64
|
  • x86_64
  • ARM64
|
  • x86_64
  • i686
  • AArch64
|
  • x86_64
  • i686
  • AArch64
| | CPython 3.10 |
  • x86_64
  • ARM64
|
  • x86_64
  • ARM64
|
  • x86_64
  • i686
  • AArch64
|
  • x86_64
  • i686
  • AArch64
| | CPython 3.11 |
  • x86_64
  • ARM64
|
  • x86_64
  • ARM64
|
  • x86_64
  • i686
  • AArch64
|
  • x86_64
  • i686
  • AArch64
| | CPython 3.12 |
  • x86_64
  • ARM64
|
  • x86_64
  • ARM64
|
  • x86_64
  • i686
  • AArch64
|
  • x86_64
  • i686
  • AArch64
| diff --git a/hatch.toml b/hatch.toml index cfc065162..e0e0b9220 100644 --- a/hatch.toml +++ b/hatch.toml @@ -10,7 +10,7 @@ dependencies = ["ruff==0.13.0"] dev-mode = false [[envs.hatch-test.matrix]] -python = ["3.14t", "3.14", "3.13", "3.12", "3.11", "3.10", "3.9"] +python = ["3.14t", "3.14", "3.13", "3.12", "3.11", "3.10"] [envs.types] dependencies = [ diff --git a/pyproject.toml b/pyproject.toml index 058118a1a..57f4820be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ license-files = [ "LICENSE-MIT", "NOTICE", ] -requires-python = ">=3.9" +requires-python = ">=3.10" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -103,7 +103,6 @@ VENDORED_LIBRARY_STATIC_BUILD = { env = "COINCURVE_SECP256K1_STATIC", default = # --- cibuildwheel --- [tool.cibuildwheel] build = [ - "cp39-*", "cp310-*", "cp311-*", "cp312-*", diff --git a/scripts/check_dist.py b/scripts/check_dist.py index 21dd98f04..929fa4d43 100644 --- a/scripts/check_dist.py +++ b/scripts/check_dist.py @@ -3,7 +3,7 @@ import argparse from pathlib import Path -PYTHON_TAGS = {"cp39", "cp310", "cp311", "cp312", "cp313", "cp314", "cp314t"} +PYTHON_TAGS = {"cp310", "cp311", "cp312", "cp313", "cp314", "cp314t"} PLATFORM_TAGS = { "macos-arm64", "macos-x86_64", From a4cfa04e3660c22c069a3520a24857ee66ab71e2 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sat, 18 Jul 2026 12:11:14 -0400 Subject: [PATCH 4/7] Fix Windows shared wheel verification --- .github/workflows/verify_shared_build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/verify_shared_build.yml b/.github/workflows/verify_shared_build.yml index c5987088a..5e4db761c 100644 --- a/.github/workflows/verify_shared_build.yml +++ b/.github/workflows/verify_shared_build.yml @@ -25,6 +25,11 @@ env: COINCURVE_SECP256K1_STATIC COINCURVE_CROSS_HOST CIBW_BEFORE_ALL_MACOS: ./.github/scripts/install-macos-build-deps.sh + # The shared Windows wheel already installs libsecp256k1 beside the extension. + # delvewheel only searches outside the wheel for that dependency, so preserve + # the already self-contained wheel instead of trying to vendor it a second time. + CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: >- + python -c "import shutil; shutil.copy2(r'{wheel}', r'{dest_dir}')" CIBW_TEST_REQUIRES: pytest CIBW_TEST_COMMAND: > python -c From 57731e09157814d26d1ee03d94dc6ff42a6299ca Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sat, 18 Jul 2026 12:23:56 -0400 Subject: [PATCH 5/7] Drop 32-bit Linux wheels --- .github/workflows/build.yml | 6 +++--- docs/history.md | 1 + docs/install.md | 12 ++++++------ scripts/check_dist.py | 4 +--- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 46692ba38..6cf4f233c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -177,7 +177,7 @@ jobs: run: python -m pytest tests linux-wheels-x86_64: - name: Build Linux wheels for x86-64 and i686 + name: Build Linux wheels for x86-64 needs: - quality - test @@ -189,11 +189,11 @@ jobs: - name: Build wheels uses: pypa/cibuildwheel@v4.1.0 env: - CIBW_ARCHS_LINUX: x86_64 i686 + CIBW_ARCHS_LINUX: x86_64 - uses: actions/upload-artifact@v7 with: - name: artifact-linux-wheels-x86_64-i686 + name: artifact-linux-wheels-x86_64 path: wheelhouse/*.whl if-no-files-found: error diff --git a/docs/history.md b/docs/history.md index d47b58da6..5cdbe0e20 100644 --- a/docs/history.md +++ b/docs/history.md @@ -7,6 +7,7 @@ Important changes are emphasized. ## 22.0.0 - **Breaking:** Drop support for Python 3.9 +- **Breaking:** Stop building 32-bit Linux wheels - Add binary wheels for Python 3.14 - Add binary wheels for free-threaded Python 3.14 - Add source-build support for Python 3.15, including free-threaded builds diff --git a/docs/install.md b/docs/install.md index 4e4b013b5..0fbdfabbd 100644 --- a/docs/install.md +++ b/docs/install.md @@ -16,12 +16,12 @@ particularly when installing free-threaded wheels. | | | | | | | --- | --- | --- | --- | --- | | | macOS | Windows | Linux (glibc) | Linux (musl) | -| CPython 3.10 |
  • x86_64
  • ARM64
|
  • x86_64
  • ARM64
|
  • x86_64
  • i686
  • AArch64
|
  • x86_64
  • i686
  • AArch64
| -| CPython 3.11 |
  • x86_64
  • ARM64
|
  • x86_64
  • ARM64
|
  • x86_64
  • i686
  • AArch64
|
  • x86_64
  • i686
  • AArch64
| -| CPython 3.12 |
  • x86_64
  • ARM64
|
  • x86_64
  • ARM64
|
  • x86_64
  • i686
  • AArch64
|
  • x86_64
  • i686
  • AArch64
| -| CPython 3.13 |
  • x86_64
  • ARM64
|
  • x86_64
  • ARM64
|
  • x86_64
  • i686
  • AArch64
|
  • x86_64
  • i686
  • AArch64
| -| CPython 3.14 |
  • x86_64
  • ARM64
|
  • x86_64
  • ARM64
|
  • x86_64
  • i686
  • AArch64
|
  • x86_64
  • i686
  • AArch64
| -| CPython 3.14t (free-threaded) |
  • x86_64
  • ARM64
|
  • x86_64
  • ARM64
|
  • x86_64
  • i686
  • AArch64
|
  • x86_64
  • i686
  • AArch64
| +| CPython 3.10 |
  • x86_64
  • ARM64
|
  • x86_64
  • ARM64
|
  • x86_64
  • AArch64
|
  • x86_64
  • AArch64
| +| CPython 3.11 |
  • x86_64
  • ARM64
|
  • x86_64
  • ARM64
|
  • x86_64
  • AArch64
|
  • x86_64
  • AArch64
| +| CPython 3.12 |
  • x86_64
  • ARM64
|
  • x86_64
  • ARM64
|
  • x86_64
  • AArch64
|
  • x86_64
  • AArch64
| +| CPython 3.13 |
  • x86_64
  • ARM64
|
  • x86_64
  • ARM64
|
  • x86_64
  • AArch64
|
  • x86_64
  • AArch64
| +| CPython 3.14 |
  • x86_64
  • ARM64
|
  • x86_64
  • ARM64
|
  • x86_64
  • AArch64
|
  • x86_64
  • AArch64
| +| CPython 3.14t (free-threaded) |
  • x86_64
  • ARM64
|
  • x86_64
  • ARM64
|
  • x86_64
  • AArch64
|
  • x86_64
  • AArch64
| The x86-64 macOS wheels require macOS 10.15 or later. Python 3.15 prereleases, including free-threaded builds, are supported through source installation; binary wheels will be added diff --git a/scripts/check_dist.py b/scripts/check_dist.py index 929fa4d43..4fc6ec280 100644 --- a/scripts/check_dist.py +++ b/scripts/check_dist.py @@ -8,10 +8,8 @@ "macos-arm64", "macos-x86_64", "manylinux-aarch64", - "manylinux-i686", "manylinux-x86_64", "musllinux-aarch64", - "musllinux-i686", "musllinux-x86_64", "windows-amd64", "windows-arm64", @@ -33,7 +31,7 @@ def classify_platform(platform: str) -> str: message = f"Unsupported wheel platform tag: {platform}" raise ValueError(message) - for architecture in ("aarch64", "arm64", "i686", "x86_64"): + for architecture in ("aarch64", "arm64", "x86_64"): if platform.endswith(f"_{architecture}"): return f"{family}-{architecture}" From 7063cd2272cf66118f0187d684112f1a779b2bf6 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sat, 18 Jul 2026 13:01:34 -0400 Subject: [PATCH 6/7] Test against any Python 3.15 prerelease --- scripts/check_prerelease_python.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/scripts/check_prerelease_python.py b/scripts/check_prerelease_python.py index 247cdcfe7..9b458a44b 100644 --- a/scripts/check_prerelease_python.py +++ b/scripts/check_prerelease_python.py @@ -4,17 +4,12 @@ import sys import sysconfig -MINIMUM_BETA_SERIAL = 4 - def main() -> None: version = sys.version_info if version[:2] != (3, 15): message = f"Expected Python 3.15, found {sys.version}" raise RuntimeError(message) - if version.releaselevel == "alpha" or (version.releaselevel == "beta" and version.serial < MINIMUM_BETA_SERIAL): - message = f"Expected Python 3.15.0b4 or newer, found {sys.version}" - raise RuntimeError(message) expected_free_threaded = os.environ["EXPECTED_FREE_THREADED"] == "true" free_threaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) From b700db3132bbe71cbed556fd3e069149f67f9a88 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sat, 18 Jul 2026 13:17:34 -0400 Subject: [PATCH 7/7] Require PEP 639 metadata validation support --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6cf4f233c..78f194e83 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -327,7 +327,7 @@ jobs: path: dist - name: Install validation tools - run: python -m pip install check-wheel-contents twine + run: python -m pip install "packaging>=24.2" check-wheel-contents twine - name: Validate metadata and wheel contents run: |