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