Skip to content

Commit 03d078e

Browse files
akaszynskiclaude
andauthored
feat(packaging): 3-wheel tier split (core / rendering / io) (#172)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f88f80e commit 03d078e

39 files changed

Lines changed: 1423 additions & 176 deletions

.github/workflows/_tier-wheels.yml

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
# Reusable workflow: build the 3-wheel tier split on all three desktop OSes.
2+
#
3+
# Builds the TIERED wheel (ci/cmake/<os>-tiered.cmake disables the few kit-mixing
4+
# niche IO readers) and partitions the built cvista/ tree into three stackable
5+
# wheels — cvista (core) / cvista-rendering / cvista-io — that share the cvista/
6+
# import namespace and resolve each other's libs via $ORIGIN/@loader_path/.libs
7+
# RUNPATH. The self-containment AUDIT (every tier lib resolves within {own tier} ∪
8+
# core) is a hard gate; the stacking smoke installs core→io→rendering and asserts
9+
# each layer. Each OS uploads a `cvista-tier-wheels-<os>` artifact (3 wheels).
10+
#
11+
# Called by:
12+
# - ci.yml `split` job — on-demand tier-split validation (dispatch / label)
13+
# - release-tiers.yml — the build stage of the publish pipeline
14+
#
15+
# The wheel version comes from the git tag via setuptools-scm (on a release), or
16+
# a dev version off the branch (on a build-only dispatch — never published).
17+
name: _tier-wheels (reusable)
18+
19+
on:
20+
workflow_call:
21+
22+
jobs:
23+
linux:
24+
name: 3-wheel tier split (linux)
25+
runs-on: ${{ vars.CVISTA_RUNNER_LINUX || 'ubuntu-latest' }}
26+
timeout-minutes: 75
27+
steps:
28+
- uses: actions/checkout@v5
29+
with:
30+
fetch-depth: 0
31+
- name: Make ccache dir world-writable (container build user differs)
32+
run: mkdir -p /tmp/cvista-cibw-ccache && chmod 0777 /tmp/cvista-cibw-ccache
33+
- name: Restore ccache seed (cross-run, shared with the full linux leg)
34+
uses: actions/cache@v4
35+
with:
36+
path: /tmp/cvista-cibw-ccache
37+
key: cvista-ccache-linux-tiered-${{ github.run_id }}
38+
restore-keys: |
39+
cvista-ccache-linux-tiered-
40+
cvista-ccache-linux-cp312abi3-
41+
- name: Build TIERED wheel (cibuildwheel)
42+
uses: pypa/cibuildwheel@v3.1.4
43+
env:
44+
CIBW_BUILD: "cp312-* cp313-* cp314-*"
45+
# Same fast bit-exact-gate flags as the full leg, PLUS the tiered
46+
# init-cache (disables the kit-mixing niche IO readers so the tree
47+
# partitions cleanly). CVISTA_CMAKE_INIT is read by ci/cibw/cvista_backend.py.
48+
CIBW_ENVIRONMENT: >-
49+
CVISTA_LTO=0 CVISTA_GATE_O2=1 CVISTA_SOURCE_UNITY=1
50+
CVISTA_CMAKE_INIT=ci/cmake/linux-tiered.cmake
51+
CIBW_CONTAINER_ENGINE: "docker; create_args: -v /tmp/cvista-cibw-ccache:/ccache"
52+
- name: Tooling for partition/pack (patchelf, binutils, wheel)
53+
run: |
54+
sudo apt-get update -qq && sudo apt-get install -y -qq patchelf binutils
55+
python3 -m pip -q install wheel
56+
- name: Unpack tiered wheel + derive version/tag
57+
id: unpack
58+
run: |
59+
set -euo pipefail
60+
whl=$(ls wheelhouse/cvista-*.whl | head -1)
61+
echo "built wheel: $whl"
62+
base=$(basename "$whl" .whl) # cvista-9.6.2.0-cp312-abi3-manylinux_2_28_x86_64
63+
ver=$(echo "$base" | cut -d- -f2)
64+
tag=$(echo "$base" | cut -d- -f3-) # cp312-abi3-manylinux_2_28_x86_64
65+
echo "ver=$ver" >> "$GITHUB_OUTPUT"
66+
echo "tag=$tag" >> "$GITHUB_OUTPUT"
67+
python3 -m wheel unpack "$whl" -d /tmp/unpack
68+
initpy=$(find /tmp/unpack -maxdepth 3 -path '*/cvista/__init__.py' | head -1)
69+
root=$(dirname "$(dirname "$initpy")") # wheel ROOT: holds cvista/ + cvista.libs/
70+
echo "root=$root" >> "$GITHUB_OUTPUT"
71+
echo "=== unpacked wheel root (expect cvista/ AND the vendored cvista.libs/) ==="
72+
ls -la "$root"
73+
- name: Partition + self-containment AUDIT (hard gate)
74+
run: |
75+
set -euo pipefail
76+
python3 cvista-config/wheel-split/partition_wheels.py \
77+
"${{ steps.unpack.outputs.root }}" /tmp/tiers
78+
- name: Pack 3 tier wheels
79+
env:
80+
CVISTA_WHEEL_VERSION: ${{ steps.unpack.outputs.ver }}
81+
CVISTA_WHEEL_TAG: ${{ steps.unpack.outputs.tag }}
82+
run: |
83+
set -euo pipefail
84+
python3 cvista-config/wheel-split/pack_tier_wheels.py /tmp/tiers /tmp/tier-wheels
85+
ls -la /tmp/tier-wheels
86+
- name: Stacking smoke (hard gate — core offline, +io, +rendering)
87+
run: |
88+
set -euo pipefail
89+
sudo apt-get install -y -qq libgl1 libglx-mesa0 libxt6 libx11-6 libglu1-mesa || true
90+
WD=/tmp/tier-wheels PYBIN=python3 bash cvista-config/wheel-split/validate_stacking.sh
91+
- uses: actions/upload-artifact@v4
92+
if: always() # keep artifacts even if the stacking gate fails, for inspection
93+
with:
94+
name: cvista-tier-wheels-linux
95+
path: /tmp/tier-wheels/*.whl
96+
if-no-files-found: warn
97+
98+
macos:
99+
name: 3-wheel tier split (macOS)
100+
runs-on: ${{ vars.CVISTA_RUNNER_MACOS || 'macos-14' }}
101+
timeout-minutes: 90
102+
steps:
103+
- uses: actions/checkout@v5
104+
with:
105+
fetch-depth: 0
106+
- name: Restore ccache (cross-run)
107+
uses: actions/cache@v4
108+
with:
109+
path: /tmp/cvista-cibw-ccache
110+
key: cvista-cibw-ccache-macos-tiered-${{ github.sha }}
111+
restore-keys: |
112+
cvista-cibw-ccache-macos-tiered-
113+
cvista-cibw-ccache-macos-14-
114+
- name: Build TIERED wheel (cibuildwheel)
115+
uses: pypa/cibuildwheel@v3.1.4
116+
env:
117+
CIBW_BUILD: "cp312-* cp313-* cp314-*"
118+
# Replicate pyproject [tool.cibuildwheel.macos.environment] but point the
119+
# init-cache at the tiered variant (module disables + macos.cmake).
120+
CIBW_ENVIRONMENT_MACOS: >-
121+
CVISTA_CMAKE_INIT=ci/cmake/macos-tiered.cmake CVISTA_TARGET_OS=macos
122+
MACOSX_DEPLOYMENT_TARGET=11.0 _PYTHON_HOST_PLATFORM=macosx-11.0-arm64
123+
CMAKE_C_COMPILER_LAUNCHER=ccache CMAKE_CXX_COMPILER_LAUNCHER=ccache
124+
- name: Partition + pack + stack (otool link graph; delocate .dylibs)
125+
run: |
126+
set -euo pipefail
127+
# macos-14 runner python3 is Homebrew (PEP 668 externally-managed); the
128+
# runner venv is disposable so installing into it directly is fine.
129+
python3 -m pip -q install --break-system-packages wheel
130+
whl=$(ls wheelhouse/cvista-*.whl | head -1); echo "built: $whl"
131+
base=$(basename "$whl" .whl); ver=$(echo "$base"|cut -d- -f2); tag=$(echo "$base"|cut -d- -f3-)
132+
python3 -m wheel unpack "$whl" -d /tmp/unpack
133+
initpy=$(find /tmp/unpack -maxdepth 3 -path '*/cvista/__init__.py' | head -1)
134+
root=$(dirname "$(dirname "$initpy")")
135+
echo "=== wheel root (expect cvista/ + delocate cvista/.dylibs/) ==="; ls -la "$root"; ls -la "$root/cvista/.dylibs" 2>/dev/null || true
136+
python3 cvista-config/wheel-split/partition_wheels.py "$root" /tmp/tiers
137+
CVISTA_WHEEL_VERSION="$ver" CVISTA_WHEEL_TAG="$tag" \
138+
python3 cvista-config/wheel-split/pack_tier_wheels.py /tmp/tiers /tmp/tier-wheels
139+
ls -la /tmp/tier-wheels
140+
WD=/tmp/tier-wheels PYBIN=python3 bash cvista-config/wheel-split/validate_stacking.sh
141+
- uses: actions/upload-artifact@v4
142+
if: always()
143+
with:
144+
name: cvista-tier-wheels-macos
145+
path: /tmp/tier-wheels/*.whl
146+
if-no-files-found: warn
147+
148+
windows:
149+
name: 3-wheel tier split (Windows)
150+
runs-on: ${{ vars.CVISTA_RUNNER_WINDOWS || 'windows-latest' }}
151+
timeout-minutes: 120
152+
steps:
153+
- uses: actions/checkout@v5
154+
with:
155+
fetch-depth: 0
156+
- name: Set up MSVC (vcvars) for the Ninja build
157+
uses: ilammy/msvc-dev-cmd@v1
158+
with:
159+
arch: x64
160+
- name: Build TIERED wheel (cibuildwheel)
161+
uses: pypa/cibuildwheel@v3.1.4
162+
env:
163+
CIBW_BUILD: "cp312-* cp313-* cp314-*"
164+
# Replicate pyproject [tool.cibuildwheel.windows.environment] + tiered init-cache.
165+
CIBW_ENVIRONMENT_WINDOWS: >-
166+
CVISTA_CMAKE_INIT=ci/cmake/windows-tiered.cmake CVISTA_FORCE_MSVC=ON
167+
- name: Partition + pack + AUDIT (pefile link graph; delvewheel cvista.libs)
168+
shell: bash
169+
run: |
170+
set -euo pipefail
171+
python -m pip -q install wheel pefile
172+
whl=$(ls wheelhouse/cvista-*.whl | head -1); echo "built: $whl"
173+
base=$(basename "$whl" .whl); ver=$(echo "$base"|cut -d- -f2); tag=$(echo "$base"|cut -d- -f3-)
174+
python -m wheel unpack "$whl" -d /tmp/unpack
175+
initpy=$(find /tmp/unpack -maxdepth 3 -path '*/cvista/__init__.py' | head -1)
176+
root=$(dirname "$(dirname "$initpy")")
177+
echo "=== wheel root (expect cvista/ + delvewheel cvista.libs/) ==="; ls -la "$root"
178+
python cvista-config/wheel-split/partition_wheels.py "$root" /tmp/tiers
179+
# NOTE: write the packed wheels to a WORKSPACE-RELATIVE dir, not /tmp.
180+
# The pack step is bash (MSYS: /tmp -> C:\Users\...\AppData\Local\Temp)
181+
# but the smoke + upload steps are pwsh/node (/tmp -> drive-root C:\tmp).
182+
# A relative path resolves to $GITHUB_WORKSPACE identically in all shells.
183+
CVISTA_WHEEL_VERSION="$ver" CVISTA_WHEEL_TAG="$tag" \
184+
python cvista-config/wheel-split/pack_tier_wheels.py /tmp/tiers tier-wheels
185+
ls -la tier-wheels
186+
- name: Core import smoke (native)
187+
shell: pwsh
188+
run: |
189+
$core = Get-ChildItem tier-wheels/cvista-9.6.2*.whl | Select-Object -First 1
190+
if (-not $core) { Write-Error 'core tier wheel not found in tier-wheels/'; exit 1 }
191+
python -m venv C:\v
192+
C:\v\Scripts\pip.exe -q install --no-deps $core.FullName
193+
C:\v\Scripts\python.exe -c "import cvista; from cvista.vtkFiltersSources import vtkSphereSource; s=vtkSphereSource(); s.Update(); print('win core import OK, pts:', s.GetOutput().GetNumberOfPoints())"
194+
if ($LASTEXITCODE -ne 0) { Write-Error 'core import/compute failed'; exit 1 }
195+
C:\v\Scripts\python.exe -c "import cvista.vtkRenderingCore" 2>$null
196+
if ($LASTEXITCODE -eq 0) { Write-Error 'vtkRenderingCore present in core-only (unexpected)'; exit 1 }
197+
Write-Host 'vtkRenderingCore absent in core-only [OK]'
198+
# The intentional import failure above leaves $LASTEXITCODE=1; GitHub's
199+
# pwsh wrapper ends the step with `exit $LASTEXITCODE`, so reset it.
200+
exit 0
201+
- uses: actions/upload-artifact@v4
202+
if: always()
203+
with:
204+
name: cvista-tier-wheels-windows
205+
path: tier-wheels/*.whl
206+
if-no-files-found: warn

.github/workflows/ci.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,20 @@ jobs:
160160
path: wheelhouse/*.whl
161161
if-no-files-found: error
162162

163+
# === 3-wheel tier split =====================================================
164+
# Build + partition + pack + stack-validate the three tier wheels (cvista /
165+
# cvista-rendering / cvista-io) on linux/macOS/Windows. The build logic lives in
166+
# the reusable workflow `_tier-wheels.yml` — the SAME one the release pipeline
167+
# (release-tiers.yml) calls — so there is one source of truth. Heavy (full
168+
# build), gated off normal PRs: on-demand via `gh workflow run ci.yml --ref
169+
# <branch>` (workflow_dispatch) or by adding the `wheel-split` label to a PR.
170+
split:
171+
name: 3-wheel tier split
172+
if: >-
173+
github.event_name == 'workflow_dispatch' ||
174+
contains(github.event.pull_request.labels.*.name, 'wheel-split')
175+
uses: ./.github/workflows/_tier-wheels.yml
176+
163177
build-other-os:
164178
name: build ${{ matrix.os }}
165179
# macOS/Windows build on EVERY PR (and main + workflow_dispatch). We accept a
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Publish the 3-wheel tier split — cvista (core) / cvista-rendering / cvista-io —
2+
# to PyPI via Trusted Publishing (OIDC; no stored API token).
3+
#
4+
# workflow_dispatch -> BUILD ONLY: build + partition + pack + stack-validate
5+
# all three tiers (no upload). A safe way to exercise the
6+
# pipeline without cutting a release.
7+
# release: published -> PUBLISH: build the tiers at the release tag's version
8+
# and upload each project to PyPI.
9+
#
10+
# ── Trusted-publisher setup (one per project; DISTINCT environment each) ────────
11+
# PyPI keys a (pending) publisher by owner/repo/workflow/ENVIRONMENT, so the three
12+
# projects cannot share one environment under the same workflow. Give each its own:
13+
#
14+
# Project Owner=pyvista Repo=cvista Workflow=release-tiers.yml Env
15+
# cvista .............................................. pypi-core
16+
# cvista-rendering .............................................. pypi-rendering
17+
# cvista-io .............................................. pypi-io
18+
#
19+
# All three are new projects -> register each as a PyPI "pending publisher" (the
20+
# first upload creates the project). Also create the three GitHub Environments
21+
# (Settings -> Environments): pypi-core, pypi-rendering, pypi-io — names must match
22+
# the PyPI "Environment name" field exactly. Add release approvers to them if you
23+
# want a manual gate before a publish.
24+
name: Release tier wheels (core / rendering / io)
25+
26+
on:
27+
workflow_dispatch:
28+
release:
29+
types: [published]
30+
31+
concurrency:
32+
# Never cancel an in-flight publish — a half-uploaded release is worse than a wait.
33+
group: release-tiers-${{ github.ref }}
34+
cancel-in-progress: false
35+
36+
jobs:
37+
# Build the 3 tier wheels on linux/macOS/Windows (uploads cvista-tier-wheels-*).
38+
# On a release the version comes from the git tag via setuptools-scm.
39+
build:
40+
uses: ./.github/workflows/_tier-wheels.yml
41+
42+
# One publish job per project, each running in the project's OWN environment so
43+
# its OIDC token carries the environment PyPI expects. Each job uploads ONLY its
44+
# project's wheels (3 — one per OS). Release-only.
45+
publish:
46+
if: github.event_name == 'release'
47+
needs: build
48+
strategy:
49+
fail-fast: false # a failure publishing one tier must not abort the others
50+
matrix:
51+
include:
52+
- project: cvista
53+
environment: pypi-core
54+
glob: cvista-*.whl # core: 'cvista-' (hyphen) never matches cvista_rendering/_io
55+
- project: cvista-rendering
56+
environment: pypi-rendering
57+
glob: cvista_rendering-*.whl
58+
- project: cvista-io
59+
environment: pypi-io
60+
glob: cvista_io-*.whl
61+
runs-on: ${{ vars.CVISTA_RUNNER_LINUX_SMALL || 'ubuntu-latest' }}
62+
timeout-minutes: 20
63+
environment:
64+
name: ${{ matrix.environment }}
65+
url: https://pypi.org/p/${{ matrix.project }}
66+
permissions:
67+
# id-token: write mints the short-lived OIDC token PyPI exchanges for an
68+
# upload token — no stored secret. PyPI matches repo + this workflow file +
69+
# the environment above to the project's trusted-publisher config.
70+
id-token: write
71+
steps:
72+
- name: Collect all tier wheels
73+
uses: actions/download-artifact@v4
74+
with:
75+
pattern: cvista-tier-wheels-*
76+
path: all
77+
merge-multiple: true
78+
- name: Isolate ${{ matrix.project }} wheels into dist/
79+
run: |
80+
set -euo pipefail
81+
mkdir -p dist
82+
cp all/${{ matrix.glob }} dist/
83+
echo "=== ${{ matrix.project }} wheels to publish ==="; ls -l dist
84+
n=$(ls dist/*.whl 2>/dev/null | wc -l)
85+
# 3 OSes (linux/macOS/Windows) -> expect 3 wheels for this project.
86+
test "$n" -ge 3 || { echo "::error::expected >=3 ${{ matrix.project }} wheels, found $n"; exit 1; }
87+
- name: Publish ${{ matrix.project }} to PyPI
88+
uses: pypa/gh-action-pypi-publish@release/v1
89+
with:
90+
packages-dir: dist

Filters/FlowPaths/vtk.module

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ PRIVATE_DEPENDS
2525
VTK::FiltersGeometry
2626
VTK::FiltersModeling
2727
VTK::FiltersSources
28-
VTK::IOCore
2928
VTK::ParallelCore
3029
VTK::eigen
3130
TEST_DEPENDS

Filters/FlowPaths/vtkParticleTracerBase.cxx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
#include "vtkParticleTracerBase.h"
55

6-
#include "vtkAbstractParticleWriter.h"
76
#include "vtkAppendDataSets.h"
87
#include "vtkCellArray.h"
98
#include "vtkCellData.h"
@@ -58,7 +57,32 @@ using namespace vtkParticleTracerBaseNamespace;
5857
using IDStates = vtkTemporalInterpolatedVelocityField::IDStates;
5958

6059
//------------------------------------------------------------------------------
61-
vtkCxxSetObjectMacro(vtkParticleTracerBase, ParticleWriter, vtkAbstractParticleWriter);
60+
// vtkAbstractParticleWriter lives in VTK::IOCore. To keep VTK::FiltersFlowPaths
61+
// free of any IO dependency, the writer is held as an incomplete type and its
62+
// reference counting is routed through vtkObjectBase (every VTK object derives
63+
// from vtkObjectBase at offset 0). The base class only stores the user-supplied
64+
// writer; subclasses that actually emit particles include the concrete header.
65+
void vtkParticleTracerBase::SetParticleWriter(vtkAbstractParticleWriter* pw)
66+
{
67+
if (this->ParticleWriter == pw)
68+
{
69+
return;
70+
}
71+
vtkObjectBase* newWriter = reinterpret_cast<vtkObjectBase*>(pw);
72+
vtkObjectBase* oldWriter = reinterpret_cast<vtkObjectBase*>(this->ParticleWriter);
73+
if (newWriter)
74+
{
75+
newWriter->Register(this);
76+
}
77+
this->ParticleWriter = pw;
78+
if (oldWriter)
79+
{
80+
oldWriter->UnRegister(this);
81+
}
82+
this->Modified();
83+
}
84+
85+
//------------------------------------------------------------------------------
6286
vtkCxxSetObjectMacro(vtkParticleTracerBase, Integrator, vtkInitialValueProblemSolver);
6387
vtkCxxSetObjectMacro(vtkParticleTracerBase, Controller, vtkMultiProcessController);
6488

Filters/Parallel/vtk.module

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ DEPENDS
3333
PRIVATE_DEPENDS
3434
VTK::CommonSystem
3535
VTK::CommonTransforms
36-
VTK::IOLegacy
3736
TEST_DEPENDS
3837
VTK::FiltersFlowPaths
3938
VTK::FiltersParallelDIY2

Filters/ParallelDIY2/vtk.module

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ PRIVATE_DEPENDS
2727
VTK::CommonDataModel
2828
VTK::CommonExecutionModel
2929
VTK::ImagingCore
30-
VTK::IOXML
3130
VTK::ParallelCore
3231
VTK::FiltersTemporal
3332
TEST_DEPENDS

IO/CGNS/vtk.module

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ NAME
22
VTK::IOCGNSReader
33
LIBRARY_NAME
44
vtkIOCGNSReader
5-
KIT
6-
VTK::Parallel
75
GROUPS
86
StandAlone
97
SPDX_LICENSE_IDENTIFIER

0 commit comments

Comments
 (0)