From 6fd9c413c1b98522997143b12500d326daf06616 Mon Sep 17 00:00:00 2001 From: Alex Hill Date: Fri, 3 Jul 2026 09:20:18 +0800 Subject: [PATCH 1/2] Build and publish macOS wheels via cibuildwheel Add a macos-15 (arm64) runner to the release workflow, with before-all/before-build scripts that port the Linux wheel recipe to macOS. The Boost, casacore and extension compiles all go through ccache, persisted with actions/cache, to save on recompilations between pipeline runs and Python versions. Dependency versions and locations are defined once in cibw_env.sh. The Linux-only CXXFLAGS/LD_LIBRARY_PATH move into [tool.cibuildwheel.linux]. Closes #232. --- .github/scripts/cibw_before_all_macos.sh | 34 ++++++++ .github/scripts/cibw_before_build_macos.sh | 95 ++++++++++++++++++++++ .github/scripts/cibw_env.sh | 20 +++++ .github/workflows/build_release.yml | 13 ++- pyproject.toml | 30 +++++-- 5 files changed, 185 insertions(+), 7 deletions(-) create mode 100755 .github/scripts/cibw_before_all_macos.sh create mode 100755 .github/scripts/cibw_before_build_macos.sh create mode 100755 .github/scripts/cibw_env.sh diff --git a/.github/scripts/cibw_before_all_macos.sh b/.github/scripts/cibw_before_all_macos.sh new file mode 100755 index 0000000..41f6dcf --- /dev/null +++ b/.github/scripts/cibw_before_all_macos.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# One-time (per runner) setup for building macOS wheels with cibuildwheel. +# Installs the non-Python-specific casacore dependencies via Homebrew and +# downloads the sources that cibw_before_build_macos.sh compiles once per +# Python version, mirroring docker/py_wheel.docker in the casacore repo. +set -euxo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/cibw_env.sh" + +# bison: casacore needs >= 3 and macOS ships 2.3. flex ships with macOS. +# No BLAS package: casacore links the Accelerate framework instead. +brew install bison ccache cfitsio fftw gcc gsl hdf5 libdeflate wcslib +ccache -M 1G + +mkdir -p "$CIBW_SRC" +cd "$CIBW_SRC" + +# Boost source: Boost.Python must be compiled per Python version. +boost_underscored=${CIBW_BOOST_VERSION//./_} +curl -fsSL --retry 3 \ + "https://archives.boost.io/release/${CIBW_BOOST_VERSION}/source/boost_${boost_underscored}.tar.bz2" \ + -o boost.tar.bz2 +tar xjf boost.tar.bz2 + +# casacore source: libcasa_python3 must be compiled per Python version. +curl -fsSL --retry 3 \ + "https://github.com/casacore/casacore/archive/refs/tags/v${CIBW_CASACORE_VERSION}.tar.gz" \ + -o casacore.tar.gz +tar xzf casacore.tar.gz + +# Measures data, bundled into the wheels; see CIBW_DATA in cibw_env.sh. +mkdir -p "$CIBW_DATA" +curl -fsSL --retry 3 https://www.astron.nl/iers/WSRT_Measures.ztar -o measures.tgz +tar xzf measures.tgz -C "$CIBW_DATA" diff --git a/.github/scripts/cibw_before_build_macos.sh b/.github/scripts/cibw_before_build_macos.sh new file mode 100755 index 0000000..8282239 --- /dev/null +++ b/.github/scripts/cibw_before_build_macos.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# Per-Python-version build of Boost.Python and casacore for macOS wheels. +# cibuildwheel runs this before building each wheel, with the target Python +# first on PATH as `python`. It is the macOS analogue of docker/py_wheel.docker +# in the casacore repo: libcasa_python3 embeds the CPython ABI, so both +# libraries are rebuilt per Python version into CIBW_PREFIX, where the +# python-casacore build finds them via CMAKE_PREFIX_PATH. +# +# As in the Linux wheel images, libcasa_python3 must not link a real +# libpython, or the wheels would only work with one specific interpreter: +# Python3_LIBRARY is pointed at Boost.Python and Python symbols are left +# undefined, to be resolved from the host interpreter at import time. +set -euxo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/cibw_env.sh" + +BREW_PREFIX=$(brew --prefix) +export PATH="$BREW_PREFIX/opt/bison/bin:$PATH" +NCPU=$(sysctl -n hw.ncpu) + +PYTHON=$(command -v python) +PY_VER=$("$PYTHON" -c 'import sys; print("%d.%d" % sys.version_info[:2])') +PY_XY=${PY_VER//./} +PY_INCLUDE=$("$PYTHON" -c 'import sysconfig; print(sysconfig.get_path("include"))') + +rm -rf "$CIBW_PREFIX" + +# ---- Boost.Python against the target Python ------------------------------ +# numpy is installed only afterwards, so that b2 skips boost_numpy, matching +# the build order in docker/py_wheel.docker. +cd "$CIBW_SRC/boost_${CIBW_BOOST_VERSION//./_}" +b2_args=() +if command -v ccache >/dev/null; then + # Route b2 through ccache too, via the darwin toolset so Boost.Python + # keeps its macOS-specific linking (no libpython; see the header). + echo "using darwin : : ccache clang++ ;" > user-config.jam + b2_args=(--user-config=user-config.jam) +fi +./bootstrap.sh --prefix="$CIBW_PREFIX" \ + --with-libraries=python \ + --with-python="$PYTHON" \ + --with-python-version="$PY_VER" +# b2 cannot derive the include dir from a virtualenv Python; the same +# workaround as in docker/py_wheel.docker. +./b2 -j"$NCPU" "${b2_args[@]}" cxxflags="-fPIC -I$PY_INCLUDE" install + +# ---- casacore against the target Python ---------------------------------- +"$PYTHON" -m pip install numpy + +# Flags taken from casacore's own recipes: docker/py_wheel.docker and its +# Homebrew formulae. +casacore_args=( + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_INSTALL_PREFIX="$CIBW_PREFIX" + -DBUILD_TESTING=OFF + -DBUILD_PYTHON=OFF + -DBUILD_PYTHON3=ON + -DPython3_EXECUTABLE="$PYTHON" + -DPython3_INCLUDE_DIR="$PY_INCLUDE" + # Boost.Python stands in for libpython; see the header comment. + -DPython3_LIBRARY="$CIBW_PREFIX/lib/libboost_python${PY_XY}.dylib" + -DPORTABLE=TRUE + -DUSE_PCH=FALSE + -DUSE_OPENMP=OFF + -DUSE_FFTW3=ON + -DUSE_HDF5=ON + -DDATA_DIR="$CIBW_DATA" +) + +# macOS-specific additions, not present in the casacore recipes. +macos_args=( + # Find the Homebrew-installed dependencies. + -DCMAKE_PREFIX_PATH="$BREW_PREFIX" + # Find the per-Python Boost built above. + -DBoost_ROOT="$CIBW_PREFIX" + # Allow undefined symbols, so that Python symbols resolve from the host + # interpreter at import time. ELF linkers allow this by default, which is + # why the Linux recipe needs no such flag. + -DCMAKE_SHARED_LINKER_FLAGS="-Wl,-undefined,dynamic_lookup" +) + +# ccache makes the repeated casacore builds cheap after the first. +CCACHE_ARGS=() +if command -v ccache >/dev/null; then + CCACHE_ARGS=( + -DCMAKE_C_COMPILER_LAUNCHER=ccache + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + ) +fi + +rm -rf "$CIBW_SRC/casacore-build" +cmake -S "$CIBW_SRC/casacore-$CIBW_CASACORE_VERSION" -B "$CIBW_SRC/casacore-build" \ + "${casacore_args[@]}" "${macos_args[@]}" "${CCACHE_ARGS[@]}" +cmake --build "$CIBW_SRC/casacore-build" --parallel "$NCPU" +cmake --install "$CIBW_SRC/casacore-build" diff --git a/.github/scripts/cibw_env.sh b/.github/scripts/cibw_env.sh new file mode 100755 index 0000000..c9fdc75 --- /dev/null +++ b/.github/scripts/cibw_env.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Shared configuration for the macOS cibuildwheel scripts. On Linux the +# equivalent values are frozen into the per-Python wheel images built from +# docker/py_wheel.docker in the casacore repo; macOS has no such images, so +# the before-* scripts build the dependencies at CI time. + +# Dependency versions. +export CIBW_BOOST_VERSION="1.91.0" +export CIBW_CASACORE_VERSION="3.8.1" + +# Everything under one root, so a single CMAKE_PREFIX_PATH finds both installs. +# CIBW_PREFIX, CIBW_DATA and CCACHE_DIR are duplicated as literals in +# pyproject.toml; CCACHE_DIR also appears in build_release.yml. +export CIBW_ROOT="$HOME/cibw" +export CIBW_SRC="$CIBW_ROOT/src" # extracted boost + casacore sources +export CIBW_PREFIX="$CIBW_ROOT/local" # install prefix for boost + casacore +export CCACHE_DIR="$CIBW_ROOT/ccache" # persisted across CI runs +# The leaf directory must be named "data": python-casacore installs it into +# the wheel by basename, and casacore/__init__.py expects casacore/data. +export CIBW_DATA="$CIBW_ROOT/measures/data" diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml index 1c3e8f6..898da52 100644 --- a/.github/workflows/build_release.yml +++ b/.github/workflows/build_release.yml @@ -33,7 +33,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest] #, macos-latest] + os: [ubuntu-latest, macos-15] # macos-15 is Sequoia, arm64 steps: - uses: actions/checkout@v7 @@ -41,6 +41,17 @@ jobs: # Needed for `setuptools-scm` fetch-depth: 0 + - name: Cache compiler output from the casacore builds + if: runner.os == 'macOS' + uses: actions/cache@v6 + with: + # CCACHE_DIR in .github/scripts/cibw_env.sh + path: ~/cibw/ccache + key: ccache-${{ matrix.os }}-${{ hashFiles('.github/scripts/cibw_env.sh') }} + restore-keys: ccache-${{ matrix.os }}- + + # No MACOSX_DEPLOYMENT_TARGET: delocate tags each macOS wheel from the + # minimum-OS of the Homebrew libraries it bundles. - name: Build wheels uses: pypa/cibuildwheel@v4.1 diff --git a/pyproject.toml b/pyproject.toml index 69da3f2..7383932 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,23 +51,41 @@ Homepage = "https://github.com/casacore/python-casacore" ######################### [tool.cibuildwheel] -build = "cp3{10,11,12,13,14}-*_x86_64" +# Each runner builds only its native architecture: x86_64 on Linux, +# arm64 on the macOS runner. +build = "cp3{10,11,12,13,14}-*" build-verbosity = 1 -environment = """ \ - CXXFLAGS="-I/usr/include/cfitsio" \ - LD_LIBRARY_PATH=/opt/boost/lib \ -""" test-command = "cd {package}/tests && pytest" test-requires = "pytest" [tool.cibuildwheel.macos] +# before-build compiles Boost.Python and casacore against each Python version, +# the macOS analogue of the per-Python Linux images below; see the script for +# details. +before-all = "bash {project}/.github/scripts/cibw_before_all_macos.sh" +before-build = "bash {project}/.github/scripts/cibw_before_build_macos.sh" +# cibuildwheel cannot source cibw_env.sh, so these are literals. Keep the +# paths in sync with CIBW_PREFIX, CIBW_DATA and CCACHE_DIR there. CMAKE_ARGS +# routes the extension build through the same ccache as the dependencies. +environment = """ \ + CMAKE_PREFIX_PATH=$HOME/cibw/local \ + CASACORE_DATA=$HOME/cibw/measures/data \ + CCACHE_DIR=$HOME/cibw/ccache \ + CMAKE_ARGS="-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache" \ +""" repair-wheel-command = """\ - DYLD_LIBRARY_PATH=${BOOST_INSTALL_DIR}/lib delocate-wheel \ + source {project}/.github/scripts/cibw_env.sh && \ + DYLD_LIBRARY_PATH=$CIBW_PREFIX/lib delocate-wheel \ --require-archs {delocate_archs} -w {dest_dir} -v {wheel}\ """ [tool.cibuildwheel.linux] skip = ["*-musllinux_*"] +# These paths exist inside the prebuilt wheel images selected below. +environment = """ \ + CXXFLAGS="-I/usr/include/cfitsio" \ + LD_LIBRARY_PATH=/opt/boost/lib \ +""" [[tool.cibuildwheel.overrides]] select="cp310-*" From 9dedede2332c05a1a248aa990f64977127bdf775 Mon Sep 17 00:00:00 2001 From: Alex Hill Date: Mon, 6 Jul 2026 22:50:19 +0800 Subject: [PATCH 2/2] Pin MACOSX_DEPLOYMENT_TARGET to the runner's macOS version cibuildwheel defaults arm64 wheels to 11.0, but the bundled Homebrew libraries are built against the runner's macOS version, which causes build failures at the delocatestep. Derive MACOSX_DEPLOYMENT_TARGET from runner's macOS version. --- .github/workflows/build_release.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml index 898da52..0bd25fd 100644 --- a/.github/workflows/build_release.yml +++ b/.github/workflows/build_release.yml @@ -50,8 +50,12 @@ jobs: key: ccache-${{ matrix.os }}-${{ hashFiles('.github/scripts/cibw_env.sh') }} restore-keys: ccache-${{ matrix.os }}- - # No MACOSX_DEPLOYMENT_TARGET: delocate tags each macOS wheel from the - # minimum-OS of the Homebrew libraries it bundles. + # We're using Homebrew for deps, so we can only build + # wheels targeting the runner's macOS version + - name: Match wheel deployment target to the runner + if: runner.os == 'macOS' + run: echo "MACOSX_DEPLOYMENT_TARGET=$(sw_vers -productVersion | cut -d. -f1).0" >> "$GITHUB_ENV" + - name: Build wheels uses: pypa/cibuildwheel@v4.1