From fd023e9f5eb091d8926e836dda21a0919c03f2e0 Mon Sep 17 00:00:00 2001 From: Alexander Pletzer Date: Tue, 14 Jul 2026 10:45:56 +1200 Subject: [PATCH] fixed the build logic so one no longer needs to have VTK installed with conda --- setup.py | 249 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 188 insertions(+), 61 deletions(-) diff --git a/setup.py b/setup.py index e09e0730..d5f72066 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,7 @@ import os from pathlib import Path import re +import sysconfig from setuptools import setup, Extension import sys @@ -35,69 +36,195 @@ def get_ext_filename(self, ext_name): PACKAGE = "mint" -def getCondaVTK(): - """get the VTK installed by conda""" - - vtk_libs = [ - "vtkCommonComputationalGeometry", - "vtkIOCore", - "vtkIOLegacy", - "vtkCommonExecutionModel", - "vtkCommonDataModel", - "vtkCommonTransforms", - "vtkCommonMisc", - "vtkCommonMath", - "vtkCommonSystem", - "vtkCommonCore", - "vtksys", - ] - - include_dir = Path(sys.exec_prefix) / Path("include") - if os.name == 'nt': - # On Windows the include files are under Library - include_dir = Path(sys.exec_prefix) / Path("Library") / Path("include") +VTK_LIB_NAMES = [ + "vtkCommonComputationalGeometry", + "vtkIOCore", + "vtkIOLegacy", + "vtkCommonExecutionModel", + "vtkCommonDataModel", + "vtkCommonTransforms", + "vtkCommonMisc", + "vtkCommonMath", + "vtkCommonSystem", + "vtkCommonCore", + "vtksys", +] - try: - version = list(include_dir.glob("vtk-*"))[-1].name - except IndexError: - raise RuntimeError('ERROR: you need to "conda install -c conda-forge vtk"') - version = re.sub(r"vtk-", "", version) - include_dir = str(include_dir / Path(f"vtk-{version}")) - libraries_dir = str(Path(sys.exec_prefix) / Path("lib")) - if os.name == 'nt': - # Windows - libraries_dir = str(Path(sys.exec_prefix) / Path("Library") / Path("lib")) - libraries = [f"{lib}-{version}" for lib in vtk_libs] - result = { - "VTK_VERSION": version, - "VTK_INCLUDE_DIR": include_dir, - "VTK_LIBRARIES_DIR": libraries_dir, - "VTK_LIBRARIES": libraries, - } - return result - - -def getCondaNetCDF(): - """Get the NetCDF installed by conda.""" - - include_dir = str(Path(sys.exec_prefix) / Path("include")) - libraries_dir = str(Path(sys.exec_prefix) / Path("lib")) +def _multiarch_lib_dirs(): + """Extra library search directories used by Debian/Ubuntu-style + multiarch system installs (e.g. /usr/lib/x86_64-linux-gnu).""" + dirs = [] + multiarch = sysconfig.get_config_var("MULTIARCH") + if multiarch: + dirs.append(Path("/usr/lib") / multiarch) + dirs += [Path("/usr/lib64"), Path("/usr/lib"), Path("/usr/local/lib"), Path("/usr/local/lib64")] + return [d for d in dirs if d.is_dir()] + + +def _lib_dir_has_vtk(lib_dir, version): + if lib_dir is None or not Path(lib_dir).is_dir(): + return False + return len(list(Path(lib_dir).glob(f"*vtkIOCore-{version}.*"))) > 0 + + +def getVTK(): + """Locate the VTK headers and libraries needed to build the mint + extension. + + Resolution order: + 1. Explicit override via the VTK_INCLUDE_DIR / VTK_LIBRARIES_DIR / + (optionally) VTK_VERSION environment variables -- use this if VTK + was built from source or installed somewhere non-standard. + 2. A conda/virtualenv environment (sys.exec_prefix/include/vtk-*). + 3. A system package install, e.g. Debian/Ubuntu's "libvtk*-dev", + Homebrew, or a "make install" under /usr/local. + """ + + env_include = os.getenv("VTK_INCLUDE_DIR") + env_libdir = os.getenv("VTK_LIBRARIES_DIR") + if env_include and env_libdir: + version = os.getenv("VTK_VERSION") + if not version: + m = re.search(r"vtk-([0-9]+\.[0-9]+)", env_include) + if not m: + raise RuntimeError( + "Could not infer VTK_VERSION from VTK_INCLUDE_DIR " + f'("{env_include}"); set the VTK_VERSION environment ' + "variable explicitly (e.g. VTK_VERSION=9.3)." + ) + version = m.group(1) + return { + "VTK_VERSION": version, + "VTK_INCLUDE_DIR": env_include, + "VTK_LIBRARIES_DIR": env_libdir, + "VTK_LIBRARIES": [f"{lib}-{version}" for lib in VTK_LIB_NAMES], + } + + # candidate (include_root, preferred_lib_dir) pairs to probe, in order if os.name == 'nt': - # Windows - include_dir = str(Path(sys.exec_prefix) / Path("Library") / Path("include")) - libraries_dir = str(Path(sys.exec_prefix) / Path("Library") / Path("lib")) - libraries = ["netcdf", "hdf5"] + candidates = [ + (Path(sys.exec_prefix) / "Library" / "include", Path(sys.exec_prefix) / "Library" / "lib"), + ] + else: + candidates = [ + (Path(sys.exec_prefix) / "include", Path(sys.exec_prefix) / "lib"), + (Path("/usr/local/include"), Path("/usr/local/lib")), + (Path("/usr/include"), Path("/usr/lib")), + (Path("/opt/homebrew/include"), Path("/opt/homebrew/lib")), + ] + + for include_root, preferred_lib_dir in candidates: + if not include_root.is_dir(): + continue + matches = sorted(include_root.glob("vtk-*")) + if not matches: + continue + include_dir = matches[-1] + version = re.sub(r"vtk-", "", include_dir.name) - if not (include_dir / Path("netcdf.h")).exists(): - raise RuntimeError('ERROR: you need to "conda install libnetcdf"') + lib_dir_candidates = [preferred_lib_dir] + _multiarch_lib_dirs() + libraries_dir = next( + (d for d in lib_dir_candidates if _lib_dir_has_vtk(d, version)), + None, + ) + if libraries_dir is None: + # headers found but no matching libraries in any known location; + # keep looking in case another candidate root matches fully + continue - result = { - "NETCDF_INCLUDE_DIR": include_dir, - "NETCDF_LIBRARIES_DIR": libraries_dir, - "NETCDF_LIBRARIES": libraries, - } - return result + return { + "VTK_VERSION": version, + "VTK_INCLUDE_DIR": str(include_dir), + "VTK_LIBRARIES_DIR": str(libraries_dir), + "VTK_LIBRARIES": [f"{lib}-{version}" for lib in VTK_LIB_NAMES], + } + + raise RuntimeError( + "ERROR: could not locate a VTK development install.\n" + 'Either "conda install -c conda-forge vtk", install your system\'s ' + 'VTK development package (e.g. "apt install libvtk9-dev" on ' + "Debian/Ubuntu, \"brew install vtk\" on macOS), build VTK from " + "source, or point at an existing install by setting the " + "VTK_INCLUDE_DIR and VTK_LIBRARIES_DIR environment variables " + "(and VTK_VERSION if it can't be inferred from the include " + "directory name)." + ) + + +def getNetCDF(): + """Locate the NetCDF headers and libraries. + + Resolution order: + 1. Explicit override via the NETCDF_INCLUDE_DIR / NETCDF_LIBRARIES_DIR + environment variables. + 2. "nc-config", the standard way to discover a NetCDF-C install (used + by the plain CMake build too) -- this works for conda, system + packages (apt/dnf/brew), and installs built from source alike. + 3. A conda/virtualenv environment or common system prefixes, as a + last-ditch fallback if nc-config isn't on PATH. + """ + import shutil + import subprocess + + env_include = os.getenv("NETCDF_INCLUDE_DIR") + env_libdir = os.getenv("NETCDF_LIBRARIES_DIR") + if env_include and env_libdir: + return { + "NETCDF_INCLUDE_DIR": env_include, + "NETCDF_LIBRARIES_DIR": env_libdir, + "NETCDF_LIBRARIES": ["netcdf", "hdf5"], + } + + if shutil.which("nc-config"): + include_dir = subprocess.check_output( + ["nc-config", "--includedir"], text=True + ).strip() + libs_flags = subprocess.check_output( + ["nc-config", "--libs"], text=True + ).strip().split() + libraries_dir = None + libraries = [] + for flag in libs_flags: + if flag.startswith("-L"): + libraries_dir = flag[2:] + elif flag.startswith("-l"): + libraries.append(flag[2:]) + if include_dir and libraries_dir and libraries: + return { + "NETCDF_INCLUDE_DIR": include_dir, + "NETCDF_LIBRARIES_DIR": libraries_dir, + "NETCDF_LIBRARIES": libraries, + } + + # fall back to probing common prefixes directly + if os.name == 'nt': + candidates = [ + (Path(sys.exec_prefix) / "Library" / "include", Path(sys.exec_prefix) / "Library" / "lib"), + ] + else: + candidates = [ + (Path(sys.exec_prefix) / "include", Path(sys.exec_prefix) / "lib"), + (Path("/usr/local/include"), Path("/usr/local/lib")), + (Path("/usr/include"), Path("/usr/lib")), + (Path("/opt/homebrew/include"), Path("/opt/homebrew/lib")), + ] + for include_dir, libraries_dir in candidates: + if (include_dir / "netcdf.h").exists(): + return { + "NETCDF_INCLUDE_DIR": str(include_dir), + "NETCDF_LIBRARIES_DIR": str(libraries_dir), + "NETCDF_LIBRARIES": ["netcdf", "hdf5"], + } + + raise RuntimeError( + "ERROR: could not locate a NetCDF-C development install.\n" + 'Either "conda install -c conda-forge libnetcdf", install your ' + 'system\'s NetCDF development package (e.g. "apt install ' + 'libnetcdf-dev" on Debian/Ubuntu, "brew install netcdf" on macOS), ' + "make sure nc-config is on your PATH, or set the " + "NETCDF_INCLUDE_DIR and NETCDF_LIBRARIES_DIR environment variables." + ) # extract the MINT version from file version.txt @@ -111,8 +238,8 @@ def getCondaNetCDF(): with open(f"{PACKAGE}/__init__.py", "w") as fo: fo.write(init_file) -vtklib = getCondaVTK() -nclib = getCondaNetCDF() +vtklib = getVTK() +nclib = getNetCDF() extra_compile_args = [] cpp_flags = os.getenv("CPPFLAGS") @@ -141,7 +268,7 @@ def getCondaNetCDF(): define_macros=[], include_dirs=["src/", vtklib["VTK_INCLUDE_DIR"], nclib["NETCDF_INCLUDE_DIR"]], libraries=vtklib["VTK_LIBRARIES"] + nclib["NETCDF_LIBRARIES"], - library_dirs=[vtklib["VTK_INCLUDE_DIR"], nclib["NETCDF_LIBRARIES_DIR"]], + library_dirs=[vtklib["VTK_LIBRARIES_DIR"], nclib["NETCDF_LIBRARIES_DIR"]], extra_compile_args=extra_compile_args, language="c++", )