374 lines
13 KiB
Python
374 lines
13 KiB
Python
'''
|
|
Pyinstaller hooks
|
|
=================
|
|
|
|
Module that exports pyinstaller related methods and parameters.
|
|
|
|
Hooks
|
|
-----
|
|
|
|
PyInstaller comes with a default hook for kivy that lists the indirectly
|
|
imported modules that pyinstaller would not find on its own using
|
|
:func:`get_deps_all`. :func:`hookspath` returns the path to an alternate kivy
|
|
hook, ``kivy/tools/packaging/pyinstaller_hooks/kivy-hook.py`` that does not
|
|
add these dependencies to its list of hidden imports and they have to be
|
|
explicitly included instead.
|
|
|
|
One can overwrite the default hook by providing on the command line the
|
|
``--additional-hooks-dir=HOOKSPATH`` option. Because although the default
|
|
hook will still run, the `important global variables
|
|
<https://pythonhosted.org/PyInstaller/#hook-global-variables>`_, e.g.
|
|
``excludedimports`` and ``hiddenimports`` will be overwritten by the
|
|
new hook, if set there.
|
|
|
|
Additionally, one can add a hook to be run after the default hook by
|
|
passing e.g. ``hookspath=[HOOKSPATH]`` to the ``Analysis`` class. In both
|
|
cases, ``HOOKSPATH`` is the path to a directory containing a file named
|
|
``hook-kivy.py`` that is the pyinstaller hook for kivy to be processed
|
|
after the default hook.
|
|
|
|
hiddenimports
|
|
-------------
|
|
|
|
When a module is imported indirectly, e.g. with ``__import__``, pyinstaller
|
|
won't know about it and the module has to be added through ``hiddenimports``.
|
|
|
|
``hiddenimports`` and other hook variables can be specified within a hook as
|
|
described above. Also, these variable can be passed to ``Analysis`` and their
|
|
values are then appended to the hook's values for these variables.
|
|
|
|
Most of kivy's core modules, e.g. video are imported indirectly and therefore
|
|
need to be added in hiddenimports. The default PyInstaller hook adds all the
|
|
providers. To overwrite, a modified kivy-hook similar to the default hook, such
|
|
as :func:`hookspath` that only imports the desired modules can be added. One
|
|
then uses :func:`get_deps_minimal` or :func:`get_deps_all` to get the list of
|
|
modules and adds them manually in a modified hook or passes them to
|
|
``Analysis`` in the spec file.
|
|
|
|
Hook generator
|
|
--------------
|
|
|
|
:mod:`pyinstaller_hooks` includes a tool to generate a hook which lists
|
|
all the provider modules in a list so that one can manually comment out
|
|
the providers not to be included. To use, do::
|
|
|
|
python -m kivy.tools.packaging.pyinstaller_hooks hook filename
|
|
|
|
``filename`` is the name and path of the hook file to create. If ``filename``
|
|
is not provided the hook is printed to the terminal.
|
|
'''
|
|
|
|
import os
|
|
import sys
|
|
import pkgutil
|
|
import logging
|
|
from os.path import dirname, join
|
|
import importlib
|
|
import subprocess
|
|
import re
|
|
import glob
|
|
|
|
import kivy
|
|
try:
|
|
from kivy import deps as old_deps
|
|
except ImportError:
|
|
old_deps = None
|
|
try:
|
|
import kivy_deps
|
|
except ImportError:
|
|
kivy_deps = None
|
|
from kivy.factory import Factory
|
|
from PyInstaller.depend import bindepend
|
|
|
|
from os import environ
|
|
if 'KIVY_DOC' not in environ:
|
|
from PyInstaller.utils.hooks import collect_submodules
|
|
|
|
curdir = dirname(__file__)
|
|
|
|
kivy_modules = [
|
|
'xml.etree.cElementTree',
|
|
'kivy.core.gl',
|
|
'kivy.weakmethod',
|
|
'kivy.core.window.window_info',
|
|
] + collect_submodules('kivy.graphics')
|
|
'''List of kivy modules that are always needed as hiddenimports of
|
|
pyinstaller.
|
|
'''
|
|
|
|
excludedimports = ['tkinter', '_tkinter', 'twisted']
|
|
'''List of excludedimports that should always be excluded from
|
|
pyinstaller.
|
|
'''
|
|
|
|
datas = [
|
|
(kivy.kivy_data_dir,
|
|
os.path.join('kivy_install', os.path.basename(kivy.kivy_data_dir))),
|
|
(kivy.kivy_modules_dir,
|
|
os.path.join('kivy_install', os.path.basename(kivy.kivy_modules_dir)))
|
|
]
|
|
'''List of data to be included by pyinstaller.
|
|
'''
|
|
|
|
|
|
def runtime_hooks():
|
|
'''Returns a list with the runtime hooks for kivy. It can be used with
|
|
``runtime_hooks=runtime_hooks()`` in the spec file. Pyinstaller comes
|
|
preinstalled with this hook.
|
|
'''
|
|
return [join(curdir, 'pyi_rth_kivy.py')]
|
|
|
|
|
|
def hookspath():
|
|
'''Returns a list with the directory that contains the alternate (not
|
|
the default included with pyinstaller) pyinstaller hook for kivy,
|
|
``kivy/tools/packaging/pyinstaller_hooks/kivy-hook.py``. It is
|
|
typically used with ``hookspath=hookspath()`` in the spec
|
|
file.
|
|
|
|
The default pyinstaller hook returns all the core providers used using
|
|
:func:`get_deps_minimal` to add to its list of hidden imports. This
|
|
alternate hook only included the essential modules and leaves the core
|
|
providers to be included additionally with :func:`get_deps_minimal`
|
|
or :func:`get_deps_all`.
|
|
'''
|
|
return [curdir]
|
|
|
|
|
|
def get_hooks():
|
|
'''Returns the dict for the spec ``hookspath`` and ``runtime_hooks``
|
|
values.
|
|
'''
|
|
return {'hookspath': hookspath(), 'runtime_hooks': runtime_hooks()}
|
|
|
|
|
|
def get_deps_minimal(exclude_ignored=True, **kwargs):
|
|
'''Returns Kivy hidden modules as well as excluded modules to be used
|
|
with ``Analysis``.
|
|
|
|
The function takes core modules as keyword arguments and their value
|
|
indicates which of the providers to include/exclude from the compiled app.
|
|
|
|
The possible keyword names are ``audio, camera, clipboard, image, spelling,
|
|
text, video, and window``. Their values can be:
|
|
|
|
``True``: Include current provider
|
|
The providers imported when the core module is
|
|
loaded on this system are added to hidden imports. This is the
|
|
default if the keyword name is not specified.
|
|
``None``: Exclude
|
|
Don't return this core module at all.
|
|
``A string or list of strings``: Providers to include
|
|
Each string is the name of a provider for this module to be
|
|
included.
|
|
|
|
For example, ``get_deps_minimal(video=None, window=True,
|
|
audio=['gstplayer', 'ffpyplayer'], spelling='enchant')`` will exclude all
|
|
the video providers, will include the gstreamer and ffpyplayer providers
|
|
for audio, will include the enchant provider for spelling, and will use the
|
|
current default provider for ``window``.
|
|
|
|
``exclude_ignored``, if ``True`` (the default), if the value for a core
|
|
library is ``None``, then if ``exclude_ignored`` is True, not only will the
|
|
library not be included in the hiddenimports but it'll also added to the
|
|
excluded imports to prevent it being included accidentally by pyinstaller.
|
|
|
|
:returns:
|
|
|
|
A dict with three keys, ``hiddenimports``, ``excludes``, and
|
|
``binaries``. Their values are a list of the corresponding modules to
|
|
include/exclude. This can be passed directly to `Analysis`` with
|
|
e.g. ::
|
|
|
|
a = Analysis(['..\\kivy\\examples\\demo\\touchtracer\\main.py'],
|
|
...
|
|
hookspath=hookspath(),
|
|
runtime_hooks=[],
|
|
win_no_prefer_redirects=False,
|
|
win_private_assemblies=False,
|
|
cipher=block_cipher,
|
|
**get_deps_minimal(video=None, audio=None))
|
|
'''
|
|
core_mods = ['audio', 'camera', 'clipboard', 'image', 'spelling', 'text',
|
|
'video', 'window']
|
|
mods = kivy_modules[:]
|
|
excludes = excludedimports[:]
|
|
|
|
for mod_name, val in kwargs.items():
|
|
if mod_name not in core_mods:
|
|
raise KeyError('{} not found in {}'.format(mod_name, core_mods))
|
|
|
|
full_name = 'kivy.core.{}'.format(mod_name)
|
|
if not val:
|
|
core_mods.remove(mod_name)
|
|
if exclude_ignored:
|
|
excludes.extend(collect_submodules(full_name))
|
|
continue
|
|
if val is True:
|
|
continue
|
|
core_mods.remove(mod_name)
|
|
|
|
mods.append(full_name)
|
|
single_mod = False
|
|
if isinstance(val, (str, bytes)):
|
|
single_mod = True
|
|
mods.append('kivy.core.{0}.{0}_{1}'.format(mod_name, val))
|
|
if not single_mod:
|
|
for v in val:
|
|
mods.append('kivy.core.{0}.{0}_{1}'.format(mod_name, v))
|
|
|
|
for mod_name in core_mods: # process remaining default modules
|
|
full_name = 'kivy.core.{}'.format(mod_name)
|
|
mods.append(full_name)
|
|
m = importlib.import_module(full_name)
|
|
|
|
if mod_name == 'clipboard' and m.CutBuffer:
|
|
mods.append(m.CutBuffer.__module__)
|
|
if hasattr(m, mod_name.capitalize()): # e.g. video -> Video
|
|
val = getattr(m, mod_name.capitalize())
|
|
if val:
|
|
mods.append(getattr(val, '__module__'))
|
|
|
|
if hasattr(m, 'libs_loaded') and m.libs_loaded:
|
|
for name in m.libs_loaded:
|
|
mods.append('kivy.core.{}.{}'.format(mod_name, name))
|
|
|
|
mods = sorted(set(mods))
|
|
|
|
binaries = []
|
|
if any('gstplayer' in m for m in mods):
|
|
binaries = _find_gst_binaries()
|
|
elif exclude_ignored:
|
|
excludes.append('kivy.lib.gstplayer')
|
|
|
|
return {
|
|
'hiddenimports': mods,
|
|
'excludes': excludes,
|
|
'binaries': binaries,
|
|
}
|
|
|
|
|
|
def get_deps_all():
|
|
'''Similar to :func:`get_deps_minimal`, but this returns all the
|
|
kivy modules that can indirectly imported. Which includes all
|
|
the possible kivy providers.
|
|
|
|
This can be used to get a list of all the possible providers
|
|
which can then manually be included/excluded by commenting out elements
|
|
in the list instead of passing on all the items. See module description.
|
|
|
|
:returns:
|
|
|
|
A dict with three keys, ``hiddenimports``, ``excludes``, and
|
|
``binaries``. Their values are a list of the corresponding modules to
|
|
include/exclude. This can be passed directly to `Analysis`` with
|
|
e.g. ::
|
|
|
|
a = Analysis(['..\\kivy\\examples\\demo\\touchtracer\\main.py'],
|
|
...
|
|
**get_deps_all())
|
|
'''
|
|
return {
|
|
'binaries': _find_gst_binaries(),
|
|
'hiddenimports': sorted(set(kivy_modules +
|
|
collect_submodules('kivy.core'))),
|
|
'excludes': []}
|
|
|
|
|
|
def get_factory_modules():
|
|
'''Returns a list of all the modules registered in the kivy factory.
|
|
'''
|
|
mods = [x.get('module', None) for x in Factory.classes.values()]
|
|
return [m for m in mods if m]
|
|
|
|
|
|
def add_dep_paths():
|
|
'''Should be called by the hook. It adds the paths with the binary
|
|
dependencies to the system path so that pyinstaller can find the binaries
|
|
during its crawling stage.
|
|
'''
|
|
paths = []
|
|
if old_deps is not None:
|
|
for importer, modname, ispkg in pkgutil.iter_modules(
|
|
old_deps.__path__):
|
|
if not ispkg:
|
|
continue
|
|
try:
|
|
mod = importer.find_module(modname).load_module(modname)
|
|
except ImportError as e:
|
|
logging.warn(
|
|
"deps: Error importing dependency: {}".format(str(e)))
|
|
continue
|
|
|
|
if hasattr(mod, 'dep_bins'):
|
|
paths.extend(mod.dep_bins)
|
|
sys.path.extend(paths)
|
|
|
|
if kivy_deps is None:
|
|
return
|
|
|
|
paths = []
|
|
for importer, modname, ispkg in pkgutil.iter_modules(kivy_deps.__path__):
|
|
if not ispkg:
|
|
continue
|
|
try:
|
|
mod = importer.find_module(modname).load_module(modname)
|
|
except ImportError as e:
|
|
logging.warn("deps: Error importing dependency: {}".format(str(e)))
|
|
continue
|
|
|
|
if hasattr(mod, 'dep_bins'):
|
|
paths.extend(mod.dep_bins)
|
|
sys.path.extend(paths)
|
|
|
|
|
|
def _find_gst_plugin_path():
|
|
'''Returns a list of directories to search for GStreamer plugins.
|
|
'''
|
|
if 'GST_PLUGIN_PATH' in environ:
|
|
return [
|
|
os.path.abspath(os.path.expanduser(path))
|
|
for path in environ['GST_PLUGIN_PATH'].split(os.pathsep)
|
|
]
|
|
|
|
try:
|
|
p = subprocess.Popen(
|
|
['gst-inspect-1.0', 'coreelements'],
|
|
stdout=subprocess.PIPE, universal_newlines=True)
|
|
except:
|
|
return []
|
|
(stdoutdata, stderrdata) = p.communicate()
|
|
|
|
match = re.search(r'\s+(\S+libgstcoreelements\.\S+)', stdoutdata)
|
|
|
|
if not match:
|
|
return []
|
|
|
|
return [os.path.dirname(match.group(1))]
|
|
|
|
|
|
def _find_gst_binaries():
|
|
'''Returns a list of GStreamer plugins and libraries to pass as the
|
|
``binaries`` argument of ``Analysis``.
|
|
'''
|
|
gst_plugin_path = _find_gst_plugin_path()
|
|
|
|
plugin_filepaths = []
|
|
for plugin_dir in gst_plugin_path:
|
|
plugin_filepaths.extend(
|
|
glob.glob(os.path.join(plugin_dir, 'libgst*')))
|
|
if len(plugin_filepaths) == 0:
|
|
logging.warn('Could not find GStreamer plugins. ' +
|
|
'Possible solution: set GST_PLUGIN_PATH')
|
|
return []
|
|
|
|
lib_filepaths = set()
|
|
for plugin_filepath in plugin_filepaths:
|
|
plugin_deps = bindepend.selectImports(plugin_filepath)
|
|
lib_filepaths.update([path for _, path in plugin_deps])
|
|
|
|
plugin_binaries = [(f, 'gst-plugins') for f in plugin_filepaths]
|
|
lib_binaries = [(f, '.') for f in lib_filepaths]
|
|
|
|
return plugin_binaries + lib_binaries
|