qt.py 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818
  1. # ----------------------------------------------------------------------------
  2. # Copyright (c) 2005-2021, PyInstaller Development Team.
  3. #
  4. # Distributed under the terms of the GNU General Public License (version 2
  5. # or later) with exception for distributing the bootloader.
  6. #
  7. # The full license is in the file COPYING.txt, distributed with this software.
  8. #
  9. # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
  10. #-----------------------------------------------------------------------------
  11. import glob
  12. import json
  13. import os
  14. from PyInstaller import compat
  15. from PyInstaller import log as logging
  16. from PyInstaller.depend import bindepend
  17. from PyInstaller.utils import hooks, misc
  18. logger = logging.getLogger(__name__)
  19. # QtLibraryInfo
  20. # --------------
  21. # This class uses introspection to determine the location of Qt files. This is essential to deal with the many variants
  22. # of the PyQt5/6 and PySide2/6 package, each of which places files in a different location. Therefore, this class
  23. # provides all location-related members of `QLibraryInfo <http://doc.qt.io/qt-5/qlibraryinfo.html>`_.
  24. class QtLibraryInfo:
  25. def __init__(self, namespace):
  26. if namespace not in ['PyQt5', 'PyQt6', 'PySide2', 'PySide6']:
  27. raise Exception('Invalid namespace: {0}'.format(namespace))
  28. self.namespace = namespace
  29. # Distinction between PyQt5/6 and PySide2/6
  30. self.is_pyqt = namespace in {'PyQt5', 'PyQt6'}
  31. # Distinction between Qt5 and Qt6
  32. self.qt_major = 6 if namespace in {'PyQt6', 'PySide6'} else 5
  33. # Determine relative path where Qt libraries and data need to be collected in the frozen application. This
  34. # varies between PyQt5/PyQt6/PySide2/PySide6, their versions, and platforms. NOTE: it is tempting to consider
  35. # deriving this path as simply the value of QLibraryInfo.PrefixPath, taken relative to the package's root
  36. # directory. However, we also need to support non-wheel deployments (e.g., with Qt installed in custom path on
  37. # Windows, or with Qt and PyQt5 installed on linux using native package manager), and in those, the Qt
  38. # PrefixPath does not reflect the required relative target path for the frozen application.
  39. if namespace == 'PyQt5':
  40. # PyQt5 uses PyQt5/Qt on all platforms, or PyQt5/Qt5 from version 5.15.4 on
  41. try:
  42. # The call below might fail with AttributeError on some PyQt5 versions (e.g., 5.9.2 from conda's main
  43. # channel); missing dist information forces a fallback codepath that tries to check for __version__
  44. # attribute that does not exist, either. So handle the error gracefully and assume old layout.
  45. new_layout = hooks.is_module_satisfies("PyQt5 >= 5.15.4")
  46. except AttributeError:
  47. new_layout = False
  48. if new_layout:
  49. self.qt_rel_dir = os.path.join('PyQt5', 'Qt5')
  50. else:
  51. self.qt_rel_dir = os.path.join('PyQt5', 'Qt')
  52. elif namespace == 'PyQt6':
  53. # Similarly to PyQt5, PyQt6 switched from PyQt6/Qt to PyQt6/Qt6 in 6.0.3
  54. try:
  55. # The call below might fail with AttributeError in case of a partial PyQt6 installation. For example,
  56. # user installs PyQt6 via pip, which also installs PyQt6-Qt6 and PyQt6-sip. Then they naively uninstall
  57. # PyQt6 package, which leaves the other two behind. PyQt6 now becomes a namespace package and there is
  58. # no dist metadata, so a fallback codepath in is_module_satisfies tries to check for __version__
  59. # attribute that does not exist, either. Handle such errors gracefully and assume new layout (with
  60. # PyQt6, the new layout is more likely); it does not really matter what layout we assume, as library is
  61. # not usable anyway, but we do neeed to be able to return an instance of QtLibraryInfo with "version"
  62. # attribute set to a falsey value.
  63. new_layout = hooks.is_module_satisfies("PyQt6 >= 6.0.3")
  64. except AttributeError:
  65. new_layout = True
  66. if new_layout:
  67. self.qt_rel_dir = os.path.join('PyQt6', 'Qt6')
  68. else:
  69. self.qt_rel_dir = os.path.join('PyQt6', 'Qt')
  70. elif namespace == 'PySide2':
  71. # PySide2 uses PySide2/Qt on linux and macOS, and PySide2 on Windows
  72. if compat.is_win:
  73. self.qt_rel_dir = 'PySide2'
  74. else:
  75. self.qt_rel_dir = os.path.join('PySide2', 'Qt')
  76. else:
  77. # PySide6 follows the same logic as PySide2
  78. if compat.is_win:
  79. self.qt_rel_dir = 'PySide6'
  80. else:
  81. self.qt_rel_dir = os.path.join('PySide6', 'Qt')
  82. # Initialize most of this class only when values are first requested from it.
  83. def __getattr__(self, name):
  84. if 'version' in self.__dict__:
  85. # Initialization was already done, but requested attribute is not availiable.
  86. raise AttributeError(name)
  87. else:
  88. # Ensure self.version exists, even if PyQt{5,6}/PySide{2,6} cannot be imported. Hooks and util functions use
  89. # `if .version` to check whether package was imported and other attributes are expected to be available.
  90. # This also serves as a marker that initialization was already done.
  91. self.version = None
  92. # Get library path information from Qt. See QLibraryInfo_.
  93. json_str = hooks.exec_statement(
  94. """
  95. import sys
  96. # exec_statement only captures stdout. If there are errors, capture them to stdout so they can be
  97. # displayed to the user. Do this early, in case package imports produce stderr output.
  98. sys.stderr = sys.stdout
  99. import json
  100. try:
  101. from %s.QtCore import QLibraryInfo, QCoreApplication
  102. except Exception:
  103. print('False')
  104. raise SystemExit(0)
  105. # QLibraryInfo is not always valid until a QCoreApplication is instantiated.
  106. app = QCoreApplication(sys.argv)
  107. # Qt6 deprecated QLibraryInfo.location() in favor of QLibraryInfo.path(), and
  108. # QLibraryInfo.LibraryLocation enum was replaced by QLibraryInfo.LibraryPath.
  109. if hasattr(QLibraryInfo, 'path'):
  110. # Qt6; enumerate path enum values directly from the QLibraryInfo.LibraryPath enum.
  111. path_names = [x for x in dir(QLibraryInfo.LibraryPath) if x.endswith('Path')]
  112. location = {x: QLibraryInfo.path(getattr(QLibraryInfo.LibraryPath, x)) for x in path_names}
  113. else:
  114. # Qt5; in recent versions, location enum values can be enumeratd from QLibraryInfo.LibraryLocation.
  115. # However, in older versions of Qt5 and its python bindings, that is unavailable. Hence the
  116. # enumeration of "*Path"-named members of QLibraryInfo.
  117. path_names = [x for x in dir(QLibraryInfo) if x.endswith('Path')]
  118. location = {x: QLibraryInfo.location(getattr(QLibraryInfo, x)) for x in path_names}
  119. # Determine Qt version. Works for Qt 5.8 and later, where QLibraryInfo.version() was introduced.
  120. try:
  121. version = QLibraryInfo.version().segments()
  122. except AttributeError:
  123. version = []
  124. print(json.dumps({
  125. 'isDebugBuild': QLibraryInfo.isDebugBuild(),
  126. 'version': version,
  127. 'location': location,
  128. }))
  129. """ % self.namespace
  130. )
  131. try:
  132. qli = json.loads(json_str)
  133. except Exception as e:
  134. logger.warning('Cannot read QLibraryInfo output: raised %s when decoding:\n%s', str(e), json_str)
  135. qli = {}
  136. for k, v in qli.items():
  137. setattr(self, k, v)
  138. return getattr(self, name)
  139. # Provide single instances of this class to avoid each hook constructing its own.
  140. pyqt5_library_info = QtLibraryInfo('PyQt5')
  141. pyqt6_library_info = QtLibraryInfo('PyQt6')
  142. pyside2_library_info = QtLibraryInfo('PySide2')
  143. pyside6_library_info = QtLibraryInfo('PySide6')
  144. def get_qt_library_info(namespace):
  145. """
  146. Return Qt5LibraryInfo instance for the given namespace.
  147. """
  148. if namespace == 'PyQt5':
  149. return pyqt5_library_info
  150. if namespace == 'PyQt6':
  151. return pyqt6_library_info
  152. elif namespace == 'PySide2':
  153. return pyside2_library_info
  154. elif namespace == 'PySide6':
  155. return pyside6_library_info
  156. raise ValueError(f'Invalid namespace: {namespace}!')
  157. def qt_plugins_dir(namespace):
  158. """
  159. Return list of paths searched for plugins.
  160. :param namespace: Import namespace (PyQt5, PyQt6, PySide2, or PySide6).
  161. :return: Plugin directory paths
  162. """
  163. qt_info = get_qt_library_info(namespace)
  164. paths = [qt_info.location['PluginsPath']]
  165. if not paths:
  166. raise Exception('Cannot find {0} plugin directories'.format(namespace))
  167. else:
  168. valid_paths = []
  169. for path in paths:
  170. if os.path.isdir(path):
  171. valid_paths.append(str(path)) # must be 8-bit chars for one-file builds
  172. qt_plugin_paths = valid_paths
  173. if not qt_plugin_paths:
  174. raise Exception(
  175. "Cannot find existing {0} plugin directories. Paths checked: {1}".format(namespace, ", ".join(paths))
  176. )
  177. return qt_plugin_paths
  178. def _qt_filter_release_plugins(plugin_files):
  179. """
  180. Filter the provided list of Qt plugin files and remove the debug variants, under the assumption that both the
  181. release version of a plugin (qtplugin.dll) and its debug variant (qtplugind.dll) appear in the list.
  182. """
  183. # All basenames for lookup
  184. plugin_basenames = {os.path.normcase(os.path.basename(f)) for f in plugin_files}
  185. # Process all given filenames
  186. release_plugin_files = []
  187. for plugin_filename in plugin_files:
  188. plugin_basename = os.path.normcase(os.path.basename(plugin_filename))
  189. if plugin_basename.endswith('d.dll'):
  190. # If we can find a variant without trailing 'd' in the plugin list, then the DLL we are dealing with is a
  191. # debug variant and needs to be excluded.
  192. release_name = os.path.splitext(plugin_basename)[0][:-1] + '.dll'
  193. if release_name in plugin_basenames:
  194. continue
  195. release_plugin_files.append(plugin_filename)
  196. return release_plugin_files
  197. def qt_plugins_binaries(plugin_type, namespace):
  198. """
  199. Return list of dynamic libraries formatted for mod.binaries.
  200. :param plugin_type: Plugin to look for
  201. :param namespace: Import namespace (PyQt5, PyQt6, PySide2, or PySide6)
  202. :return: Plugin directory path corresponding to the given plugin_type
  203. """
  204. qt_info = get_qt_library_info(namespace)
  205. pdir = qt_plugins_dir(namespace)
  206. files = []
  207. for path in pdir:
  208. files.extend(misc.dlls_in_dir(os.path.join(path, plugin_type)))
  209. # Windows:
  210. #
  211. # dlls_in_dir() grabs all files ending with ``*.dll``, ``*.so`` and ``*.dylib`` in a certain directory. On Windows
  212. # this would grab debug copies of Qt plugins, which then causes PyInstaller to add a dependency on the Debug CRT
  213. # *in addition* to the release CRT.
  214. if compat.is_win:
  215. files = _qt_filter_release_plugins(files)
  216. logger.debug("Found plugin files %s for plugin %s", files, plugin_type)
  217. dest_dir = os.path.join(qt_info.qt_rel_dir, 'plugins', plugin_type)
  218. binaries = [(f, dest_dir) for f in files]
  219. return binaries
  220. # Qt deployment approach
  221. # ----------------------
  222. # This is the core of PyInstaller's approach to Qt deployment. It's based on:
  223. #
  224. # - Discovering the location of Qt libraries by introspection, using QtLibraryInfo_. This provides compatibility with
  225. # many variants of Qt5/6 (conda, self-compiled, provided by a Linux distro, etc.) and many versions of Qt5/6, all of
  226. # which vary in the location of Qt files.
  227. # - Placing all frozen PyQt5/6 or PySide2/6 Qt files in a standard subdirectory layout, which matches the layout of the
  228. # corresponding wheel on PyPI. This is necessary to support Qt installs which are not in a subdirectory of the PyQt5/6
  229. # or PySide2/6 wrappers. See ``hooks/rthooks/pyi_rth_qt5.py`` for the use of environment variables to establish this
  230. # layout.
  231. # - Emitting warnings on missing QML and translation files which some installations don't have.
  232. # - Determining additional files needed for deployment by following the Qt deployment process using
  233. # _qt_dynamic_dependencies_dict`_ and add_qt_dependencies_.
  234. #
  235. # _qt_dynamic_dependencies_dict
  236. # -----------------------------
  237. # This dictionary provides dynamics dependencies (plugins and translations) that cannot be discovered using
  238. # ``getImports``. It was built by combining information from:
  239. #
  240. # - Qt `deployment <http://doc.qt.io/qt-5/deployment.html>`_ docs. Specifically:
  241. #
  242. # - The `deploying Qt for Linux/X11 <http://doc.qt.io/qt-5/linux-deployment.html#qt-plugins>`_ page specifies
  243. # including the Qt Platform Abstraction (QPA) plugin, ``libqxcb.so``. There's little other guidance provided.
  244. # - The `Qt for Windows - Deployment
  245. # <http://doc.qt.io/qt-5/windows-deployment.html#creating-the-application-package>`_ page likewise specifies
  246. # the ``qwindows.dll`` QPA. This is found by the dependency walker, so it does not need to explicitly specified.
  247. #
  248. # - For dynamic OpenGL applications, the ``libEGL.dll``, ``libGLESv2.dll``, ``d3dcompiler_XX.dll`` (the XX is a
  249. # version number), and ``opengl32sw.dll`` libraries are also needed.
  250. # - If Qt was configured to use ICU, the ``icudtXX.dll``, ``icuinXX.dll``, and ``icuucXX.dll`` libraries are
  251. # needed.
  252. #
  253. # These are included by ``hook-PyQt5.py``.
  254. #
  255. # - The `Qt for macOS - Deployment <http://doc.qt.io/qt-5/osx-deployment.html#qt-plugins>`_ page specifies the
  256. # ``libqcocoa.dylib`` QPA, but little else. The `Mac deployment tool
  257. # <http://doc.qt.io/qt-5/osx-deployment.html#the-mac-deployment-tool>`_ provides the following rules:
  258. #
  259. # - The platform plugin is always deployed.
  260. # - The image format plugins are always deployed.
  261. # - The print support plugin is always deployed.
  262. # - SQL driver plugins are deployed if the application uses the Qt SQL module.
  263. # - Script plugins are deployed if the application uses the Qt Script module.
  264. # - The SVG icon plugin is deployed if the application uses the Qt SVG module.
  265. # - The accessibility plugin is always deployed.
  266. #
  267. # - Per the `Deploying QML Applications <http://doc.qt.io/qt-5/qtquick-deployment.html>`_ page, QML-based
  268. # applications need the ``qml/`` directory available.
  269. #
  270. # This is handled by ``hook-PyQt5.QtQuick.py``.
  271. #
  272. # - Per the `Deploying Qt WebEngine Applications <https://doc.qt.io/qt-5.10/qtwebengine-deploying.html>`_
  273. # page, deployment may include:
  274. #
  275. # - Libraries (handled when PyInstaller following dependencies).
  276. # - QML imports (if Qt Quick integration is used).
  277. # - Qt WebEngine process, which should be located at
  278. # ``QLibraryInfo::location(QLibraryInfo::LibraryExecutablesPath)``
  279. # for Windows and Linux, and in ``.app/Helpers/QtWebEngineProcess`` for Mac.
  280. # - Resources: the files listed in deployWebEngineCore_.
  281. # - Translations: on macOS: ``.app/Content/Resources``; on Linux and Windows: ``qtwebengine_locales``
  282. # directory in the directory specified by ``QLibraryInfo::location(QLibraryInfo::TranslationsPath)``.
  283. # - Audio and video codecs: Probably covered if Qt5Multimedia is referenced?
  284. #
  285. # This is handled by ``hook-PyQt5.QtWebEngineWidgets.py``.
  286. #
  287. # - Since `QAxContainer <http://doc.qt.io/qt-5/activeqt-index.html>`_ is a statically-linked library, it
  288. # does not need any special handling.
  289. #
  290. # - Sources for the `Windows Deployment Tool
  291. # <http://doc.qt.io/qt-5/windows-deployment.html#the-windows-deployment-tool>`_ show more detail:
  292. #
  293. # - The `PluginModuleMapping struct
  294. # <https://code.woboq.org/qt5/qttools/src/windeployqt/main.cpp.html#PluginModuleMapping>`_ and the following
  295. # ``pluginModuleMappings`` global provide a mapping between a plugin directory name and an `enum of Qt plugin
  296. # names <https://code.woboq.org/qt5/qttools/src/windeployqt/main.cpp.html#QtModule>`_.
  297. # - The `QtModuleEntry struct <https://code.woboq.org/qt5/qttools/src/windeployqt/main.cpp.html#QtModuleEntry>`_
  298. # and ``qtModuleEntries`` global connect this enum to the name of the Qt5 library it represents and to the
  299. # translation files this library requires. (Ignore the ``option`` member -- it's just for command-line parsing.)
  300. #
  301. # Manually combining these two provides a mapping of Qt library names to the translation and plugin(s) needed by the
  302. # library. The process is: take the key of the dict below from ``QtModuleEntry.libraryName``, but make it lowercase
  303. # (since Windows files will be normalized to lowercase). The ``QtModuleEntry.translation`` provides the
  304. # ``translation_base``. Match the ``QtModuleEntry.module`` with ``PluginModuleMapping.module`` to find the
  305. # ``PluginModuleMapping.directoryName`` for the required plugin(s).
  306. #
  307. # - The `deployWebEngineCore
  308. # <https://code.woboq.org/qt5/qttools/src/windeployqt/main.cpp.html#_ZL19deployWebEngineCoreRK4QMapI7QStringS0_ERK7OptionsbPS0_>`_
  309. # function copies the following files from ``resources/``, and also copies the web engine process executable.
  310. #
  311. # - ``icudtl.dat``
  312. # - ``qtwebengine_devtools_resources.pak``
  313. # - ``qtwebengine_resources.pak``
  314. # - ``qtwebengine_resources_100p.pak``
  315. # - ``qtwebengine_resources_200p.pak``
  316. #
  317. # - Sources for the `Mac deployment tool`_ are less helpful. The `deployPlugins
  318. # <https://code.woboq.org/qt5/qttools/src/macdeployqt/shared/shared.cpp.html#_Z13deployPluginsRK21ApplicationBundleInfoRK7QStringS2_14DeploymentInfob>`_
  319. # function seems to:
  320. #
  321. # - Always include ``platforms/libqcocoa.dylib``.
  322. # - Always include ``printsupport/libcocoaprintersupport.dylib``
  323. # - Include ``bearer/`` if ``QtNetwork`` is included (and some other condition I didn't look up).
  324. # - Always include ``imageformats/``, except for ``qsvg``.
  325. # - Include ``imageformats/qsvg`` if ``QtSvg`` is included.
  326. # - Always include ``iconengines/``.
  327. # - Include ``sqldrivers/`` if ``QtSql`` is included.
  328. # - Include ``mediaservice/`` and ``audio/`` if ``QtMultimedia`` is included.
  329. #
  330. # The always includes will be handled by ``hook-PyQt5.py`` or ``hook-PySide2.py``; optional includes are already
  331. # covered by the dict below.
  332. #
  333. _qt5_dynamic_dependencies_dict = {
  334. #- "lib_name": (.hiddenimports, translations_base, zero or more plugins...)
  335. "qt53dcore": (None, None, ), # noqa
  336. "qt53dinput": (None, None, ), # noqa
  337. "qt53dquick": (None, None, ), # noqa
  338. "qt53dquickrender": (None, None, ), # noqa
  339. "qt53drender": (None, None, "sceneparsers", "renderplugins", "geometryloaders"), # noqa
  340. "qt5bluetooth": (".QtBluetooth", None, ), # noqa
  341. "qt5concurrent": (None, "qtbase", ), # noqa
  342. "qt5core": (".QtCore", "qtbase", ), # noqa
  343. "qt5dbus": (".QtDBus", None, ), # noqa
  344. "qt5declarative": (None, "qtquick1", "qml1tooling"), # noqa
  345. "qt5designer": (".QtDesigner", None, ), # noqa
  346. "qt5designercomponents": (None, None, ), # noqa
  347. "qt5gamepad": (None, None, "gamepads"),
  348. # Qt5Gui:
  349. # The ``platformthemes`` plugins are available only on Linux.
  350. # Same goes for ``xcbglintegrations`` and ``egldeviceintegrations`` plugins.
  351. # The ``wayland-decoration-client``, ``wayland-graphics-integration-client``, and ``wayland-shell-integration``
  352. # plugins are part of Qt5WaylandClient Qt module, whose shared library (e.g., libQt5WaylandClient.so) is linked
  353. # by the wayland-related ``platforms`` plugins. Ideally, we would collect these plugins based on the
  354. # Qt5WaylandClient shared library entry, but as our Qt hook utilities do not scan the plugins using this dictionary,
  355. # that would not work. So instead we list these plugins under Qt5Gui to achieve pretty much the same end result.
  356. "qt5gui": (".QtGui", "qtbase", "accessible", "iconengines", "imageformats", "platforms", "platforminputcontexts", "platformthemes", "xcbglintegrations", "egldeviceintegrations", "wayland-decoration-client", "wayland-graphics-integration-client", "wayland-shell-integration"), # noqa
  357. "qt5help": (".QtHelp", "qt_help", ), # noqa
  358. "qt5location": (".QtLocation", None, "geoservices"), # noqa
  359. "qt5macextras": (".QtMacExtras", None, ), # noqa
  360. "qt5multimedia": (".QtMultimedia", "qtmultimedia", "audio", "mediaservice", "playlistformats"), # noqa
  361. "qt5multimediaquick": (None, "qtmultimedia", ), # noqa
  362. "qt5multimediawidgets": (".QtMultimediaWidgets", "qtmultimedia", ), # noqa
  363. "qt5network": (".QtNetwork", "qtbase", "bearer"), # noqa
  364. "qt5nfc": (".QtNfc", None, ), # noqa
  365. "qt5opengl": (".QtOpenGL", None, ), # noqa
  366. "qt5positioning": (".QtPositioning", None, "position"), # noqa
  367. "qt5printsupport": (".QtPrintSupport", None, "printsupport"), # noqa
  368. "qt5qml": (".QtQml", "qtdeclarative", ), # noqa
  369. "qt5quick": (".QtQuick", "qtdeclarative", "scenegraph", "qmltooling"), # noqa
  370. "qt5quickparticles": (None, None, ), # noqa
  371. "qt5quickwidgets": (".QtQuickWidgets", None, ), # noqa
  372. "qt5script": (None, "qtscript", ), # noqa
  373. "qt5scripttools": (None, "qtscript", ), # noqa
  374. "qt5sensors": (".QtSensors", None, "sensors", "sensorgestures"), # noqa
  375. "qt5serialbus": (None, None, "canbus"), # noqa
  376. "qt5serialport": (".QtSerialPort", "qtserialport", ), # noqa
  377. "qt5sql": (".QtSql", "qtbase", "sqldrivers"), # noqa
  378. "qt5svg": (".QtSvg", None, ), # noqa
  379. "qt5test": (".QtTest", "qtbase", ), # noqa
  380. "qt5texttospeech": (None, None, "texttospeech"), # noqa
  381. "qt5webchannel": (".QtWebChannel", None, ), # noqa
  382. "qt5webengine": (".QtWebEngine", "qtwebengine", "qtwebengine"), # noqa
  383. "qt5webenginecore": (".QtWebEngineCore", None, "qtwebengine"), # noqa
  384. "qt5webenginewidgets": (".QtWebEngineWidgets", None, "qtwebengine"), # noqa
  385. "qt5webkit": (None, None, ), # noqa
  386. "qt5webkitwidgets": (None, None, ), # noqa
  387. "qt5websockets": (".QtWebSockets", None, ), # noqa
  388. "qt5widgets": (".QtWidgets", "qtbase", "styles"), # noqa
  389. "qt5winextras": (".QtWinExtras", None, ), # noqa
  390. "qt5xml": (".QtXml", "qtbase", ), # noqa
  391. "qt5xmlpatterns": (".QXmlPatterns", "qtxmlpatterns", ), # noqa
  392. } # yapf: disable
  393. # The dynamic dependency dictionary for Qt6 is constructed automatically from its Qt5 counterpart, by copying the
  394. # entries and substituting qt5 in the name with qt6. If the entry already exists in the dictionary, it is not
  395. # copied, which allows us to provide Qt6-specific overrides, should they prove necessary.
  396. _qt6_dynamic_dependencies_dict = {
  397. # Qt6Network:
  398. # networkinformationbackends plugins were introduced in Qt 6.1, and renamed to networkinformation in Qt 6.2
  399. # tls plugins were introduced in Qt 6.2
  400. "qt6network": (".QtNetwork", "qtbase", "networkinformationbackend", "networkinformation", "tls"), # noqa
  401. "qt6openglwidgets": (".QtOpenGLWidgets", "qtbase", ), # noqa
  402. } # yapf: disable
  403. for lib_name, content in _qt5_dynamic_dependencies_dict.items():
  404. if lib_name.startswith('qt5'):
  405. lib_name = 'qt6' + lib_name[3:]
  406. if lib_name not in _qt6_dynamic_dependencies_dict:
  407. _qt6_dynamic_dependencies_dict[lib_name] = content
  408. del lib_name, content
  409. # add_qt_dependencies
  410. # --------------------
  411. # Generic implemnentation that finds the Qt 5/6 dependencies based on the hook name of a PyQt5/PyQt6/PySide2/PySide6
  412. # hook. Returns (hiddenimports, binaries, datas). Typical usage:
  413. # ``hiddenimports, binaries, datas = add_qt5_dependencies(__file__)``.
  414. def add_qt_dependencies(hook_file):
  415. # Accumulate all dependencies in a set to avoid duplicates.
  416. hiddenimports = set()
  417. translations_base = set()
  418. plugins = set()
  419. # Find the module underlying this Qt hook: change ``/path/to/hook-PyQt5.blah.py`` to ``PyQt5.blah``.
  420. hook_name, hook_ext = os.path.splitext(os.path.basename(hook_file))
  421. assert hook_ext.startswith('.py')
  422. assert hook_name.startswith('hook-')
  423. module_name = hook_name[5:]
  424. namespace = module_name.split('.')[0]
  425. # Retrieve Qt library info structure.
  426. qt_info = get_qt_library_info(namespace)
  427. # Exit if the requested library cannot be imported.
  428. # NOTE: qt_info.version can be empty list on older Qt5 versions (#5381).
  429. if qt_info.version is None:
  430. return [], [], []
  431. # Look up the module returned by this import.
  432. module = hooks.get_module_file_attribute(module_name)
  433. logger.debug('add_qt%d_dependencies: Examining %s, based on hook of %s.', qt_info.qt_major, module, hook_file)
  434. # Walk through all the static dependencies of a dynamically-linked library (``.so``/``.dll``/``.dylib``).
  435. imports = set(bindepend.getImports(module))
  436. while imports:
  437. imp = imports.pop()
  438. # On Windows, find this library; other platforms already provide the full path.
  439. if compat.is_win:
  440. # First, look for Qt binaries in the local Qt install.
  441. imp = bindepend.getfullnameof(imp, qt_info.location['BinariesPath'])
  442. # Strip off the extension and ``lib`` prefix (Linux/Mac) to give the raw name.
  443. # Lowercase (since Windows always normalizes names to lowercase).
  444. lib_name = os.path.splitext(os.path.basename(imp))[0].lower()
  445. # Linux libraries sometimes have a dotted version number -- ``libfoo.so.3``. It is now ''libfoo.so``,
  446. # but the ``.so`` must also be removed.
  447. if compat.is_linux and os.path.splitext(lib_name)[1] == '.so':
  448. lib_name = os.path.splitext(lib_name)[0]
  449. if lib_name.startswith('lib'):
  450. lib_name = lib_name[3:]
  451. # Mac OS: handle different naming schemes. PyPI wheels ship framework-enabled Qt builds, where shared libraries
  452. # are part of .framework bundles (e.g., ``PyQt5/Qt5/lib/QtCore.framework/Versions/5/QtCore``). In Anaconda
  453. # (Py)Qt installations, the shared libraries are installed in environment's library directory, and contain
  454. # versioned extensions, e.g., ``libQt5Core.5.dylib``.
  455. if compat.is_darwin:
  456. if lib_name.startswith('qt') and not lib_name.startswith('qt' + str(qt_info.qt_major)):
  457. # Qt library from a framework bundle (e.g., ``QtCore``); change prefix from ``qt`` to ``qt5`` or ``qt6``
  458. # to match names in Windows/Linux.
  459. lib_name = 'qt' + str(qt_info.qt_major) + lib_name[2:]
  460. if lib_name.endswith('.' + str(qt_info.qt_major)):
  461. # Qt library from Anaconda, which originally had versioned extension, e.g., ``libfoo.5.dynlib``.
  462. # The above processing turned it into ``foo.5``, so we need to remove the last two characters.
  463. lib_name = lib_name[:-2]
  464. # Match libs with QT_LIBINFIX set to '_conda', i.e. conda-forge builds.
  465. if lib_name.endswith('_conda'):
  466. lib_name = lib_name[:-6]
  467. logger.debug('add_qt%d_dependencies: raw lib %s -> parsed lib %s', qt_info.qt_major, imp, lib_name)
  468. # PySide2 and PySide6 on linux seem to link all extension modules against libQt5Core, libQt5Network, and
  469. # libQt5Qml (or their libQt6* equivalents). While the first two are reasonable, the libQt5Qml dependency pulls
  470. # in whole QtQml module, along with its data and plugins, which in turn pull in several other Qt libraries,
  471. # greatly inflating the bundle size (see #6447).
  472. #
  473. # Similarly, some extension modules (QtWebChannel, QtWebEngine*) seem to be also linked against libQt5Qml,
  474. # even when the module can be used without having the whole QtQml module collected.
  475. #
  476. # Therefore, we explicitly prevent inclusion of QtQml based on the dynamic library dependency, except for
  477. # QtQml* and QtQuick* modules, whose use directly implies the use of QtQml.
  478. if lib_name in ("qt5qml", "qt6qml"):
  479. short_module_name = module_name.split('.', 1)[-1] # PySide2.QtModule -> QtModule
  480. if not short_module_name.startswith(('QtQml', 'QtQuick')):
  481. logger.debug('add_qt%d_dependencies: Ignoring import of %s.', qt_info.qt_major, imp)
  482. continue
  483. # Follow only Qt dependencies.
  484. _qt_dynamic_dependencies_dict = (
  485. _qt5_dynamic_dependencies_dict if qt_info.qt_major == 5 else _qt6_dynamic_dependencies_dict
  486. )
  487. if lib_name in _qt_dynamic_dependencies_dict:
  488. # Follow these to find additional dependencies.
  489. logger.debug('add_qt%d_dependencies: Import of %s.', qt_info.qt_major, imp)
  490. imports.update(bindepend.getImports(imp))
  491. # Look up which plugins and translations are needed.
  492. dd = _qt_dynamic_dependencies_dict[lib_name]
  493. lib_name_hiddenimports, lib_name_translations_base = dd[:2]
  494. lib_name_plugins = dd[2:]
  495. # Add them in.
  496. if lib_name_hiddenimports:
  497. hiddenimports.update([namespace + lib_name_hiddenimports])
  498. plugins.update(lib_name_plugins)
  499. if lib_name_translations_base:
  500. translations_base.update([lib_name_translations_base])
  501. # Change plugins into binaries.
  502. binaries = []
  503. for plugin in plugins:
  504. more_binaries = qt_plugins_binaries(plugin, namespace=namespace)
  505. binaries.extend(more_binaries)
  506. # Change translation_base to datas.
  507. tp = qt_info.location['TranslationsPath']
  508. tp_dst = os.path.join(qt_info.qt_rel_dir, 'translations')
  509. datas = []
  510. for tb in translations_base:
  511. src = os.path.join(tp, tb + '_*.qm')
  512. # Not all PyQt5 installations include translations. See
  513. # https://github.com/pyinstaller/pyinstaller/pull/3229#issuecomment-359479893
  514. # and
  515. # https://github.com/pyinstaller/pyinstaller/issues/2857#issuecomment-368744341.
  516. if glob.glob(src):
  517. datas.append((src, tp_dst))
  518. else:
  519. logger.warning(
  520. 'Unable to find Qt%d translations %s. These translations were not packaged.', qt_info.qt_major, src
  521. )
  522. # Change hiddenimports to a list.
  523. hiddenimports = list(hiddenimports)
  524. logger.debug(
  525. 'add_qt%d_dependencies: imports from %s:\n'
  526. ' hiddenimports = %s\n'
  527. ' binaries = %s\n'
  528. ' datas = %s', qt_info.qt_major, hook_name, hiddenimports, binaries, datas
  529. )
  530. return hiddenimports, binaries, datas
  531. # add_qt5_dependencies
  532. # --------------------
  533. # Find the Qt5 dependencies based on the hook name of a PySide2/PyQt5 hook. Returns (hiddenimports, binaries, datas).
  534. # Typical usage: ``hiddenimports, binaries, datas = add_qt5_dependencies(__file__)``.
  535. add_qt5_dependencies = add_qt_dependencies # Use generic implementation
  536. # add_qt6_dependencies
  537. # --------------------
  538. # Find the Qt6 dependencies based on the hook name of a PySide6/PyQt6 hook. Returns (hiddenimports, binaries, datas).
  539. # Typical usage: ``hiddenimports, binaries, datas = add_qt6_dependencies(__file__)``.
  540. add_qt6_dependencies = add_qt_dependencies # Use generic implementation
  541. def _find_all_or_none(globs_to_include, num_files, qt_library_info):
  542. """
  543. globs_to_include is a list of file name globs.
  544. If the number of found files does not match num_files, no files will be included.
  545. """
  546. # This function is required because CI is failing to include libEGL.
  547. # The error in AppVeyor is::
  548. #
  549. # [2312] LOADER: Running pyi_lib_PyQt5-uic.py
  550. # Failed to load libEGL (Access is denied.)
  551. # More info: https://github.com/pyinstaller/pyinstaller/pull/3568
  552. #
  553. # Since old PyQt5 wheels do not include d3dcompiler_4?.dll, libEGL.dll and libGLESv2.dll will not be included
  554. # for PyQt5 builds during CI.
  555. to_include = []
  556. dst_dll_path = '.'
  557. for dll in globs_to_include:
  558. # In PyQt5/PyQt6, the DLLs we are looking for are located in location['BinariesPath'], whereas in
  559. # PySide2/PySide6, they are located in location['PrefixPath'].
  560. dll_path = os.path.join(
  561. qt_library_info.location['BinariesPath' if qt_library_info.is_pyqt else 'PrefixPath'], dll
  562. )
  563. dll_file_paths = glob.glob(dll_path)
  564. for dll_file_path in dll_file_paths:
  565. to_include.append((dll_file_path, dst_dll_path))
  566. if len(to_include) == num_files:
  567. return to_include
  568. return []
  569. # Collect required Qt binaries, but only if all binaries in a group exist.
  570. def get_qt_binaries(qt_library_info):
  571. binaries = []
  572. angle_files = ['libEGL.dll', 'libGLESv2.dll', 'd3dcompiler_??.dll']
  573. binaries += _find_all_or_none(angle_files, 3, qt_library_info)
  574. opengl_software_renderer = ['opengl32sw.dll']
  575. binaries += _find_all_or_none(opengl_software_renderer, 1, qt_library_info)
  576. # Include ICU files, if they exist.
  577. # See the "Deployment approach" section in ``PyInstaller/utils/hooks/qt.py``.
  578. icu_files = ['icudt??.dll', 'icuin??.dll', 'icuuc??.dll']
  579. binaries += _find_all_or_none(icu_files, 3, qt_library_info)
  580. return binaries
  581. # Collect additional shared libraries required for SSL support in QtNetwork, if they are available.
  582. # Applicable only to Windows. See issue #3520, #4048.
  583. def get_qt_network_ssl_binaries(qt_library_info):
  584. # No-op if requested Qt-based package is not available.
  585. if qt_library_info.version is None:
  586. return []
  587. # Applicable only to Windows.
  588. if not compat.is_win:
  589. return []
  590. # Check if QtNetwork supports SSL.
  591. ssl_enabled = hooks.eval_statement(
  592. """
  593. from {}.QtNetwork import QSslSocket
  594. print(QSslSocket.supportsSsl())
  595. """.format(qt_library_info.namespace)
  596. )
  597. if not ssl_enabled:
  598. return []
  599. # PyPI version of PySide2 requires user to manually install SSL libraries into the PrefixPath. Other versions
  600. # (e.g., the one provided by Conda) put the libraries into the BinariesPath. PyQt5 also uses BinariesPath.
  601. # Accommodate both options by searching both locations...
  602. locations = (qt_library_info.location['BinariesPath'], qt_library_info.location['PrefixPath'])
  603. dll_names = ('libeay32.dll', 'ssleay32.dll', 'libssl-1_1-x64.dll', 'libcrypto-1_1-x64.dll')
  604. binaries = []
  605. for location in locations:
  606. for dll in dll_names:
  607. dll_path = os.path.join(location, dll)
  608. if os.path.exists(dll_path):
  609. binaries.append((dll_path, '.'))
  610. return binaries
  611. # Collect additional binaries and data for QtQml module.
  612. def get_qt_qml_files(qt_library_info):
  613. # No-op if requested Qt-based package is not available.
  614. if qt_library_info.version is None:
  615. return [], []
  616. # Not all PyQt5/PySide2 installs have QML files. In this case, location['Qml2ImportsPath'] is empty.
  617. # Furthermore, even if location path is provided, the directory itself may not exist.
  618. #
  619. # https://github.com/pyinstaller/pyinstaller/pull/3229#issuecomment-359735031
  620. # https://github.com/pyinstaller/pyinstaller/issues/3864
  621. qmldir = qt_library_info.location['Qml2ImportsPath']
  622. if not qmldir or not os.path.exists(qmldir):
  623. logger.warning(
  624. 'QML directory for %s, %r, does not exist. QML files not packaged.', qt_library_info.namespace, qmldir
  625. )
  626. return [], []
  627. qml_rel_dir = os.path.join(qt_library_info.qt_rel_dir, 'qml')
  628. datas = [(qmldir, qml_rel_dir)]
  629. binaries = [
  630. # Produce ``/path/to/Qt/Qml/path_to_qml_binary/qml_binary, PyQt5/Qt/Qml/path_to_qml_binary``.
  631. (f, os.path.join(qml_rel_dir, os.path.dirname(os.path.relpath(f, qmldir))))
  632. for f in misc.dlls_in_subdirs(qmldir)
  633. ]
  634. return binaries, datas
  635. # Collect the ``qt.conf`` file.
  636. def get_qt_conf_file(qt_library_info):
  637. # No-op if requested Qt-based package is not available.
  638. if qt_library_info.version is None:
  639. return []
  640. # Find ``qt.conf`` in location['PrefixPath'].
  641. datas = [
  642. x for x in hooks.collect_system_data_files(qt_library_info.location['PrefixPath'], qt_library_info.qt_rel_dir)
  643. if os.path.basename(x[0]) == 'qt.conf'
  644. ]
  645. return datas
  646. # Collect QtWebEngine helper process executable, translations, and resources.
  647. def get_qt_webengine_binaries_and_data_files(qt_library_info):
  648. binaries = []
  649. datas = []
  650. # Output directory (varies between PyQt and PySide and among OSes; the difference is abstracted by
  651. # qt_library_info.qt_rel_dir)
  652. rel_data_path = qt_library_info.qt_rel_dir
  653. is_macos_framework = False
  654. if compat.is_darwin:
  655. # Determine if we are dealing with a framework-based Qt build (e.g., PyPI wheels) or a dylib-based one
  656. # (e.g., Anaconda). The former requires special handling, while the latter is handled in the same way as
  657. # Windows and Linux builds.
  658. is_macos_framework = os.path.exists(
  659. os.path.join(qt_library_info.location['LibrariesPath'], 'QtWebEngineCore.framework')
  660. )
  661. if is_macos_framework:
  662. # On macOS, Qt shared libraries are provided in form of .framework bundles. However, PyInstaller collects shared
  663. # library from the bundle into top-level application directory, breaking the bundle structure.
  664. #
  665. # QtWebEngine and its underlying Chromium engine, however, have very strict data file layout requirements due to
  666. # sandboxing, and does not work if the helper process executable does not load the shared library from
  667. # QtWebEngineCore.framework (which also needs to contain all resources).
  668. #
  669. # Therefore, we collect the QtWebEngineCore.framework manually, in order to obtain a working QtWebEngineProcess
  670. # helper executable. But because that bypasses our dependency scanner, we need to collect the dependent
  671. # .framework bundles as well. And we need to override QTWEBENGINEPROCESS_PATH in rthook, because the
  672. # QtWebEngineWidgets python extension actually loads up the copy of shared library that is located in
  673. # sys._MEIPASS (as opposed to the manually-copied one in .framework bundle). Furthermore, because the extension
  674. # modules use Qt shared libraries in sys._MEIPASS, we also copy all contents of
  675. # QtWebEngineCore.framework/Resources into sys._MEIPASS to make resource loading in the main process work.
  676. #
  677. # Besides being ugly, this approach has three main ramifications:
  678. # 1. we bundle two copies of each Qt shared library involved: the copy used by main process, picked up by
  679. # dependency scanner; and a copy in manually-collected .framework bundle that is used by the helper process.
  680. # 2. the trick with copying contents of Resource directory of QtWebEngineCore.framework does not work in onefile
  681. # mode, and consequently QtWebEngine does not work in onefile mode.
  682. # 3. copying contents of QtWebEngineCore.framework/Resource means that its Info.plist ends up in sys._MEIPASS,
  683. # causing the main process in onedir mode to be mis-identified as "QtWebEngineProcess".
  684. #
  685. # In the near future, this quagmire will hopefully be properly sorted out, but in the mean time, we have to live
  686. # with what we have been given.
  687. data_path = qt_library_info.location['DataPath']
  688. libraries = [
  689. 'QtCore', 'QtWebEngineCore', 'QtQuick', 'QtQml', 'QtQmlModels', 'QtNetwork', 'QtGui', 'QtWebChannel',
  690. 'QtPositioning'
  691. ]
  692. for i in libraries:
  693. framework_dir = i + '.framework'
  694. datas += hooks.collect_system_data_files(
  695. os.path.join(data_path, 'lib', framework_dir), os.path.join(rel_data_path, 'lib', framework_dir), True
  696. )
  697. datas += [(os.path.join(data_path, 'lib', 'QtWebEngineCore.framework', 'Resources'), os.curdir)]
  698. else:
  699. # Windows and linux (or Anaconda on macOS)
  700. locales = 'qtwebengine_locales'
  701. resources = 'resources'
  702. # Translations
  703. datas.append((
  704. os.path.join(qt_library_info.location['TranslationsPath'], locales),
  705. os.path.join(rel_data_path, 'translations', locales),
  706. ))
  707. # Resources; ``DataPath`` is the base directory for ``resources``, as per the
  708. # `docs <https://doc.qt.io/qt-5.10/qtwebengine-deploying.html#deploying-resources>`_.
  709. datas.append(
  710. (os.path.join(qt_library_info.location['DataPath'], resources), os.path.join(rel_data_path, resources)),
  711. )
  712. # Helper process executable (QtWebEngineProcess), located in ``LibraryExecutablesPath``.
  713. dest = os.path.join(
  714. rel_data_path,
  715. os.path.relpath(qt_library_info.location['LibraryExecutablesPath'], qt_library_info.location['PrefixPath'])
  716. )
  717. binaries.append((os.path.join(qt_library_info.location['LibraryExecutablesPath'], 'QtWebEngineProcess*'), dest))
  718. # Add Linux-specific libraries.
  719. if compat.is_linux:
  720. # The automatic library detection fails for `NSS <https://packages.ubuntu.com/search?keywords=libnss3>`_, which
  721. # is used by QtWebEngine. In some distributions, the ``libnss`` supporting libraries are stored in a
  722. # subdirectory ``nss``. Since ``libnss`` is not statically linked to these, but dynamically loads them, we need
  723. # to search for and add them.
  724. # First, get all libraries linked to ``QtWebEngineWidgets`` extension module.
  725. module_file = hooks.get_module_file_attribute(qt_library_info.namespace + '.QtWebEngineWidgets')
  726. module_imports = bindepend.getImports(module_file)
  727. for imp in module_imports:
  728. # Look for ``libnss3.so``.
  729. if os.path.basename(imp).startswith('libnss3.so'):
  730. # Find the location of NSS: given a ``/path/to/libnss.so``, add ``/path/to/nss/*.so`` to get the
  731. # missing NSS libraries.
  732. nss_glob = os.path.join(os.path.dirname(imp), 'nss', '*.so')
  733. if glob.glob(nss_glob):
  734. binaries.append((nss_glob, 'nss'))
  735. return binaries, datas