I'm working on an installable ObsPy-based application using PyInstaller, and have run into a number of issues. Primarily these are due to places where code/data are pulled from the source code at runtime.
1. Module data
For example, the data under obspy.imaging.data is loaded at runtime. I was able to fix these by adding a bunch of datas entries. One particularly thorny spot is obspy.core.util.libnames._load_cdll(), which looks up its own place in the filesystem then navigates across directories to find a library.
2. Plugins
Plugins are discovered by looking at pkg_resources.iter_entry_points(), which gets the entry points from the .egg-info file. This needs to be added as metadata, but there is a catch -- loading all of the metadata causes other errors, so I extracted just the entry_points.txt file.
3. os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
This is used in a number of places to establish the path to a given module. This doesn't work in PyInstaller, but PyInstaller does support using __file__. (I see both approaches used in various places in ObsPy, it's not clear to me whether there's a meaningful difference here.) Fixing this required a (really ugly) run-time patch that overrides inspect.getfile() and tries to extract __file__ from the given execution frame.
I'm including the hooks I wrote to get this working. I'm happy to put these into a pull request, but they are not very thoroughly tested and especially in the case of #3 the it seems much cleaner to change the ObsPy code to use __file__ rather than monkey-patch the inspect library. I guess I'm mainly curious whether there's any interest in trying to bring PyInstaller support into the ObsPy codebase.
Cheers,
Adam
hook-obspy.py -- this is the build-time hook:
from PyInstaller.utils.hooks import collect_dynamic_libs, collect_data_files, exec_statement, copy_metadata,\
collect_submodules, get_package_paths
import os.path
(_, obspy_root) = get_package_paths('obspy')
binaries = collect_dynamic_libs('obspy')
datas = [
# Dummy path, this needs to exist for obspy.core.util.libnames._load_cdll
(os.path.join(obspy_root, "*.txt"), os.path.join('obspy', 'core', 'util')),
# Data
(os.path.join(obspy_root, "imaging", "data"), os.path.join('obspy', 'imaging', 'data')),
(os.path.join(obspy_root, "taup", "data"), os.path.join('obspy', 'taup', 'data')),
(os.path.join(obspy_root, "geodetics", "data"), os.path.join('obspy', 'geodetics', 'data')),
]
# Plugins are defined in the metadata (.egg-info) directory, but if we grab the whole thing it causes
# other errors, so include only entry_points.txt
metadata = copy_metadata('obspy')
egg = metadata[0]
if '.egg' not in egg[0]:
raise Exception("Unexpected metadata: %s" % (metadata,))
# Specify the source as just the entry points file
metadata = [(os.path.join(egg[0], 'entry_points.txt'), egg[1])]
datas += metadata
# Thse are the actual plugin packages
hiddenimports = collect_submodules('obspy.io')
rthook-obspy.py -- this is the run-time hook to monkey-patch inspect.getfile()
import inspect
_old_getfile = inspect.getfile
def _getfile(object):
"""
Override inspect.getfile to try to return __file__ from the given frame
"""
if inspect.isframe(object):
try:
file = object.f_globals['__file__']
# print("inspect.getfile returning %s" % file)
return file
except:
pass
return _old_getfile(object)
inspect.getfile = _getfile