forked from pyinstaller/pyinstaller-hooks-contrib
-
Notifications
You must be signed in to change notification settings - Fork 0
/
hook-cv2.py
168 lines (143 loc) · 7.93 KB
/
hook-cv2.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# ------------------------------------------------------------------
# Copyright (c) 2020 PyInstaller Development Team.
#
# This file is distributed under the terms of the GNU General Public
# License (version 2.0 or later).
#
# The full license is available in LICENSE.GPL.txt, distributed with
# this software.
#
# SPDX-License-Identifier: GPL-2.0-or-later
# ------------------------------------------------------------------
import sys
import os
import glob
import pathlib
import PyInstaller.utils.hooks as hookutils
from PyInstaller import compat
hiddenimports = ['numpy']
# On Windows, make sure that opencv_videoio_ffmpeg*.dll is bundled
binaries = []
if compat.is_win:
# If conda is active, look for the DLL in its library path
if compat.is_conda:
libdir = os.path.join(compat.base_prefix, 'Library', 'bin')
pattern = os.path.join(libdir, 'opencv_videoio_ffmpeg*.dll')
for f in glob.glob(pattern):
binaries.append((f, '.'))
# Include any DLLs from site-packages/cv2 (opencv_videoio_ffmpeg*.dll
# can be found there in the PyPI version)
binaries += hookutils.collect_dynamic_libs('cv2')
# Collect auxiliary sub-packages, such as `cv2.gapi`, `cv2.mat_wrapper`, `cv2.misc`, and `cv2.utils`. This also
# picks up submodules with valid module names, such as `cv2.config`, `cv2.load_config_py2`, and `cv2.load_config_py3`.
# Therefore, filter out `cv2.load_config_py2`.
hiddenimports += hookutils.collect_submodules('cv2', filter=lambda name: name != 'cv2.load_config_py2')
# We also need to explicitly exclude `cv2.load_config_py2` due to it being imported in `cv2.__init__`.
excludedimports = ['cv2.load_config_py2']
# OpenCV loader from 4.5.4.60 requires extra config files and modules.
# We need to collect `config.py` and `load_config_py3`; to improve compatibility with PyInstaller < 5.2, where
# `module_collection_mode` (see below) is not implemented.
# We also need to collect `config-3.py` or `config-3.X.py`, whichever is available (the former is usually
# provided by PyPI wheels, while the latter seems to be used when user builds OpenCV from source).
datas = hookutils.collect_data_files(
'cv2',
include_py_files=True,
includes=[
'config.py',
f'config-{sys.version_info[0]}.{sys.version_info[1]}.py',
'config-3.py',
'load_config_py3.py',
],
)
# The OpenCV versions that attempt to perform module substitution via sys.path manipulation (== 4.5.4.58, >= 4.6.0.66)
# do not directly import the cv2.cv2 extension anymore, so in order to ensure it is collected, we would need to add it
# to hidden imports. However, when OpenCV is built by user from source, the extension is not located in the package's
# root directory, but in python-3.X sub-directory, which precludes referencing via module name due to sub-directory
# not being a valid subpackage name. Hence, emulate the OpenCV's loader and execute `config-3.py` or `config-3.X.py`
# to obtain the search path.
def find_cv2_extension(config_file):
# Prepare environment
PYTHON_EXTENSIONS_PATHS = []
LOADER_DIR = os.path.dirname(os.path.abspath(os.path.realpath(config_file)))
global_vars = globals().copy()
local_vars = locals().copy()
# Exec the config file
with open(config_file) as fp:
code = compile(fp.read(), os.path.basename(config_file), 'exec')
exec(code, global_vars, local_vars)
# Read the modified PYTHON_EXTENSIONS_PATHS
PYTHON_EXTENSIONS_PATHS = local_vars['PYTHON_EXTENSIONS_PATHS']
if not PYTHON_EXTENSIONS_PATHS:
return None
# Search for extension file
for extension_path in PYTHON_EXTENSIONS_PATHS:
extension_path = pathlib.Path(extension_path)
if compat.is_win:
extension_files = list(extension_path.glob('cv2*.pyd'))
else:
extension_files = list(extension_path.glob('cv2*.so'))
if extension_files:
if len(extension_files) > 1:
hookutils.logger.warning("Found multiple cv2 extension candidates: %s", extension_files)
extension_file = extension_files[0] # Take first (or hopefully the only one)
hookutils.logger.debug("Found cv2 extension module: %s", extension_file)
# Compute path relative to parent of config file (which should be the package's root)
dest_dir = pathlib.Path("cv2") / extension_file.parent.relative_to(LOADER_DIR)
return str(extension_file), str(dest_dir)
hookutils.logger.warning(
"Could not find cv2 extension module! Config file: %s, search paths: %s",
config_file, PYTHON_EXTENSIONS_PATHS)
return None
config_file = [
src_path for src_path, _ in datas
if os.path.basename(src_path) in (f'config-{sys.version_info[0]}.{sys.version_info[1]}.py', 'config-3.py')
]
if config_file:
try:
extension_info = find_cv2_extension(config_file[0])
if extension_info:
ext_src, ext_dst = extension_info
# Due to bug in PyInstaller's TOC structure implementation (affecting PyInstaller up to latest version at
# the time of writing, 5.9), we fail to properly resolve `cv2.cv2` EXTENSION entry's destination name if
# we already have a BINARY entry with the same destination name. This results in verbatim `cv2.cv2` file
# created in application directory in addition to the proper copy in the `cv2` sub-directoy.
# Therefoe, if destination directory of the cv2 extension module is the top-level package directory, fall
# back to using hiddenimports instead.
if ext_dst == 'cv2':
# Extension found in top-level package directory; likely a PyPI wheel.
hiddenimports += ['cv2.cv2']
else:
# Extension found in sub-directory; use BINARY entry
binaries += [extension_info]
except Exception:
hookutils.logger.warning("Failed to determine location of cv2 extension module!", exc_info=True)
# Mark the cv2 package to be collected in source form, bypassing PyInstaller's PYZ archive and FrozenImporter. This is
# necessary because recent versions of cv2 package attempt to perform module substritution via sys.path manipulation,
# which is incompatible with the way that FrozenImporter works. This requires pyinstaller/pyinstaller#6945, i.e.,
# PyInstaller >= 5.3. On earlier versions, the following statement does nothing, and problematic cv2 versions
# (== 4.5.4.58, >= 4.6.0.66) will not work.
#
# Note that the collect_data_files() above is still necessary, because some of the cv2 loader's config scripts are not
# valid module names (e.g., config-3.py). So the two collection approaches are complementary, and any overlap in files
# (e.g., __init__.py) is handled gracefully due to PyInstaller's uniqueness constraints on collected files.
module_collection_mode = 'py'
# In linux PyPI opencv-python wheels, the cv2 extension is linked against Qt, and the wheel bundles a basic subset of Qt
# shared libraries, plugins, and font files. This is not the case on other OSes (presumably native UI APIs are used by
# OpenCV HighGUI module), nor in the headless PyPI wheels (opencv-python-headless).
# The bundled Qt shared libraries should be picked up automatically due to binary dependency analysis, but we need to
# collect plugins and font files from the `qt` subdirectory.
if compat.is_linux:
pkg_path = pathlib.Path(hookutils.get_module_file_attribute('cv2')).parent
# Collect .ttf files fron fonts directory.
# NOTE: since we are using glob, we can skip checks for (sub)directories' existence.
qt_fonts_dir = pkg_path / 'qt' / 'fonts'
datas += [
(str(font_file), str(font_file.parent.relative_to(pkg_path.parent)))
for font_file in qt_fonts_dir.rglob('*.ttf')
]
# Collect .so files from plugins directory.
qt_plugins_dir = pkg_path / 'qt' / 'plugins'
binaries += [
(str(plugin_file), str(plugin_file.parent.relative_to(pkg_path.parent)))
for plugin_file in qt_plugins_dir.rglob('*.so')
]