Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
rhtooks: qt: create embedded qt.conf if it does not exist
Have the run-time hooks for `PySide2`, `PySide6`, `PyQt5`, and `PyQt6` check for the presence of embedded `:/qt/etc/qt.conf` resource, and if not present, inject their own version. This aims to ensure that the bundled Qt is always relocatable. All Qt bindings packages, when packaged as PyPI wheels, inject embedded `qt.conf` file as Qt embedded resource during their initialization, and use it to set the `PrefixPath` to the top-level Qt directory within the package's directory. This enures that all paths in `QLibraryInfo` are properly set, and is essential for making the package and its bundled Qt relocatable. Qt bindings packaged by other providers (for example, Anaconda packages on all OSes, or packages provided by linux distributions) do not seem to use this mechanism when running under unfrozen python. However, when collected into PyInstaller-frozen application, `PyQt5` and `PyQt6` packages, even if originally not packaged as PyPI wheels, seem to enable the embedded `qt.conf` injection, making themselves relocatable. On the other hand, `PySide2` package does not, which makes it non-relocatable. For example, when using Fedora-packaged `PySide2` in a frozen application, the `PrefixPath` remains set to `/usr`, and thus a `QtWebEngine`-enabled frozen application ends up using system-installed `QtWebEngineProcess` helper in `/usr/lib64/qt5/libexec`, if available (and failing to start if it is unavailable). In the case of Anaconda-packaged `PySide2`, the `PrefixPath` path ends up being `/`, and the collected `QtWebEngineProcess` fails to be discovered. Therefore, if the embedded `qt.conf` resource is not provided by the package itself, we attempt to provide our own, to ensure that the package is relocatable, and that paths in `QLibraryInfo` are properly set (i.e., that they match the relocatable PyPI wheel layout, which we are trying to reconstruct in our Qt collection approach).
- Loading branch information
Showing
6 changed files
with
133 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
# ----------------------------------------------------------------------------- | ||
# 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 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): | ||
QtCore = importlib.import_module(qt_bindings + ".QtCore") | ||
|
||
# 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, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
Have run-time hooks for Qt bindings (``PySide2``, ``PySide6``, ``PyQt5``, | ||
and ``PyQt6``) check for presence of the embedded ``:/qt/etc/qt.conf`` | ||
resource, and if not present, inject their own version. This | ||
aims to ensure that the bundled Qt is always relocatable, even if the | ||
package does not perform injection of embedded ``qt.conf`` file (most | ||
notably, this seems to be the case with ``PySide2`` collected from | ||
Linux distribution packages, and ``PySide2`` collected from Anaconda | ||
on Windows, Linux, and macOS). |