diff --git a/suitesparse.sh b/suitesparse.sh index ad05bbf..aab7fd3 100755 --- a/suitesparse.sh +++ b/suitesparse.sh @@ -27,6 +27,20 @@ if [ -z "${NPROC}" ]; then fi cmake_params=() + +# Fail the build if OpenMP is missing instead of silently producing a serial +# library. GraphBLAS only warns and carries on, so a serial build stays +# invisible until someone measures it: conda-forge's graphblas 10.5.0 shipped +# that way on osx-arm64 while still depending on llvm-openmp. The matching +# runtime check on the built wheel is tests/test_package.py::test_openmp. +cmake_params+=(-DSUITESPARSE_USE_OPENMP=ON) +cmake_params+=(-DSUITESPARSE_USE_STRICT=ON) +# STRICT makes any requested-but-missing feature fatal, and SuiteSparsePolicy +# defaults both of these to ON, so they must be turned off explicitly or the +# configure step dies on "CUDA required for SuiteSparse but not found". +cmake_params+=(-DSUITESPARSE_USE_CUDA=OFF) +cmake_params+=(-DSUITESPARSE_USE_FORTRAN=OFF) + if [ -n "${BREW_LIBOMP}" ]; then # macOS OpenMP flags. # FindOpenMP doesn't find brew's libomp, so set the necessary configs manually. diff --git a/suitesparse_graphblas/__init__.py b/suitesparse_graphblas/__init__.py index 895a66f..7d08d23 100644 --- a/suitesparse_graphblas/__init__.py +++ b/suitesparse_graphblas/__init__.py @@ -24,6 +24,15 @@ ) from exc del importlib, platform +# The SuiteSparse:GraphBLAS C library this binding was built against, as +# (major, minor, sub). ``__version__`` is the version of *this package*, +# which should normally match. +libgraphblas_version = ( + lib.GxB_IMPLEMENTATION_MAJOR, + lib.GxB_IMPLEMENTATION_MINOR, + lib.GxB_IMPLEMENTATION_SUB, +) + # It is strongly recommended to use the non-variadic version of functions to be # compatible with the most number of architectures. For example, you should use # GxB_Matrix_Option_get_INT32 instead of GxB_Matrix_Option_get. diff --git a/suitesparse_graphblas/tests/test_package.py b/suitesparse_graphblas/tests/test_package.py index 77086a6..b0e2d32 100644 --- a/suitesparse_graphblas/tests/test_package.py +++ b/suitesparse_graphblas/tests/test_package.py @@ -1,5 +1,7 @@ +import pytest + import suitesparse_graphblas -from suitesparse_graphblas import ffi, lib # noqa: F401 +from suitesparse_graphblas import ffi, lib def test_matrix_existence(): @@ -12,3 +14,55 @@ def test_version(): version = suitesparse_graphblas.__version__ version = [int(x) for x in version.split("+")[0].split(".")] assert version > [9, 4, 4, 0] + + +def test_openmp(): + # GraphBLAS only warns at configure time when OpenMP is missing and still + # builds a working, but serial, library, so a serial build is invisible + # from the outside: conda-forge's graphblas 10.5.0 shipped that way on + # osx-arm64. Wheels build GraphBLAS from source in suitesparse.sh, where + # macOS has to point CMake at Homebrew's libomp by hand, so the same slip + # is possible here. + val_ptr = ffi.new("int32_t*") + info = lib.GrB_Global_get_INT32(lib.GrB_GLOBAL, val_ptr, lib.GxB_LIBRARY_OPENMP) + assert info == lib.GrB_SUCCESS + assert val_ptr[0], "libgraphblas was built without OpenMP" + + +def test_libgraphblas_version_matches_the_loaded_library(): + """The GraphBLAS this extension was compiled against must be the one it loads. + + ``libgraphblas_version`` reads cffi ``#define`` constants, which are + resolved when the C extension is compiled against a particular + GraphBLAS.h. The values below come from the shared library actually loaded + at runtime. When those disagree, every cffi call is reading a struct laid + out by a different build -- a mismatch that surfaces later as an + unexplained ``GrB_OUT_OF_MEMORY`` or a crash, never as an import error. + """ + val_ptr = ffi.new("int32_t*") + runtime = [] + for field in ( + lib.GrB_LIBRARY_VER_MAJOR, + lib.GrB_LIBRARY_VER_MINOR, + lib.GrB_LIBRARY_VER_PATCH, + ): + assert lib.GrB_Global_get_INT32(lib.GrB_GLOBAL, val_ptr, field) == lib.GrB_SUCCESS + runtime.append(val_ptr[0]) + assert tuple(runtime) == suitesparse_graphblas.libgraphblas_version + + +def test_version_tracks_libgraphblas(): + """A released version's first three parts are the SuiteSparse:GraphBLAS version. + + That convention is what lets a caller infer the library version from the + package version, and it only holds for a release: between releases the + version reports the previous tag, which is why ``libgraphblas_version`` + exists and why anything gating on a library feature should read that + instead. Skipped on a development build rather than asserted, since + disagreeing there is the expected state, not a defect. + """ + version = suitesparse_graphblas.__version__ + if "+" in version: + pytest.skip(f"development build between releases: {version}") + parts = tuple(int(x) for x in version.split(".")[:3]) + assert parts == suitesparse_graphblas.libgraphblas_version