Skip to content
Closed
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
123 changes: 123 additions & 0 deletions .github/workflows/wheel-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: CMOR Wheel Build
run-name: CMOR Wheel Build

on:
push:
branches:
- "main"
pull_request:
branches:
- "main"
release:
types: [published]

permissions:
contents: write

jobs:
wheels:
name: Build wheels
strategy:
fail-fast: false
matrix:
runner:
- RUNNER_OS: "ubuntu-24.04"
OS: linux-64
OS_NAME: linux_64
MINIFORGE_INSTALLER_URL: https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh
C_COMPILER: gcc_linux-64
WHEEL_REPAIR_PACKAGES: "auditwheel patchelf"
- RUNNER_OS: "macos-14"
OS: osx-arm64
OS_NAME: osx_arm64
MINIFORGE_INSTALLER_URL: https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-MacOSX-arm64.sh
C_COMPILER: clang_osx-arm64
WHEEL_REPAIR_PACKAGES: "delocate"
- RUNNER_OS: "macos-15-intel"
OS: osx-64
OS_NAME: osx_64
MINIFORGE_INSTALLER_URL: https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-MacOSX-x86_64.sh
C_COMPILER: clang_osx-64
WHEEL_REPAIR_PACKAGES: "delocate"
python_version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
runs-on: ${{ matrix.runner.RUNNER_OS }}
env:
PYTHON_VERSION: ${{ matrix.python_version }}
CONDA_FORGE_CHANNEL: conda-forge
MINIFORGE_INSTALLER_URL: ${{ matrix.runner.MINIFORGE_INSTALLER_URL }}
C_COMPILER: ${{ matrix.runner.C_COMPILER }}
WHEEL_REPAIR_PACKAGES: ${{ matrix.runner.WHEEL_REPAIR_PACKAGES }}
WHEEL_DIR: ${{ github.workspace }}/wheelhouse
steps:
- name: Checkout branch
if: ${{ github.event_name != 'release' }}
uses: actions/checkout@v4

- name: Checkout release tag
if: ${{ github.event_name == 'release' }}
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}

- name: Pull Submodules
run: |
git submodule init
git submodule update

- name: Setup Miniforge
run: |
curl -L "$MINIFORGE_INSTALLER_URL" -o miniforge3.sh
bash miniforge3.sh -b -p "$HOME/miniforge3"

- name: Create wheel build environment
run: |
source "$HOME/miniforge3/etc/profile.d/conda.sh"
eval "$(mamba shell hook --shell bash)"
mamba activate

mamba create -y -n "wheel_build_py${PYTHON_VERSION//./}" -c "$CONDA_FORGE_CHANNEL" \
"python=$PYTHON_VERSION" \
pip \
setuptools \
wheel \
numpy \
json-c \
udunits2 \
libnetcdf \
libuuid \
$WHEEL_REPAIR_PACKAGES \
"$C_COMPILER"

- name: Create wheel test environment
run: |
source "$HOME/miniforge3/etc/profile.d/conda.sh"
eval "$(mamba shell hook --shell bash)"
mamba activate

conda env remove -y -n "wheel_test_py${PYTHON_VERSION//./}" >/dev/null 2>&1 || true
conda create -y -n "wheel_test_py${PYTHON_VERSION//./}" \
--clone "wheel_build_py${PYTHON_VERSION//./}"

- name: Build wheel
run: |
source "$HOME/miniforge3/etc/profile.d/conda.sh"
eval "$(mamba shell hook --shell bash)"
mamba activate "wheel_build_py${PYTHON_VERSION//./}"

bash ./ci-support/build-wheel.sh

- name: Smoke test wheel
run: |
source "$HOME/miniforge3/etc/profile.d/conda.sh"
eval "$(mamba shell hook --shell bash)"
mamba activate "wheel_test_py${PYTHON_VERSION//./}"

bash ./ci-support/test-wheel.sh

