Skip to content

Commit e2b3016

Browse files
authored
Merge pull request #7 from SCIInstitute/support-build-and-publish-as-vtk-python-wheels
ENH: Add infrastructure for building the python package using GitHub Actions
2 parents 462f17c + b0c4e3b commit e2b3016

File tree

14 files changed

+605
-2
lines changed

14 files changed

+605
-2
lines changed

.github/scripts/cibw_before_build.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env bash
2+
set -ev
3+
4+
if [[ $RUNNER_OS == "Linux" ]]; then
5+
# Some of the VTK modules use this library, and auditwheel will
6+
# complain if it can't find it. We will remove it from the wheel
7+
# after the repair is complete.
8+
yum install libXcursor-devel -y
9+
10+
# Make sure this is removed before every build, so we always
11+
# get the correct one.
12+
rm -rf $VTK_WHEEL_SDK_INSTALL_PATH
13+
fi

.github/scripts/install.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env bash
2+
set -ev
3+
4+
#### Install cibuildwheel ####
5+
pip install cibuildwheel
6+
7+
if [[ $RUNNER_OS == "macOS" ]]; then
8+
# VTK is expecting the xcode path to be slightly different.
9+
# Specifically, the VTK::RenderingOpenGL2 imported target seems
10+
# to expect the absolute path to the OpenGL library to match.
11+
ln -s /Applications/Xcode_13.1.app/ /Applications/Xcode-13.1.app
12+
fi

.github/scripts/linux_repair_wheel.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!/usr/bin/env python3
2+
3+
import glob
4+
import os
5+
from pathlib import Path
6+
import shutil
7+
import subprocess
8+
import sys
9+
10+
from utils import change_dir, unzip_file
11+
12+
"""
13+
This script does the following:
14+
15+
1. Copy all vtk modules into the wheel
16+
2. Run auditwheel repair
17+
3. Remove anything that wasn't there before the repair
18+
19+
This is done so that we can get proper tags on the wheel, without the
20+
RPATHs or contents of the wheel being affected.
21+
22+
Note: if we make the VTK modules visible to auditwheel by modifying the
23+
LD_LIBRARY_PATH, auditwheel copies them into the project, but modifies
24+
the RPATHs of our libraries. We do not want auditwheel to modify the
25+
RPATHs, so we copy the VTK modules in ourselves.
26+
27+
It would be great if auditwheel, at some point, allowed us to add tags
28+
to the wheel *without* repairing it...
29+
"""
30+
31+
if len(sys.argv) < 3:
32+
sys.exit('Usage: <script> <wheel_path> <output_dir>')
33+
34+
wheel_path = sys.argv[1]
35+
output_dir = sys.argv[2]
36+
37+
wheel_sdk_path = os.getenv('VTK_WHEEL_SDK_INSTALL_PATH')
38+
if wheel_sdk_path is None:
39+
raise Exception('VTK_WHEEL_SDK_INSTALL_PATH must be set')
40+
41+
vtkmodules_glob_pattern = Path(wheel_sdk_path) / 'build/*/vtkmodules/*.so'
42+
vtkmodules = glob.glob(str(vtkmodules_glob_pattern))
43+
if not vtkmodules:
44+
raise Exception(f'Failed to find vtkmodules at: {vtkmodules_glob_pattern}')
45+
46+
with unzip_file(wheel_path) as unpacked_path:
47+
# Take a snapshot of the directory contents. After the wheel repair, we
48+
# will remove all files except for these.
49+
with change_dir(unpacked_path):
50+
contents_snapshot = list(Path().rglob('*'))
51+
52+
# Find the path to the RECORD file
53+
record_glob = list(Path().glob('*.dist-info/RECORD'))
54+
if len(record_glob) != 1:
55+
raise Exception('Failed to find RECORD file')
56+
57+
record_path = record_glob[0]
58+
59+
# Read in the RECORD file. We will write it back out after the repair.
60+
with open(record_path, 'r') as rf:
61+
record_text = rf.read()
62+
63+
# Copy the vtkmodules in
64+
destination = Path(unpacked_path) / 'vtkmodules'
65+
for f in vtkmodules:
66+
output = destination / Path(f).name
67+
if output.exists():
68+
# Don't overwrite any files
69+
continue
70+
71+
shutil.copyfile(f, output)
72+
73+
# Now, perform the auditwheel repair
74+
cmd = ['auditwheel', 'repair', wheel_path, '-w', output_dir]
75+
subprocess.check_call(cmd)
76+
77+
# There should now be a wheel in the wheelhouse
78+
output = glob.glob(f'{output_dir}/*.whl')
79+
if len(output) != 1:
80+
raise Exception(f'Failed to find wheel in `{output_dir}`')
81+
82+
output_wheel = output[0]
83+
84+
# Now, remove all newly added files
85+
with unzip_file(output_wheel) as unpacked_path:
86+
with change_dir(unpacked_path):
87+
for item in Path().rglob('*'):
88+
if item not in contents_snapshot:
89+
if item.is_dir():
90+
shutil.rmtree(item)
91+
else:
92+
item.unlink()
93+
94+
# Replace the RECORD file with the old one
95+
with open(record_path, 'w') as wf:
96+
wf.write(record_text)

