Skip to content

Commit

Permalink
Include spatialindex headers (#292)
Browse files Browse the repository at this point in the history
* Add spatialindex headerfiles to package
* Avoid packaging header data for sdist
* Enable install.finalize_options to copy lib and include to wheel
* Rewrite parts of finder to use Pathlib; add get_include() and tests

---------

Co-authored-by: Mike Taves <[email protected]>
  • Loading branch information
JDBetteridge and mwtoews authored Jan 18, 2024
1 parent 63efe57 commit d816ef2
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 87 deletions.
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()

0 comments on commit d816ef2

Please sign in to comment.