# ------------------------------------------------------------------ # 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') ]