Skip to content
Merged
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
52 changes: 52 additions & 0 deletions .github/scripts/check_macos_wheel_arch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Check that every macOS wheel in a directory carries a library built for the
# architecture its filename claims, and for no older macOS than the tag allows.
#
# check_macos_wheel_arch.sh <wheelhouse>
#
# The x86_64 wheels are cross-compiled on an arm64 runner, where a build that
# ignored ARCHFLAGS would quietly produce an arm64 library inside an
# x86_64-tagged wheel. pip would install it on an Intel Mac and the import would
# then fail to load the library, so catch that here instead.
set -euo pipefail

dir="${1:?usage: $0 <wheelhouse>}"
shopt -s nullglob
wheels=("$dir"/*macosx*.whl)
if [ "${#wheels[@]}" -eq 0 ]; then
echo "No macOS wheels found in $dir"; exit 1
fi

failed=0
for whl in "${wheels[@]}"; do
name=$(basename "$whl" .whl)
plat=${name##*-} # e.g. macosx_10_13_x86_64
tag_arch=$(echo "$plat" | sed -E 's/^macosx_[0-9]+_[0-9]+_//')
tag_min=$(echo "$plat" | sed -E 's/^macosx_([0-9]+)_([0-9]+)_.*/\1.\2/')
case "$tag_arch" in
universal2) want="arm64 x86_64" ;;
x86_64|arm64) want="$tag_arch" ;;
*) echo "FAIL $name: unexpected platform tag $plat"; failed=1; continue ;;
esac

tmp=$(mktemp -d)
unzip -q "$whl" -d "$tmp"
libs=$(find "$tmp" -name '*.dylib')
if [ -z "$libs" ]; then
echo "FAIL $name: no .dylib inside the wheel"; failed=1; rm -rf "$tmp"; continue
fi
while IFS= read -r lib; do
got=$(lipo -archs "$lib" | tr ' ' '\n' | sort | xargs)
minos=$(otool -l "$lib" | awk '/LC_BUILD_VERSION/{b=1} b&&/minos/{print $2; exit} /LC_VERSION_MIN_MACOSX/{v=1} v&&/version/{print $2; exit}')
rel=${lib#"$tmp"/}
if [ "$got" != "$want" ]; then
echo "FAIL $name: $rel is [$got], tag says [$want]"; failed=1
elif [ "$(printf '%s\n%s\n' "$minos" "$tag_min" | sort -V | tail -n1)" != "$tag_min" ]; then
echo "FAIL $name: $rel needs macOS $minos, tag says $tag_min"; failed=1
else
echo "ok $name: $rel is [$got], minos $minos ($(file -b "$lib"))"
fi
done <<< "$libs"
rm -rf "$tmp"
done
exit "$failed"
10 changes: 8 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ jobs:
# ubuntu-24.04-arm for the same reason: to prove the arm64 label works
# on every PR, not first at release time.
#
# macOS wheels are arm64 only; Intel Macs install from the sdist, which
# builds the library during the install.
# macos-latest builds both Mac architectures: arm64 natively and x86_64
# cross-compiled (and tested under Rosetta 2), since GitHub no longer
# offers an Intel macOS runner. See [tool.cibuildwheel.macos] in
# pyproject.toml.
os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-latest]

steps:
Expand All @@ -101,6 +103,10 @@ jobs:
python -m pip install --upgrade pip cibuildwheel
python -m cibuildwheel --output-dir wheelhouse

- name: Check macOS Wheel Architectures
if: runner.os == 'macOS'
run: .github/scripts/check_macos_wheel_arch.sh wheelhouse

- name: Upload Wheels
uses: actions/upload-artifact@v4
with:
Expand Down
50 changes: 50 additions & 0 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: macOS Wheels

# Builds and tests the macOS wheels exactly as release.yml does, so the x86_64
# wheels (cross-compiled on the arm64 runner and tested under Rosetta 2) are
# proven on the PR that changes how they are built, not first at release time.
on:
pull_request:
branches: [ main ]
paths:
- 'src/c/**'
- 'src/python/**'
- 'include/**'
- 'CMakeLists.txt'
- 'pyproject.toml'
- '.github/workflows/wheels.yml'
- '.github/workflows/release.yml'
- '.github/scripts/check_macos_wheel_arch.sh'
workflow_dispatch:

