qt.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. # -----------------------------------------------------------------------------
  2. # Copyright (c) 2024, PyInstaller Development Team.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. #
  7. # The full license is in the file COPYING.txt, distributed with this software.
  8. #
  9. # SPDX-License-Identifier: Apache-2.0
  10. # -----------------------------------------------------------------------------
  11. import os
  12. import importlib
  13. import atexit
  14. # Helper for ensuring that only one Qt bindings package is registered at run-time via run-time hooks.
  15. _registered_qt_bindings = None
  16. def ensure_single_qt_bindings_package(qt_bindings):
  17. global _registered_qt_bindings
  18. if _registered_qt_bindings is not None:
  19. raise RuntimeError(
  20. f"Cannot execute run-time hook for {qt_bindings!r} because run-time hook for {_registered_qt_bindings!r} "
  21. "has been run before, and PyInstaller-frozen applications do not support multiple Qt bindings in the same "
  22. "application!"
  23. )
  24. _registered_qt_bindings = qt_bindings
  25. # Helper for relocating Qt prefix via embedded qt.conf file.
  26. _QT_CONF_FILENAME = ":/qt/etc/qt.conf"
  27. _QT_CONF_RESOURCE_NAME = (
  28. # qt
  29. b"\x00\x02"
  30. b"\x00\x00\x07\x84"
  31. b"\x00\x71"
  32. b"\x00\x74"
  33. # etc
  34. b"\x00\x03"
  35. b"\x00\x00\x6c\xa3"
  36. b"\x00\x65"
  37. b"\x00\x74\x00\x63"
  38. # qt.conf
  39. b"\x00\x07"
  40. b"\x08\x74\xa6\xa6"
  41. b"\x00\x71"
  42. b"\x00\x74\x00\x2e\x00\x63\x00\x6f\x00\x6e\x00\x66"
  43. )
  44. _QT_CONF_RESOURCE_STRUCT = (
  45. # :
  46. b"\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01"
  47. # :/qt
  48. b"\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02"
  49. # :/qt/etc
  50. b"\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03"
  51. # :/qt/etc/qt.conf
  52. b"\x00\x00\x00\x16\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00"
  53. )
  54. def create_embedded_qt_conf(qt_bindings, prefix_path):
  55. # The QtCore module might be unavailable if we collected just the top-level binding package (e.g., PyQt5) without
  56. # any of its submodules. Since this helper is called from run-time hook for the binding package, we need to handle
  57. # that scenario here.
  58. try:
  59. QtCore = importlib.import_module(qt_bindings + ".QtCore")
  60. except ImportError:
  61. return
  62. # No-op if embedded qt.conf already exists
  63. if QtCore.QFile.exists(_QT_CONF_FILENAME):
  64. return
  65. # Create qt.conf file that relocates Qt prefix.
  66. # NOTE: paths should use POSIX-style forward slashes as separator, even on Windows.
  67. if os.sep == '\\':
  68. prefix_path = prefix_path.replace(os.sep, '/')
  69. qt_conf = f"[Paths]\nPrefix = {prefix_path}\n"
  70. if os.name == 'nt' and qt_bindings in {"PySide2", "PySide6"}:
  71. # PySide PyPI wheels on Windows set LibraryExecutablesPath to PrefixPath
  72. qt_conf += f"LibraryExecutables = {prefix_path}"
  73. # Encode the contents; in Qt5, QSettings uses Latin1 encoding, in Qt6, it uses UTF8.
  74. if qt_bindings in {"PySide2", "PyQt5"}:
  75. qt_conf = qt_conf.encode("latin1")
  76. else:
  77. qt_conf = qt_conf.encode("utf-8")
  78. # Prepend data size (32-bit integer, big endian)
  79. qt_conf_size = len(qt_conf)
  80. qt_resource_data = qt_conf_size.to_bytes(4, 'big') + qt_conf
  81. # Register
  82. succeeded = QtCore.qRegisterResourceData(
  83. 0x01,
  84. _QT_CONF_RESOURCE_STRUCT,
  85. _QT_CONF_RESOURCE_NAME,
  86. qt_resource_data,
  87. )
  88. if not succeeded:
  89. return # Tough luck
  90. # Unregister the resource at exit, to ensure that the registered resource on Qt/C++ side does not outlive the
  91. # `_qt_resource_data` python variable and its data buffer. This also adds a reference to the `_qt_resource_data`,
  92. # which conveniently ensures that the data is not garbage collected before we perform the cleanup (otherwise garbage
  93. # collector might kick in at any time after we exit this helper function, and `qRegisterResourceData` does not seem
  94. # to make a copy of the data!).
  95. atexit.register(
  96. QtCore.qUnregisterResourceData,
  97. 0x01,
  98. _QT_CONF_RESOURCE_STRUCT,
  99. _QT_CONF_RESOURCE_NAME,
  100. qt_resource_data,
  101. )