Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include spatialindex headers #292

Merged
merged 10 commits into from
Jan 18, 2024
26 changes: 20 additions & 6 deletions ci/install_libspatialindex.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")"
Expand All @@ -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
Expand Down Expand Up @@ -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
2 changes: 2 additions & 0 deletions ci/install_libspatialindex.bat
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
105 changes: 71 additions & 34 deletions rtree/finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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}")
Expand All @@ -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"

Expand All @@ -88,49 +83,91 @@ 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

# 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 ""
93 changes: 47 additions & 46 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
#!/usr/bin/env python3
import os
import sys
from pathlib import Path

from setuptools import setup
from setuptools.command.install import install
from setuptools.dist import Distribution
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]
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions tests/test_finder.py
Original file line number Diff line number Diff line change
@@ -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()
Loading