# ---------------------------------------------------------------------------- # Copyright (c) 2005-2021, PyInstaller Development Team. # # Distributed under the terms of the GNU General Public License (version 2 # or later) with exception for distributing the bootloader. # # The full license is in the file COPYING.txt, distributed with this software. # # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception) #----------------------------------------------------------------------------- import glob import json import os from PyInstaller import compat from PyInstaller import log as logging from PyInstaller.depend import bindepend from PyInstaller.utils import hooks, misc logger = logging.getLogger(__name__) # QtLibraryInfo # -------------- # This class uses introspection to determine the location of Qt files. This is essential to deal with the many variants # of the PyQt5/6 and PySide2/6 package, each of which places files in a different location. Therefore, this class # provides all location-related members of `QLibraryInfo `_. class QtLibraryInfo: def __init__(self, namespace): if namespace not in ['PyQt5', 'PyQt6', 'PySide2', 'PySide6']: raise Exception('Invalid namespace: {0}'.format(namespace)) self.namespace = namespace # Distinction between PyQt5/6 and PySide2/6 self.is_pyqt = namespace in {'PyQt5', 'PyQt6'} # Distinction between Qt5 and Qt6 self.qt_major = 6 if namespace in {'PyQt6', 'PySide6'} else 5 # Determine relative path where Qt libraries and data need to be collected in the frozen application. This # varies between PyQt5/PyQt6/PySide2/PySide6, their versions, and platforms. NOTE: it is tempting to consider # deriving this path as simply the value of QLibraryInfo.PrefixPath, taken relative to the package's root # directory. However, we also need to support non-wheel deployments (e.g., with Qt installed in custom path on # Windows, or with Qt and PyQt5 installed on linux using native package manager), and in those, the Qt # PrefixPath does not reflect the required relative target path for the frozen application. if namespace == 'PyQt5': # PyQt5 uses PyQt5/Qt on all platforms, or PyQt5/Qt5 from version 5.15.4 on try: # The call below might fail with AttributeError on some PyQt5 versions (e.g., 5.9.2 from conda's main # channel); missing dist information forces a fallback codepath that tries to check for __version__ # attribute that does not exist, either. So handle the error gracefully and assume old layout. new_layout = hooks.is_module_satisfies("PyQt5 >= 5.15.4") except AttributeError: new_layout = False if new_layout: self.qt_rel_dir = os.path.join('PyQt5', 'Qt5') else: self.qt_rel_dir = os.path.join('PyQt5', 'Qt') elif namespace == 'PyQt6': # Similarly to PyQt5, PyQt6 switched from PyQt6/Qt to PyQt6/Qt6 in 6.0.3 try: # The call below might fail with AttributeError in case of a partial PyQt6 installation. For example, # user installs PyQt6 via pip, which also installs PyQt6-Qt6 and PyQt6-sip. Then they naively uninstall # PyQt6 package, which leaves the other two behind. PyQt6 now becomes a namespace package and there is # no dist metadata, so a fallback codepath in is_module_satisfies tries to check for __version__ # attribute that does not exist, either. Handle such errors gracefully and assume new layout (with # PyQt6, the new layout is more likely); it does not really matter what layout we assume, as library is # not usable anyway, but we do neeed to be able to return an instance of QtLibraryInfo with "version" # attribute set to a falsey value. new_layout = hooks.is_module_satisfies("PyQt6 >= 6.0.3") except AttributeError: new_layout = True if new_layout: self.qt_rel_dir = os.path.join('PyQt6', 'Qt6') else: self.qt_rel_dir = os.path.join('PyQt6', 'Qt') elif namespace == 'PySide2': # PySide2 uses PySide2/Qt on linux and macOS, and PySide2 on Windows if compat.is_win: self.qt_rel_dir = 'PySide2' else: self.qt_rel_dir = os.path.join('PySide2', 'Qt') else: # PySide6 follows the same logic as PySide2 if compat.is_win: self.qt_rel_dir = 'PySide6' else: self.qt_rel_dir = os.path.join('PySide6', 'Qt') # Initialize most of this class only when values are first requested from it. def __getattr__(self, name): if 'version' in self.__dict__: # Initialization was already done, but requested attribute is not availiable. raise AttributeError(name) else: # Ensure self.version exists, even if PyQt{5,6}/PySide{2,6} cannot be imported. Hooks and util functions use # `if .version` to check whether package was imported and other attributes are expected to be available. # This also serves as a marker that initialization was already done. self.version = None # Get library path information from Qt. See QLibraryInfo_. json_str = hooks.exec_statement( """ import sys # exec_statement only captures stdout. If there are errors, capture them to stdout so they can be # displayed to the user. Do this early, in case package imports produce stderr output. sys.stderr = sys.stdout import json try: from %s.QtCore import QLibraryInfo, QCoreApplication except Exception: print('False') raise SystemExit(0) # QLibraryInfo is not always valid until a QCoreApplication is instantiated. app = QCoreApplication(sys.argv) # Qt6 deprecated QLibraryInfo.location() in favor of QLibraryInfo.path(), and # QLibraryInfo.LibraryLocation enum was replaced by QLibraryInfo.LibraryPath. if hasattr(QLibraryInfo, 'path'): # Qt6; enumerate path enum values directly from the QLibraryInfo.LibraryPath enum. path_names = [x for x in dir(QLibraryInfo.LibraryPath) if x.endswith('Path')] location = {x: QLibraryInfo.path(getattr(QLibraryInfo.LibraryPath, x)) for x in path_names} else: # Qt5; in recent versions, location enum values can be enumeratd from QLibraryInfo.LibraryLocation. # However, in older versions of Qt5 and its python bindings, that is unavailable. Hence the # enumeration of "*Path"-named members of QLibraryInfo. path_names = [x for x in dir(QLibraryInfo) if x.endswith('Path')] location = {x: QLibraryInfo.location(getattr(QLibraryInfo, x)) for x in path_names} # Determine Qt version. Works for Qt 5.8 and later, where QLibraryInfo.version() was introduced. try: version = QLibraryInfo.version().segments() except AttributeError: version = [] print(json.dumps({ 'isDebugBuild': QLibraryInfo.isDebugBuild(), 'version': version, 'location': location, })) """ % self.namespace ) try: qli = json.loads(json_str) except Exception as e: logger.warning('Cannot read QLibraryInfo output: raised %s when decoding:\n%s', str(e), json_str) qli = {} for k, v in qli.items(): setattr(self, k, v) return getattr(self, name) # Provide single instances of this class to avoid each hook constructing its own. pyqt5_library_info = QtLibraryInfo('PyQt5') pyqt6_library_info = QtLibraryInfo('PyQt6') pyside2_library_info = QtLibraryInfo('PySide2') pyside6_library_info = QtLibraryInfo('PySide6') def get_qt_library_info(namespace): """ Return Qt5LibraryInfo instance for the given namespace. """ if namespace == 'PyQt5': return pyqt5_library_info if namespace == 'PyQt6': return pyqt6_library_info elif namespace == 'PySide2': return pyside2_library_info elif namespace == 'PySide6': return pyside6_library_info raise ValueError(f'Invalid namespace: {namespace}!') def qt_plugins_dir(namespace): """ Return list of paths searched for plugins. :param namespace: Import namespace (PyQt5, PyQt6, PySide2, or PySide6). :return: Plugin directory paths """ qt_info = get_qt_library_info(namespace) paths = [qt_info.location['PluginsPath']] if not paths: raise Exception('Cannot find {0} plugin directories'.format(namespace)) else: valid_paths = [] for path in paths: if os.path.isdir(path): valid_paths.append(str(path)) # must be 8-bit chars for one-file builds qt_plugin_paths = valid_paths if not qt_plugin_paths: raise Exception( "Cannot find existing {0} plugin directories. Paths checked: {1}".format(namespace, ", ".join(paths)) ) return qt_plugin_paths def _qt_filter_release_plugins(plugin_files): """ Filter the provided list of Qt plugin files and remove the debug variants, under the assumption that both the release version of a plugin (qtplugin.dll) and its debug variant (qtplugind.dll) appear in the list. """ # All basenames for lookup plugin_basenames = {os.path.normcase(os.path.basename(f)) for f in plugin_files} # Process all given filenames release_plugin_files = [] for plugin_filename in plugin_files: plugin_basename = os.path.normcase(os.path.basename(plugin_filename)) if plugin_basename.endswith('d.dll'): # If we can find a variant without trailing 'd' in the plugin list, then the DLL we are dealing with is a # debug variant and needs to be excluded. release_name = os.path.splitext(plugin_basename)[0][:-1] + '.dll' if release_name in plugin_basenames: continue release_plugin_files.append(plugin_filename) return release_plugin_files def qt_plugins_binaries(plugin_type, namespace): """ Return list of dynamic libraries formatted for mod.binaries. :param plugin_type: Plugin to look for :param namespace: Import namespace (PyQt5, PyQt6, PySide2, or PySide6) :return: Plugin directory path corresponding to the given plugin_type """ qt_info = get_qt_library_info(namespace) pdir = qt_plugins_dir(namespace) files = [] for path in pdir: files.extend(misc.dlls_in_dir(os.path.join(path, plugin_type))) # Windows: # # dlls_in_dir() grabs all files ending with ``*.dll``, ``*.so`` and ``*.dylib`` in a certain directory. On Windows # this would grab debug copies of Qt plugins, which then causes PyInstaller to add a dependency on the Debug CRT # *in addition* to the release CRT. if compat.is_win: files = _qt_filter_release_plugins(files) logger.debug("Found plugin files %s for plugin %s", files, plugin_type) dest_dir = os.path.join(qt_info.qt_rel_dir, 'plugins', plugin_type) binaries = [(f, dest_dir) for f in files] return binaries # Qt deployment approach # ---------------------- # This is the core of PyInstaller's approach to Qt deployment. It's based on: # # - Discovering the location of Qt libraries by introspection, using QtLibraryInfo_. This provides compatibility with # many variants of Qt5/6 (conda, self-compiled, provided by a Linux distro, etc.) and many versions of Qt5/6, all of # which vary in the location of Qt files. # - Placing all frozen PyQt5/6 or PySide2/6 Qt files in a standard subdirectory layout, which matches the layout of the # corresponding wheel on PyPI. This is necessary to support Qt installs which are not in a subdirectory of the PyQt5/6 # or PySide2/6 wrappers. See ``hooks/rthooks/pyi_rth_qt5.py`` for the use of environment variables to establish this # layout. # - Emitting warnings on missing QML and translation files which some installations don't have. # - Determining additional files needed for deployment by following the Qt deployment process using # _qt_dynamic_dependencies_dict`_ and add_qt_dependencies_. # # _qt_dynamic_dependencies_dict # ----------------------------- # This dictionary provides dynamics dependencies (plugins and translations) that cannot be discovered using # ``getImports``. It was built by combining information from: # # - Qt `deployment `_ docs. Specifically: # # - The `deploying Qt for Linux/X11 `_ page specifies # including the Qt Platform Abstraction (QPA) plugin, ``libqxcb.so``. There's little other guidance provided. # - The `Qt for Windows - Deployment # `_ page likewise specifies # the ``qwindows.dll`` QPA. This is found by the dependency walker, so it does not need to explicitly specified. # # - For dynamic OpenGL applications, the ``libEGL.dll``, ``libGLESv2.dll``, ``d3dcompiler_XX.dll`` (the XX is a # version number), and ``opengl32sw.dll`` libraries are also needed. # - If Qt was configured to use ICU, the ``icudtXX.dll``, ``icuinXX.dll``, and ``icuucXX.dll`` libraries are # needed. # # These are included by ``hook-PyQt5.py``. # # - The `Qt for macOS - Deployment `_ page specifies the # ``libqcocoa.dylib`` QPA, but little else. The `Mac deployment tool # `_ provides the following rules: # # - The platform plugin is always deployed. # - The image format plugins are always deployed. # - The print support plugin is always deployed. # - SQL driver plugins are deployed if the application uses the Qt SQL module. # - Script plugins are deployed if the application uses the Qt Script module. # - The SVG icon plugin is deployed if the application uses the Qt SVG module. # - The accessibility plugin is always deployed. # # - Per the `Deploying QML Applications `_ page, QML-based # applications need the ``qml/`` directory available. # # This is handled by ``hook-PyQt5.QtQuick.py``. # # - Per the `Deploying Qt WebEngine Applications `_ # page, deployment may include: # # - Libraries (handled when PyInstaller following dependencies). # - QML imports (if Qt Quick integration is used). # - Qt WebEngine process, which should be located at # ``QLibraryInfo::location(QLibraryInfo::LibraryExecutablesPath)`` # for Windows and Linux, and in ``.app/Helpers/QtWebEngineProcess`` for Mac. # - Resources: the files listed in deployWebEngineCore_. # - Translations: on macOS: ``.app/Content/Resources``; on Linux and Windows: ``qtwebengine_locales`` # directory in the directory specified by ``QLibraryInfo::location(QLibraryInfo::TranslationsPath)``. # - Audio and video codecs: Probably covered if Qt5Multimedia is referenced? # # This is handled by ``hook-PyQt5.QtWebEngineWidgets.py``. # # - Since `QAxContainer `_ is a statically-linked library, it # does not need any special handling. # # - Sources for the `Windows Deployment Tool # `_ show more detail: # # - The `PluginModuleMapping struct # `_ and the following # ``pluginModuleMappings`` global provide a mapping between a plugin directory name and an `enum of Qt plugin # names `_. # - The `QtModuleEntry struct `_ # and ``qtModuleEntries`` global connect this enum to the name of the Qt5 library it represents and to the # translation files this library requires. (Ignore the ``option`` member -- it's just for command-line parsing.) # # Manually combining these two provides a mapping of Qt library names to the translation and plugin(s) needed by the # library. The process is: take the key of the dict below from ``QtModuleEntry.libraryName``, but make it lowercase # (since Windows files will be normalized to lowercase). The ``QtModuleEntry.translation`` provides the # ``translation_base``. Match the ``QtModuleEntry.module`` with ``PluginModuleMapping.module`` to find the # ``PluginModuleMapping.directoryName`` for the required plugin(s). # # - The `deployWebEngineCore # `_ # function copies the following files from ``resources/``, and also copies the web engine process executable. # # - ``icudtl.dat`` # - ``qtwebengine_devtools_resources.pak`` # - ``qtwebengine_resources.pak`` # - ``qtwebengine_resources_100p.pak`` # - ``qtwebengine_resources_200p.pak`` # # - Sources for the `Mac deployment tool`_ are less helpful. The `deployPlugins # `_ # function seems to: # # - Always include ``platforms/libqcocoa.dylib``. # - Always include ``printsupport/libcocoaprintersupport.dylib`` # - Include ``bearer/`` if ``QtNetwork`` is included (and some other condition I didn't look up). # - Always include ``imageformats/``, except for ``qsvg``. # - Include ``imageformats/qsvg`` if ``QtSvg`` is included. # - Always include ``iconengines/``. # - Include ``sqldrivers/`` if ``QtSql`` is included. # - Include ``mediaservice/`` and ``audio/`` if ``QtMultimedia`` is included. # # The always includes will be handled by ``hook-PyQt5.py`` or ``hook-PySide2.py``; optional includes are already # covered by the dict below. # _qt5_dynamic_dependencies_dict = { #- "lib_name": (.hiddenimports, translations_base, zero or more plugins...) "qt53dcore": (None, None, ), # noqa "qt53dinput": (None, None, ), # noqa "qt53dquick": (None, None, ), # noqa "qt53dquickrender": (None, None, ), # noqa "qt53drender": (None, None, "sceneparsers", "renderplugins", "geometryloaders"), # noqa "qt5bluetooth": (".QtBluetooth", None, ), # noqa "qt5concurrent": (None, "qtbase", ), # noqa "qt5core": (".QtCore", "qtbase", ), # noqa "qt5dbus": (".QtDBus", None, ), # noqa "qt5declarative": (None, "qtquick1", "qml1tooling"), # noqa "qt5designer": (".QtDesigner", None, ), # noqa "qt5designercomponents": (None, None, ), # noqa "qt5gamepad": (None, None, "gamepads"), # Qt5Gui: # The ``platformthemes`` plugins are available only on Linux. # Same goes for ``xcbglintegrations`` and ``egldeviceintegrations`` plugins. # The ``wayland-decoration-client``, ``wayland-graphics-integration-client``, and ``wayland-shell-integration`` # plugins are part of Qt5WaylandClient Qt module, whose shared library (e.g., libQt5WaylandClient.so) is linked # by the wayland-related ``platforms`` plugins. Ideally, we would collect these plugins based on the # Qt5WaylandClient shared library entry, but as our Qt hook utilities do not scan the plugins using this dictionary, # that would not work. So instead we list these plugins under Qt5Gui to achieve pretty much the same end result. "qt5gui": (".QtGui", "qtbase", "accessible", "iconengines", "imageformats", "platforms", "platforminputcontexts", "platformthemes", "xcbglintegrations", "egldeviceintegrations", "wayland-decoration-client", "wayland-graphics-integration-client", "wayland-shell-integration"), # noqa "qt5help": (".QtHelp", "qt_help", ), # noqa "qt5location": (".QtLocation", None, "geoservices"), # noqa "qt5macextras": (".QtMacExtras", None, ), # noqa "qt5multimedia": (".QtMultimedia", "qtmultimedia", "audio", "mediaservice", "playlistformats"), # noqa "qt5multimediaquick": (None, "qtmultimedia", ), # noqa "qt5multimediawidgets": (".QtMultimediaWidgets", "qtmultimedia", ), # noqa "qt5network": (".QtNetwork", "qtbase", "bearer"), # noqa "qt5nfc": (".QtNfc", None, ), # noqa "qt5opengl": (".QtOpenGL", None, ), # noqa "qt5positioning": (".QtPositioning", None, "position"), # noqa "qt5printsupport": (".QtPrintSupport", None, "printsupport"), # noqa "qt5qml": (".QtQml", "qtdeclarative", ), # noqa "qt5quick": (".QtQuick", "qtdeclarative", "scenegraph", "qmltooling"), # noqa "qt5quickparticles": (None, None, ), # noqa "qt5quickwidgets": (".QtQuickWidgets", None, ), # noqa "qt5script": (None, "qtscript", ), # noqa "qt5scripttools": (None, "qtscript", ), # noqa "qt5sensors": (".QtSensors", None, "sensors", "sensorgestures"), # noqa "qt5serialbus": (None, None, "canbus"), # noqa "qt5serialport": (".QtSerialPort", "qtserialport", ), # noqa "qt5sql": (".QtSql", "qtbase", "sqldrivers"), # noqa "qt5svg": (".QtSvg", None, ), # noqa "qt5test": (".QtTest", "qtbase", ), # noqa "qt5texttospeech": (None, None, "texttospeech"), # noqa "qt5webchannel": (".QtWebChannel", None, ), # noqa "qt5webengine": (".QtWebEngine", "qtwebengine", "qtwebengine"), # noqa "qt5webenginecore": (".QtWebEngineCore", None, "qtwebengine"), # noqa "qt5webenginewidgets": (".QtWebEngineWidgets", None, "qtwebengine"), # noqa "qt5webkit": (None, None, ), # noqa "qt5webkitwidgets": (None, None, ), # noqa "qt5websockets": (".QtWebSockets", None, ), # noqa "qt5widgets": (".QtWidgets", "qtbase", "styles"), # noqa "qt5winextras": (".QtWinExtras", None, ), # noqa "qt5xml": (".QtXml", "qtbase", ), # noqa "qt5xmlpatterns": (".QXmlPatterns", "qtxmlpatterns", ), # noqa } # yapf: disable # The dynamic dependency dictionary for Qt6 is constructed automatically from its Qt5 counterpart, by copying the # entries and substituting qt5 in the name with qt6. If the entry already exists in the dictionary, it is not # copied, which allows us to provide Qt6-specific overrides, should they prove necessary. _qt6_dynamic_dependencies_dict = { # Qt6Network: # networkinformationbackends plugins were introduced in Qt 6.1, and renamed to networkinformation in Qt 6.2 # tls plugins were introduced in Qt 6.2 "qt6network": (".QtNetwork", "qtbase", "networkinformationbackend", "networkinformation", "tls"), # noqa "qt6openglwidgets": (".QtOpenGLWidgets", "qtbase", ), # noqa } # yapf: disable for lib_name, content in _qt5_dynamic_dependencies_dict.items(): if lib_name.startswith('qt5'): lib_name = 'qt6' + lib_name[3:] if lib_name not in _qt6_dynamic_dependencies_dict: _qt6_dynamic_dependencies_dict[lib_name] = content del lib_name, content # add_qt_dependencies # -------------------- # Generic implemnentation that finds the Qt 5/6 dependencies based on the hook name of a PyQt5/PyQt6/PySide2/PySide6 # hook. Returns (hiddenimports, binaries, datas). Typical usage: # ``hiddenimports, binaries, datas = add_qt5_dependencies(__file__)``. def add_qt_dependencies(hook_file): # Accumulate all dependencies in a set to avoid duplicates. hiddenimports = set() translations_base = set() plugins = set() # Find the module underlying this Qt hook: change ``/path/to/hook-PyQt5.blah.py`` to ``PyQt5.blah``. hook_name, hook_ext = os.path.splitext(os.path.basename(hook_file)) assert hook_ext.startswith('.py') assert hook_name.startswith('hook-') module_name = hook_name[5:] namespace = module_name.split('.')[0] # Retrieve Qt library info structure. qt_info = get_qt_library_info(namespace) # Exit if the requested library cannot be imported. # NOTE: qt_info.version can be empty list on older Qt5 versions (#5381). if qt_info.version is None: return [], [], [] # Look up the module returned by this import. module = hooks.get_module_file_attribute(module_name) logger.debug('add_qt%d_dependencies: Examining %s, based on hook of %s.', qt_info.qt_major, module, hook_file) # Walk through all the static dependencies of a dynamically-linked library (``.so``/``.dll``/``.dylib``). imports = set(bindepend.getImports(module)) while imports: imp = imports.pop() # On Windows, find this library; other platforms already provide the full path. if compat.is_win: # First, look for Qt binaries in the local Qt install. imp = bindepend.getfullnameof(imp, qt_info.location['BinariesPath']) # Strip off the extension and ``lib`` prefix (Linux/Mac) to give the raw name. # Lowercase (since Windows always normalizes names to lowercase). lib_name = os.path.splitext(os.path.basename(imp))[0].lower() # Linux libraries sometimes have a dotted version number -- ``libfoo.so.3``. It is now ''libfoo.so``, # but the ``.so`` must also be removed. if compat.is_linux and os.path.splitext(lib_name)[1] == '.so': lib_name = os.path.splitext(lib_name)[0] if lib_name.startswith('lib'): lib_name = lib_name[3:] # Mac OS: handle different naming schemes. PyPI wheels ship framework-enabled Qt builds, where shared libraries # are part of .framework bundles (e.g., ``PyQt5/Qt5/lib/QtCore.framework/Versions/5/QtCore``). In Anaconda # (Py)Qt installations, the shared libraries are installed in environment's library directory, and contain # versioned extensions, e.g., ``libQt5Core.5.dylib``. if compat.is_darwin: if lib_name.startswith('qt') and not lib_name.startswith('qt' + str(qt_info.qt_major)): # Qt library from a framework bundle (e.g., ``QtCore``); change prefix from ``qt`` to ``qt5`` or ``qt6`` # to match names in Windows/Linux. lib_name = 'qt' + str(qt_info.qt_major) + lib_name[2:] if lib_name.endswith('.' + str(qt_info.qt_major)): # Qt library from Anaconda, which originally had versioned extension, e.g., ``libfoo.5.dynlib``. # The above processing turned it into ``foo.5``, so we need to remove the last two characters. lib_name = lib_name[:-2] # Match libs with QT_LIBINFIX set to '_conda', i.e. conda-forge builds. if lib_name.endswith('_conda'): lib_name = lib_name[:-6] logger.debug('add_qt%d_dependencies: raw lib %s -> parsed lib %s', qt_info.qt_major, imp, lib_name) # PySide2 and PySide6 on linux seem to link all extension modules against libQt5Core, libQt5Network, and # libQt5Qml (or their libQt6* equivalents). While the first two are reasonable, the libQt5Qml dependency pulls # in whole QtQml module, along with its data and plugins, which in turn pull in several other Qt libraries, # greatly inflating the bundle size (see #6447). # # Similarly, some extension modules (QtWebChannel, QtWebEngine*) seem to be also linked against libQt5Qml, # even when the module can be used without having the whole QtQml module collected. # # Therefore, we explicitly prevent inclusion of QtQml based on the dynamic library dependency, except for # QtQml* and QtQuick* modules, whose use directly implies the use of QtQml. if lib_name in ("qt5qml", "qt6qml"): short_module_name = module_name.split('.', 1)[-1] # PySide2.QtModule -> QtModule if not short_module_name.startswith(('QtQml', 'QtQuick')): logger.debug('add_qt%d_dependencies: Ignoring import of %s.', qt_info.qt_major, imp) continue # Follow only Qt dependencies. _qt_dynamic_dependencies_dict = ( _qt5_dynamic_dependencies_dict if qt_info.qt_major == 5 else _qt6_dynamic_dependencies_dict ) if lib_name in _qt_dynamic_dependencies_dict: # Follow these to find additional dependencies. logger.debug('add_qt%d_dependencies: Import of %s.', qt_info.qt_major, imp) imports.update(bindepend.getImports(imp)) # Look up which plugins and translations are needed. dd = _qt_dynamic_dependencies_dict[lib_name] lib_name_hiddenimports, lib_name_translations_base = dd[:2] lib_name_plugins = dd[2:] # Add them in. if lib_name_hiddenimports: hiddenimports.update([namespace + lib_name_hiddenimports]) plugins.update(lib_name_plugins) if lib_name_translations_base: translations_base.update([lib_name_translations_base]) # Change plugins into binaries. binaries = [] for plugin in plugins: more_binaries = qt_plugins_binaries(plugin, namespace=namespace) binaries.extend(more_binaries) # Change translation_base to datas. tp = qt_info.location['TranslationsPath'] tp_dst = os.path.join(qt_info.qt_rel_dir, 'translations') datas = [] for tb in translations_base: src = os.path.join(tp, tb + '_*.qm') # Not all PyQt5 installations include translations. See # https://github.com/pyinstaller/pyinstaller/pull/3229#issuecomment-359479893 # and # https://github.com/pyinstaller/pyinstaller/issues/2857#issuecomment-368744341. if glob.glob(src): datas.append((src, tp_dst)) else: logger.warning( 'Unable to find Qt%d translations %s. These translations were not packaged.', qt_info.qt_major, src ) # Change hiddenimports to a list. hiddenimports = list(hiddenimports) logger.debug( 'add_qt%d_dependencies: imports from %s:\n' ' hiddenimports = %s\n' ' binaries = %s\n' ' datas = %s', qt_info.qt_major, hook_name, hiddenimports, binaries, datas ) return hiddenimports, binaries, datas # add_qt5_dependencies # -------------------- # Find the Qt5 dependencies based on the hook name of a PySide2/PyQt5 hook. Returns (hiddenimports, binaries, datas). # Typical usage: ``hiddenimports, binaries, datas = add_qt5_dependencies(__file__)``. add_qt5_dependencies = add_qt_dependencies # Use generic implementation # add_qt6_dependencies # -------------------- # Find the Qt6 dependencies based on the hook name of a PySide6/PyQt6 hook. Returns (hiddenimports, binaries, datas). # Typical usage: ``hiddenimports, binaries, datas = add_qt6_dependencies(__file__)``. add_qt6_dependencies = add_qt_dependencies # Use generic implementation def _find_all_or_none(globs_to_include, num_files, qt_library_info): """ globs_to_include is a list of file name globs. If the number of found files does not match num_files, no files will be included. """ # This function is required because CI is failing to include libEGL. # The error in AppVeyor is:: # # [2312] LOADER: Running pyi_lib_PyQt5-uic.py # Failed to load libEGL (Access is denied.) # More info: https://github.com/pyinstaller/pyinstaller/pull/3568 # # Since old PyQt5 wheels do not include d3dcompiler_4?.dll, libEGL.dll and libGLESv2.dll will not be included # for PyQt5 builds during CI. to_include = [] dst_dll_path = '.' for dll in globs_to_include: # In PyQt5/PyQt6, the DLLs we are looking for are located in location['BinariesPath'], whereas in # PySide2/PySide6, they are located in location['PrefixPath']. dll_path = os.path.join( qt_library_info.location['BinariesPath' if qt_library_info.is_pyqt else 'PrefixPath'], dll ) dll_file_paths = glob.glob(dll_path) for dll_file_path in dll_file_paths: to_include.append((dll_file_path, dst_dll_path)) if len(to_include) == num_files: return to_include return [] # Collect required Qt binaries, but only if all binaries in a group exist. def get_qt_binaries(qt_library_info): binaries = [] angle_files = ['libEGL.dll', 'libGLESv2.dll', 'd3dcompiler_??.dll'] binaries += _find_all_or_none(angle_files, 3, qt_library_info) opengl_software_renderer = ['opengl32sw.dll'] binaries += _find_all_or_none(opengl_software_renderer, 1, qt_library_info) # Include ICU files, if they exist. # See the "Deployment approach" section in ``PyInstaller/utils/hooks/qt.py``. icu_files = ['icudt??.dll', 'icuin??.dll', 'icuuc??.dll'] binaries += _find_all_or_none(icu_files, 3, qt_library_info) return binaries # Collect additional shared libraries required for SSL support in QtNetwork, if they are available. # Applicable only to Windows. See issue #3520, #4048. def get_qt_network_ssl_binaries(qt_library_info): # No-op if requested Qt-based package is not available. if qt_library_info.version is None: return [] # Applicable only to Windows. if not compat.is_win: return [] # Check if QtNetwork supports SSL. ssl_enabled = hooks.eval_statement( """ from {}.QtNetwork import QSslSocket print(QSslSocket.supportsSsl()) """.format(qt_library_info.namespace) ) if not ssl_enabled: return [] # PyPI version of PySide2 requires user to manually install SSL libraries into the PrefixPath. Other versions # (e.g., the one provided by Conda) put the libraries into the BinariesPath. PyQt5 also uses BinariesPath. # Accommodate both options by searching both locations... locations = (qt_library_info.location['BinariesPath'], qt_library_info.location['PrefixPath']) dll_names = ('libeay32.dll', 'ssleay32.dll', 'libssl-1_1-x64.dll', 'libcrypto-1_1-x64.dll') binaries = [] for location in locations: for dll in dll_names: dll_path = os.path.join(location, dll) if os.path.exists(dll_path): binaries.append((dll_path, '.')) return binaries # Collect additional binaries and data for QtQml module. def get_qt_qml_files(qt_library_info): # No-op if requested Qt-based package is not available. if qt_library_info.version is None: return [], [] # Not all PyQt5/PySide2 installs have QML files. In this case, location['Qml2ImportsPath'] is empty. # Furthermore, even if location path is provided, the directory itself may not exist. # # https://github.com/pyinstaller/pyinstaller/pull/3229#issuecomment-359735031 # https://github.com/pyinstaller/pyinstaller/issues/3864 qmldir = qt_library_info.location['Qml2ImportsPath'] if not qmldir or not os.path.exists(qmldir): logger.warning( 'QML directory for %s, %r, does not exist. QML files not packaged.', qt_library_info.namespace, qmldir ) return [], [] qml_rel_dir = os.path.join(qt_library_info.qt_rel_dir, 'qml') datas = [(qmldir, qml_rel_dir)] binaries = [ # Produce ``/path/to/Qt/Qml/path_to_qml_binary/qml_binary, PyQt5/Qt/Qml/path_to_qml_binary``. (f, os.path.join(qml_rel_dir, os.path.dirname(os.path.relpath(f, qmldir)))) for f in misc.dlls_in_subdirs(qmldir) ] return binaries, datas # Collect the ``qt.conf`` file. def get_qt_conf_file(qt_library_info): # No-op if requested Qt-based package is not available. if qt_library_info.version is None: return [] # Find ``qt.conf`` in location['PrefixPath']. datas = [ x for x in hooks.collect_system_data_files(qt_library_info.location['PrefixPath'], qt_library_info.qt_rel_dir) if os.path.basename(x[0]) == 'qt.conf' ] return datas # Collect QtWebEngine helper process executable, translations, and resources. def get_qt_webengine_binaries_and_data_files(qt_library_info): binaries = [] datas = [] # Output directory (varies between PyQt and PySide and among OSes; the difference is abstracted by # qt_library_info.qt_rel_dir) rel_data_path = qt_library_info.qt_rel_dir is_macos_framework = False if compat.is_darwin: # Determine if we are dealing with a framework-based Qt build (e.g., PyPI wheels) or a dylib-based one # (e.g., Anaconda). The former requires special handling, while the latter is handled in the same way as # Windows and Linux builds. is_macos_framework = os.path.exists( os.path.join(qt_library_info.location['LibrariesPath'], 'QtWebEngineCore.framework') ) if is_macos_framework: # On macOS, Qt shared libraries are provided in form of .framework bundles. However, PyInstaller collects shared # library from the bundle into top-level application directory, breaking the bundle structure. # # QtWebEngine and its underlying Chromium engine, however, have very strict data file layout requirements due to # sandboxing, and does not work if the helper process executable does not load the shared library from # QtWebEngineCore.framework (which also needs to contain all resources). # # Therefore, we collect the QtWebEngineCore.framework manually, in order to obtain a working QtWebEngineProcess # helper executable. But because that bypasses our dependency scanner, we need to collect the dependent # .framework bundles as well. And we need to override QTWEBENGINEPROCESS_PATH in rthook, because the # QtWebEngineWidgets python extension actually loads up the copy of shared library that is located in # sys._MEIPASS (as opposed to the manually-copied one in .framework bundle). Furthermore, because the extension # modules use Qt shared libraries in sys._MEIPASS, we also copy all contents of # QtWebEngineCore.framework/Resources into sys._MEIPASS to make resource loading in the main process work. # # Besides being ugly, this approach has three main ramifications: # 1. we bundle two copies of each Qt shared library involved: the copy used by main process, picked up by # dependency scanner; and a copy in manually-collected .framework bundle that is used by the helper process. # 2. the trick with copying contents of Resource directory of QtWebEngineCore.framework does not work in onefile # mode, and consequently QtWebEngine does not work in onefile mode. # 3. copying contents of QtWebEngineCore.framework/Resource means that its Info.plist ends up in sys._MEIPASS, # causing the main process in onedir mode to be mis-identified as "QtWebEngineProcess". # # In the near future, this quagmire will hopefully be properly sorted out, but in the mean time, we have to live # with what we have been given. data_path = qt_library_info.location['DataPath'] libraries = [ 'QtCore', 'QtWebEngineCore', 'QtQuick', 'QtQml', 'QtQmlModels', 'QtNetwork', 'QtGui', 'QtWebChannel', 'QtPositioning' ] for i in libraries: framework_dir = i + '.framework' datas += hooks.collect_system_data_files( os.path.join(data_path, 'lib', framework_dir), os.path.join(rel_data_path, 'lib', framework_dir), True ) datas += [(os.path.join(data_path, 'lib', 'QtWebEngineCore.framework', 'Resources'), os.curdir)] else: # Windows and linux (or Anaconda on macOS) locales = 'qtwebengine_locales' resources = 'resources' # Translations datas.append(( os.path.join(qt_library_info.location['TranslationsPath'], locales), os.path.join(rel_data_path, 'translations', locales), )) # Resources; ``DataPath`` is the base directory for ``resources``, as per the # `docs `_. datas.append( (os.path.join(qt_library_info.location['DataPath'], resources), os.path.join(rel_data_path, resources)), ) # Helper process executable (QtWebEngineProcess), located in ``LibraryExecutablesPath``. dest = os.path.join( rel_data_path, os.path.relpath(qt_library_info.location['LibraryExecutablesPath'], qt_library_info.location['PrefixPath']) ) binaries.append((os.path.join(qt_library_info.location['LibraryExecutablesPath'], 'QtWebEngineProcess*'), dest)) # Add Linux-specific libraries. if compat.is_linux: # The automatic library detection fails for `NSS `_, which # is used by QtWebEngine. In some distributions, the ``libnss`` supporting libraries are stored in a # subdirectory ``nss``. Since ``libnss`` is not statically linked to these, but dynamically loads them, we need # to search for and add them. # First, get all libraries linked to ``QtWebEngineWidgets`` extension module. module_file = hooks.get_module_file_attribute(qt_library_info.namespace + '.QtWebEngineWidgets') module_imports = bindepend.getImports(module_file) for imp in module_imports: # Look for ``libnss3.so``. if os.path.basename(imp).startswith('libnss3.so'): # Find the location of NSS: given a ``/path/to/libnss.so``, add ``/path/to/nss/*.so`` to get the # missing NSS libraries. nss_glob = os.path.join(os.path.dirname(imp), 'nss', '*.so') if glob.glob(nss_glob): binaries.append((nss_glob, 'nss')) return binaries, datas