diff --git a/MANIFEST.in b/MANIFEST.in index 4d7b6d3..c2e230d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include setup.py +include build_graphblas_cffi.py include README.md include LICENSE include suitesparse_graphblas/*.pxd diff --git a/build_graphblas_cffi.py b/build_graphblas_cffi.py new file mode 100644 index 0000000..0de462b --- /dev/null +++ b/build_graphblas_cffi.py @@ -0,0 +1,70 @@ +import os +import sys +from pathlib import Path + +from cffi import FFI +from setuptools import Extension + +is_win = sys.platform.startswith("win") +ss_g = Path(__file__).parent / "suitesparse_graphblas" + +ffibuilder = FFI() + +include_dirs = [os.path.join(sys.prefix, "include")] +library_dirs = [os.path.join(sys.prefix, "lib")] +if is_win: + include_dirs.append(os.path.join(sys.prefix, "Library", "include")) + library_dirs.append(os.path.join(sys.prefix, "Library", "lib")) + +ffibuilder.set_source( + "suitesparse_graphblas._graphblas", + (ss_g / "source.c").read_text(), + libraries=["graphblas"], + include_dirs=include_dirs, + library_dirs=library_dirs, +) + +ffibuilder.cdef((ss_g / "suitesparse_graphblas.h").read_text()) + + +def get_extension(apply_msvc_patch: bool = None, extra_compile_args=()): + """ + Get a setuptools.Extension version of this CFFI builder. + + In other words, enables `setup(ext_modules=[get_extension()])` + instead of `setup(cffi_modules=["build_graphblas_cffi.py:ffibuilder"])`. + + The main reason for this is to allow a patch for complex values when compiling on MSVC. + MSVC famously lacks support for standard C complex types like `double _Complex` and + `float _Complex`. Instead, MSVC has its own `_Dcomplex` and `_Fcomplex` types. + Cffi's machinery cannot be made to work with these types, so we instead + emit the regular standard C code and patch it manually. + + :param apply_msvc_patch: whether to apply the MSVC patch. + If None then auto-detect based on platform. + :param extra_compile_args: forwarded to Extension constructor. + """ + code_path = ss_g / "_graphblas.c" + ffibuilder.emit_c_code(str(code_path)) + + if apply_msvc_patch is None: + apply_msvc_patch = is_win + + if apply_msvc_patch: + msvc_code = code_path.read_text() + msvc_code = msvc_code.replace("float _Complex", "_Fcomplex") + msvc_code = msvc_code.replace("double _Complex", "_Dcomplex") + code_path.write_text(msvc_code) + + return Extension( + "suitesparse_graphblas._graphblas", + [os.path.join("suitesparse_graphblas", "_graphblas.c")], + libraries=["graphblas"], + include_dirs=include_dirs, + library_dirs=library_dirs, + extra_compile_args=extra_compile_args, + ) + + +if __name__ == "__main__": + ffibuilder.compile(verbose=True) diff --git a/setup.py b/setup.py index 2312fc3..53d4d58 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,10 @@ import numpy as np from setuptools import Extension, setup +# Add current directory to the Python path because it's not present when running `pip install .` +sys.path.append(os.path.dirname(__file__)) +import build_graphblas_cffi # noqa: E402 # isort:skip + try: from Cython.Build import cythonize from Cython.Compiler.Options import get_directive_defaults @@ -15,6 +19,13 @@ define_macros = [("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")] +# /d2FH4- flag needed only for early Python 3.8 builds on Windows. +# See https://cibuildwheel.readthedocs.io/en/stable/faq/ +# (Search for flag on page. Full link is long and causes the linter to fail the tests.) +# +# The /std:c11 flag is because the MSVC default is C89. +extra_compile_args = ["/d2FH4-", "/std:c11"] if sys.platform == "win32" else [] + if use_cython: suffix = ".pyx" directive_defaults = get_directive_defaults() @@ -40,13 +51,15 @@ [name], include_dirs=include_dirs, define_macros=define_macros, + extra_compile_args=extra_compile_args, ) for name in glob(f"suitesparse_graphblas/**/*{suffix}", recursive=True) ] if use_cython: ext_modules = cythonize(ext_modules, include_path=include_dirs) +ext_modules.append(build_graphblas_cffi.get_extension(extra_compile_args=extra_compile_args)) + setup( ext_modules=ext_modules, - cffi_modules=["suitesparse_graphblas/build.py:ffibuilder"], ) diff --git a/suitesparse_graphblas/build.py b/suitesparse_graphblas/build.py deleted file mode 100644 index 86a9ead..0000000 --- a/suitesparse_graphblas/build.py +++ /dev/null @@ -1,41 +0,0 @@ -import os -import platform -import sys - -from cffi import FFI - -is_win = sys.platform.startswith("win") -is_arm64 = platform.machine() == "arm64" -is_ppc64le = platform.machine() == "ppc64le" # Use same header as arm64, which *may* work -thisdir = os.path.dirname(__file__) - -ffibuilder = FFI() - -with open(os.path.join(thisdir, "source.c")) as f: - source = f.read() - -include_dirs = [os.path.join(sys.prefix, "include")] -library_dirs = [os.path.join(sys.prefix, "lib")] -if is_win: - include_dirs.append(os.path.join(sys.prefix, "Library", "include")) - library_dirs.append(os.path.join(sys.prefix, "Library", "lib")) - -ffibuilder.set_source( - "suitesparse_graphblas._graphblas", - source, - libraries=["graphblas"], - include_dirs=include_dirs, - library_dirs=library_dirs, -) - -header = "suitesparse_graphblas.h" -if is_win: - header = "suitesparse_graphblas_no_complex.h" -if is_arm64 or is_ppc64le: - header = "suitesparse_graphblas_arm64.h" -gb_cdef = open(os.path.join(thisdir, header)) - -ffibuilder.cdef(gb_cdef.read()) - -if __name__ == "__main__": - ffibuilder.compile(verbose=True)