# ---------------------------------------------------------------------------- # Copyright (c) 2022-2023, 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) #----------------------------------------------------------------------------- # Qt modules information - the core of our Qt collection approach # ---------------------------------------------------------------- # # The python bindings for Qt (``PySide2``, ``PyQt5``, ``PySide6``, ``PyQt6``) consist of several python binary extension # modules that provide bindings for corresponding Qt modules. For example, the ``PySide2.QtNetwork`` python extension # module provides bindings for the ``QtNetwork`` Qt module from the ``qt/qtbase`` Qt repository. # # A Qt module can be considered as consisting of: # * a shared library (for example, on Linux, the shared library names for the ``QtNetwork`` Qt module in Qt5 and Qt6 # are ``libQt5Network.so`` and ``libQt6Network.so``, respectively). # * plugins: a certain type (or class) of plugins is usually associated with a single Qt module (for example, # ``imageformats`` plugins are associated with the ``QtGui`` Qt module from the ``qt/qtbase`` Qt repository), but # additional plugins of that type may come from other Qt repositories. For example, ``imageformats/qsvg`` plugin # is provided by ``qtsvg/src/plugins/imageformats/svg`` from the ``qt/qtsvg`` repository, and ``imageformats/qpdf`` # is provided by ``qtwebengine/src/pdf/plugins/imageformats/pdf`` from the ``qt/qtwebengine`` repository. # * translation files: names of translation files consist of a base name, which typically corresponds to the Qt # repository name, and language code. A single translation file usually covers all Qt modules contained within # the same repository. For example, translation files with base name ``qtbase`` contain translations for ``QtCore``, # ``QtGui``, ``QtWidgets``, ``QtNetwork``, and other Qt modules from the ``qt/qtbase`` Qt repository. # # The PyInstaller's built-in analysis of link-time dependencies ensures that when collecting a Qt python extension # module, we automatically pick up the linked Qt shared libraries. However, collection of linked Qt shared libraries # does not result in collection of plugins, nor translation files. In addition, the dependency of a Qt python extension # module on other Qt python extension modules (i.e., at the bindings level) cannot be automatically determined due to # PyInstaller's inability to scan imports in binary extensions. # # PyInstaller < 5.7 solved this problem using a dictionary that associated a Qt shared library name with python # extension name, plugins, and translation files. For each hooked Qt python extension module, the hook calls a helper # that analyzes the extension file for link-time dependencies, and matches those against the dictionary. Therefore, # based on linked shared libraries, we could recursively infer the list of files to collect in addition to the shared # libraries themselves: # - plugins and translation files belonging to Qt modules whose shared libraries we collect # - Qt python extension modules corresponding to the Qt modules that we collect # # The above approach ensures that even if analyzed python script contains only ``from PySide2 import QtWidgets``, # we would also collect ``PySide2.QtGui`` and ``PySide2.QtCore``, as well as all corresponding Qt module files # (the shared libraries, plugins, translation files). For this to work, a hook must be provided for the # ``PySide2.QtWidgets`` that performs the recursive analysis of the extension module file; so to ensure that each # Qt python extension module by itself ensures collection of all its dependencies, we need to hook all Qt python # extension modules provided by specific python Qt bindings package. # # The above approach with single dictionary, however, has several limitations: # - it cannot provide association for Qt python module that binds a Qt module without a shared library (i.e., a # headers-only module, or a statically-built module). In such cases, potential plugins and translations should # be associated directly with the Qt python extension file instead of the Qt module's (non-existent) shared library. # - it cannot (directly) handle differences between Qt5 and Qt6; we had to build a second dictionary # - it cannot handle differences between the bindings themselves; for example, PyQt5 binds some Qt modules that # PySide2 does not bind. Or, the binding's Qt python extension module is named differently in PyQt and PySide # bindings (or just differently in PyQt5, while PySide2, PySide6, and PyQt6 use the same name). # # In order address the above shortcomings, we now store all information a list of structures that contain information # for a particular Qt python extension and/or Qt module (shared library): # - python extension name (if applicable) # - Qt module name base (if applicable) # - plugins # - translation files base name # - applicable Qt version (if necessary) # - applicable Qt bindings (if necessary) # # This list is used to dynamically construct two dictionaries (based on the bindings name and Qt version): # - mapping python extension names to associated module information # - mapping Qt shared library names to associated module information # This allows us to associate plugins and translations with either Qt python extension or with the Qt module's shared # library (or both), whichever is applicable. # # The `qt_dynamic_dependencies_dict`_ from the original approach was constructed using several information sources, as # documented `here # ˙_. # # In the current approach, the relations stored in the `QT_MODULES_INFO`_ list were determined directly, by inspecting # the Qt source code. This requires some prior knowledge of how the Qt code is organized (repositories and individual Qt # modules within them), as well as some searching based on guesswork. The procedure can be outlined as follows: # * check out the `main Qt repository `_. This repository contains references to all other # Qt repositories in the form of git submodules. # * for Qt5: # * check out the latest release tag, e.g., v5.15.2, then check out the submodules. # * search the Qt modules' qmake .pro files; for example, ``qtbase/src/network/network.pro`` for QtNetwork module. # The plugin types associated with the module are listed in the ``MODULE_PLUGIN_TYPES`` variable (in this case, # ``bearer``). # * all translations are gathered in ``qttranslations`` sub-module/repository, and their association with # individual repositories can be seen in ``qttranslations/translations/translations.pro``. # * for Qt6: # * check out the latest release tag, e.g., v6.3.1, then check out the submodules. # * search the Qt modules' CMake files; for example, ``qtbase/src/network/CMakeLists.txt`` for QtNetwork module. # The plugin types associated with the module are listed under ``PLUGIN_TYPES`` argument of the # ``qt_internal_add_module()`` function that defines the Qt module. # # The idea is to make a list of all extension modules found in a Qt bindings package, as well as all available plugin # directories (which correspond to plugin types) and translation files. For each extension, identify the corresponding # Qt module (shared library name) and its associated plugins and translation files. Once this is done, most of available # plugins and translations in the python bindings package should have a corresponding python Qt extension module # available; this gives us associations based on the python extension module names as well as based on the Qt shared # library names. For any plugins and translation files remaining unassociated, identify the corresponding Qt module; # this gives us associations based only on Qt shared library names. While this second group of associations are never # processed directly (due to lack of corresponding python extension), they may end up being processed during the # recursive dependency analysis, if the corresponding Qt shared library is linked against by some Qt python extension # or another Qt shared library. # This structure is used to define Qt module information, such as python module/extension name, Qt module (shared # library) name, translations file base name, plugins, as well as associated python bindings (which implicitly # also encode major Qt version). class _QtModuleDef: def __init__(self, module, shared_lib=None, translation=None, plugins=None, bindings=None): # Python module (extension) name without package namespace. For example, `QtCore`. # Can be None if python bindings do not bind the module, but we still need to establish relationship between # the Qt module (shared library) and its plugins and translations. self.module = module # Associated Qt module (shared library), if any. Used during recursive dependency analysis, where a python # module (extension) is analyzed for linked Qt modules (shared libraries), and then their corresponding # python modules (extensions) are added to hidden imports. For example, the Qt module name is `Qt5Core` or # `Qt6Core`, depending on the Qt version. Can be None for python modules that are not tied to a particular # Qt shared library (for example, the corresponding Qt module is headers-only) and hence they cannot be # inferred from recursive link-time dependency analysis. self.shared_lib = shared_lib # Base name of translation files (if any) associated with the Qt module. For example, `qtcore`. self.translation = translation # List of plugins associated with the Qt module. self.plugins = plugins or [] # List of bindings (PySide2, PyQt5, PySide6, PyQt6) that provide the python module. This allows association of # plugins and translations with shared libraries even for bindings that do not provide python module binding # for the Qt module. self.bindings = set(bindings or []) # All Qt-based bindings. ALL_QT_BINDINGS = {"PySide2", "PyQt5", "PySide6", "PyQt6"} # Qt modules information - the core of our Qt collection approach. # # For every python module/extension (i.e., entry in the list below that has valid `module`), we need a corresponding # hook, ensuring that the extension file is analyzed, so that we collect the associated plugins and translation # files, as well as perform recursive analysis of link-time binary dependencies (so that plugins and translation files # belonging to those dependencies are collected as well). QT_MODULES_INFO = ( # *** qt/qt3d *** _QtModuleDef("Qt3DAnimation", shared_lib="3DAnimation"), _QtModuleDef("Qt3DCore", shared_lib="3DCore"), _QtModuleDef("Qt3DExtras", shared_lib="3DExtras"), _QtModuleDef("Qt3DInput", shared_lib="3DInput", plugins=["3dinputdevices"]), _QtModuleDef("Qt3DLogic", shared_lib="3DLogic"), _QtModuleDef( "Qt3DRender", shared_lib="3DRender", plugins=["geometryloaders", "renderplugins", "renderers", "sceneparsers"] ), # *** qt/qtactiveqt *** # The python module is called QAxContainer in PyQt bindings, but QtAxContainer in PySide. The associated Qt module # is header-only, so there is no shared library. _QtModuleDef("QAxContainer", bindings=["PyQt*"]), _QtModuleDef("QtAxContainer", bindings=["PySide*"]), # *** qt/qtcharts *** # The python module is called QtChart in PyQt5, and QtCharts in PySide2, PySide6, and PyQt6 (which corresponds to # the associated Qt module name, QtCharts). _QtModuleDef("QtChart", shared_lib="Charts", bindings=["PyQt5"]), _QtModuleDef("QtCharts", shared_lib="Charts", bindings=["!PyQt5"]), # *** qt/qtbase *** # QtConcurrent python module is available only in PySide bindings. _QtModuleDef(None, shared_lib="Concurrent", bindings=["PyQt*"]), _QtModuleDef("QtConcurrent", shared_lib="Concurrent", bindings=["PySide*"]), _QtModuleDef("QtCore", shared_lib="Core", translation="qtbase"), # QtDBus python module is available in all bindings but PySide2. _QtModuleDef(None, shared_lib="DBus", bindings=["PySide2"]), _QtModuleDef("QtDBus", shared_lib="DBus", bindings=["!PySide2"]), # QtNetwork uses different plugins in Qt5 and Qt6. _QtModuleDef("QtNetwork", shared_lib="Network", plugins=["bearer"], bindings=["PySide2", "PyQt5"]), _QtModuleDef( "QtNetwork", shared_lib="Network", plugins=["networkaccess", "networkinformation", "tls"], bindings=["PySide6", "PyQt6"] ), _QtModuleDef( "QtGui", shared_lib="Gui", plugins=[ "accessiblebridge", "egldeviceintegrations", "generic", "iconengines", "imageformats", "platforms", "platforms/darwin", "platforminputcontexts", "platformthemes", "xcbglintegrations", # The ``wayland-*`` plugins are part of QtWaylandClient 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 QtWaylandClient shared library entry, but as our Qt hook utilities do # not scan the plugins for dependencies, that would not work. So instead we list these plugins under QtGui # to achieve pretty much the same end result. "wayland-decoration-client", "wayland-graphics-integration-client", "wayland-shell-integration" ] ), _QtModuleDef("QtOpenGL", shared_lib="OpenGL"), # This python module is specific to PySide2 and has no associated Qt module. _QtModuleDef("QtOpenGLFunctions", bindings=["PySide2"]), # This Qt module was introduced with Qt6. _QtModuleDef("QtOpenGLWidgets", shared_lib="OpenGLWidgets", bindings=["PySide6", "PyQt6"]), _QtModuleDef("QtPrintSupport", shared_lib="PrintSupport", plugins=["printsupport"]), _QtModuleDef("QtSql", shared_lib="Sql", plugins=["sqldrivers"]), _QtModuleDef("QtTest", shared_lib="Test"), _QtModuleDef("QtWidgets", shared_lib="Widgets", plugins=["styles"]), _QtModuleDef("QtXml", shared_lib="Xml"), # *** qt/qtconnectivity *** _QtModuleDef("QtBluetooth", shared_lib="QtBluetooth", translation="qtconnectivity"), _QtModuleDef("QtNfc", shared_lib="Nfc", translation="qtconnectivity"), # *** qt/qtdatavis3d *** _QtModuleDef("QtDataVisualization", shared_lib="DataVisualization"), # *** qt/qtdeclarative *** _QtModuleDef("QtQml", shared_lib="Qml", translation="qtdeclarative", plugins=["qmltooling"]), # Have the Qt5 variant collect translations for qtquickcontrols (qt/qtquickcontrols provides only QtQuick plugins). _QtModuleDef( "QtQuick", shared_lib="Quick", translation="qtquickcontrols", plugins=["scenegraph"], bindings=["PySide2", "PyQt5"] ), _QtModuleDef("QtQuick", shared_lib="Quick", plugins=["scenegraph"], bindings=["PySide6", "PyQt6"]), # Qt6-only; in Qt5, this module is part of qt/qtquickcontrols2. Python module is available only in PySide6. _QtModuleDef(None, shared_lib="QuickControls2", bindings=["PyQt6"]), _QtModuleDef("QtQuickControls2", shared_lib="QuickControls2", bindings=["PySide6"]), _QtModuleDef("QtQuickWidgets", shared_lib="QuickWidgets"), # *** qt/qtgamepad *** # No python module; shared library -> plugins association entry. _QtModuleDef(None, shared_lib="Gamepad", plugins=["gamepads"]), # *** qt/qthttpserver *** # Qt6 >= 6.4.0; python module is available only in PySide6. _QtModuleDef("QtHttpServer", shared_lib="HttpServer", bindings=["PySide6"]), # *** qt/qtlocation *** # QtLocation was reintroduced in Qt6 v6.5.0. _QtModuleDef( "QtLocation", shared_lib="Location", translation="qtlocation", plugins=["geoservices"], bindings=["PySide2", "PyQt5", "PySide6"] ), _QtModuleDef( "QtPositioning", shared_lib="Positioning", translation="qtlocation", plugins=["position"], ), # *** qt/qtmacextras *** # Qt5-only Qt module. _QtModuleDef("QtMacExtras", shared_lib="MacExtras", bindings=["PySide2", "PyQt5"]), # *** qt/qtmultimedia *** # QtMultimedia on Qt6 currently uses only a subset of plugin names from Qt5 counterpart. _QtModuleDef( "QtMultimedia", shared_lib="Multimedia", translation="qtmultimedia", plugins=[ "mediaservice", "audio", "video/bufferpool", "video/gstvideorenderer", "video/videonode", "playlistformats", "resourcepolicy" ], bindings=["PySide2", "PyQt5"] ), _QtModuleDef( "QtMultimedia", shared_lib="Multimedia", translation="qtmultimedia", # `multimedia` plugins are available as of Qt6 >= 6.4.0; earlier versions had `video/gstvideorenderer` and # `video/videonode` plugins. plugins=["multimedia", "video/gstvideorenderer", "video/videonode"], bindings=["PySide6", "PyQt6"] ), _QtModuleDef("QtMultimediaWidgets", shared_lib="MultimediaWidgets"), # Qt6-only Qt module; python module is available in PySide6 >= 6.4.0 and PyQt6 >= 6.5.0 _QtModuleDef("QtSpatialAudio", shared_lib="SpatialAudio", bindings=["PySide6", "PyQt6"]), # *** qt/qtnetworkauth *** # QtNetworkAuth python module is available in all bindings but PySide2. _QtModuleDef(None, shared_lib="NetworkAuth", bindings=["PySide2"]), _QtModuleDef("QtNetworkAuth", shared_lib="NetworkAuth", bindings=["!PySide2"]), # *** qt/qtpurchasing *** # Qt5-only Qt module, python module is available only in PyQt5. _QtModuleDef("QtPurchasing", shared_lib="Purchasing", bindings=["PyQt5"]), # *** qt/qtquick1 *** # This is an old, Qt 5.3-era module... _QtModuleDef( "QtDeclarative", shared_lib="Declarative", translation="qtquick1", plugins=["qml1tooling"], bindings=["PySide2", "PyQt5"] ), # *** qt/qtquick3d *** # QtQuick3D python module is available in all bindings but PySide2. _QtModuleDef(None, shared_lib="Quick3D", bindings=["PySide2"]), _QtModuleDef("QtQuick3D", shared_lib="Quick3D", bindings=["!PySide2"]), # No python module; shared library -> plugins association entry. _QtModuleDef(None, shared_lib="Quick3DAssetImport", plugins=["assetimporters"]), # *** qt/qtquickcontrols2 *** # Qt5-only module; in Qt6, this module is part of qt/declarative. Python module is available only in PySide2. _QtModuleDef(None, translation="qtquickcontrols2", shared_lib="QuickControls2", bindings=["PyQt5"]), _QtModuleDef("QtQuickControls2", translation="qtquickcontrols2", shared_lib="QuickControls2", bindings=["PySide2"]), # *** qt/qtremoteobjects *** _QtModuleDef("QtRemoteObjects", shared_lib="RemoteObjects"), # *** qt/qtscxml *** # Python module is available only in PySide bindings. Plugins are available only in Qt6. # PyQt wheels do not seem to ship the corresponding Qt modules (shared libs) at all. _QtModuleDef("QtScxml", shared_lib="Scxml", bindings=["PySide2"]), _QtModuleDef("QtScxml", shared_lib="Scxml", plugins=["scxmldatamodel"], bindings=["PySide6"]), # Qt6-only Qt module, python module is available only in PySide6. _QtModuleDef("QtStateMachine", shared_lib="StateMachine", bindings=["PySide6"]), # *** qt/qtsensors *** _QtModuleDef("QtSensors", shared_lib="Sensors", plugins=["sensors", "sensorgestures"]), # *** qt/qtserialport *** _QtModuleDef("QtSerialPort", shared_lib="SerialPort", translation="qtserialport"), # *** qt/qtscript *** # Qt5-only Qt module, python module is available only in PySide2. PyQt5 wheels do not seem to ship the corresponding # Qt modules (shared libs) at all. _QtModuleDef("QtScript", shared_lib="Script", translation="qtscript", plugins=["script"], bindings=["PySide2"]), _QtModuleDef("QtScriptTools", shared_lib="ScriptTools", bindings=["PySide2"]), # *** qt/qtserialbus *** # No python module; shared library -> plugins association entry. # PySide6 6.5.0 introduced python module. _QtModuleDef(None, shared_lib="SerialBus", plugins=["canbus"], bindings=["!PySide6"]), _QtModuleDef("QtSerialBus", shared_lib="SerialBus", plugins=["canbus"], bindings=["PySide6"]), # *** qt/qtsvg *** _QtModuleDef("QtSvg", shared_lib="Svg"), # Qt6-only Qt module. _QtModuleDef("QtSvgWidgets", shared_lib="SvgWidgets", bindings=["PySide6", "PyQt6"]), # *** qt/qtspeech *** _QtModuleDef("QtTextToSpeech", shared_lib="TextToSpeech", plugins=["texttospeech"]), # *** qt/qttools *** # QtDesigner python module is available in all bindings but PySide2. _QtModuleDef(None, shared_lib="Designer", plugins=["designer"], bindings=["PySide2"]), _QtModuleDef( "QtDesigner", shared_lib="Designer", translation="designer", plugins=["designer"], bindings=["!PySide2"] ), _QtModuleDef("QtHelp", shared_lib="Help", translation="qt_help"), # Python module is available only in PySide bindings. _QtModuleDef("QtUiTools", shared_lib="UiTools", bindings=["PySide*"]), # *** qt/qtvirtualkeyboard *** # No python module; shared library -> plugins association entry. _QtModuleDef(None, shared_lib="VirtualKeyboard", plugins=["virtualkeyboard"]), # *** qt/qtwebchannel *** _QtModuleDef("QtWebChannel", shared_lib="WebChannel"), # *** qt/qtwebengine *** # QtWebEngine is Qt5-only module (replaced by QtWebEngineQuick in Qt6). _QtModuleDef("QtWebEngine", shared_lib="WebEngine", bindings=["PySide2", "PyQt5"]), _QtModuleDef("QtWebEngineCore", shared_lib="WebEngineCore", translation="qtwebengine"), # QtWebEngineQuick is Qt6-only module (replacement for QtWebEngine in Qt5). _QtModuleDef("QtWebEngineQuick", shared_lib="WebEngineQuick", bindings=["PySide6", "PyQt6"]), _QtModuleDef("QtWebEngineWidgets", shared_lib="WebEngineWidgets"), # QtPdf and QtPdfWidgets have python module available in PySide6 and PyQt6 >= 6.4.0. _QtModuleDef("QtPdf", shared_lib="Pdf", bindings=["PySide6", "PyQt6"]), _QtModuleDef("QtPdfWidgets", shared_lib="PdfWidgets", bindings=["PySide6", "PyQt6"]), # *** qt/qtwebsockets *** _QtModuleDef("QtWebSockets", shared_lib="WebSockets", translation="qtwebsockets"), # *** qt/qtwebview *** # No python module; shared library -> plugins association entry. _QtModuleDef(None, shared_lib="WebView", plugins=["webview"]), # *** qt/qtwinextras *** # Qt5-only Qt module. _QtModuleDef("QtWinExtras", shared_lib="WinExtras", bindings=["PySide2", "PyQt5"]), # *** qt/qtx11extras *** # Qt5-only Qt module. _QtModuleDef("QtX11Extras", shared_lib="X11Extras", bindings=["PySide2", "PyQt5"]), # *** qt/qtxmlpatterns *** # Qt5-only Qt module. _QtModuleDef("QtXmlPatterns", shared_lib="XmlPatterns", translation="qtxmlpatterns", bindings=["PySide2", "PyQt5"]), # *** qscintilla *** # Python module is available only in PyQt bindings. No associated shared library. _QtModuleDef("Qsci", translation="qscintilla", bindings=["PyQt*"]), ) # Helpers for turning Qt namespace specifiers, such as "!PySide2" or "PyQt*", into set of applicable # namespaces. def process_namespace_strings(namespaces): """"Process list of Qt namespace specifier strings into set of namespaces.""" bindings = set() for namespace in namespaces: bindings |= _process_namespace_string(namespace) return bindings def _process_namespace_string(namespace): """Expand a Qt namespace specifier string into set of namespaces.""" if namespace.startswith("!"): bindings = _process_namespace_string(namespace[1:]) return ALL_QT_BINDINGS - bindings else: if namespace == "PySide*": return {"PySide2", "PySide6"} elif namespace == "PyQt*": return {"PyQt5", "PyQt6"} elif namespace in ALL_QT_BINDINGS: return {namespace} else: raise ValueError(f"Invalid Qt namespace specifier: {namespace}!")