From 9b4c0164a3e4d3542e9e6fea69fc516eef1f0a95 Mon Sep 17 00:00:00 2001 From: Thomas Paviot Date: Sun, 18 Oct 2026 07:19:26 +0200 Subject: [PATCH 1/9] Build the PyPI wheels with cibuildwheel on GitHub Actions pyproject.toml (scikit-build-core) and the cibuildwheel configuration, the native dependencies build scripts and the workflow come from the pythonocc-pypi repository. The wheels cover Linux x86_64/aarch64, macOS x86_64/arm64 and Windows AMD64 for CPython 3.10 to 3.14. A tag push uploads them to PyPI through trusted publishing. --- .github/workflows/wheels.yml | 190 +++++++++++++++++++++ CMakeLists.txt | 26 ++- ci/wheels/deps/linux.sh | 102 +++++++++++ ci/wheels/deps/macos.sh | 119 +++++++++++++ ci/wheels/deps/windows.ps1 | 93 ++++++++++ ci/wheels/triplets/arm64-osx-pyocc.cmake | 10 ++ ci/wheels/triplets/x64-osx-pyocc.cmake | 10 ++ ci/wheels/triplets/x64-windows-pyocc.cmake | 5 + pyproject.toml | 124 ++++++++++++++ 9 files changed, 673 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/wheels.yml create mode 100755 ci/wheels/deps/linux.sh create mode 100644 ci/wheels/deps/macos.sh create mode 100644 ci/wheels/deps/windows.ps1 create mode 100644 ci/wheels/triplets/arm64-osx-pyocc.cmake create mode 100644 ci/wheels/triplets/x64-osx-pyocc.cmake create mode 100644 ci/wheels/triplets/x64-windows-pyocc.cmake create mode 100644 pyproject.toml diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml new file mode 100644 index 000000000..11d146d07 --- /dev/null +++ b/.github/workflows/wheels.yml @@ -0,0 +1,190 @@ +# pythonocc-core wheels for PyPI: Linux x86_64/aarch64, macOS x86_64/arm64, +# Windows AMD64, for CPython 3.10 to 3.14, plus the sdist. +# +# For each platform, a first job builds the native dependencies (SWIG, OCCT +# and its 3rd parties) with the scripts of ci/wheels/deps/ and saves them to +# the GitHub cache, then one job per Python version restores them and runs +# cibuildwheel (configured in pyproject.toml). Pushing a tag also uploads +# the wheels and the sdist to PyPI, through trusted publishing. + +name: Wheels + +on: + push: + branches: ["master", "review/**"] + # the paths filter does not apply to the tags + tags: ["*"] + paths: + - pyproject.toml + - CMakeLists.txt + - ci/wheels/** + - .github/workflows/wheels.yml + pull_request: + paths: + - pyproject.toml + - CMakeLists.txt + - ci/wheels/** + - .github/workflows/wheels.yml + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + deps: + name: "${{ matrix.platform.name }}: dependencies" + runs-on: ${{ matrix.platform.os }} + timeout-minutes: 360 + strategy: + fail-fast: false + matrix: + platform: &platforms + # deps_path: the Linux tarball directory (mounted at /host in the + # manylinux container), the macOS/Windows install prefix. The + # prefix must be the same in the deps and wheels jobs. + - name: linux-x86_64 + os: ubuntu-24.04 + manylinux: quay.io/pypa/manylinux_2_28_x86_64 + deps_path: /home/runner/pyocc-deps-cache + deps_files: ci/wheels/deps/linux.sh + - name: linux-aarch64 + os: ubuntu-24.04-arm + manylinux: quay.io/pypa/manylinux_2_28_aarch64 + deps_path: /home/runner/pyocc-deps-cache + deps_files: ci/wheels/deps/linux.sh + - name: macos-x86_64 + os: macos-15-intel + arch: x86_64 + deps_path: /Users/runner/pyocc-deps + deps_files: ci/wheels/deps/macos.sh ci/wheels/triplets/x64-osx-pyocc.cmake + - name: macos-arm64 + os: macos-15 + arch: arm64 + deps_path: /Users/runner/pyocc-deps + deps_files: ci/wheels/deps/macos.sh ci/wheels/triplets/arm64-osx-pyocc.cmake + - name: windows-AMD64 + os: windows-2022 + arch: AMD64 + deps_path: C:/pyocc-deps + deps_files: ci/wheels/deps/windows.ps1 ci/wheels/triplets/x64-windows-pyocc.cmake + steps: + - uses: actions/checkout@v7 + + - id: key + name: Dependencies cache key + shell: bash + run: | + set -eu + # bump DEPS_CACHE_VERSION to invalidate the caches + DEPS_CACHE_VERSION=1 + HASH=$(cat ${{ matrix.platform.deps_files }} | sha256sum | cut -c1-16) + echo "key=pyocc-deps-${{ matrix.platform.name }}-${DEPS_CACHE_VERSION}-${HASH}" >> "$GITHUB_OUTPUT" + + - id: cache + uses: actions/cache@v6 + with: + path: ${{ matrix.platform.deps_path }} + key: ${{ steps.key.outputs.key }} + + - name: Build SWIG and OCCT (manylinux container) + if: steps.cache.outputs.cache-hit != 'true' && runner.os == 'Linux' + run: | + set -eux + mkdir -p "${{ matrix.platform.deps_path }}" + docker run --rm \ + -v "$GITHUB_WORKSPACE:/project" \ + -v "${{ matrix.platform.deps_path }}:/deps-cache" \ + -e PYOCC_DEPS_CACHE=/deps-cache \ + ${{ matrix.platform.manylinux }} \ + bash /project/ci/wheels/deps/linux.sh + + - name: Build SWIG and OCCT + if: steps.cache.outputs.cache-hit != 'true' && runner.os == 'macOS' + run: bash ci/wheels/deps/macos.sh + env: + ARCH: ${{ matrix.platform.arch }} + PYOCC_DEPS_PREFIX: ${{ matrix.platform.deps_path }} + + - name: Build OCCT + if: steps.cache.outputs.cache-hit != 'true' && runner.os == 'Windows' + run: ./ci/wheels/deps/windows.ps1 + shell: pwsh + env: + PYOCC_DEPS_PREFIX: ${{ matrix.platform.deps_path }} + + wheels: + name: "${{ matrix.platform.name }}: ${{ matrix.python }}" + needs: deps + runs-on: ${{ matrix.platform.os }} + timeout-minutes: 360 + strategy: + fail-fast: false + matrix: + platform: *platforms + python: [cp310, cp311, cp312, cp313, cp314] + steps: + - uses: actions/checkout@v7 + + - id: key + name: Dependencies cache key + shell: bash + run: | + set -eu + # must match the deps job + DEPS_CACHE_VERSION=1 + HASH=$(cat ${{ matrix.platform.deps_files }} | sha256sum | cut -c1-16) + echo "key=pyocc-deps-${{ matrix.platform.name }}-${DEPS_CACHE_VERSION}-${HASH}" >> "$GITHUB_OUTPUT" + + - uses: actions/cache/restore@v6 + with: + path: ${{ matrix.platform.deps_path }} + key: ${{ steps.key.outputs.key }} + fail-on-cache-miss: true + + - uses: pypa/cibuildwheel@v4.2.1 + env: + CIBW_BUILD: ${{ matrix.python }}-* + CIBW_ARCHS_MACOS: ${{ matrix.platform.arch }} + # Linux: the host filesystem is mounted at /host in the container + PYOCC_DEPS_CACHE: /host${{ matrix.platform.deps_path }} + PYOCC_DEPS_PREFIX: ${{ matrix.platform.deps_path }} + + - uses: actions/upload-artifact@v7 + with: + name: wheels-${{ matrix.platform.name }}-${{ matrix.python }} + path: wheelhouse/*.whl + + sdist: + name: Source distribution + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + - run: pipx run build --sdist + - uses: actions/upload-artifact@v7 + with: + name: sdist + path: dist/*.tar.gz + + publish: + name: Upload to PyPI + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + needs: [wheels, sdist] + runs-on: ubuntu-24.04 + # the trusted publisher configured on PyPI for this repository, this + # workflow and this environment + environment: + name: pypi + url: https://pypi.org/p/pythonocc-core + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v8 + with: + path: dist + merge-multiple: true + - run: ls -l dist + - uses: pypa/gh-action-pypi-publish@v1.14.2 diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e985022a..f3acfeedf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,13 @@ if(APPLE) set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) endif(APPLE) +if(SKBUILD AND UNIX AND NOT APPLE) + # wheel build: keep the OCCT library directory in the RPATH so that the + # unrepaired wheel is usable on the build machine. auditwheel replaces it + # with the vendored libraries location when repairing the wheel. + set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) +endif() + ##################################################################### # OpenGL. If available, enable compilation for Visualization module # ##################################################################### @@ -99,7 +106,7 @@ set(Python3_FIND_STRATEGY LOCATION) set(Python3_FIND_FRAMEWORK NEVER) # numpy is required: the Geom, Geom2d, Poly, TColStd, TColgp and TShort modules # use its C API. PYTHONOCC_MESHDS_NUMPY only enables the numpy arrays of MeshDS -find_package(Python3 COMPONENTS Interpreter Development NumPy REQUIRED) +find_package(Python3 COMPONENTS Interpreter Development.Module NumPy REQUIRED) include_directories(${Python3_NumPy_INCLUDE_DIRS}) message(STATUS "Numpy include directory: ${Python3_NumPy_INCLUDE_DIRS}") if(PYTHONOCC_MESHDS_NUMPY) @@ -165,7 +172,11 @@ endif() # by default, installed to site-packages/OCC # ############################################## if(NOT DEFINED PYTHONOCC_INSTALL_DIRECTORY) - if(DEFINED ENV{SP_DIR} AND WIN32) + if(SKBUILD) + # wheel build (pip/scikit-build-core): the install prefix is the + # wheel platlib directory, install relative to it + set(PYTHONOCC_INSTALL_DIRECTORY OCC CACHE PATH "pythonocc install directory") + elseif(DEFINED ENV{SP_DIR} AND WIN32) # TODO: following hack is azure specific, a recent update in azure # prevent cmake to find correct paths of python3 on windows # this should be removed as soon as possible @@ -177,7 +188,7 @@ if(NOT DEFINED PYTHONOCC_INSTALL_DIRECTORY) OUTPUT_VARIABLE python_lib OUTPUT_STRIP_TRAILING_WHITESPACE) set(PYTHONOCC_INSTALL_DIRECTORY ${python_lib}/OCC CACHE PATH "pythonocc install directory") - endif(DEFINED ENV{SP_DIR} AND WIN32) + endif() endif(NOT DEFINED PYTHONOCC_INSTALL_DIRECTORY) message(STATUS "pythonocc modules will be installed to: ${PYTHONOCC_INSTALL_DIRECTORY}") @@ -460,7 +471,8 @@ if(PYTHONOCC_WRAP_VISU) endif(PYTHONOCC_WRAP_VISU) # install Display -install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/Display DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY}) +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/Display DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY} + PATTERN "__pycache__" EXCLUDE PATTERN ".ruff_cache" EXCLUDE) # install LICENSE file install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY}) @@ -477,7 +489,9 @@ install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/PkgBase/Exception.py DESTINATION $ install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/PkgBase/py.typed DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY}/Core) # install Extend -install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/Extend DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY}) +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/Extend DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY} + PATTERN "__pycache__" EXCLUDE PATTERN ".ruff_cache" EXCLUDE) # install Wrapper utils -install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/Wrapper DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY}) +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/Wrapper DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY} + PATTERN "__pycache__" EXCLUDE PATTERN ".ruff_cache" EXCLUDE) diff --git a/ci/wheels/deps/linux.sh b/ci/wheels/deps/linux.sh new file mode 100755 index 000000000..e67a2df38 --- /dev/null +++ b/ci/wheels/deps/linux.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# Build the native dependencies of the pythonocc-core Linux wheels (SWIG, +# OCCT) and install them to $DEPS_PREFIX. Runs as the cibuildwheel +# "before-all" step, inside the manylinux_2_28 container. +# +# If PYOCC_DEPS_CACHE points to a writable directory (on the host, through +# the /host mount), the installed prefix is saved there as a tarball and +# reused by the next runs. +set -euo pipefail + +# lowest priority for the whole build (inherited by the child processes), so +# that the CI runner agent still gets CPU time for its heartbeats: a starved +# agent is reported as lost +renice -n 19 $$ > /dev/null + +SWIG_VERSION=4.5.1 +OCCT_VERSION=8.0.1 +OCCT_TAG=V8_0_1 +RAPIDJSON_COMMIT=24b5e7a8b27f42fa16b96fc70aade9106cf7102f +DEPS_PREFIX=${DEPS_PREFIX:-/opt/pyocc-deps} + +# bump the last component to invalidate caches when the build options change +CACHE_KEY="swig-${SWIG_VERSION}_occt-${OCCT_VERSION}_$(uname -m)_2" + +# system packages, also needed when the cache is used: X11/OpenGL headers +# for the visualization toolkits, freetype/fontconfig for text rendering, +# FreeImage (EPEL) for image files I/O, pcre2 for SWIG +dnf install -y epel-release +dnf install -y freetype-devel fontconfig-devel freeimage-devel mesa-libGL-devel libX11-devel pcre2-devel + +CACHE_TARBALL="" +if [ -n "${PYOCC_DEPS_CACHE:-}" ]; then + CACHE_TARBALL="${PYOCC_DEPS_CACHE}/pyocc-deps_${CACHE_KEY}.tar.gz" + if [ -f "${CACHE_TARBALL}" ]; then + echo "Using cached dependencies ${CACHE_TARBALL}" + mkdir -p "${DEPS_PREFIX}" + tar xzf "${CACHE_TARBALL}" -C "${DEPS_PREFIX}" + exit 0 + fi +fi + +# one compile job per core, but at most one per 3 GB of memory: some OCCT +# sources need more than 1 GB to compile, the agent is lost when it runs out +MEM_JOBS=$(awk '/MemTotal/ {print int($2 / (3 * 1024 * 1024))}' /proc/meminfo) +NPROC=$(nproc) +if [ "${MEM_JOBS}" -ge 1 ] && [ "${MEM_JOBS}" -lt "${NPROC}" ]; then NPROC=${MEM_JOBS}; fi +echo "Building with ${NPROC} jobs" +WORK=$(mktemp -d) +cd "${WORK}" + +# SWIG +curl -fsSL "https://downloads.sourceforge.net/project/swig/swig/swig-${SWIG_VERSION}/swig-${SWIG_VERSION}.tar.gz" | tar xz +pushd "swig-${SWIG_VERSION}" +./configure --prefix="${DEPS_PREFIX}" --without-alllang --with-python3 +make -j"${NPROC}" +make install +popd + +# RapidJSON (header only, required by the glTF reader/writer) +curl -fsSL "https://github.com/Tencent/rapidjson/archive/${RAPIDJSON_COMMIT}.tar.gz" | tar xz +mkdir -p "${DEPS_PREFIX}/include" +cp -r "rapidjson-${RAPIDJSON_COMMIT}/include/rapidjson" "${DEPS_PREFIX}/include/" + +# OCCT +mkdir occt-src +curl -fsSL "https://github.com/Open-Cascade-SAS/OCCT/archive/refs/tags/${OCCT_TAG}.tar.gz" | tar xz --strip-components=1 -C occt-src +cmake -S occt-src -B occt-build -G "Unix Makefiles" \ + -DCMAKE_BUILD_TYPE=Release \ + -DINSTALL_DIR="${DEPS_PREFIX}" \ + -DINSTALL_DIR_LAYOUT=Unix \ + -DBUILD_LIBRARY_TYPE=Shared \ + -DBUILD_RELEASE_DISABLE_EXCEPTIONS=OFF \ + -DBUILD_MODULE_Draw=OFF \ + -DBUILD_DOC_Overview=OFF \ + -DUSE_TCL=OFF \ + -DUSE_TK=OFF \ + -DUSE_FREETYPE=ON \ + -DUSE_RAPIDJSON=ON \ + -D3RDPARTY_RAPIDJSON_INCLUDE_DIR="${DEPS_PREFIX}/include" \ + -DUSE_FREEIMAGE=ON \ + -D3RDPARTY_FREEIMAGE_INCLUDE_DIR=/usr/include \ + -D3RDPARTY_FREEIMAGE_LIBRARY_DIR=/usr/lib64 \ + -DUSE_DRACO=OFF \ + -DUSE_TBB=OFF \ + -DUSE_VTK=OFF \ + -DUSE_OPENVR=OFF \ + -DUSE_FFMPEG=OFF +cmake --build occt-build -j"${NPROC}" +cmake --install occt-build + +# the wheels only need the libraries, headers and cmake config +rm -rf "${DEPS_PREFIX}/share/doc" "${DEPS_PREFIX}/share/opencascade/samples" + +cd / +rm -rf "${WORK}" + +if [ -n "${CACHE_TARBALL}" ]; then + mkdir -p "${PYOCC_DEPS_CACHE}" + tar czf "${CACHE_TARBALL}.tmp" -C "${DEPS_PREFIX}" . + mv "${CACHE_TARBALL}.tmp" "${CACHE_TARBALL}" + echo "Saved dependencies to ${CACHE_TARBALL}" +fi diff --git a/ci/wheels/deps/macos.sh b/ci/wheels/deps/macos.sh new file mode 100644 index 000000000..7dacfc8ea --- /dev/null +++ b/ci/wheels/deps/macos.sh @@ -0,0 +1,119 @@ +#!/bin/bash +# Build the native dependencies of the pythonocc-core macOS wheels and +# install them to $PYOCC_DEPS_PREFIX: +# * SWIG, for the build machine +# * OCCT, for $ARCH (x86_64 or arm64, may differ from the build machine). +# Its 3rd parties (freetype, FreeImage, RapidJSON) are built by vcpkg, +# through the OCCT vcpkg manifest, and copied to the prefix. +# +# Nothing is done if the prefix is already complete (restored from cache). +set -euo pipefail + +# lowest priority for the whole build (inherited by the child processes), so +# that the CI runner agent still gets CPU time for its heartbeats: a starved +# agent is reported as lost +renice -n 19 $$ > /dev/null + +SWIG_VERSION=4.5.1 +OCCT_TAG=V8_0_1 +ARCH=${ARCH:?ARCH must be x86_64 or arm64} +DEPS_PREFIX=${PYOCC_DEPS_PREFIX:?PYOCC_DEPS_PREFIX must be set} +export MACOSX_DEPLOYMENT_TARGET=11.0 + +HERE=$(cd "$(dirname "$0")/.." && pwd) +COMPLETE_MARK="${DEPS_PREFIX}/.pyocc-deps-complete" +if [ -f "${COMPLETE_MARK}" ]; then + echo "Dependencies already installed in ${DEPS_PREFIX}" + exit 0 +fi + +case "${ARCH}" in + x86_64) TRIPLET=x64-osx-pyocc ;; + arm64) TRIPLET=arm64-osx-pyocc ;; + *) echo "unsupported ARCH ${ARCH}"; exit 1 ;; +esac + +# one compile job per core, but at most one per 3 GB of memory: the hosted +# agent is lost when the build exhausts the memory +MEM_JOBS=$(( $(sysctl -n hw.memsize) / (3 * 1024 * 1024 * 1024) )) +NPROC=$(sysctl -n hw.ncpu) +if [ "${MEM_JOBS}" -ge 1 ] && [ "${MEM_JOBS}" -lt "${NPROC}" ]; then NPROC=${MEM_JOBS}; fi +echo "Building with ${NPROC} jobs" + +# log the free memory and disk space, to diagnose a lost agent +( while true; do + sleep 120 + echo "[resources] $(date +%H:%M:%S) $(vm_stat | awk -v ps="$(sysctl -n hw.pagesize)" '/Pages free/ {f = $3} /Pages inactive/ {i = $3} END {printf "free %d MB inactive %d MB", f * ps / 1048576, i * ps / 1048576}') $(sysctl -n vm.swapusage | awk '{print "swap used " $6}') disk $(df -h / | awk 'NR == 2 {print $4}') free" + done ) & +MONITOR_PID=$! +trap 'kill ${MONITOR_PID} 2>/dev/null || true' EXIT + +WORK=$(mktemp -d) +cd "${WORK}" + +# SWIG, runs on the build machine only. PCRE is only needed by the +# %rename regex matching, which pythonocc does not use +curl -fsSL "https://downloads.sourceforge.net/project/swig/swig/swig-${SWIG_VERSION}/swig-${SWIG_VERSION}.tar.gz" | tar xz +pushd "swig-${SWIG_VERSION}" +./configure --prefix="${DEPS_PREFIX}" --without-alllang --with-python3 --without-pcre +make -j"${NPROC}" +make install +popd + +# autotools, needed by some vcpkg ports (the OCCT macOS CI installs the same) +brew install autoconf autoconf-archive automake libtool nasm pkg-config || true + +# vcpkg: a full clone, the OCCT manifest pins a baseline commit +git clone https://github.com/microsoft/vcpkg.git vcpkg +./vcpkg/bootstrap-vcpkg.sh -disableMetrics + +# OCCT +mkdir occt-src +curl -fsSL "https://github.com/Open-Cascade-SAS/OCCT/archive/refs/tags/${OCCT_TAG}.tar.gz" | tar xz --strip-components=1 -C occt-src +cmake -S occt-src -B occt-build -G "Unix Makefiles" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_OSX_ARCHITECTURES="${ARCH}" \ + -DCMAKE_OSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET}" \ + -DCMAKE_INSTALL_RPATH="${DEPS_PREFIX}/lib" \ + -DBUILD_USE_VCPKG=ON \ + -DVCPKG_ROOT="${WORK}/vcpkg" \ + -DVCPKG_TARGET_TRIPLET="${TRIPLET}" \ + -DVCPKG_OVERLAY_TRIPLETS="${HERE}/triplets" \ + -DVCPKG_INSTALL_OPTIONS=--clean-after-build \ + -DINSTALL_DIR="${DEPS_PREFIX}" \ + -DINSTALL_DIR_LAYOUT=Unix \ + -DBUILD_LIBRARY_TYPE=Shared \ + -DBUILD_RELEASE_DISABLE_EXCEPTIONS=OFF \ + -DBUILD_MODULE_Draw=OFF \ + -DBUILD_DOC_Overview=OFF \ + -DUSE_TCL=OFF \ + -DUSE_TK=OFF \ + -DUSE_FREETYPE=ON \ + -DUSE_RAPIDJSON=ON \ + -DUSE_FREEIMAGE=ON \ + -DUSE_DRACO=OFF \ + -DUSE_TBB=OFF \ + -DUSE_VTK=OFF \ + -DUSE_OPENVR=OFF \ + -DUSE_FFMPEG=OFF \ + || { + # vcpkg runs during the configuration, show why it failed at the end of the log + grep -n -A30 "^error:" occt-build/vcpkg-manifest-install.log | tail -n 120 || true + exit 1 + } +cmake --build occt-build -j"${NPROC}" +cmake --install occt-build + +# the 3rd party dylibs, for delocate. RapidJSON is header only, its +# headers are included by the OCCT glTF headers: put them next to the +# OCCT headers so that the pythonocc build finds them +VCPKG_INSTALLED="occt-build/vcpkg_installed/${TRIPLET}" +cp -a "${VCPKG_INSTALLED}"/lib/*.dylib "${DEPS_PREFIX}/lib/" +cp -a "${VCPKG_INSTALLED}/include/rapidjson" "${DEPS_PREFIX}/include/opencascade/" + +# the wheels only need the libraries, headers and cmake config +rm -rf "${DEPS_PREFIX}/share/doc" "${DEPS_PREFIX}/share/opencascade/samples" + +cd / +rm -rf "${WORK}" +date > "${COMPLETE_MARK}" diff --git a/ci/wheels/deps/windows.ps1 b/ci/wheels/deps/windows.ps1 new file mode 100644 index 000000000..aeb839565 --- /dev/null +++ b/ci/wheels/deps/windows.ps1 @@ -0,0 +1,93 @@ +# Build the native dependencies of the pythonocc-core Windows wheels and +# install them to $env:PYOCC_DEPS_PREFIX: +# * SWIG (swigwin binary distribution), in /swig +# * OCCT, built with MSVC. Its 3rd parties (freetype, FreeImage, RapidJSON) +# are built by vcpkg, through the OCCT vcpkg manifest, and their DLLs +# copied to /bin. +# +# Nothing is done if the prefix is already complete (restored from cache). +$ErrorActionPreference = "Stop" +$PSNativeCommandUseErrorActionPreference = $true + +# below normal priority for the whole build (inherited by the child +# processes), so that the CI runner agent still gets CPU time for its +# heartbeats: a starved agent is reported as lost +(Get-Process -Id $PID).PriorityClass = "BelowNormal" + +$SwigVersion = "4.5.1" +$OcctTag = "V8_0_1" +$Triplet = "x64-windows-pyocc" +$DepsPrefix = $env:PYOCC_DEPS_PREFIX +if (-not $DepsPrefix) { throw "PYOCC_DEPS_PREFIX must be set" } +$Here = Split-Path -Parent $PSScriptRoot + +$CompleteMark = Join-Path $DepsPrefix ".pyocc-deps-complete" +if (Test-Path $CompleteMark) { + Write-Host "Dependencies already installed in $DepsPrefix" + exit 0 +} + +# short work directory: the vcpkg and OCCT build trees hit MAX_PATH otherwise +$Work = if ($env:PYOCC_DEPS_WORK) { $env:PYOCC_DEPS_WORK } else { "C:\pyocc-work" } +New-Item -ItemType Directory -Force -Path $Work, $DepsPrefix | Out-Null +Set-Location $Work + +# SWIG +curl.exe -fsSL -o swigwin.zip "https://downloads.sourceforge.net/project/swig/swigwin/swigwin-$SwigVersion/swigwin-$SwigVersion.zip" +Expand-Archive swigwin.zip -DestinationPath . +Move-Item "swigwin-$SwigVersion" (Join-Path $DepsPrefix "swig") + +# vcpkg: a full clone, the OCCT manifest pins a baseline commit +git clone https://github.com/microsoft/vcpkg.git vcpkg +.\vcpkg\bootstrap-vcpkg.bat -disableMetrics + +# OCCT +New-Item -ItemType Directory -Force -Path occt-src | Out-Null +curl.exe -fsSL -o occt.tar.gz "https://github.com/Open-Cascade-SAS/OCCT/archive/refs/tags/$OcctTag.tar.gz" +tar xzf occt.tar.gz --strip-components=1 -C occt-src +cmake -S occt-src -B ob -G "Visual Studio 17 2022" -A x64 ` + -DBUILD_USE_VCPKG=ON ` + "-DVCPKG_ROOT=$Work/vcpkg" ` + "-DVCPKG_TARGET_TRIPLET=$Triplet" ` + "-DVCPKG_OVERLAY_TRIPLETS=$Here/triplets" ` + "-DINSTALL_DIR=$DepsPrefix" ` + -DINSTALL_DIR_LAYOUT=Unix ` + -DBUILD_LIBRARY_TYPE=Shared ` + -DBUILD_RELEASE_DISABLE_EXCEPTIONS=OFF ` + -DBUILD_MODULE_Draw=OFF ` + -DBUILD_DOC_Overview=OFF ` + -DUSE_TCL=OFF ` + -DUSE_TK=OFF ` + -DUSE_FREETYPE=ON ` + -DUSE_RAPIDJSON=ON ` + -DUSE_FREEIMAGE=ON ` + -DUSE_DRACO=OFF ` + -DUSE_TBB=OFF ` + -DUSE_VTK=OFF ` + -DUSE_OPENVR=OFF ` + -DUSE_FFMPEG=OFF +# no --parallel: OCCT already compiles each project with /MP (one cl.exe per +# core), MSBuild /m on top would run several projects at once and exhaust the +# agent memory. _CL_ is appended to the cl.exe command line, it overrides the +# OCCT /MP with one compiler per core, but at most one per 3 GB of memory +$MemJobs = [math]::Floor((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 3GB) +$Jobs = [math]::Max(1, [math]::Min([Environment]::ProcessorCount, $MemJobs)) +Write-Host "Building with $Jobs compilers" +$env:_CL_ = "/MP$Jobs" +cmake --build ob --config Release +cmake --install ob --config Release + +# the 3rd party DLLs, for delvewheel. RapidJSON is header only, its headers +# are included by the OCCT glTF headers: put them next to the OCCT headers +# so that the pythonocc build finds them +$VcpkgInstalled = "ob/vcpkg_installed/$Triplet" +Copy-Item "$VcpkgInstalled/bin/*.dll" (Join-Path $DepsPrefix "bin") +Copy-Item -Recurse "$VcpkgInstalled/include/rapidjson" (Join-Path $DepsPrefix "include/opencascade") + +# the wheels only need the libraries, headers and cmake config +Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ` + (Join-Path $DepsPrefix "share/doc"), (Join-Path $DepsPrefix "share/opencascade/samples") + +Set-Location \ +Remove-Item -Recurse -Force $Work +Get-Date | Out-File $CompleteMark diff --git a/ci/wheels/triplets/arm64-osx-pyocc.cmake b/ci/wheels/triplets/arm64-osx-pyocc.cmake new file mode 100644 index 000000000..a35b6c3b0 --- /dev/null +++ b/ci/wheels/triplets/arm64-osx-pyocc.cmake @@ -0,0 +1,10 @@ +# OCCT 3rd parties for the macOS arm64 wheels: shared, release only +set(VCPKG_TARGET_ARCHITECTURE arm64) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE dynamic) +set(VCPKG_BUILD_TYPE release) + +set(VCPKG_CMAKE_SYSTEM_NAME Darwin) +set(VCPKG_OSX_ARCHITECTURES arm64) +# must match MACOSX_DEPLOYMENT_TARGET in cibuildwheel.toml +set(VCPKG_OSX_DEPLOYMENT_TARGET 11.0) diff --git a/ci/wheels/triplets/x64-osx-pyocc.cmake b/ci/wheels/triplets/x64-osx-pyocc.cmake new file mode 100644 index 000000000..3b01b6af3 --- /dev/null +++ b/ci/wheels/triplets/x64-osx-pyocc.cmake @@ -0,0 +1,10 @@ +# OCCT 3rd parties for the macOS x86_64 wheels: shared, release only +set(VCPKG_TARGET_ARCHITECTURE x64) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE dynamic) +set(VCPKG_BUILD_TYPE release) + +set(VCPKG_CMAKE_SYSTEM_NAME Darwin) +set(VCPKG_OSX_ARCHITECTURES x86_64) +# must match MACOSX_DEPLOYMENT_TARGET in cibuildwheel.toml +set(VCPKG_OSX_DEPLOYMENT_TARGET 11.0) diff --git a/ci/wheels/triplets/x64-windows-pyocc.cmake b/ci/wheels/triplets/x64-windows-pyocc.cmake new file mode 100644 index 000000000..beb0b90e5 --- /dev/null +++ b/ci/wheels/triplets/x64-windows-pyocc.cmake @@ -0,0 +1,5 @@ +# OCCT 3rd parties for the Windows wheels: DLLs, release only +set(VCPKG_TARGET_ARCHITECTURE x64) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE dynamic) +set(VCPKG_BUILD_TYPE release) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..c2114f198 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,124 @@ +# pip/wheel build of pythonocc-core (scikit-build-core). +# +# SWIG (>= 4.5.1) and OpenCASCADE (== 8.0.1) must be available on the build +# machine: the swig package on PyPI does not ship 4.5.1 yet, and OCCT is +# not available on PyPI. The PyPI wheels are built by cibuildwheel, see the +# [tool.cibuildwheel] section below and .github/workflows/wheels.yml. +[build-system] +requires = ["scikit-build-core>=0.10", "numpy>=2.0"] +build-backend = "scikit_build_core.build" + +[project] +name = "pythonocc-core" +dynamic = ["version"] +description = "Python package for 3D geometry CAD/BIM/CAM" +readme = "README.md" +license = { file = "LICENSE" } +authors = [{ name = "Thomas Paviot", email = "tpaviot@gmail.com" }] +requires-python = ">=3.10" +# built against numpy 2.x, which remains compatible with numpy >= 1.19 at runtime +dependencies = ["numpy>=1.19"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Science/Research", + "Intended Audience :: Manufacturing", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Programming Language :: C++", + "Programming Language :: Python :: 3", + "Topic :: Scientific/Engineering", + "Topic :: Multimedia :: Graphics :: 3D Modeling", +] + +[project.optional-dependencies] +qt = ["PySide6>=6.10"] +svg = ["svgwrite>=1.4"] +test = ["pytest", "mypy", "svgwrite>=1.4"] + +[project.urls] +Homepage = "https://github.com/tpaviot/pythonocc-core" +Repository = "https://github.com/tpaviot/pythonocc-core" +Issues = "https://github.com/tpaviot/pythonocc-core/issues" + +[tool.scikit-build] +minimum-version = "build-system.requires" +cmake.version = ">=3.18" +cmake.build-type = "Release" +build-dir = "build/{wheel_tag}" +# everything is installed by CMakeLists.txt +wheel.packages = [] +sdist.exclude = [ + ".github", + "ci", + "sandbox", + # files generated by the test suite + "test/test_io/sample*", + "test/test_io/*_generated.*", + "test/test_io/sphere.x3d", + "test/test_io/torus.x3d", +] + +[tool.scikit-build.cmake.define] +PYTHONOCC_MESHDS_NUMPY = "ON" +SWIG_HIDE_WARNINGS = "ON" + +[tool.scikit-build.metadata.version] +provider = "scikit_build_core.metadata.regex" +input = "CMakeLists.txt" +regex = '''(?sx) +set\(PYTHONOCC_VERSION_MAJOR\s+(?P\d+)\).*? +set\(PYTHONOCC_VERSION_MINOR\s+(?P\d+)\).*? +set\(PYTHONOCC_VERSION_PATCH\s+(?P\d+)\).*? +set\(PYTHONOCC_VERSION_DEVEL\s*(?P[^)\s]*)\s*\) +''' +# PYTHONOCC_VERSION_DEVEL is empty for the official releases +result = "{major}.{minor}.{patch}{devel}" + +# cibuildwheel configuration of the PyPI wheels, used by +# .github/workflows/wheels.yml. The native dependencies (SWIG, OCCT and its +# 3rd parties) are built by the scripts in ci/wheels/deps/. +[tool.cibuildwheel] +build = ["cp310-*", "cp311-*", "cp312-*", "cp313-*", "cp314-*"] +skip = ["*-musllinux_*"] +build-verbosity = 1 +test-requires = ["pytest", "svgwrite>=1.4"] +test-sources = ["test"] +# the tests use paths relative to the test directory +test-command = "python -c \"import os, sys, pytest; os.chdir('test'); sys.exit(pytest.main(['-q', '-p', 'no:cacheprovider']))\"" + +[tool.cibuildwheel.linux] +archs = ["auto64"] +manylinux-x86_64-image = "manylinux_2_28" +manylinux-aarch64-image = "manylinux_2_28" +# SWIG and OCCT are built from sources in the container (or restored from +# the PYOCC_DEPS_CACHE tarball), auditwheel then vendors the libraries +before-all = "bash {project}/ci/wheels/deps/linux.sh" +environment-pass = ["PYOCC_DEPS_CACHE"] + +[tool.cibuildwheel.linux.environment] +PATH = "/opt/pyocc-deps/bin:$PATH" +CMAKE_PREFIX_PATH = "/opt/pyocc-deps" +LD_LIBRARY_PATH = "/opt/pyocc-deps/lib:$LD_LIBRARY_PATH" +# RapidJSON headers, included by the OCCT glTF headers +CPLUS_INCLUDE_PATH = "/opt/pyocc-deps/include" + +# macOS and Windows: the dependencies are built on the runner by +# ci/wheels/deps/macos.sh and ci/wheels/deps/windows.ps1, and installed +# to $PYOCC_DEPS_PREFIX + +[tool.cibuildwheel.macos] +repair-wheel-command = "DYLD_LIBRARY_PATH=$PYOCC_DEPS_PREFIX/lib delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}" + +[tool.cibuildwheel.macos.environment] +PATH = "$PYOCC_DEPS_PREFIX/bin:$PATH" +CMAKE_PREFIX_PATH = "$PYOCC_DEPS_PREFIX" +# must match ci/wheels/deps/macos.sh and ci/wheels/triplets/*-osx-pyocc.cmake +MACOSX_DEPLOYMENT_TARGET = "11.0" + +[tool.cibuildwheel.windows] +archs = ["AMD64"] +before-build = "pip install delvewheel" +repair-wheel-command = "delvewheel repair --add-path %PYOCC_DEPS_PREFIX%/bin -w {dest_dir} {wheel}" + +[tool.cibuildwheel.windows.environment] +PATH = "$PYOCC_DEPS_PREFIX/swig;$PATH" +CMAKE_PREFIX_PATH = "$PYOCC_DEPS_PREFIX" From f70ab5c441b3bc32b9b8dfaedf81cf00286b4310 Mon Sep 17 00:00:00 2001 From: Thomas Paviot Date: Sun, 18 Oct 2026 07:31:22 +0200 Subject: [PATCH 2/9] Wheels: document the build and the PyPI release, no pull_request trigger --- .github/workflows/wheels.yml | 8 ++--- .gitignore | 3 ++ ci/wheels/README.md | 60 ++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 ci/wheels/README.md diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 11d146d07..2cb7ca067 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -9,6 +9,8 @@ name: Wheels +# no pull_request trigger: the review/* branches are already built on push, +# a full build takes hours on: push: branches: ["master", "review/**"] @@ -19,12 +21,6 @@ on: - CMakeLists.txt - ci/wheels/** - .github/workflows/wheels.yml - pull_request: - paths: - - pyproject.toml - - CMakeLists.txt - - ci/wheels/** - - .github/workflows/wheels.yml workflow_dispatch: concurrency: diff --git a/.gitignore b/.gitignore index 9ee456137..d90932dc8 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,6 @@ _build/ .Trashes ehthumbs.db Thumbs.db + +# cibuildwheel +wheelhouse/ diff --git a/ci/wheels/README.md b/ci/wheels/README.md new file mode 100644 index 000000000..59d64d48c --- /dev/null +++ b/ci/wheels/README.md @@ -0,0 +1,60 @@ +# PyPI wheels + +The pythonocc-core wheels published on PyPI are built by +[cibuildwheel](https://cibuildwheel.pypa.io) in the GitHub Actions workflow +`.github/workflows/wheels.yml`: + +| Platform | Wheels | Runner | +|----------|--------------------------------------------|------------------------------| +| Linux | `manylinux_2_28_x86_64`, `manylinux_2_28_aarch64` | `ubuntu-24.04`, `ubuntu-24.04-arm` | +| macOS | `macosx_11_0_x86_64`, `macosx_11_0_arm64` | `macos-15-intel`, `macos-15` | +| Windows | `win_amd64` | `windows-2022` | + +for CPython 3.10 to 3.14, plus the sdist. + +* `pyproject.toml`: the scikit-build-core build, the version is read from + `CMakeLists.txt`, and the cibuildwheel configuration. +* `deps/`: the native dependencies build scripts: SWIG, OCCT and its 3rd + parties (freetype, FreeImage, RapidJSON). + * Linux: built in the manylinux_2_28 container, 3rd parties from the + system (EPEL for FreeImage). + * macOS, Windows: built on the runner, 3rd parties built by vcpkg through + the OCCT vcpkg manifest, with the triplets of `triplets/`. + +For each platform, a first job builds the dependencies and saves them to the +GitHub cache (the key is a hash of the platform scripts and triplet), then one +job per Python version restores them and builds and tests a wheel. The +libraries are vendored in the wheels by auditwheel (Linux), delocate (macOS) +and delvewheel (Windows). + +The workflow runs on the pushes to master and review/* that change the +packaging, on demand (workflow_dispatch), and on the tags. + +## Releasing + +Pushing a tag builds the wheels and the sdist and uploads them to PyPI +through [trusted publishing](https://docs.pypi.org/trusted-publishers/). +This requires, once: + +* on PyPI, a trusted publisher for the `pythonocc-core` project: owner + `tpaviot`, repository `pythonocc-core`, workflow `wheels.yml`, environment + `pypi`. +* on GitHub, a `pypi` environment in the repository settings (optionally + with required reviewers, to approve each upload). + +When upgrading SWIG or OCCT, update the versions in the three `deps/` +scripts, the cache is invalidated by their change. + +## Building locally + +Linux wheels can be built on any machine with docker: + +```bash +pip install cibuildwheel +# optional: cache SWIG and OCCT between runs (/host is the host filesystem) +export PYOCC_DEPS_CACHE=/host$HOME/.cache/pyocc-deps +CIBW_BUILD='cp312-*' cibuildwheel --platform linux +``` + +from a clean checkout: cibuildwheel copies the whole source tree into the +container. From 5fce647befe78e7c70068367aaf53a375ea253e5 Mon Sep 17 00:00:00 2001 From: Thomas Paviot Date: Sun, 18 Oct 2026 07:34:20 +0200 Subject: [PATCH 3/9] Wheels: build the pypi/* branches, leave review/* to Azure --- .github/workflows/wheels.yml | 6 +++--- ci/wheels/README.md | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 2cb7ca067..ba372b27c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -9,11 +9,11 @@ name: Wheels -# no pull_request trigger: the review/* branches are already built on push, -# a full build takes hours +# the pypi/* branches build the wheels, the review/* branches are left to +# the Azure conda pipeline. No pull_request trigger: a full build takes hours on: push: - branches: ["master", "review/**"] + branches: ["master", "pypi/**"] # the paths filter does not apply to the tags tags: ["*"] paths: diff --git a/ci/wheels/README.md b/ci/wheels/README.md index 59d64d48c..61b89313d 100644 --- a/ci/wheels/README.md +++ b/ci/wheels/README.md @@ -27,8 +27,9 @@ job per Python version restores them and builds and tests a wheel. The libraries are vendored in the wheels by auditwheel (Linux), delocate (macOS) and delvewheel (Windows). -The workflow runs on the pushes to master and review/* that change the -packaging, on demand (workflow_dispatch), and on the tags. +The workflow runs on the pushes to master and pypi/* that change the +packaging, on demand (workflow_dispatch), and on the tags. The review/* +branches are only built by the Azure conda pipeline. ## Releasing From 7f8a19b2f198cbb9ed18535a94599802d414abce Mon Sep 17 00:00:00 2001 From: Thomas Paviot Date: Sun, 18 Oct 2026 09:41:34 +0200 Subject: [PATCH 4/9] Wheels: copy the vcpkg manual-link dylibs on macOS vcpkg installs the non thread safe libraw, needed by FreeImage, to lib/manual-link: delocate could not find it. --- ci/wheels/deps/macos.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ci/wheels/deps/macos.sh b/ci/wheels/deps/macos.sh index 7dacfc8ea..bc1b0c21a 100644 --- a/ci/wheels/deps/macos.sh +++ b/ci/wheels/deps/macos.sh @@ -104,11 +104,12 @@ cmake -S occt-src -B occt-build -G "Unix Makefiles" \ cmake --build occt-build -j"${NPROC}" cmake --install occt-build -# the 3rd party dylibs, for delocate. RapidJSON is header only, its -# headers are included by the OCCT glTF headers: put them next to the -# OCCT headers so that the pythonocc build finds them +# the 3rd party dylibs, for delocate. vcpkg installs the non thread safe +# libraw, needed by FreeImage, to lib/manual-link. RapidJSON is header +# only, its headers are included by the OCCT glTF headers: put them next +# to the OCCT headers so that the pythonocc build finds them VCPKG_INSTALLED="occt-build/vcpkg_installed/${TRIPLET}" -cp -a "${VCPKG_INSTALLED}"/lib/*.dylib "${DEPS_PREFIX}/lib/" +cp -a "${VCPKG_INSTALLED}"/lib/*.dylib "${VCPKG_INSTALLED}"/lib/manual-link/*.dylib "${DEPS_PREFIX}/lib/" cp -a "${VCPKG_INSTALLED}/include/rapidjson" "${DEPS_PREFIX}/include/opencascade/" # the wheels only need the libraries, headers and cmake config From 7e05f2778d729891e17a36a1bcf52f99e3862875 Mon Sep 17 00:00:00 2001 From: Thomas Paviot Date: Sun, 18 Oct 2026 09:42:55 +0200 Subject: [PATCH 5/9] Test: compare the normalized gp arrays with a tolerance gp_Dir normalizes in C++, the result may differ from numpy by a few ulps (FMA contraction on aarch64): the exact comparison failed on the linux-aarch64 wheels. --- test/test_core_numpy.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/test_core_numpy.py b/test/test_core_numpy.py index c8033d937..d398040a4 100644 --- a/test/test_core_numpy.py +++ b/test/test_core_numpy.py @@ -100,7 +100,10 @@ def test_array1_of_gp(type_, array_types_2d, normalizing_array_types): np_arr_out = gp_arr.to_numpy_array() if type_ in normalizing_array_types: - assert np.all(np_arr_out == p / np.linalg.norm(p, axis=-1, keepdims=True)) + # gp_Dir normalizes in C++, may differ by a few ulps (FMA on aarch64) + assert np.allclose( + np_arr_out, p / np.linalg.norm(p, axis=-1, keepdims=True), rtol=0, atol=1e-15 + ) else: assert np.all(np_arr_out == p) @@ -126,7 +129,10 @@ def test_array2_of_gp(type_, array_types_2d, normalizing_array_types): np_arr_out = arr_occ.to_numpy_array() if type_ in normalizing_array_types: - assert np.all(np_arr_out == p / np.linalg.norm(p, axis=-1, keepdims=True)) + # gp_Dir normalizes in C++, may differ by a few ulps (FMA on aarch64) + assert np.allclose( + np_arr_out, p / np.linalg.norm(p, axis=-1, keepdims=True), rtol=0, atol=1e-15 + ) else: assert np.all(np_arr_out == p) From 556053ee070dd20313812d7bc345c79683dc666b Mon Sep 17 00:00:00 2001 From: Thomas Paviot Date: Sun, 18 Oct 2026 09:44:39 +0200 Subject: [PATCH 6/9] Wheels: drop CPython 3.10 Build the wheels for CPython 3.11 to 3.14, and require Python >= 3.11. --- .github/workflows/wheels.yml | 4 ++-- ci/wheels/README.md | 2 +- pyproject.toml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index ba372b27c..917584142 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -1,5 +1,5 @@ # pythonocc-core wheels for PyPI: Linux x86_64/aarch64, macOS x86_64/arm64, -# Windows AMD64, for CPython 3.10 to 3.14, plus the sdist. +# Windows AMD64, for CPython 3.11 to 3.14, plus the sdist. # # For each platform, a first job builds the native dependencies (SWIG, OCCT # and its 3rd parties) with the scripts of ci/wheels/deps/ and saves them to @@ -121,7 +121,7 @@ jobs: fail-fast: false matrix: platform: *platforms - python: [cp310, cp311, cp312, cp313, cp314] + python: [cp311, cp312, cp313, cp314] steps: - uses: actions/checkout@v7 diff --git a/ci/wheels/README.md b/ci/wheels/README.md index 61b89313d..ef582cc50 100644 --- a/ci/wheels/README.md +++ b/ci/wheels/README.md @@ -10,7 +10,7 @@ The pythonocc-core wheels published on PyPI are built by | macOS | `macosx_11_0_x86_64`, `macosx_11_0_arm64` | `macos-15-intel`, `macos-15` | | Windows | `win_amd64` | `windows-2022` | -for CPython 3.10 to 3.14, plus the sdist. +for CPython 3.11 to 3.14, plus the sdist. * `pyproject.toml`: the scikit-build-core build, the version is read from `CMakeLists.txt`, and the cibuildwheel configuration. diff --git a/pyproject.toml b/pyproject.toml index c2114f198..3a225402f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ description = "Python package for 3D geometry CAD/BIM/CAM" readme = "README.md" license = { file = "LICENSE" } authors = [{ name = "Thomas Paviot", email = "tpaviot@gmail.com" }] -requires-python = ">=3.10" +requires-python = ">=3.11" # built against numpy 2.x, which remains compatible with numpy >= 1.19 at runtime dependencies = ["numpy>=1.19"] classifiers = [ @@ -77,7 +77,7 @@ result = "{major}.{minor}.{patch}{devel}" # .github/workflows/wheels.yml. The native dependencies (SWIG, OCCT and its # 3rd parties) are built by the scripts in ci/wheels/deps/. [tool.cibuildwheel] -build = ["cp310-*", "cp311-*", "cp312-*", "cp313-*", "cp314-*"] +build = ["cp311-*", "cp312-*", "cp313-*", "cp314-*"] skip = ["*-musllinux_*"] build-verbosity = 1 test-requires = ["pytest", "svgwrite>=1.4"] From e4e14a5fd518e962b1042ef9420bfa5bc2f69da8 Mon Sep 17 00:00:00 2001 From: Thomas Paviot Date: Sun, 18 Oct 2026 09:45:24 +0200 Subject: [PATCH 7/9] Wheels: add CPython 3.15 Build the wheels for CPython 3.11 to 3.15, with cibuildwheel v4.3.1 which ships the final CPython 3.15.0. --- .github/workflows/wheels.yml | 6 +++--- ci/wheels/README.md | 2 +- pyproject.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 917584142..59933eeca 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -1,5 +1,5 @@ # pythonocc-core wheels for PyPI: Linux x86_64/aarch64, macOS x86_64/arm64, -# Windows AMD64, for CPython 3.11 to 3.14, plus the sdist. +# Windows AMD64, for CPython 3.11 to 3.15, plus the sdist. # # For each platform, a first job builds the native dependencies (SWIG, OCCT # and its 3rd parties) with the scripts of ci/wheels/deps/ and saves them to @@ -121,7 +121,7 @@ jobs: fail-fast: false matrix: platform: *platforms - python: [cp311, cp312, cp313, cp314] + python: [cp311, cp312, cp313, cp314, cp315] steps: - uses: actions/checkout@v7 @@ -141,7 +141,7 @@ jobs: key: ${{ steps.key.outputs.key }} fail-on-cache-miss: true - - uses: pypa/cibuildwheel@v4.2.1 + - uses: pypa/cibuildwheel@v4.3.1 env: CIBW_BUILD: ${{ matrix.python }}-* CIBW_ARCHS_MACOS: ${{ matrix.platform.arch }} diff --git a/ci/wheels/README.md b/ci/wheels/README.md index ef582cc50..0e9d9534c 100644 --- a/ci/wheels/README.md +++ b/ci/wheels/README.md @@ -10,7 +10,7 @@ The pythonocc-core wheels published on PyPI are built by | macOS | `macosx_11_0_x86_64`, `macosx_11_0_arm64` | `macos-15-intel`, `macos-15` | | Windows | `win_amd64` | `windows-2022` | -for CPython 3.11 to 3.14, plus the sdist. +for CPython 3.11 to 3.15, plus the sdist. * `pyproject.toml`: the scikit-build-core build, the version is read from `CMakeLists.txt`, and the cibuildwheel configuration. diff --git a/pyproject.toml b/pyproject.toml index 3a225402f..b0fc73f12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ result = "{major}.{minor}.{patch}{devel}" # .github/workflows/wheels.yml. The native dependencies (SWIG, OCCT and its # 3rd parties) are built by the scripts in ci/wheels/deps/. [tool.cibuildwheel] -build = ["cp311-*", "cp312-*", "cp313-*", "cp314-*"] +build = ["cp311-*", "cp312-*", "cp313-*", "cp314-*", "cp315-*"] skip = ["*-musllinux_*"] build-verbosity = 1 test-requires = ["pytest", "svgwrite>=1.4"] From 655125ae331dbbc0e0b328cd48899e8c08c396ce Mon Sep 17 00:00:00 2001 From: Thomas Paviot Date: Sun, 18 Oct 2026 16:24:32 +0200 Subject: [PATCH 8/9] Wheels: do not run the GUI backends test test_display_sideeffects.py opens on screen windows: the view creation crashes on the headless macOS runners, and the Windows CPython used by cibuildwheel has no tkinter. --- pyproject.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b0fc73f12..f482748e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,8 +82,10 @@ skip = ["*-musllinux_*"] build-verbosity = 1 test-requires = ["pytest", "svgwrite>=1.4"] test-sources = ["test"] -# the tests use paths relative to the test directory -test-command = "python -c \"import os, sys, pytest; os.chdir('test'); sys.exit(pytest.main(['-q', '-p', 'no:cacheprovider']))\"" +# the tests use paths relative to the test directory. The GUI backends test +# opens on screen windows: it crashes on the headless macOS runners, and the +# Windows CPython used by cibuildwheel has no tkinter +test-command = "python -c \"import os, sys, pytest; os.chdir('test'); sys.exit(pytest.main(['-q', '-p', 'no:cacheprovider', '--ignore=test_display_sideeffects.py']))\"" [tool.cibuildwheel.linux] archs = ["auto64"] From 5c9905a8590efe63416444db40607f54fd9af140 Mon Sep 17 00:00:00 2001 From: Thomas Paviot Date: Sun, 18 Oct 2026 16:25:44 +0200 Subject: [PATCH 9/9] Test: skip the tk display backend when tkinter is missing The tk backend was always considered available, its tests failed on the Python builds without tkinter (the Windows NuGet package). --- test/test_display_sideeffects.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/test_display_sideeffects.py b/test/test_display_sideeffects.py index a69dbee4b..d130a11e6 100644 --- a/test/test_display_sideeffects.py +++ b/test/test_display_sideeffects.py @@ -23,6 +23,9 @@ def backend(request): try: if backend_str != "tk": assert load_backend(backend_str) == backend_str + else: + # some Python builds have no tkinter (the Windows NuGet package) + pytest.importorskip("tkinter") return backend_str except (AssertionError, ValueError) as e: pytest.skip(str(e))