Skip to content

Commit d816ef2

Browse files
Include spatialindex headers (#292)
* 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]>
1 parent 63efe57 commit d816ef2

File tree

6 files changed

+161
-87
lines changed

6 files changed

+161
-87
lines changed

Diff for: ci/install_libspatialindex.bash

+20-6
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ SHA256=63a03bfb26aa65cf0159f925f6c3491b6ef79bc0e3db5a631d96772d6541187e
77

88
# where to copy resulting files
99
# this has to be run before `cd`-ing anywhere
10-
gentarget() {
10+
libtarget() {
1111
OURPWD=$PWD
1212
cd "$(dirname "$0")"
1313
mkdir -p ../rtree/lib
@@ -17,6 +17,16 @@ gentarget() {
1717
echo $arr
1818
}
1919

20+
headertarget() {
21+
OURPWD=$PWD
22+
cd "$(dirname "$0")"
23+
mkdir -p ../rtree/include
24+
cd ../rtree/include
25+
arr=$(pwd)
26+
cd "$OURPWD"
27+
echo $arr
28+
}
29+
2030
scriptloc() {
2131
OURPWD=$PWD
2232
cd "$(dirname "$0")"
@@ -26,7 +36,8 @@ scriptloc() {
2636
}
2737
# note that we're doing this convoluted thing to get
2838
# an absolute path so mac doesn't yell at us
29-
TARGET=`gentarget`
39+
LIBTARGET=`libtarget`
40+
HEADERTARGET=`headertarget`
3041
SL=`scriptloc`
3142

3243
rm $VERSION.zip || true
@@ -60,10 +71,13 @@ if [ "$(uname)" == "Darwin" ]; then
6071
# change the rpath in the dylib to point to the same directory
6172
install_name_tool -change @rpath/libspatialindex.6.dylib @loader_path/libspatialindex.dylib bin/libspatialindex_c.dylib
6273
# copy the dylib files to the target director
63-
cp bin/libspatialindex.dylib $TARGET
64-
cp bin/libspatialindex_c.dylib $TARGET
74+
cp bin/libspatialindex.dylib $LIBTARGET
75+
cp bin/libspatialindex_c.dylib $LIBTARGET
76+
cp -r ../include/* $HEADERTARGET
6577
else
66-
cp -d bin/* $TARGET
78+
cp -L bin/* $LIBTARGET
79+
cp -r ../include/* $HEADERTARGET
6780
fi
6881

69-
ls $TARGET
82+
ls $LIBTARGET
83+
ls -R $HEADERTARGET

Diff for: ci/install_libspatialindex.bat

+2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ ninja
2121

2222
mkdir %~dp0\..\rtree\lib
2323
copy bin\*.dll %~dp0\..\rtree\lib
24+
xcopy /S ..\include\* %~dp0\..\rtree\include\
2425
rmdir /Q /S bin
2526

2627
dir %~dp0\..\rtree\
2728
dir %~dp0\..\rtree\lib
29+
dir %~dp0\..\rtree\include

Diff for: pyproject.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ Repository = "https://github.com/Toblerity/rtree"
3939
[tool.setuptools]
4040
packages = ["rtree"]
4141
zip-safe = false
42+
include-package-data = false
4243

4344
[tool.setuptools.dynamic]
4445
version = {attr = "rtree.__version__"}
4546

4647
[tool.setuptools.package-data]
47-
rtree = ["lib", "py.typed"]
48+
rtree = ["py.typed"]
4849

4950
[tool.black]
5051
target-version = ["py38", "py39", "py310", "py311", "py312"]

Diff for: rtree/finder.py

+71-34
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,22 @@
77
from __future__ import annotations
88

99
import ctypes
10+
import importlib.metadata
1011
import os
1112
import platform
1213
import sys
1314
from ctypes.util import find_library
15+
from pathlib import Path
1416

15-
# the current working directory of this file
16-
_cwd = os.path.abspath(os.path.expanduser(os.path.dirname(__file__)))
17+
_cwd = Path(__file__).parent
18+
_sys_prefix = Path(sys.prefix)
1719

1820
# generate a bunch of candidate locations where the
1921
# libspatialindex shared library *might* be hanging out
20-
_candidates = [
21-
os.environ.get("SPATIALINDEX_C_LIBRARY", None),
22-
os.path.join(_cwd, "lib"),
23-
_cwd,
24-
"",
25-
]
22+
_candidates = []
23+
if "SPATIALINDEX_C_LIBRARY" in os.environ:
24+
_candidates.append(Path(os.environ["SPATIALINDEX_C_LIBRARY"]))
25+
_candidates += [_cwd / "lib", _cwd, Path("")]
2626

2727

2828
def load() -> ctypes.CDLL:
@@ -39,29 +39,26 @@ def load() -> ctypes.CDLL:
3939
lib_name = f"spatialindex_c-{arch}.dll"
4040

4141
# add search paths for conda installs
42-
if (
43-
os.path.exists(os.path.join(sys.prefix, "conda-meta"))
44-
or "conda" in sys.version
45-
):
46-
_candidates.append(os.path.join(sys.prefix, "Library", "bin"))
42+
if (_sys_prefix / "conda-meta").exists() or "conda" in sys.version:
43+
_candidates.append(_sys_prefix / "Library" / "bin")
4744

4845
# get the current PATH
4946
oldenv = os.environ.get("PATH", "").strip().rstrip(";")
5047
# run through our list of candidate locations
5148
for path in _candidates:
52-
if not path or not os.path.exists(path):
49+
if not path.exists():
5350
continue
5451
# temporarily add the path to the PATH environment variable
5552
# so Windows can find additional DLL dependencies.
56-
os.environ["PATH"] = ";".join([path, oldenv])
53+
os.environ["PATH"] = ";".join([str(path), oldenv])
5754
try:
58-
rt = ctypes.cdll.LoadLibrary(os.path.join(path, lib_name))
55+
rt = ctypes.cdll.LoadLibrary(str(path / lib_name))
5956
if rt is not None:
6057
return rt
6158
except OSError:
6259
pass
63-
except BaseException as E:
64-
print(f"rtree.finder unexpected error: {E!s}")
60+
except BaseException as err:
61+
print(f"rtree.finder unexpected error: {err!s}", file=sys.stderr)
6562
finally:
6663
os.environ["PATH"] = oldenv
6764
raise OSError(f"could not find or load {lib_name}")
@@ -73,8 +70,6 @@ def load() -> ctypes.CDLL:
7370
# macos shared libraries are `.dylib`
7471
lib_name = "libspatialindex_c.dylib"
7572
else:
76-
import importlib.metadata
77-
7873
# linux shared libraries are `.so`
7974
lib_name = "libspatialindex_c.so"
8075

@@ -88,49 +83,91 @@ def load() -> ctypes.CDLL:
8883
and file.stem.startswith("libspatialindex")
8984
and ".so" in file.suffixes
9085
):
91-
_candidates.insert(1, os.path.join(str(file.locate())))
86+
_candidates.insert(1, Path(file.locate()))
9287
break
9388
except importlib.metadata.PackageNotFoundError:
9489
pass
9590

9691
# get the starting working directory
9792
cwd = os.getcwd()
9893
for cand in _candidates:
99-
if cand is None:
100-
continue
101-
elif os.path.isdir(cand):
94+
if cand.is_dir():
10295
# if our candidate is a directory use best guess
10396
path = cand
104-
target = os.path.join(cand, lib_name)
105-
elif os.path.isfile(cand):
97+
target = cand / lib_name
98+
elif cand.is_file():
10699
# if candidate is just a file use that
107-
path = os.path.split(cand)[0]
100+
path = cand.parent
108101
target = cand
109102
else:
110103
continue
111104

112-
if not os.path.exists(target):
105+
if not target.exists():
113106
continue
114107

115108
try:
116109
# move to the location we're checking
117110
os.chdir(path)
118111
# try loading the target file candidate
119-
rt = ctypes.cdll.LoadLibrary(target)
112+
rt = ctypes.cdll.LoadLibrary(str(target))
120113
if rt is not None:
121114
return rt
122-
except BaseException as E:
123-
print(f"rtree.finder ({target}) unexpected error: {E!s}")
115+
except BaseException as err:
116+
print(
117+
f"rtree.finder ({target}) unexpected error: {err!s}",
118+
file=sys.stderr,
119+
)
124120
finally:
125121
os.chdir(cwd)
126122

127123
try:
128124
# try loading library using LD path search
129-
path = find_library("spatialindex_c")
130-
if path is not None:
131-
return ctypes.cdll.LoadLibrary(path)
125+
pth = find_library("spatialindex_c")
126+
if pth is not None:
127+
return ctypes.cdll.LoadLibrary(pth)
132128

133129
except BaseException:
134130
pass
135131

136132
raise OSError("Could not load libspatialindex_c library")
133+
134+
135+
def get_include() -> str:
136+
"""Return the directory that contains the spatialindex \\*.h files.
137+
138+
:returns: Path to include directory or "" if not found.
139+
"""
140+
# check if was bundled with a binary wheel
141+
try:
142+
pkg_files = importlib.metadata.files("rtree")
143+
if pkg_files is not None:
144+
for path in pkg_files: # type: ignore
145+
if path.name == "SpatialIndex.h":
146+
return str(Path(path.locate()).parent.parent)
147+
except importlib.metadata.PackageNotFoundError:
148+
pass
149+
150+
# look for this header file in a few directories
151+
path_to_spatialindex_h = Path("include/spatialindex/SpatialIndex.h")
152+
153+
# check sys.prefix, e.g. conda's libspatialindex package
154+
if os.name == "nt":
155+
file = _sys_prefix / "Library" / path_to_spatialindex_h
156+
else:
157+
file = _sys_prefix / path_to_spatialindex_h
158+
if file.is_file():
159+
return str(file.parent.parent)
160+
161+
# check if relative to lib
162+
libdir = Path(load()._name).parent
163+
file = libdir.parent / path_to_spatialindex_h
164+
if file.is_file():
165+
return str(file.parent.parent)
166+
167+
# check system install
168+
file = Path("/usr") / path_to_spatialindex_h
169+
if file.is_file():
170+
return str(file.parent.parent)
171+
172+
# not found
173+
return ""

Diff for: setup.py

+47-46
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
#!/usr/bin/env python3
2-
import os
2+
import sys
3+
from pathlib import Path
34

45
from setuptools import setup
56
from setuptools.command.install import install
67
from setuptools.dist import Distribution
78
from wheel.bdist_wheel import bdist_wheel as _bdist_wheel
89

910
# current working directory of this setup.py file
10-
_cwd = os.path.abspath(os.path.split(__file__)[0])
11+
_cwd = Path(__file__).resolve().parent
1112

1213

1314
class bdist_wheel(_bdist_wheel): # type: ignore[misc]
@@ -26,54 +27,54 @@ def has_ext_modules(foo) -> bool:
2627
class InstallPlatlib(install): # type: ignore[misc]
2728
def finalize_options(self) -> None:
2829
"""
29-
Copy the shared libraries into the wheel. Note that this
30-
will *only* check in `rtree/lib` rather than anywhere on
31-
the system so if you are building a wheel you *must* copy or
32-
symlink the `.so`/`.dll`/`.dylib` files into `rtree/lib`.
30+
Copy the shared libraries and header files into the wheel. Note that
31+
this will *only* check in `rtree/lib` and `include` rather than
32+
anywhere on the system so if you are building a wheel you *must* copy
33+
or symlink the `.so`/`.dll`/`.dylib` files into `rtree/lib` and
34+
`.h` into `rtree/include`.
3335
"""
34-
# use for checking extension types
35-
from fnmatch import fnmatch
36-
3736
install.finalize_options(self)
3837
if self.distribution.has_ext_modules():
3938
self.install_lib = self.install_platlib
40-
# now copy over libspatialindex
41-
# get the location of the shared library on the filesystem
42-
43-
# where we're putting the shared library in the build directory
44-
target_dir = os.path.join(self.build_lib, "rtree", "lib")
45-
# where are we checking for shared libraries
46-
source_dir = os.path.join(_cwd, "rtree", "lib")
47-
48-
# what patterns represent shared libraries
49-
patterns = {"*.so", "libspatialindex*dylib", "*.dll"}
50-
51-
if not os.path.isdir(source_dir):
52-
# no copying of binary parts to library
53-
# this is so `pip install .` works even
54-
# if `rtree/lib` isn't populated
55-
return
56-
57-
for file_name in os.listdir(source_dir):
58-
# make sure file name is lower case
59-
check = file_name.lower()
60-
# use filename pattern matching to see if it is
61-
# a shared library format file
62-
if not any(fnmatch(check, p) for p in patterns):
63-
continue
64-
65-
# if the source isn't a file skip it
66-
if not os.path.isfile(os.path.join(source_dir, file_name)):
67-
continue
68-
69-
# make build directory if it doesn't exist yet
70-
if not os.path.isdir(target_dir):
71-
os.makedirs(target_dir)
72-
73-
# copy the source file to the target directory
74-
self.copy_file(
75-
os.path.join(source_dir, file_name), os.path.join(target_dir, file_name)
76-
)
39+
40+
# source files to copy
41+
source_dir = _cwd / "rtree"
42+
43+
# destination for the files in the build directory
44+
target_dir = Path(self.build_lib) / "rtree"
45+
46+
source_lib = source_dir / "lib"
47+
target_lib = target_dir / "lib"
48+
if source_lib.is_dir():
49+
# what patterns represent shared libraries for supported platforms
50+
if sys.platform.startswith("win"):
51+
lib_pattern = "*.dll"
52+
elif sys.platform.startswith("linux"):
53+
lib_pattern = "*.so*"
54+
elif sys.platform == "darwin":
55+
lib_pattern = "libspatialindex*dylib"
56+
else:
57+
raise ValueError(f"unhandled platform {sys.platform!r}")
58+
59+
target_lib.mkdir(parents=True, exist_ok=True)
60+
for pth in source_lib.glob(lib_pattern):
61+
# if the source isn't a file skip it
62+
if not pth.is_file():
63+
continue
64+
65+
# copy the source file to the target directory
66+
self.copy_file(str(pth), str(target_lib / pth.name))
67+
68+
source_include = source_dir / "include"
69+
target_include = target_dir / "include"
70+
if source_include.is_dir():
71+
for pth in source_include.rglob("*.h"):
72+
rpth = pth.relative_to(source_include)
73+
74+
# copy the source file to the target directory
75+
target_subdir = target_include / rpth.parent
76+
target_subdir.mkdir(parents=True, exist_ok=True)
77+
self.copy_file(str(pth), str(target_subdir))
7778

7879

7980
# See pyproject.toml for other project metadata

Diff for: tests/test_finder.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from ctypes import CDLL
2+
from pathlib import Path
3+
4+
from rtree import finder
5+
6+
7+
def test_load():
8+
lib = finder.load()
9+
assert isinstance(lib, CDLL)
10+
11+
12+
def test_get_include():
13+
incl = finder.get_include()
14+
assert isinstance(incl, str)
15+
if incl:
16+
path = Path(incl)
17+
assert path.is_dir()
18+
assert (path / "spatialindex").is_dir()
19+
assert (path / "spatialindex" / "SpatialIndex.h").is_file()

0 commit comments

Comments
 (0)