Skip to content
Open
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
23 changes: 23 additions & 0 deletions .github/scripts/ci/bootstrap_conda_tools.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail

need_conda_build=0

if ! conda build --help >/dev/null 2>&1; then
need_conda_build=1
fi

if [[ $need_conda_build -eq 0 ]]; then
echo "conda-build is already available."
exit 0
fi

echo "Installing required conda tooling: conda-build"
conda install -y -n base -c conda-forge conda-build

if ! conda build --help >/dev/null 2>&1; then
echo "Failed to provision conda-build." >&2
exit 1
fi

echo "conda-build is ready."
122 changes: 122 additions & 0 deletions .github/scripts/ci/ensure_deps.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
set -euo pipefail

platform="osx-arm64"
mpi=""
openmp=""
tecio="off"
owner="opflow-dev"
channels=()

while [[ $# -gt 0 ]]; do
case "$1" in
--platform)
platform="$2"
shift 2
;;
--mpi)
mpi="$2"
shift 2
;;
--openmp)
openmp="$2"
shift 2
;;
--tecio)
tecio="$2"
shift 2
;;
--owner)
owner="$2"
shift 2
;;
--channel)
channels+=("$2")
shift 2
;;
*)
echo "Unknown argument: $1" >&2
exit 2
;;
esac
done

if [[ -z "$mpi" || -z "$openmp" ]]; then
echo "Usage: ensure_deps.sh --mpi <nompi|openmpi> --openmp <on|off> [--tecio <on|off>] [--platform <subdir>] [--owner <org>]" >&2
exit 2
fi
if [[ "$tecio" != "on" && "$tecio" != "off" ]]; then
echo "Invalid --tecio value: $tecio (expected on|off)" >&2
exit 2
fi