- name: Upload wheels to release
if: ${{ github.event_name == 'release' }}
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.release.tag_name }}
files: wheelhouse/*.whl
fail_on_unmatched_files: true
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ CMIP5
Makefile
build
dist
wheelhouse
test_bin
tests
tests_html
Expand All @@ -27,4 +28,4 @@ setup.py
include/cmor_locale.h
recipes/cmor/meta.yaml
CMOR.egg-info
cmor.log*
cmor.log*
12 changes: 9 additions & 3 deletions Lib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import os
import sys
xml_pth = os.path.join(sys.prefix, "share", "udunits", "udunits2.xml")
if os.path.exists(xml_pth):
os.environ["UDUNITS2_XML_PATH"] = xml_pth

prefix_xml_pth = os.path.join(sys.prefix, "share", "udunits", "udunits2.xml")
wheel_xml_pth = os.path.join(os.path.dirname(__file__), "data", "udunits2.xml")

if not os.environ.get("UDUNITS2_XML_PATH"):
if os.path.exists(wheel_xml_pth):
os.environ["UDUNITS2_XML_PATH"] = wheel_xml_pth
elif os.path.exists(prefix_xml_pth):
os.environ["UDUNITS2_XML_PATH"] = prefix_xml_pth

from cmor.cmor_const import *

Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,22 @@ The content of this repository is developed by climate and computer scientists f
The code of CMOR is released under the BSD 3-Clause License. For more details, see the [LICENSE](LICENSE) File.

LLNL-CODE-2005936

## Wheels

GitHub Actions wheel builds are defined in `.github/workflows/wheel-build.yml`.
The workflow follows the existing nightly conda build pattern on Linux, macOS Intel, and Apple Silicon, then:

- builds a Python wheel after running `./configure` against conda-forge dependencies
- vendors non-system shared libraries into the wheel with `auditwheel` on Linux and `delocate` on macOS
- bundles `udunits2.xml` so `pip install` does not require a local UDUNITS2 data install
- smoke-tests the installed wheel by importing `cmor` and running `cmor.setup()`
- uploads the repaired wheels as assets on the published GitHub release only

The workflow builds and tests wheels on pushes to `main` and pull requests targeting `main`.
When a GitHub release is published, it builds from that release tag and uploads the wheels to the release.

For local Apple Silicon validation with an existing `mamba` install, run:
`./ci-support/run-wheel-ci-macos-arm64.sh`
You can limit the run to specific interpreters, for example:
`./ci-support/run-wheel-ci-macos-arm64.sh 3.12 3.13`
71 changes: 71 additions & 0 deletions ci-support/build-wheel.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env bash

set -euo pipefail

ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
BUILD_ROOT=${BUILD_ROOT:-"${ROOT_DIR}/build/wheel-build"}
STAGE_DIR="${BUILD_ROOT}/source"
WHEEL_DIR=${WHEEL_DIR:-"${ROOT_DIR}/wheelhouse"}
RAW_WHEEL_DIR="${WHEEL_DIR}/raw"
REPAIRED_WHEEL_DIR="${WHEEL_DIR}/repaired"
PYTHON_BIN=${PYTHON_BIN:-python}
DELOCATE_WHEEL_BIN=${DELOCATE_WHEEL_BIN:-delocate-wheel}
AUDITWHEEL_BIN=${AUDITWHEEL_BIN:-auditwheel}

if [ -z "${CONDA_PREFIX:-}" ]; then
echo "CONDA_PREFIX must be set to build wheels" >&2
exit 1
fi

UDUNITS_DATA_DIR="${CONDA_PREFIX}/share/udunits"
if [ ! -d "${UDUNITS_DATA_DIR}" ]; then
echo "Could not find ${UDUNITS_DATA_DIR}" >&2
exit 1
fi

rm -rf "${BUILD_ROOT}" "${RAW_WHEEL_DIR}" "${REPAIRED_WHEEL_DIR}"
mkdir -p "${STAGE_DIR}" "${RAW_WHEEL_DIR}" "${REPAIRED_WHEEL_DIR}"

rsync -a "${ROOT_DIR}/" "${STAGE_DIR}/" \
--exclude '.git/' \
--exclude 'build/' \
--exclude 'dist/' \
--exclude 'wheelhouse/' \
--exclude '__pycache__/' \
--exclude '.pytest_cache/'

mkdir -p "${STAGE_DIR}/Lib/data"
cp "${UDUNITS_DATA_DIR}"/udunits2*.xml "${STAGE_DIR}/Lib/data/"

pushd "${STAGE_DIR}" >/dev/null

./configure \
--prefix="${CONDA_PREFIX}" \
--with-python="${CONDA_PREFIX}" \
--enable-fortran=no \
--with-uuid="${CONDA_PREFIX}" \
--with-json-c="${CONDA_PREFIX}" \
--with-udunits2="${CONDA_PREFIX}" \
--with-netcdf="${CONDA_PREFIX}"

"${PYTHON_BIN}" -m pip wheel --no-build-isolation --no-deps --wheel-dir "${RAW_WHEEL_DIR}" .

case "$(uname -s)" in
Darwin)
"${DELOCATE_WHEEL_BIN}" -w "${REPAIRED_WHEEL_DIR}" -v "${RAW_WHEEL_DIR}"/*.whl
;;
Linux)
"${AUDITWHEEL_BIN}" repair -w "${REPAIRED_WHEEL_DIR}" "${RAW_WHEEL_DIR}"/*.whl
;;
*)
echo "Unsupported platform for wheel repair: $(uname -s)" >&2
exit 1
;;
esac

popd >/dev/null

mv "${REPAIRED_WHEEL_DIR}"/*.whl "${WHEEL_DIR}/"
rm -rf "${RAW_WHEEL_DIR}" "${REPAIRED_WHEEL_DIR}"

ls -lh "${WHEEL_DIR}"
116 changes: 116 additions & 0 deletions ci-support/run-wheel-ci-macos-arm64.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/usr/bin/env bash

set -euo pipefail

ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
MAMBA_BIN=${MAMBA_BIN:-mamba}
CONDA_FORGE_CHANNEL=${CONDA_FORGE_CHANNEL:-conda-forge}
C_COMPILER=${C_COMPILER:-clang_osx-arm64}
ENV_PREFIX=${ENV_PREFIX:-cmor-wheel}
UPDATE_SUBMODULES=${UPDATE_SUBMODULES:-1}

if [ "$#" -gt 0 ]; then
PYTHON_VERSIONS=("$@")
else
PYTHON_VERSIONS=(3.10 3.11 3.12 3.13 3.14)
fi

if [ "$(uname -s)" != "Darwin" ]; then
echo "This script only supports macOS." >&2
exit 1
fi

if [ "$(uname -m)" != "arm64" ]; then
echo "This script is intended for Apple Silicon (arm64)." >&2
exit 1
fi

if ! command -v "${MAMBA_BIN}" >/dev/null 2>&1; then
echo "Could not find '${MAMBA_BIN}' in PATH." >&2
exit 1
fi

eval "$("${MAMBA_BIN}" shell hook --shell bash)"

if [ "${UPDATE_SUBMODULES}" = "1" ]; then
git -C "${ROOT_DIR}" submodule update --init
fi

ensure_env() {
local env_name=$1
shift
local packages=("$@")

if "${MAMBA_BIN}" run -n "${env_name}" python --version >/dev/null 2>&1; then
"${MAMBA_BIN}" install -y -n "${env_name}" -c "${CONDA_FORGE_CHANNEL}" "${packages[@]}"
else
"${MAMBA_BIN}" create -y -n "${env_name}" -c "${CONDA_FORGE_CHANNEL}" "${packages[@]}"
fi
}

activate_env() {
set +u
mamba activate "$1"
set -u
}

deactivate_env() {
set +u
mamba deactivate
set -u
}

recreate_test_env() {
local build_env_name=$1
local test_env_name=$2

if "${MAMBA_BIN}" run -n "${test_env_name}" python --version >/dev/null 2>&1; then
conda env remove -y -n "${test_env_name}"
fi

conda create -y -n "${test_env_name}" --clone "${build_env_name}"
}

for python_version in "${PYTHON_VERSIONS[@]}"; do
build_env_name="${ENV_PREFIX}-build-py${python_version//./}"
test_env_name="${ENV_PREFIX}-test-py${python_version//./}"
wheel_dir="${ROOT_DIR}/wheelhouse/py${python_version}"
build_root="${ROOT_DIR}/build/wheel-build-py${python_version//./}"
test_venv_dir="${ROOT_DIR}/build/wheel-test-venv-py${python_version//./}"

echo "==> Preparing ${build_env_name}"
ensure_env \
"${build_env_name}" \
"python=${python_version}" \
pip \
setuptools \
wheel \
numpy \
delocate \
json-c \
udunits2 \
libnetcdf \
libuuid \
"${C_COMPILER}"

echo "==> Preparing ${test_env_name}"
recreate_test_env "${build_env_name}" "${test_env_name}"

activate_env "${build_env_name}"
rm -rf "${wheel_dir}" "${build_root}" "${test_venv_dir}"

echo "==> Building wheel for Python ${python_version}"
WHEEL_DIR="${wheel_dir}" \
BUILD_ROOT="${build_root}" \
bash "${ROOT_DIR}/ci-support/build-wheel.sh"

deactivate_env
activate_env "${test_env_name}"

echo "==> Testing wheel for Python ${python_version}"
WHEEL_DIR="${wheel_dir}" \
TEST_VENV_DIR="${test_venv_dir}" \
bash "${ROOT_DIR}/ci-support/test-wheel.sh"

deactivate_env
done
Loading
Loading