diff --git a/.github/scripts/check_macos_wheel_arch.sh b/.github/scripts/check_macos_wheel_arch.sh new file mode 100755 index 0000000..56be0f3 --- /dev/null +++ b/.github/scripts/check_macos_wheel_arch.sh @@ -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 +# +# 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 }" +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" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be95083..51f8600 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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: @@ -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: diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml new file mode 100644 index 0000000..fac6b36 --- /dev/null +++ b/.github/workflows/wheels.yml @@ -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 diff --git a/README.md b/README.md index ebee14a..894e25d 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,8 @@ 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 @@ -45,7 +45,9 @@ pip install https://github.com/VH-Lab/Pyraview/releases/download/v0.4.1/