diff --git a/ci/install_libspatialindex.bash b/ci/install_libspatialindex.bash index 0174f33e..4b38503e 100755 --- a/ci/install_libspatialindex.bash +++ b/ci/install_libspatialindex.bash @@ -7,7 +7,7 @@ SHA256=63a03bfb26aa65cf0159f925f6c3491b6ef79bc0e3db5a631d96772d6541187e # where to copy resulting files # this has to be run before `cd`-ing anywhere -gentarget() { +libtarget() { OURPWD=$PWD cd "$(dirname "$0")" mkdir -p ../rtree/lib @@ -17,6 +17,16 @@ gentarget() { echo $arr } +headertarget() { + OURPWD=$PWD + cd "$(dirname "$0")" + mkdir -p ../rtree/include + cd ../rtree/include + arr=$(pwd) + cd "$OURPWD" + echo $arr +} + scriptloc() { OURPWD=$PWD cd "$(dirname "$0")" @@ -26,7 +36,8 @@ scriptloc() { } # note that we're doing this convoluted thing to get # an absolute path so mac doesn't yell at us -TARGET=`gentarget` +LIBTARGET=`libtarget` +HEADERTARGET=`headertarget` SL=`scriptloc` rm $VERSION.zip || true @@ -60,10 +71,13 @@ if [ "$(uname)" == "Darwin" ]; then # change the rpath in the dylib to point to the same directory install_name_tool -change @rpath/libspatialindex.6.dylib @loader_path/libspatialindex.dylib bin/libspatialindex_c.dylib # copy the dylib files to the target director - cp bin/libspatialindex.dylib $TARGET - cp bin/libspatialindex_c.dylib $TARGET + cp bin/libspatialindex.dylib $LIBTARGET + cp bin/libspatialindex_c.dylib $LIBTARGET + cp -r ../include/* $HEADERTARGET else - cp -d bin/* $TARGET + cp -L bin/* $LIBTARGET + cp -r ../include/* $HEADERTARGET fi -ls $TARGET +ls $LIBTARGET +ls -R $HEADERTARGET diff --git a/ci/install_libspatialindex.bat b/ci/install_libspatialindex.bat index c08d3677..a2ce9a95 100755 --- a/ci/install_libspatialindex.bat +++ b/ci/install_libspatialindex.bat @@ -21,7 +21,9 @@ ninja mkdir %~dp0\..\rtree\lib copy bin\*.dll %~dp0\..\rtree\lib +xcopy /S ..\include\* %~dp0\..\rtree\include\ rmdir /Q /S bin dir %~dp0\..\rtree\ dir %~dp0\..\rtree\lib +dir %~dp0\..\rtree\include diff --git a/pyproject.toml b/pyproject.toml index aac4283a..84873643 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,12 +39,13 @@ Repository = "https://github.com/Toblerity/rtree" [tool.setuptools] packages = ["rtree"] zip-safe = false +include-package-data = false [tool.setuptools.dynamic] version = {attr = "rtree.__version__"} [tool.setuptools.package-data] -rtree = ["lib", "py.typed"] +rtree = ["py.typed"] [tool.black] target-version = ["py38", "py39", "py310", "py311", "py312"] diff --git a/rtree/finder.py b/rtree/finder.py index 10899bca..e03d2127 100644 --- a/rtree/finder.py +++ b/rtree/finder.py @@ -7,22 +7,22 @@ from __future__ import annotations import ctypes +import importlib.metadata import os import platform import sys from ctypes.util import find_library +from pathlib import Path -# the current working directory of this file -_cwd = os.path.abspath(os.path.expanduser(os.path.dirname(__file__))) +_cwd = Path(__file__).parent +_sys_prefix = Path(sys.prefix) # generate a bunch of candidate locations where the # libspatialindex shared library *might* be hanging out -_candidates = [ - os.environ.get("SPATIALINDEX_C_LIBRARY", None), - os.path.join(_cwd, "lib"), - _cwd, - "", -] +_candidates = [] +if "SPATIALINDEX_C_LIBRARY" in os.environ: + _candidates.append(Path(os.environ["SPATIALINDEX_C_LIBRARY"])) +_candidates += [_cwd / "lib", _cwd, Path("")] def load() -> ctypes.CDLL: @@ -39,29 +39,26 @@ def load() -> ctypes.CDLL: lib_name = f"spatialindex_c-{arch}.dll" # add search paths for conda installs - if ( - os.path.exists(os.path.join(sys.prefix, "conda-meta")) - or "conda" in sys.version - ): - _candidates.append(os.path.join(sys.prefix, "Library", "bin")) + if (_sys_prefix / "conda-meta").exists() or "conda" in sys.version: + _candidates.append(_sys_prefix / "Library" / "bin") # get the current PATH oldenv = os.environ.get("PATH", "").strip().rstrip(";") # run through our list of candidate locations for path in _candidates: - if not path or not os.path.exists(path): + if not path.exists(): continue # temporarily add the path to the PATH environment variable # so Windows can find additional DLL dependencies. - os.environ["PATH"] = ";".join([path, oldenv]) + os.environ["PATH"] = ";".join([str(path), oldenv]) try: - rt = ctypes.cdll.LoadLibrary(os.path.join(path, lib_name)) + rt = ctypes.cdll.LoadLibrary(str(path / lib_name)) if rt is not None: return rt except OSError: pass - except BaseException as E: - print(f"rtree.finder unexpected error: {E!s}") + except BaseException as err: + print(f"rtree.finder unexpected error: {err!s}", file=sys.stderr) finally: os.environ["PATH"] = oldenv raise OSError(f"could not find or load {lib_name}") @@ -73,8 +70,6 @@ def load() -> ctypes.CDLL: # macos shared libraries are `.dylib` lib_name = "libspatialindex_c.dylib" else: - import importlib.metadata - # linux shared libraries are `.so` lib_name = "libspatialindex_c.so" @@ -88,7 +83,7 @@ def load() -> ctypes.CDLL: and file.stem.startswith("libspatialindex") and ".so" in file.suffixes ): - _candidates.insert(1, os.path.join(str(file.locate()))) + _candidates.insert(1, Path(file.locate())) break except importlib.metadata.PackageNotFoundError: pass @@ -96,41 +91,83 @@ def load() -> ctypes.CDLL: # get the starting working directory cwd = os.getcwd() for cand in _candidates: - if cand is None: - continue - elif os.path.isdir(cand): + if cand.is_dir(): # if our candidate is a directory use best guess path = cand - target = os.path.join(cand, lib_name) - elif os.path.isfile(cand): + target = cand / lib_name + elif cand.is_file(): # if candidate is just a file use that - path = os.path.split(cand)[0] + path = cand.parent target = cand else: continue - if not os.path.exists(target): + if not target.exists(): continue try: # move to the location we're checking os.chdir(path) # try loading the target file candidate - rt = ctypes.cdll.LoadLibrary(target) + rt = ctypes.cdll.LoadLibrary(str(target)) if rt is not None: return rt - except BaseException as E: - print(f"rtree.finder ({target}) unexpected error: {E!s}") + except BaseException as err: + print( + f"rtree.finder ({target}) unexpected error: {err!s}", + file=sys.stderr, + ) finally: os.chdir(cwd) try: # try loading library using LD path search - path = find_library("spatialindex_c") - if path is not None: - return ctypes.cdll.LoadLibrary(path) + pth = find_library("spatialindex_c") + if pth is not None: + return ctypes.cdll.LoadLibrary(pth) except BaseException: pass raise OSError("Could not load libspatialindex_c library") + + +def get_include() -> str: + """Return the directory that contains the spatialindex \\*.h files. + + :returns: Path to include directory or "" if not found. + """ + # check if was bundled with a binary wheel + try: + pkg_files = importlib.metadata.files("rtree") + if pkg_files is not None: + for path in pkg_files: # type: ignore + if path.name == "SpatialIndex.h": + return str(Path(path.locate()).parent.parent) + except importlib.metadata.PackageNotFoundError: + pass + + # look for this header file in a few directories + path_to_spatialindex_h = Path("include/spatialindex/SpatialIndex.h") + + # check sys.prefix, e.g. conda's libspatialindex package + if os.name == "nt": + file = _sys_prefix / "Library" / path_to_spatialindex_h + else: + file = _sys_prefix / path_to_spatialindex_h + if file.is_file(): + return str(file.parent.parent) + + # check if relative to lib + libdir = Path(load()._name).parent + file = libdir.parent / path_to_spatialindex_h + if file.is_file(): + return str(file.parent.parent) + + # check system install + file = Path("/usr") / path_to_spatialindex_h + if file.is_file(): + return str(file.parent.parent) + + # not found + return "" diff --git a/setup.py b/setup.py index 98887ddc..a1fc40d9 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -import os +import sys +from pathlib import Path from setuptools import setup from setuptools.command.install import install @@ -7,7 +8,7 @@ from wheel.bdist_wheel import bdist_wheel as _bdist_wheel # current working directory of this setup.py file -_cwd = os.path.abspath(os.path.split(__file__)[0]) +_cwd = Path(__file__).resolve().parent class bdist_wheel(_bdist_wheel): # type: ignore[misc] @@ -26,54 +27,54 @@ def has_ext_modules(foo) -> bool: class InstallPlatlib(install): # type: ignore[misc] def finalize_options(self) -> None: """ - Copy the shared libraries into the wheel. Note that this - will *only* check in `rtree/lib` rather than anywhere on - the system so if you are building a wheel you *must* copy or - symlink the `.so`/`.dll`/`.dylib` files into `rtree/lib`. + Copy the shared libraries and header files into the wheel. Note that + this will *only* check in `rtree/lib` and `include` rather than + anywhere on the system so if you are building a wheel you *must* copy + or symlink the `.so`/`.dll`/`.dylib` files into `rtree/lib` and + `.h` into `rtree/include`. """ - # use for checking extension types - from fnmatch import fnmatch - install.finalize_options(self) if self.distribution.has_ext_modules(): self.install_lib = self.install_platlib - # now copy over libspatialindex - # get the location of the shared library on the filesystem - - # where we're putting the shared library in the build directory - target_dir = os.path.join(self.build_lib, "rtree", "lib") - # where are we checking for shared libraries - source_dir = os.path.join(_cwd, "rtree", "lib") - - # what patterns represent shared libraries - patterns = {"*.so", "libspatialindex*dylib", "*.dll"} - - if not os.path.isdir(source_dir): - # no copying of binary parts to library - # this is so `pip install .` works even - # if `rtree/lib` isn't populated - return - - for file_name in os.listdir(source_dir): - # make sure file name is lower case - check = file_name.lower() - # use filename pattern matching to see if it is - # a shared library format file - if not any(fnmatch(check, p) for p in patterns): - continue - - # if the source isn't a file skip it - if not os.path.isfile(os.path.join(source_dir, file_name)): - continue - - # make build directory if it doesn't exist yet - if not os.path.isdir(target_dir): - os.makedirs(target_dir) - - # copy the source file to the target directory - self.copy_file( - os.path.join(source_dir, file_name), os.path.join(target_dir, file_name) - ) + + # source files to copy + source_dir = _cwd / "rtree" + + # destination for the files in the build directory + target_dir = Path(self.build_lib) / "rtree" + + source_lib = source_dir / "lib" + target_lib = target_dir / "lib" + if source_lib.is_dir(): + # what patterns represent shared libraries for supported platforms + if sys.platform.startswith("win"): + lib_pattern = "*.dll" + elif sys.platform.startswith("linux"): + lib_pattern = "*.so*" + elif sys.platform == "darwin": + lib_pattern = "libspatialindex*dylib" + else: + raise ValueError(f"unhandled platform {sys.platform!r}") + + target_lib.mkdir(parents=True, exist_ok=True) + for pth in source_lib.glob(lib_pattern): + # if the source isn't a file skip it + if not pth.is_file(): + continue + + # copy the source file to the target directory + self.copy_file(str(pth), str(target_lib / pth.name)) + + source_include = source_dir / "include" + target_include = target_dir / "include" + if source_include.is_dir(): + for pth in source_include.rglob("*.h"): + rpth = pth.relative_to(source_include) + + # copy the source file to the target directory + target_subdir = target_include / rpth.parent + target_subdir.mkdir(parents=True, exist_ok=True) + self.copy_file(str(pth), str(target_subdir)) # See pyproject.toml for other project metadata diff --git a/tests/test_finder.py b/tests/test_finder.py new file mode 100644 index 00000000..4a05ad2f --- /dev/null +++ b/tests/test_finder.py @@ -0,0 +1,19 @@ +from ctypes import CDLL +from pathlib import Path + +from rtree import finder + + +def test_load(): + lib = finder.load() + assert isinstance(lib, CDLL) + + +def test_get_include(): + incl = finder.get_include() + assert isinstance(incl, str) + if incl: + path = Path(incl) + assert path.is_dir() + assert (path / "spatialindex").is_dir() + assert (path / "spatialindex" / "SpatialIndex.h").is_file()