Skip to content

Commit 9b4c016

Browse files
committed
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.
1 parent 6c0a2d8 commit 9b4c016

9 files changed

Lines changed: 673 additions & 6 deletions

File tree

‎.github/workflows/wheels.yml‎

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# pythonocc-core wheels for PyPI: Linux x86_64/aarch64, macOS x86_64/arm64,
2+
# Windows AMD64, for CPython 3.10 to 3.14, plus the sdist.
3+
#
4+
# For each platform, a first job builds the native dependencies (SWIG, OCCT
5+
# and its 3rd parties) with the scripts of ci/wheels/deps/ and saves them to
6+
# the GitHub cache, then one job per Python version restores them and runs
7+
# cibuildwheel (configured in pyproject.toml). Pushing a tag also uploads
8+
# the wheels and the sdist to PyPI, through trusted publishing.
9+
10+
name: Wheels
11+
12+
on:
13+
push:
14+
branches: ["master", "review/**"]
15+
# the paths filter does not apply to the tags
16+
tags: ["*"]
17+
paths:
18+
- pyproject.toml
19+
- CMakeLists.txt
20+
- ci/wheels/**
21+
- .github/workflows/wheels.yml
22+
pull_request:
23+
paths:
24+
- pyproject.toml
25+
- CMakeLists.txt
26+
- ci/wheels/**
27+
- .github/workflows/wheels.yml
28+
workflow_dispatch:
29+
30+
concurrency:
31+
group: ${{ github.workflow }}-${{ github.ref }}
32+
cancel-in-progress: true
33+
34+
permissions:
35+
contents: read
36+
37+
jobs:
38+
deps:
39+
name: "${{ matrix.platform.name }}: dependencies"
40+
runs-on: ${{ matrix.platform.os }}
41+
timeout-minutes: 360
42+
strategy:
43+
fail-fast: false
44+
matrix:
45+
platform: &platforms
46+
# deps_path: the Linux tarball directory (mounted at /host in the
47+
# manylinux container), the macOS/Windows install prefix. The
48+
# prefix must be the same in the deps and wheels jobs.
49+
- name: linux-x86_64
50+
os: ubuntu-24.04
51+
manylinux: quay.io/pypa/manylinux_2_28_x86_64
52+
deps_path: /home/runner/pyocc-deps-cache
53+
deps_files: ci/wheels/deps/linux.sh
54+
- name: linux-aarch64
55+
os: ubuntu-24.04-arm
56+
manylinux: quay.io/pypa/manylinux_2_28_aarch64
57+
deps_path: /home/runner/pyocc-deps-cache
58+
deps_files: ci/wheels/deps/linux.sh
59+
- name: macos-x86_64
60+
os: macos-15-intel
61+
arch: x86_64
62+
deps_path: /Users/runner/pyocc-deps
63+
deps_files: ci/wheels/deps/macos.sh ci/wheels/triplets/x64-osx-pyocc.cmake
64+
- name: macos-arm64
65+
os: macos-15
66+
arch: arm64
67+
deps_path: /Users/runner/pyocc-deps
68+
deps_files: ci/wheels/deps/macos.sh ci/wheels/triplets/arm64-osx-pyocc.cmake
69+
- name: windows-AMD64
70+
os: windows-2022
71+
arch: AMD64
72+
deps_path: C:/pyocc-deps
73+
deps_files: ci/wheels/deps/windows.ps1 ci/wheels/triplets/x64-windows-pyocc.cmake
74+
steps:
75+
- uses: actions/checkout@v7
76+
77+
- id: key
78+
name: Dependencies cache key
79+
shell: bash
80+
run: |
81+
set -eu
82+
# bump DEPS_CACHE_VERSION to invalidate the caches
83+
DEPS_CACHE_VERSION=1
84+
HASH=$(cat ${{ matrix.platform.deps_files }} | sha256sum | cut -c1-16)
85+
echo "key=pyocc-deps-${{ matrix.platform.name }}-${DEPS_CACHE_VERSION}-${HASH}" >> "$GITHUB_OUTPUT"
86+
87+
- id: cache
88+
uses: actions/cache@v6
89+
with:
90+
path: ${{ matrix.platform.deps_path }}
91+
key: ${{ steps.key.outputs.key }}
92+
93+
- name: Build SWIG and OCCT (manylinux container)
94+
if: steps.cache.outputs.cache-hit != 'true' && runner.os == 'Linux'
95+
run: |
96+
set -eux
97+
mkdir -p "${{ matrix.platform.deps_path }}"
98+
docker run --rm \
99+
-v "$GITHUB_WORKSPACE:/project" \
100+
-v "${{ matrix.platform.deps_path }}:/deps-cache" \
101+
-e PYOCC_DEPS_CACHE=/deps-cache \
102+
${{ matrix.platform.manylinux }} \
103+
bash /project/ci/wheels/deps/linux.sh
104+
105+
- name: Build SWIG and OCCT
106+
if: steps.cache.outputs.cache-hit != 'true' && runner.os == 'macOS'
107+
run: bash ci/wheels/deps/macos.sh
108+
env:
109+
ARCH: ${{ matrix.platform.arch }}
110+
PYOCC_DEPS_PREFIX: ${{ matrix.platform.deps_path }}
111+
112+
- name: Build OCCT
113+
if: steps.cache.outputs.cache-hit != 'true' && runner.os == 'Windows'
114+
run: ./ci/wheels/deps/windows.ps1
115+
shell: pwsh
116+
env:
117+
PYOCC_DEPS_PREFIX: ${{ matrix.platform.deps_path }}
118+
119+
wheels:
120+
name: "${{ matrix.platform.name }}: ${{ matrix.python }}"
121+
needs: deps
122+
runs-on: ${{ matrix.platform.os }}
123+
timeout-minutes: 360
124+
strategy:
125+
fail-fast: false
126+
matrix:
127+
platform: *platforms
128+
python: [cp310, cp311, cp312, cp313, cp314]
129+
steps:
130+
- uses: actions/checkout@v7
131+
132+
- id: key
133+
name: Dependencies cache key
134+
shell: bash
135+
run: |
136+
set -eu
137+
# must match the deps job
138+
DEPS_CACHE_VERSION=1
139+
HASH=$(cat ${{ matrix.platform.deps_files }} | sha256sum | cut -c1-16)
140+
echo "key=pyocc-deps-${{ matrix.platform.name }}-${DEPS_CACHE_VERSION}-${HASH}" >> "$GITHUB_OUTPUT"
141+
142+
- uses: actions/cache/restore@v6
143+
with:
144+
path: ${{ matrix.platform.deps_path }}
145+
key: ${{ steps.key.outputs.key }}
146+
fail-on-cache-miss: true
147+
148+
- uses: pypa/cibuildwheel@v4.2.1
149+
env:
150+
CIBW_BUILD: ${{ matrix.python }}-*
151+
CIBW_ARCHS_MACOS: ${{ matrix.platform.arch }}
152+
# Linux: the host filesystem is mounted at /host in the container
153+
PYOCC_DEPS_CACHE: /host${{ matrix.platform.deps_path }}
154+
PYOCC_DEPS_PREFIX: ${{ matrix.platform.deps_path }}
155+
156+
- uses: actions/upload-artifact@v7
157+
with:
158+
name: wheels-${{ matrix.platform.name }}-${{ matrix.python }}
159+
path: wheelhouse/*.whl
160+
161+
sdist:
162+
name: Source distribution
163+
runs-on: ubuntu-24.04
164+
steps:
165+
- uses: actions/checkout@v7
166+
- run: pipx run build --sdist
167+
- uses: actions/upload-artifact@v7
168+
with:
169+
name: sdist
170+
path: dist/*.tar.gz
171+
172+
publish:
173+
name: Upload to PyPI
174+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
175+
needs: [wheels, sdist]
176+
runs-on: ubuntu-24.04
177+
# the trusted publisher configured on PyPI for this repository, this
178+
# workflow and this environment
179+
environment:
180+
name: pypi
181+
url: https://pypi.org/p/pythonocc-core
182+
permissions:
183+
id-token: write
184+
steps:
185+
- uses: actions/download-artifact@v8
186+
with:
187+
path: dist
188+
merge-multiple: true
189+
- run: ls -l dist
190+
- uses: pypa/gh-action-pypi-publish@v1.14.2

‎CMakeLists.txt‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ if(APPLE)
6767
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
6868
endif(APPLE)
6969

70+
if(SKBUILD AND UNIX AND NOT APPLE)
71+
# wheel build: keep the OCCT library directory in the RPATH so that the
72+
# unrepaired wheel is usable on the build machine. auditwheel replaces it
73+
# with the vendored libraries location when repairing the wheel.
74+
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
75+
endif()
76+
7077
#####################################################################
7178
# OpenGL. If available, enable compilation for Visualization module #
7279
#####################################################################
@@ -99,7 +106,7 @@ set(Python3_FIND_STRATEGY LOCATION)
99106
set(Python3_FIND_FRAMEWORK NEVER)
100107
# numpy is required: the Geom, Geom2d, Poly, TColStd, TColgp and TShort modules
101108
# use its C API. PYTHONOCC_MESHDS_NUMPY only enables the numpy arrays of MeshDS
102-
find_package(Python3 COMPONENTS Interpreter Development NumPy REQUIRED)
109+
find_package(Python3 COMPONENTS Interpreter Development.Module NumPy REQUIRED)
103110
include_directories(${Python3_NumPy_INCLUDE_DIRS})
104111
message(STATUS "Numpy include directory: ${Python3_NumPy_INCLUDE_DIRS}")
105112
if(PYTHONOCC_MESHDS_NUMPY)
@@ -165,7 +172,11 @@ endif()
165172
# by default, installed to site-packages/OCC #
166173
##############################################
167174
if(NOT DEFINED PYTHONOCC_INSTALL_DIRECTORY)
168-
if(DEFINED ENV{SP_DIR} AND WIN32)
175+
if(SKBUILD)
176+
# wheel build (pip/scikit-build-core): the install prefix is the
177+
# wheel platlib directory, install relative to it
178+
set(PYTHONOCC_INSTALL_DIRECTORY OCC CACHE PATH "pythonocc install directory")
179+
elseif(DEFINED ENV{SP_DIR} AND WIN32)
169180
# TODO: following hack is azure specific, a recent update in azure
170181
# prevent cmake to find correct paths of python3 on windows
171182
# this should be removed as soon as possible
@@ -177,7 +188,7 @@ if(NOT DEFINED PYTHONOCC_INSTALL_DIRECTORY)
177188
OUTPUT_VARIABLE python_lib
178189
OUTPUT_STRIP_TRAILING_WHITESPACE)
179190
set(PYTHONOCC_INSTALL_DIRECTORY ${python_lib}/OCC CACHE PATH "pythonocc install directory")
180-
endif(DEFINED ENV{SP_DIR} AND WIN32)
191+
endif()
181192
endif(NOT DEFINED PYTHONOCC_INSTALL_DIRECTORY)
182193

183194
message(STATUS "pythonocc modules will be installed to: ${PYTHONOCC_INSTALL_DIRECTORY}")
@@ -460,7 +471,8 @@ if(PYTHONOCC_WRAP_VISU)
460471
endif(PYTHONOCC_WRAP_VISU)
461472

462473
# install Display
463-
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/Display DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY})
474+
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/Display DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY}
475+
PATTERN "__pycache__" EXCLUDE PATTERN ".ruff_cache" EXCLUDE)
464476

465477
# install LICENSE file
466478
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 $
477489
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/PkgBase/py.typed DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY}/Core)
478490

479491
# install Extend
480-
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/Extend DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY})
492+
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/Extend DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY}
493+
PATTERN "__pycache__" EXCLUDE PATTERN ".ruff_cache" EXCLUDE)
481494

482495
# install Wrapper utils
483-
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/Wrapper DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY})
496+
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/Wrapper DESTINATION ${PYTHONOCC_INSTALL_DIRECTORY}
497+
PATTERN "__pycache__" EXCLUDE PATTERN ".ruff_cache" EXCLUDE)

‎ci/wheels/deps/linux.sh‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/bin/bash
2+
# Build the native dependencies of the pythonocc-core Linux wheels (SWIG,
3+
# OCCT) and install them to $DEPS_PREFIX. Runs as the cibuildwheel
4+
# "before-all" step, inside the manylinux_2_28 container.
5+
#
6+
# If PYOCC_DEPS_CACHE points to a writable directory (on the host, through
7+
# the /host mount), the installed prefix is saved there as a tarball and
8+
# reused by the next runs.
9+
set -euo pipefail
10+
11+
# lowest priority for the whole build (inherited by the child processes), so
12+
# that the CI runner agent still gets CPU time for its heartbeats: a starved
13+
# agent is reported as lost
14+
renice -n 19 $$ > /dev/null
15+
16+
SWIG_VERSION=4.5.1
17+
OCCT_VERSION=8.0.1
18+
OCCT_TAG=V8_0_1
19+
RAPIDJSON_COMMIT=24b5e7a8b27f42fa16b96fc70aade9106cf7102f
20+
DEPS_PREFIX=${DEPS_PREFIX:-/opt/pyocc-deps}
21+
22+
# bump the last component to invalidate caches when the build options change
23+
CACHE_KEY="swig-${SWIG_VERSION}_occt-${OCCT_VERSION}_$(uname -m)_2"
24+
25+
# system packages, also needed when the cache is used: X11/OpenGL headers
26+
# for the visualization toolkits, freetype/fontconfig for text rendering,
27+
# FreeImage (EPEL) for image files I/O, pcre2 for SWIG
28+
dnf install -y epel-release
29+
dnf install -y freetype-devel fontconfig-devel freeimage-devel mesa-libGL-devel libX11-devel pcre2-devel
30+
31+
CACHE_TARBALL=""
32+
if [ -n "${PYOCC_DEPS_CACHE:-}" ]; then
33+
CACHE_TARBALL="${PYOCC_DEPS_CACHE}/pyocc-deps_${CACHE_KEY}.tar.gz"
34+
if [ -f "${CACHE_TARBALL}" ]; then
35+
echo "Using cached dependencies ${CACHE_TARBALL}"
36+
mkdir -p "${DEPS_PREFIX}"
37+
tar xzf "${CACHE_TARBALL}" -C "${DEPS_PREFIX}"
38+
exit 0
39+
fi
40+
fi
41+
42+
# one compile job per core, but at most one per 3 GB of memory: some OCCT
43+
# sources need more than 1 GB to compile, the agent is lost when it runs out
44+
MEM_JOBS=$(awk '/MemTotal/ {print int($2 / (3 * 1024 * 1024))}' /proc/meminfo)
45+
NPROC=$(nproc)
46+
if [ "${MEM_JOBS}" -ge 1 ] && [ "${MEM_JOBS}" -lt "${NPROC}" ]; then NPROC=${MEM_JOBS}; fi
47+
echo "Building with ${NPROC} jobs"
48+
WORK=$(mktemp -d)
49+
cd "${WORK}"
50+
51+
# SWIG
52+
curl -fsSL "https://downloads.sourceforge.net/project/swig/swig/swig-${SWIG_VERSION}/swig-${SWIG_VERSION}.tar.gz" | tar xz
53+
pushd "swig-${SWIG_VERSION}"
54+
./configure --prefix="${DEPS_PREFIX}" --without-alllang --with-python3
55+
make -j"${NPROC}"
56+
make install
57+
popd
58+
59+
# RapidJSON (header only, required by the glTF reader/writer)
60+
curl -fsSL "https://github.com/Tencent/rapidjson/archive/${RAPIDJSON_COMMIT}.tar.gz" | tar xz
61+
mkdir -p "${DEPS_PREFIX}/include"
62+
cp -r "rapidjson-${RAPIDJSON_COMMIT}/include/rapidjson" "${DEPS_PREFIX}/include/"
63+
64+
# OCCT
65+
mkdir occt-src
66+
curl -fsSL "https://github.com/Open-Cascade-SAS/OCCT/archive/refs/tags/${OCCT_TAG}.tar.gz" | tar xz --strip-components=1 -C occt-src
67+
cmake -S occt-src -B occt-build -G "Unix Makefiles" \
68+
-DCMAKE_BUILD_TYPE=Release \
69+
-DINSTALL_DIR="${DEPS_PREFIX}" \
70+
-DINSTALL_DIR_LAYOUT=Unix \
71+
-DBUILD_LIBRARY_TYPE=Shared \
72+
-DBUILD_RELEASE_DISABLE_EXCEPTIONS=OFF \
73+
-DBUILD_MODULE_Draw=OFF \
74+
-DBUILD_DOC_Overview=OFF \
75+
-DUSE_TCL=OFF \
76+
-DUSE_TK=OFF \
77+
-DUSE_FREETYPE=ON \
78+
-DUSE_RAPIDJSON=ON \
79+
-D3RDPARTY_RAPIDJSON_INCLUDE_DIR="${DEPS_PREFIX}/include" \
80+
-DUSE_FREEIMAGE=ON \
81+
-D3RDPARTY_FREEIMAGE_INCLUDE_DIR=/usr/include \
82+
-D3RDPARTY_FREEIMAGE_LIBRARY_DIR=/usr/lib64 \
83+
-DUSE_DRACO=OFF \
84+
-DUSE_TBB=OFF \
85+
-DUSE_VTK=OFF \
86+
-DUSE_OPENVR=OFF \
87+
-DUSE_FFMPEG=OFF
88+
cmake --build occt-build -j"${NPROC}"
89+
cmake --install occt-build
90+
91+
# the wheels only need the libraries, headers and cmake config
92+
rm -rf "${DEPS_PREFIX}/share/doc" "${DEPS_PREFIX}/share/opencascade/samples"
93+
94+
cd /
95+
rm -rf "${WORK}"
96+
97+
if [ -n "${CACHE_TARBALL}" ]; then
98+
mkdir -p "${PYOCC_DEPS_CACHE}"
99+
tar czf "${CACHE_TARBALL}.tmp" -C "${DEPS_PREFIX}" .
100+
mv "${CACHE_TARBALL}.tmp" "${CACHE_TARBALL}"
101+
echo "Saved dependencies to ${CACHE_TARBALL}"
102+
fi

0 commit comments

Comments
 (0)