Skip to content
66 changes: 66 additions & 0 deletions .github/actions/setup-build-env/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Setup atlas4py build environment
description: Install system + Python build dependencies for atlas4py on Linux/macOS.

inputs:
python-version:
description: Python version to set up
required: true
install-build-backend:
description: >
Whether to pre-install pybind11, scikit-build-core and numpy into the
host environment. Required for `--no-build-isolation` builds; can be
skipped when pip provisions an isolated build environment.
required: false
default: 'false'

outputs:
sys-deps-cache-key:
description: >-
A string encoding the versions of installed system libraries (e.g. MPI).
Include this in cache keys that depend on compiled system dependencies.
value: ${{ steps.sys-deps-key.outputs.value }}

runs:
using: composite
steps:
- name: Set up Python ${{ inputs.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ inputs.python-version }}

- name: Install system build dependencies (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential libopenmpi-dev openmpi-bin libomp-dev

- name: Install system build dependencies (macOS)
if: runner.os == 'macOS'
shell: bash
run: |
brew update
brew install open-mpi

- name: Collect system library versions for cache key
id: sys-deps-key
shell: bash
run: |
if [ "$RUNNER_OS" = "Linux" ]; then
MPI_VERSION=$(dpkg-query --showformat='${Version}' --show libopenmpi-dev)
OMP_VERSION=$(dpkg-query --showformat='${Version}' --show libomp-dev)
echo "value=mpi-${MPI_VERSION}-omp-${OMP_VERSION}" >> "$GITHUB_OUTPUT"
else
MPI_VERSION=$(brew list --versions open-mpi | awk '{print $2}')
echo "value=mpi-${MPI_VERSION}" >> "$GITHUB_OUTPUT"
fi

- name: Upgrade pip
shell: bash
run: python -m pip install --upgrade pip

- name: Install Python build backend (for --no-build-isolation builds)
if: inputs.install-build-backend == 'true'
shell: bash
run: python -m pip install pybind11 scikit-build-core numpy
39 changes: 39 additions & 0 deletions .github/actions/verify-atlas4py-rebuild/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Verify atlas4py editable rebuild
description: >
Verify that an editable install of atlas4py picks up edits to both Python
and C++ sources (i.e. that scikit-build-core's editable.rebuild works).

runs:
using: composite
steps:
- name: Verify editable Python edits are picked up
shell: bash
run: |
python - <<'PY'
import pathlib
path = pathlib.Path("src/atlas4py/__init__.py")
marker = "\n__CI_EDITABLE_MARKER__ = 'changed'\n"
txt = path.read_text(encoding="utf-8")
if marker not in txt:
path.write_text(txt + marker, encoding="utf-8")
import atlas4py
assert getattr(atlas4py, "__CI_EDITABLE_MARKER__", None) == "changed"
print("Editable Python change reflected OK")
PY

- name: Verify editable C++ edits trigger a rebuild
shell: bash
run: |
sed -i.bak 's/ATLAS4PY_VERSION_STRING/__CI_EDITABLE_MARKER__/g' \
src/atlas4py/_atlas4py.cpp && rm -f src/atlas4py/_atlas4py.cpp.bak
python - <<'PY'
import atlas4py
assert atlas4py.__version__ == "__CI_EDITABLE_MARKER__", atlas4py.__version__
print("Editable C++ change reflected OK")
PY

- name: Restore edited sources
if: always()
shell: bash
run: |
git checkout -- src/atlas4py/__init__.py src/atlas4py/_atlas4py.cpp || true
27 changes: 27 additions & 0 deletions .github/actions/verify-atlas4py/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Verify atlas4py installation
description: Common smoke tests run after an atlas4py install (import, version, pytest).

runs:
using: composite
steps:
- name: Verify import
shell: bash
env:
ATLAS_DEBUG: "1"
run: python -c "import atlas4py; print('atlas4py', atlas4py.__version__, 'imported successfully')"

- name: Verify version matches pyproject.toml
shell: bash
run: |
python - <<'PY'
import tomllib, pathlib, atlas4py
expected = tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"]
assert atlas4py.__version__ == expected, (atlas4py.__version__, expected)
print("Version OK:", expected)
PY

- name: Run pytest suite
shell: bash
run: |
python -m pip install pytest
pytest -v tests
37 changes: 29 additions & 8 deletions .github/workflows/cibuildwheel.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
name: Build and deploy wheel

on:
# Trigger the workflow on all pushes, except on tag creation
push:
branches: [ master ]
pull_request:
branches: [ master ]
branches:
- '**'
tags-ignore:
- '**'

# Trigger the workflow on all pull requests
pull_request: ~

# Allow workflow to be dispatched on demand
workflow_dispatch: ~
jobs:
pre-check:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
runs-on: ubuntu-latest
steps:
- run: echo "Condition met, proceeding with workflow."

build_wheels:
name: Build wheels on ${{ matrix.os }} for ${{ matrix.python }}
runs-on: ${{ matrix.os }}
needs: pre-check # This job won't start if 'pre-check' is skipped
strategy:
matrix:
os:
Expand Down Expand Up @@ -41,24 +55,27 @@ jobs:
CIBW_TEST_REQUIRES: -r requirements-dev.txt
CIBW_TEST_COMMAND: pytest -v {project}/tests
- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: wheel-${{matrix.python}}-${{ matrix.os }}
path: ./dist/*.whl

build_sdist:
name: Build sdist
runs-on: ubuntu-latest
needs: pre-check # This job won't start if 'pre-check' is skipped
steps:
- name: Set up Python
uses: actions/setup-python@v2
- uses: actions/checkout@v4
uses: actions/setup-python@v6
with:
python-version: '3.13'
- uses: actions/checkout@v6
- name: Install requirements
run: python -m pip install -r requirements-dev.txt
- name: Make sdist
run: python -m build --sdist --outdir dist/
- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: sdist
path: ./dist/*.tar.gz
Expand All @@ -67,8 +84,12 @@ jobs:
name: Upload wheels to TestPyPI
needs: [build_wheels, build_sdist]
runs-on: ubuntu-latest

# Only run for pushes to master (not PRs, not workflow_dispatch)
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}

steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8
with:
path: dist
merge-multiple: true
Expand Down
175 changes: 175 additions & 0 deletions .github/workflows/editable-install.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
name: Editable install tests

on:
push:
branches:
- '**'
tags-ignore:
- '**'
pull_request: ~
workflow_dispatch: ~

jobs:
# ---------------------------------------------------------------------------
# Editable install using the bundled (FetchContent) eckit + atlas dependencies
# ---------------------------------------------------------------------------
editable-internal:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
name: Editable (bundled) - ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
python-version: ["3.13"]

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup build environment
uses: ./.github/actions/setup-build-env
with:
python-version: ${{ matrix.python-version }}
# Pre-install build backend only when we will skip pip's build isolation.
install-build-backend: 'true'

- name: Editable install
run: |
export CMAKE_ARGS="-DATLAS_ENABLE_OMP=ON"
python -m pip install --no-build-isolation -v -e .

# ----- Verification --------------------------------------------------
- name: Verify atlas4py installation
uses: ./.github/actions/verify-atlas4py

# ----- Editable-rebuild specific checks ------------------------------
- name: Verify atlas4py editable rebuild
uses: ./.github/actions/verify-atlas4py-rebuild

# ---------------------------------------------------------------------------
# Editable install using externally built eckit + atlas
# ---------------------------------------------------------------------------
editable-external:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
name: Editable (external) - ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
python-version: ["3.13"]

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup build environment
uses: ./.github/actions/setup-build-env
id: build-env
with:
python-version: ${{ matrix.python-version }}
install-build-backend: 'true'

- name: Extract pinned dependency versions from pyproject.toml
id: versions
run: |
python - <<'PY' >> "$GITHUB_ENV"
import pathlib, re, tomllib
cfg = tomllib.loads(pathlib.Path("pyproject.toml").read_text())
args = cfg["tool"]["scikit-build"]["cmake"]["args"]
pat = re.compile(r"^-D(ATLAS4PY_[A-Za-z0-9_]+)=(.*)$")
for a in args:
m = pat.match(a)
if m:
print(f"{m.group(1)}={m.group(2)}")
PY

- name: Cache external eckit + atlas install
id: cache-deps
uses: actions/cache@v5
with:
path: deps/install
key: ext-deps-atlas-${{ env.ATLAS4PY_ATLAS_VERSION }}-eckit-${{ env.ATLAS4PY_ECKIT_VERSION }}-ecbuild-${{ env.ATLAS4PY_ECBUILD_VERSION }}-${{ runner.os }}[${{ steps.build-env.outputs.sys-deps-cache-key }}]

- name: Build external eckit + atlas
if: steps.cache-deps.outputs.cache-hit != 'true'
run: |
mkdir -p deps/source
pushd deps

echo "::group::Clone ecbuild ${ATLAS4PY_ECBUILD_VERSION}"
git clone --branch ${ATLAS4PY_ECBUILD_VERSION} --single-branch https://github.com/ecmwf/ecbuild source/ecbuild
echo "::endgroup::"

echo "::group::Clone eckit ${ATLAS4PY_ECKIT_VERSION}"
git clone --branch ${ATLAS4PY_ECKIT_VERSION} --single-branch https://github.com/ecmwf/eckit source/eckit
echo "::endgroup::"

echo "::group::Clone atlas ${ATLAS4PY_ATLAS_VERSION}"
git clone --branch ${ATLAS4PY_ATLAS_VERSION} --single-branch https://github.com/ecmwf/atlas source/atlas
echo "::endgroup::"

export CMAKE_PREFIX_PATH=$PWD/install

echo "::group::Configure eckit"
cmake -S source/eckit -B build/eckit -G Ninja \
-DENABLE_TESTS=OFF -DENABLE_FORTRAN=OFF -DCMAKE_BUILD_TYPE=Release
echo "::endgroup::"

echo "::group::Build eckit"
cmake --build build/eckit --parallel
echo "::endgroup::"

echo "::group::Install eckit"
cmake --install build/eckit --prefix install/eckit
echo "::endgroup::"

echo "::group::Configure atlas"
cmake -S source/atlas -B build/atlas -G Ninja \
-DENABLE_TESTS=OFF -DENABLE_FORTRAN=OFF -DCMAKE_BUILD_TYPE=Release
echo "::endgroup::"

echo "::group::Build atlas"
cmake --build build/atlas --parallel
echo "::endgroup::"

echo "::group::Install atlas"
cmake --install build/atlas --prefix install/atlas
echo "::endgroup::"

popd

- name: Editable install against external deps
run: |
export CMAKE_PREFIX_PATH=$PWD/deps/install
python -m pip install --no-build-isolation -v -e .

# ----- Verification --------------------------------------------------
- name: Verify atlas4py installation
uses: ./.github/actions/verify-atlas4py

# ----- External-deps specific check ----------------------------------
- name: Verify atlas4py was linked against the external atlas
run: |
python - <<'PY'
import pathlib, platform, subprocess, atlas4py._atlas4py as _ext
ext = pathlib.Path(_ext.__file__).resolve()
print(ext)
if platform.system() == "Darwin":
# `otool -l` dumps load commands, including LC_RPATH entries which
# carry the absolute external install path baked in at link time.
out = subprocess.check_output(["otool", "-l", str(ext)]).decode()
else:
out = subprocess.check_output(["ldd", str(ext)]).decode()
print(out)
assert "libatlas" in out, "libatlas not linked"
# Must resolve to our externally-installed atlas
external = str(pathlib.Path("deps/install/atlas").resolve())
assert external in out, f"expected external atlas path {external} in linker output"
print("External atlas linkage OK")
PY

# ----- Editable-rebuild specific checks ------------------------------
- name: Verify atlas4py editable rebuild
uses: ./.github/actions/verify-atlas4py-rebuild
Loading
Loading