jobs:
build_macos_wheels:
name: Build & Test macOS Wheels (x86_64 + arm64)
runs-on: macos-latest

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.x'

- name: Build Wheels
run: |
python -m pip install --upgrade pip cibuildwheel
python -m cibuildwheel --output-dir wheelhouse

- name: Check macOS Wheel Architectures
run: .github/scripts/check_macos_wheel_arch.sh wheelhouse

- name: List Wheels
run: ls -1 wheelhouse

- name: Upload Wheels
uses: actions/upload-artifact@v4
with:
name: wheels-macos
path: wheelhouse/*.whl
if-no-files-found: error
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,18 @@ Pyraview is installed from this repository; it is not published on PyPI.

#### Option 1: Install from a Release Wheel (Recommended)

Each [Release](../../releases) carries wheels for Linux, Windows and Apple
Silicon macOS. A wheel bundles the compiled library inside the package, so
Each [Release](../../releases) carries wheels for Linux, Windows and macOS
(Apple Silicon and Intel, macOS 10.13 or newer on Intel). A wheel bundles the compiled library inside the package, so
installing one needs no compiler and no environment variable:

```bash
pip install https://github.com/VH-Lab/Pyraview/releases/download/v0.4.1/<wheel file>
```

Pick the wheel matching your platform and Python version -- for example
`pyraview-0.4.1-cp311-cp311-win_amd64.whl` for Python 3.11 on 64-bit Windows.
`pyraview-0.4.1-cp311-cp311-win_amd64.whl` for Python 3.11 on 64-bit Windows, or
a `macosx_..._x86_64` wheel on an Intel Mac and `macosx_11_0_arm64` on Apple
Silicon.

#### Option 2: Install from Source

Expand All @@ -55,8 +57,7 @@ pip install git+https://github.com/VH-Lab/Pyraview.git

This compiles the C++ during the install (CMake and a C++ compiler required)
and bundles the result the same way, so it works on any platform and Python
version, including Intel macOS, which has no wheel. Add `@v0.4.1` to the URL to
pin a release.
version. Add `@v0.4.1` to the URL to pin a release.

#### Option 3: Build the Library Separately
Useful when working on the C++ itself, since the Python package then picks up a
Expand Down
19 changes: 18 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,26 @@ wheel.packages = ["src/python/pyraview"]
[tool.pytest.ini_options]
testpaths = ["src/python/tests"]

# Wheels are built by .github/workflows/release.yml. The test command asserts the
# Wheels are built by .github/workflows/release.yml (and, for macOS, on PRs by
# .github/workflows/wheels.yml). The test command asserts the
# library really shipped inside the wheel, since an importable-but-unloadable
# package is exactly the failure mode wheels are meant to remove.
[tool.cibuildwheel]
skip = ["pp*", "*_i686", "*-musllinux_*"]
test-command = 'python -c "import os, pyraview; assert os.path.dirname(pyraview._lib_path) == os.path.dirname(pyraview.__file__), pyraview._lib_path; print(\"bundled:\", pyraview._lib_path)"'

# One wheel per Mac architecture. GitHub has no Intel macOS runner any more, so
# the x86_64 wheels are cross-compiled on the arm64 runner: cibuildwheel exports
# ARCHFLAGS="-arch x86_64", which scikit-build-core turns into
# CMAKE_OSX_ARCHITECTURES, and tests the result under Rosetta 2. The workflows
# check each wheel's library with lipo (.github/scripts/check_macos_wheel_arch.sh)
# so a wheel tagged for one architecture can never ship a library for another.
[tool.cibuildwheel.macos]
archs = ["x86_64", "arm64"]

# 10.13 is the oldest target current Xcode supports, and what python.org's
# CPython 3.12+ builds need anyway; cibuildwheel raises it to 10.15 for
# CPython 3.14+. arm64 wheels are 11.0, the first macOS on Apple Silicon.
[[tool.cibuildwheel.overrides]]
select = "*-macosx_x86_64"
environment = { MACOSX_DEPLOYMENT_TARGET = "10.13" }
Loading