Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# pythonocc-core wheels for PyPI: Linux x86_64/aarch64, macOS x86_64/arm64,
# 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
# 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

# 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", "pypi/**"]
# the paths filter does not apply to the tags
tags: ["*"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High · Unreviewed tags publish releases

When a contributor who can create tags but is not authorized to approve releases tags an unreviewed commit, the wildcard tag trigger runs the release workflow for any tag, and the publish job uploads its artifacts without a required approval gate. A contributor able to tag an unreviewed commit but not authorized to approve a release can therefore publish that commit's package to PyPI.

Restrict release tags and require approval through a protected GitHub environment or tag ruleset before publishing.

Also at .github/workflows/wheels.yml:170.

Prompt for AI agents
In `.github/workflows/wheels.yml` at line 18:

**Unreviewed tags publish releases**

When a contributor who can create tags but is not authorized to approve releases tags an unreviewed commit, the wildcard tag trigger runs the release workflow for any tag, and the publish job uploads its artifacts without a required approval gate. A contributor able to tag an unreviewed commit but not authorized to approve a release can therefore publish that commit's package to PyPI.

Restrict release tags and require approval through a protected GitHub environment or tag ruleset before publishing.

Also at `.github/workflows/wheels.yml:170`.

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: [cp311, cp312, cp313, cp314, cp315]
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.3.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/')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Mismatched tags publish wrong version

When a pushed release tag does not match the CMake-derived package version, the workflow publishes artifacts using the version derived from CMakeLists.txt without checking it against the tag, so a mismatched release tag uploads a differently versioned distribution to PyPI.

Validate the release tag against the built distribution version and fail before publishing when they differ.

Also at pyproject.toml:75.

Prompt for AI agents
In `.github/workflows/wheels.yml` at line 170:

**Mismatched tags publish wrong version**

When a pushed release tag does not match the CMake-derived package version, the workflow publishes artifacts using the version derived from `CMakeLists.txt` without checking it against the tag, so a mismatched release tag uploads a differently versioned distribution to PyPI.

Validate the release tag against the built distribution version and fail before publishing when they differ.

Also at `pyproject.toml:75`.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Retries leave releases incomplete

When a PyPI upload fails or is canceled after one or more distributions have been accepted, the publish action rejects already-uploaded files on retry, leaving the remaining distributions unpublished.

Configure publishing to skip files already present on PyPI so a retry can upload the remaining distributions.

Prompt for AI agents
In `.github/workflows/wheels.yml` at line 186:

**Retries leave releases incomplete**

When a PyPI upload fails or is canceled after one or more distributions have been accepted, the publish action rejects already-uploaded files on retry, leaving the remaining distributions unpublished.

Configure publishing to skip files already present on PyPI so a retry can upload the remaining distributions.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,6 @@ _build/
.Trashes
ehthumbs.db
Thumbs.db

# cibuildwheel
wheelhouse/
26 changes: 20 additions & 6 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 #
#####################################################################
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical · Wheel installs outside staging

When scikit-build-core configures a wheel build, cMake treats the cache entry as a PATH and resolves the relative OCC value to an absolute path. The later install rules then place the package outside scikit-build-core’s wheel staging prefix, so the wheel lacks OCC and its import tests fail.

Keep the install destination relative, for example by using a STRING cache entry or a normal non-cache variable.

Prompt for AI agents
In `CMakeLists.txt` at line 178:

**Wheel installs outside staging**

When scikit-build-core configures a wheel build, cMake treats the cache entry as a `PATH` and resolves the relative `OCC` value to an absolute path. The later install rules then place the package outside scikit-build-core’s wheel staging prefix, so the wheel lacks `OCC` and its import tests fail.

Keep the install destination relative, for example by using a `STRING` cache entry or a normal non-cache variable.

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
Expand All @@ -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}")
Expand Down Expand Up @@ -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})
Expand All @@ -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)
61 changes: 61 additions & 0 deletions ci/wheels/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 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.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.
* `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 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

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.
Loading
Loading