# -----------------------------------------------------------------------------
|
# Copyright (c) 2024, PyInstaller Development Team.
|
#
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
# you may not use this file except in compliance with the License.
|
#
|
# The full license is in the file COPYING.txt, distributed with this software.
|
#
|
# SPDX-License-Identifier: Apache-2.0
|
# -----------------------------------------------------------------------------
|
|
import os
|
import importlib
|
import atexit
|
|
# Helper for ensuring that only one Qt bindings package is registered at run-time via run-time hooks.
|
_registered_qt_bindings = None
|
|
|
def ensure_single_qt_bindings_package(qt_bindings):
|
global _registered_qt_bindings
|
if _registered_qt_bindings is not None:
|
raise RuntimeError(
|
f"Cannot execute run-time hook for {qt_bindings!r} because run-time hook for {_registered_qt_bindings!r} "
|
"has been run before, and PyInstaller-frozen applications do not support multiple Qt bindings in the same "
|
"application!"
|
)
|
_registered_qt_bindings = qt_bindings
|
|
|
# Helper for relocating Qt prefix via embedded qt.conf file.
|
_QT_CONF_FILENAME = ":/qt/etc/qt.conf"
|
|
_QT_CONF_RESOURCE_NAME = (
|
# qt
|
b"\x00\x02"
|
b"\x00\x00\x07\x84"
|
b"\x00\x71"
|
b"\x00\x74"
|
# etc
|
b"\x00\x03"
|
b"\x00\x00\x6c\xa3"
|
b"\x00\x65"
|
b"\x00\x74\x00\x63"
|
# qt.conf
|
b"\x00\x07"
|
b"\x08\x74\xa6\xa6"
|
b"\x00\x71"
|
b"\x00\x74\x00\x2e\x00\x63\x00\x6f\x00\x6e\x00\x66"
|
)
|
|
_QT_CONF_RESOURCE_STRUCT = (
|
# :
|
b"\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01"
|
# :/qt
|
b"\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02"
|
# :/qt/etc
|
b"\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03"
|
# :/qt/etc/qt.conf
|
b"\x00\x00\x00\x16\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00"
|
)
|
|
|
def create_embedded_qt_conf(qt_bindings, prefix_path):
|
# The QtCore module might be unavailable if we collected just the top-level binding package (e.g., PyQt5) without
|
# any of its submodules. Since this helper is called from run-time hook for the binding package, we need to handle
|
# that scenario here.
|
try:
|
QtCore = importlib.import_module(qt_bindings + ".QtCore")
|
except ImportError:
|
return
|
|
# No-op if embedded qt.conf already exists
|
if QtCore.QFile.exists(_QT_CONF_FILENAME):
|
return
|
|
# Create qt.conf file that relocates Qt prefix.
|
# NOTE: paths should use POSIX-style forward slashes as separator, even on Windows.
|
if os.sep == '\\':
|
prefix_path = prefix_path.replace(os.sep, '/')
|
|
qt_conf = f"[Paths]\nPrefix = {prefix_path}\n"
|
if os.name == 'nt' and qt_bindings in {"PySide2", "PySide6"}:
|
# PySide PyPI wheels on Windows set LibraryExecutablesPath to PrefixPath
|
qt_conf += f"LibraryExecutables = {prefix_path}"
|
|
# Encode the contents; in Qt5, QSettings uses Latin1 encoding, in Qt6, it uses UTF8.
|
if qt_bindings in {"PySide2", "PyQt5"}:
|
qt_conf = qt_conf.encode("latin1")
|
else:
|
qt_conf = qt_conf.encode("utf-8")
|
|
# Prepend data size (32-bit integer, big endian)
|
qt_conf_size = len(qt_conf)
|
qt_resource_data = qt_conf_size.to_bytes(4, 'big') + qt_conf
|
|
# Register
|
succeeded = QtCore.qRegisterResourceData(
|
0x01,
|
_QT_CONF_RESOURCE_STRUCT,
|
_QT_CONF_RESOURCE_NAME,
|
qt_resource_data,
|
)
|
if not succeeded:
|
return # Tough luck
|
|
# Unregister the resource at exit, to ensure that the registered resource on Qt/C++ side does not outlive the
|
# `_qt_resource_data` python variable and its data buffer. This also adds a reference to the `_qt_resource_data`,
|
# which conveniently ensures that the data is not garbage collected before we perform the cleanup (otherwise garbage
|
# collector might kick in at any time after we exit this helper function, and `qRegisterResourceData` does not seem
|
# to make a copy of the data!).
|
atexit.register(
|
QtCore.qUnregisterResourceData,
|
0x01,
|
_QT_CONF_RESOURCE_STRUCT,
|
_QT_CONF_RESOURCE_NAME,
|
qt_resource_data,
|
)
|