if [[ ${#channels[@]} -eq 0 ]]; then
channels=("$owner" "conda-forge")
else
has_owner=0
has_conda_forge=0
for c in "${channels[@]}"; do
[[ "$c" == "$owner" ]] && has_owner=1
[[ "$c" == "conda-forge" ]] && has_conda_forge=1
done
[[ $has_owner -eq 0 ]] && channels+=("$owner")
[[ $has_conda_forge -eq 0 ]] && channels+=("conda-forge")
fi

channel_args=()
for c in "${channels[@]}"; do
channel_args+=("-c" "$c")
done

missing=()

check_spec() {
local spec="$1"
if conda search --override-channels --platform "$platform" "${channel_args[@]}" "$spec" >/dev/null 2>&1; then
echo "[OK] $spec"
else
echo "[MISS] $spec"
missing+=("$spec")
fi
}

check_spec "cmake >=4.0.2"
check_spec "ninja"
check_spec "spdlog"
check_spec "tbb"
check_spec "${owner}::amgcl"
check_spec "vtk"
check_spec "benchmark"
check_spec "gtest"

if [[ "$mpi" == "nompi" ]]; then
check_spec "mpi * mpi_serial"
check_spec "hdf5 * nompi*"
else
check_spec "mpi * openmpi"
check_spec "openmpi"
check_spec "hdf5 * mpi_openmpi*"
fi

if [[ "$tecio" == "on" ]]; then
if [[ "$mpi" == "nompi" ]]; then
check_spec "${owner}::tecio * mpi_nompi_*"
else
check_spec "${owner}::teciompi * mpi_openmpi_*"
fi
fi

check_spec "${owner}::hypre * mpi_${mpi}_openmp_${openmp}_*"

if [[ "$openmp" == "on" && "$(uname -s)" == "Darwin" ]]; then
check_spec "llvm-openmp"
fi

if [[ ${#missing[@]} -gt 0 ]]; then
echo "Dependency check failed for platform=${platform}, mpi=${mpi}, openmp=${openmp}" >&2
printf 'Missing specs:\n' >&2
printf ' - %s\n' "${missing[@]}" >&2
exit 1
fi

echo "All dependencies available for platform=${platform}, mpi=${mpi}, openmp=${openmp}."
178 changes: 178 additions & 0 deletions .github/scripts/ci/generate_matrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
#!/usr/bin/env python3
"""Generate GitHub Actions matrix JSON from conda_build_config.yaml."""

from __future__ import annotations

import argparse
import json
import pathlib
import re
import sys
from typing import Dict, List

RUNNER_BY_PLATFORM = {
"osx-arm64": "macos-14",
"linux-64": "ubuntu-24.04",
}


def parse_conda_build_config(path: pathlib.Path) -> Dict[str, List[str]]:
data: Dict[str, List[str]] = {}
current: str | None = None

for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.split("#", 1)[0].rstrip()
if not line.strip():
continue

key_match = re.match(r"^([A-Za-z0-9_-]+):\s*$", line)
if key_match:
current = key_match.group(1)
data[current] = []
continue

value_match = re.match(r"^\s*-\s*(.+?)\s*$", line)
if value_match and current is not None:
data[current].append(value_match.group(1))

missing = [name for name in ("mpi", "openmp") if not data.get(name)]
if missing:
raise ValueError(
f"Missing required variant keys in {path}: {', '.join(missing)}"
)

return data


def parse_platforms(raw: str) -> List[str]:
platforms = [x.strip() for x in raw.split(",") if x.strip()]
if not platforms:
raise ValueError("No platform selected")

unknown = [p for p in platforms if p not in RUNNER_BY_PLATFORM]
if unknown:
raise ValueError(
"Unsupported platform(s): "
+ ", ".join(unknown)
+ ". Supported: "
+ ", ".join(sorted(RUNNER_BY_PLATFORM))
)

return platforms


def package_matrix(
variants: Dict[str, List[str]], platforms: List[str]
) -> Dict[str, List[Dict[str, str]]]:
include: List[Dict[str, str]] = []
for platform in platforms:
runner = RUNNER_BY_PLATFORM[platform]
for mpi in variants["mpi"]:
for openmp in variants["openmp"]:
include.append(
{
"platform": platform,
"runner": runner,
"mpi": mpi,
"openmp": openmp,
}
)
return {"include": include}


def source_matrix(
variants: Dict[str, List[str]], build_types: List[str], platforms: List[str]
) -> Dict[str, List[Dict[str, str]]]:
include: List[Dict[str, str]] = []
for platform in platforms:
runner = RUNNER_BY_PLATFORM[platform]
for mpi in variants["mpi"]:
for openmp in variants["openmp"]:
for build_type in build_types:
include.append(
{
"platform": platform,
"runner": runner,
"mpi": mpi,
"openmp": openmp,
"build_type": build_type,
}
)
return {"include": include}


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument(
"--recipe-config",
default="conda/recipe/conda_build_config.yaml",
help="Path to conda build variant config",
)
parser.add_argument(
"--mode",
choices=["all", "package", "source"],
default="all",
help="Which matrix to generate",
)
parser.add_argument(
"--build-types",
default="Debug,Release",
help="Comma-separated build types for source matrix",
)
parser.add_argument(
"--platforms",
default="linux-64",
help="Comma-separated conda platform subdirs, e.g. osx-arm64,linux-64",
)
parser.add_argument(
"--github-output",
default="",
help="If set, write output key=value pairs for GitHub Actions",
)
return parser.parse_args()


def main() -> int:
args = parse_args()
config_path = pathlib.Path(args.recipe_config)
if not config_path.exists():
print(f"error: config file not found: {config_path}", file=sys.stderr)
return 1

build_types = [x.strip() for x in args.build_types.split(",") if x.strip()]
if not build_types:
print("error: --build-types resolved to empty list", file=sys.stderr)
return 1

try:
platforms = parse_platforms(args.platforms)
except ValueError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1

variants = parse_conda_build_config(config_path)

payloads: Dict[str, Dict[str, List[Dict[str, str]]]] = {}
if args.mode in {"all", "package"}:
payloads["package_matrix"] = package_matrix(variants, platforms)
if args.mode in {"all", "source"}:
payloads["source_matrix"] = source_matrix(variants, build_types, platforms)

if args.github_output:
out_path = pathlib.Path(args.github_output)
with out_path.open("a", encoding="utf-8") as f:
for key, value in payloads.items():
f.write(f"{key}={json.dumps(value, separators=(',', ':'))}\n")

if args.mode == "all":
print(json.dumps(payloads, indent=2, sort_keys=True))
elif args.mode == "package":
print(json.dumps(payloads["package_matrix"], indent=2, sort_keys=True))
else:
print(json.dumps(payloads["source_matrix"], indent=2, sort_keys=True))

return 0


if __name__ == "__main__":
raise SystemExit(main())
47 changes: 47 additions & 0 deletions .github/scripts/ci/mpiexec-singleton.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail

# Fallback launcher for environments where OpenMPI cannot spawn MPI processes
# reliably (e.g., local hostname is not resolvable). It strips mpiexec options
# and runs the target command directly in singleton mode.

if [[ $# -eq 0 ]]; then
echo "Usage: mpiexec-singleton.sh <mpiexec args> <command>" >&2
exit 2
fi

args=("$@")
argc=$#
idx=0

while [[ $idx -lt $argc ]]; do
arg="${args[$idx]}"
case "$arg" in
-n|-np|--np|--n)
idx=$((idx + 2))
;;
--host|--hostfile|--bind-to|--map-by|--mca)
idx=$((idx + 2))
;;
--oversubscribe|--report-bindings)
idx=$((idx + 1))
;;
--)
idx=$((idx + 1))
break
;;
-*)
idx=$((idx + 1))
;;
*)
break
;;
esac
done

if [[ $idx -ge $argc ]]; then
echo "No executable found in mpiexec invocation: $*" >&2
exit 2
fi

exec "${args[@]:$idx}"
Loading
Loading