This is a quick note on how to get RTree (0.9.7) and PyInstaller (3.4) to play nicely together, so that if you've got a script using RTree (e.g. for example, a script that calls trimesh.proximity.thickness()) that you need to compile into a .exe so that non-technical users can run it as a standalone binary without setting up a Python dev environment first.
These instructions are based on what worked on the project I'm working on, and are only tested with the versions listed on Windows 10. I haven't actually tested this on Linux, since most of the Linux users of this program are already doing a lot more coding work and will have dev environments set up anyway, that it doesn't matter for them.
Hopefully this helps someone else someday.
From what I've found, there are 2 parts to getting them to work:
1) Tell PyInstaller to grab the spatialindex_c-{arch}.dll files, and include them as part of the build
rtree relies on the pair of .dll / .so / .dynlib files in site-packages\rtree\lib to provide much of its functionality (or seems to expect that they exist, or else it will bail). PyInstaller doesn't know about these, or cannot detect these references, hence why we must manually / explicitly tell it about them.
Disclaimer: Normally, most docs recommend using the .spec files. In my project, I've been "lazy" - actually, mostly just wary about hand coding one and then having problems down the track when introducing new deps that require re-running the automated detection stuff, which then wipes out most of my customisations. Hence, I actually have a Python script that assembles a list of string args to pass to the Python-API call that backs the "pyinstaller" command-line program's handling of args.
Hence, I have the following code:
```
# Get list of rtree .dll's (or .so's)
# See https://github.com/Toblerity/rtree/issues/88#issuecomment-828529059
def find_rtree_dlls():
if sys.platform == 'win32':
DLL_EXTENSION = '*.dll'
else:
# XXX: Does mac use this too?
DLL_EXTENSION = '*.so'
libs = []
# Check the various package install locations...
for root_path in site.getsitepackages():
# The scipy package is installed here?
if os.path.exists(os.path.join(root_path, "rtree")):
# Find and copy all the .dll's
rtree_path = Path(os.path.join(root_path, "rtree"))
libs += list(rtree_path.rglob(DLL_EXTENSION))
return libs
```
This gets called as follows:
```
# Build Executable(s) using PyInstaller
def build_exe():
## Construct set of args for PyInstaller
# TODO: Move all this to a .spec file, and use that instead
args = []
# Get rid of prompts about old/existing files
args += ['--noconfirm']
# Helper function for adding hidden-import args
def add_hidden_import(args_list, hidden_path):
args_list += ['--hidden-import=%s' % hidden_path]
# Helper function for adding binary (e.g. *.dll) files
def add_binary(args_list, binary_path):
args_list += ['--add-binary=%s;.' % binary_path]
# ... list of hidden imports / binary imports for other problem modules
# Add rtree binaries / dlls
for rtree_dll in find_rtree_dlls():
add_binary(args, rtree_dll)
# ... more imports / add_binary calls
# Application Icon (for the .exe)
# Note: Can regenerate using the "utils/svg_to_ico.py" script
args += ['--icon=icons/my_app_icon.ico']
# Add arg for the main executable that we're trying to build
args += ['my_main_app.py']
## Run PyInstaller for Main GUI App
PyInstaller.__main__.run(args)
```
2) Modify your code to do: `os.environ["SPATIALINDEX_C_LIBRARY"] = "."` on startup
Unfortunately, the "smart" code in site-packages\rtree\finder.py breaks when PyInstaller bundles the code, as it likely does some fancy stuff to remap all paths that the os module spits out (i.e. particularly the "current working directory" detection attempts.
The one "last resort" / "saving grace" it offers is that it will also search the SPATIALINDEX_C_LIBRARY environment variable to try to resolve the path. Thus, to take advantage of that, we
No comments:
Post a Comment