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..0bd25fd 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,21 @@ 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 }}- + + # 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 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-*"