qt.py 39 KB

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