.github/scripts/utils.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from contextlib import contextmanager
2+
import os
3+
from pathlib import Path
4+
import shutil
5+
import zipfile
6+
7+
8+
@contextmanager
9+
def unzip_file(zip_path, unpack_path=None):
10+
# This will unzip the file while inside the context, and re-zip it
11+
# afterwards, which allows the caller to modify the contents
12+
13+
if unpack_path is None:
14+
unpack_path = str(Path('__tmp_unzip_file_unpack').resolve())
15+
16+
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
17+
zip_ref.extractall(unpack_path)
18+
19+
try:
20+
yield unpack_path
21+
finally:
22+
name = shutil.make_archive(zip_path, 'zip', unpack_path)
23+
shutil.rmtree(unpack_path)
24+
if name.endswith('.zip'):
25+
Path(name).rename(name[:-4])
26+
27+
28+
@contextmanager
29+
def change_dir(path):
30+
# Temporarily change the directory to the path. When the context ends,
31+
# the path will be changed back.
32+
33+
origin = Path.cwd()
34+
try:
35+
os.chdir(path)
36+
yield
37+
finally:
38+
os.chdir(origin)

.github/workflows/build_wheels.yml

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
name: Build Wheels
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
tags:
8+
- '*'
9+
pull_request:
10+
branches:
11+
- main
12+
13+
workflow_dispatch:
14+
15+
env:
16+
# Only support 64-bit CPython >= 3.7
17+
# VTK does not currently build python 3.8 arm64 wheels, so skip it too
18+
CIBW_SKIP: "cp27-* cp35-* cp36-* cp37-* pp* *-manylinux_i686 *-musllinux_* *-win32 cp38-macosx_arm64"
19+
20+
# Need to match the version used by VTK
21+
CIBW_ENVIRONMENT_MACOS: MACOSX_DEPLOYMENT_TARGET=10.10
22+
23+
# In the Linux docker container, install the wheel SDKs to this location
24+
CIBW_ENVIRONMENT_LINUX: VTK_WHEEL_SDK_INSTALL_PATH=/vtk-wheel-sdk
25+
26+
# NOTE: cross-compilation is not currently working for us for arm64.
27+
# We are going to turn it off and build them manually until GitHub Actions
28+
# makes arm64 runners available.
29+
# Build both x86_64 and arm64 (through cross-compilation) wheels on Mac
30+
# CIBW_ARCHS_MACOS: x86_64 arm64
31+
32+
# VTK already fixes the rpaths, so we can skip this step for MacOS
33+
CIBW_REPAIR_WHEEL_COMMAND_MACOS:
34+
35+
# On Linux, we only need auditwheel to add the tags to the wheel.
36+
# Unfortunately, auditwheel currently requires us to repair the wheel to
37+
# add the tags, even though we do not need to repair the wheel.
38+
# Thus, we need to set everything up for a wheel repair (including placing
39+
# the VTK libraries in `vtkmodules`, where they are expected to be at
40+
# runtime), perform the wheel repair, and then remove the added libraries.
41+
# Then the tags will have been added.
42+
CIBW_REPAIR_WHEEL_COMMAND_LINUX: .github/scripts/linux_repair_wheel.py {wheel} {dest_dir}
43+
44+
# Pass these variables into the Linux docker containers
45+
CIBW_ENVIRONMENT_PASS_LINUX: RUNNER_OS VTK_WHEEL_SDK_INSTALL_PATH
46+
47+
# Run this before every build
48+
CIBW_BEFORE_BUILD: bash .github/scripts/cibw_before_build.sh
49+
50+
CIBW_TEST_COMMAND: >
51+
pip install -r {package}/Testing/Python/requirements.txt &&
52+
pytest -v {package}/Testing/Python
53+
54+
CIBW_TEST_COMMAND_WINDOWS: >
55+
pip install -r {package}/Testing/Python/requirements.txt &&
56+
pytest -v {package}/Testing/Python
57+
58+
59+
# Use bash by default for the run command
60+
defaults:
61+
run:
62+
shell: bash
63+
64+
jobs:
65+
build_wheels:
66+
name: Build wheels on ${{ matrix.os }}
67+
runs-on: ${{ matrix.os }}
68+
strategy:
69+
fail-fast: false
70+
matrix:
71+
os: [ubuntu-latest, macos-latest, windows-latest]
72+
73+
steps:
74+
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
75+
76+
- uses: actions/setup-python@57ded4d7d5e986d7296eab16560982c6dd7c923b # v4.6.0
77+
name: Install Python
78+
with:
79+
python-version: '3.9'
80+
81+
- name: Install dependencies
82+
run: bash .github/scripts/install.sh
83+
84+
- name: Build wheels
85+
run: cibuildwheel --output-dir wheelhouse
86+
87+
- name: Upload skbuild if an error occurred (for debugging)
88+
if: ${{ failure() }}
89+
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
90+
with:
91+
name: skbuild
92+
path: ${{ github.workspace }}/_skbuild
93+
94+
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
95+
with:
96+
path: ./wheelhouse/*.whl
97+
98+
upload_pypi:
99+
needs: build_wheels
100+
name: Upload wheels to PyPI
101+
runs-on: ubuntu-latest
102+
environment: pypi
103+
permissions:
104+
id-token: write
105+
# upload to PyPI on every tag push
106+
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/')
107+
steps:
108+
- uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
109+
with:
110+
name: artifact
111+
path: dist
112+
113+
- uses: pypa/gh-action-pypi-publish@0bf742be3ebe032c25dd15117957dc15d0cfc38d # v1.8.5

CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ if(NOT DEFINED CLEAVER_JsonCpp_LIBRARY)
1717
set(CLEAVER_JsonCpp_LIBRARY ${CLEAVER_LIBRARY})
1818
cmake_path(REMOVE_FILENAME CLEAVER_JsonCpp_LIBRARY)
1919
if(WIN32)
20-
cmake_path(APPEND CLEAVER_JsonCpp_LIBRARY "jsoncpp" "jsoncpp.lib")
20+
cmake_path(APPEND CLEAVER_JsonCpp_LIBRARY "jsoncpp.lib")
2121
elseif(APPLE)
22-
# TODO
22+
cmake_path(APPEND CLEAVER_JsonCpp_LIBRARY "libjsoncpp.a")
2323
else()
2424
cmake_path(APPEND CLEAVER_JsonCpp_LIBRARY "libjsoncpp.a")
2525
endif()

FetchFromUrl.cmake

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
include(FetchContent)
2+
3+
# Before calling this, the following variables must be defined:
4+
# FETCH_FROM_URL_PROJECT_NAME - a unique name for the project
5+
# FETCH_FROM_URL_URL - the URL from which to download the project
6+
# FETCH_FROM_URL_INSTALL_LOCATION - the location to unpack the project
7+
8+
FetchContent_Populate(${FETCH_FROM_URL_PROJECT_NAME}
9+
URL ${FETCH_FROM_URL_URL}
10+
SOURCE_DIR ${FETCH_FROM_URL_INSTALL_LOCATION}
11+
QUIET
12+
)
13+
14+
message(STATUS "Remote - FETCH_FROM_URL ${FETCH_FROM_URL_PROJECT_NAME} [OK]")

FetchVTKExternalModule.cmake

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
include(FetchContent)
2+
3+
set(proj VTKExternalModule)
4+
if (FETCH_${proj}_INSTALL_LOCATION)
5+
# The install location can be specified
6+
set(EP_SOURCE_DIR "${FETCH_${proj}_INSTALL_LOCATION}")
7+
else()
8+
set(EP_SOURCE_DIR ${CMAKE_BINARY_DIR}/${proj})
9+
endif()
10+
11+
FetchContent_Populate(${proj}
12+
SOURCE_DIR ${EP_SOURCE_DIR}
13+
GIT_REPOSITORY https://github.com/KitwareMedical/VTKExternalModule.git
14+
GIT_TAG c1906cf121e34b6391a91c2fffc448eca402a6cc
15+
QUIET
16+
)
17+
18+
message(STATUS "Remote - ${proj} [OK]")
19+
20+
set(VTKExternalModule_SOURCE_DIR ${EP_SOURCE_DIR})
21+
message(STATUS "Remote - VTKExternalModule_SOURCE_DIR:${VTKExternalModule_SOURCE_DIR}")

0 commit comments

Comments
 (0)