"""A collection of functions which are the base to hooks for PyQt5, PyQt6, PySide2 and PySide6. """ from __future__ import annotations import json import os import sys from contextlib import suppress from functools import lru_cache from pathlib import Path from typing import TYPE_CHECKING from cx_Freeze._compat import IS_CONDA, IS_MACOS, IS_MINGW, IS_WINDOWS if TYPE_CHECKING: from cx_Freeze.finder import ModuleFinder from cx_Freeze.module import Module def _qt_implementation(module: Module) -> str: """Helper function to get the name of the Qt implementation.""" return module.name.split(".")[0] @lru_cache(maxsize=None) def _qt_libraryinfo_paths(name: str) -> dict[str, tuple[Path, Path]]: """Cache the QtCore library paths.""" try: qtcore = __import__(name, fromlist=["QtCore"]).QtCore except RuntimeError: print("WARNING: Tried to load multiple incompatible Qt ", end="") print("wrappers. Some incorrect files may be copied.") return {} # get paths from QLibraryInfo source_paths: dict[str, Path] = {} lib = qtcore.QLibraryInfo major_version = lib.version().majorVersion() if major_version == 6: if hasattr(lib.LibraryPath, "__members__"): for key, value in lib.LibraryPath.__members__.items(): source_paths[key] = Path(lib.path(value)) else: for key, value in lib.__dict__.items(): if isinstance(value, lib.LibraryPath): source_paths[key] = Path(lib.path(value)) else: for key, value in lib.__dict__.items(): if isinstance(value, (lib.LibraryLocation, int)): source_paths[key] = Path(lib.location(value)) qt_root_dir = Path(qtcore.__file__).parent # if QLibraryInfo has incomplete information if not source_paths.get("PluginsPath"): # Qt Plugins can be in a plugins directory next to the Qt libraries plugins_path = qt_root_dir / "plugins" if not plugins_path.exists(): plugins_path = qt_root_dir / "Qt5" / "plugins" # PyQt5 5.15.4 # or in a special location in conda-forge if not plugins_path.exists(): plugins_path = Path(sys.base_prefix, "Library", "plugins") # default location if not plugins_path.exists(): plugins_path = qt_root_dir / "Qt" / "plugins" source_paths["PluginsPath"] = plugins_path source_paths.setdefault("PrefixPath", source_paths["PluginsPath"].parent) prefix_path = source_paths["PrefixPath"] source_paths.setdefault("DataPath", prefix_path) source_paths.setdefault("LibrariesPath", prefix_path / "lib") source_paths.setdefault("SettingsPath", ".") if name in ("PySide2", "PySide6") and IS_WINDOWS and not IS_CONDA: source_paths["BinariesPath"] = prefix_path source_paths["LibraryExecutablesPath"] = prefix_path # set the target paths data: dict[str, tuple[Path, Path]] = {} target_base = Path("lib", name) with suppress(ValueError): target_base = target_base / prefix_path.relative_to(qt_root_dir) if name == "PyQt5" and prefix_path.name != "Qt5": # conda pyqt target_base = target_base / "Qt5" # set some defaults or use relative path for key, source in source_paths.items(): if key == "SettingsPath": # Check for SettingsPath first target = Path("Contents/Resources" if IS_MACOS else ".") elif name in ("PySide2", "PySide6") and IS_WINDOWS and not IS_CONDA: target = target_base / source.relative_to(prefix_path) elif key in ("ArchDataPath", "DataPath", "PrefixPath"): target = target_base elif key == "BinariesPath": target = target_base / "bin" elif key == "LibrariesPath": target = target_base / "lib" elif key == "LibraryExecutablesPath": target = target_base / ( "bin" if IS_WINDOWS or IS_MINGW else "libexec" ) elif key == "PluginsPath": target = target_base / "plugins" elif key == "TranslationsPath": target = target_base / "translations" elif source == Path("."): target = target_base else: target = target_base / source.relative_to(prefix_path) data[key] = source.resolve(), target # debug if os.environ.get("QT_DEBUG"): print("QLibraryInfo:") for key, (source, target) in sorted(data.items()): print(" ", key, source, "->", target) return data def get_qt_paths(name: str, variable: str) -> tuple[Path, Path]: """Helper function to get the source and target path of Qt variable.""" libraryinfo_paths = _qt_libraryinfo_paths(name) source_path, target_path = libraryinfo_paths[variable] return (source_path, target_path) def _get_qt_files( name: str, variable: str, arg: str ) -> list[tuple[Path, Path]]: """Helper function to get Qt plugins, resources, translations, etc.""" source_path, target_path = get_qt_paths(name, variable) if source_path.joinpath(arg).is_dir(): source_path = source_path / arg target_path = target_path / arg pattern = "*" else: pattern = arg return [ (source, target_path / source.name) for source in source_path.glob(pattern) ] def get_qt_plugins_paths(name: str, plugins: str) -> list[tuple[Path, Path]]: """Helper function to get a list of source and target paths of Qt plugins, indicated to be used in include_files. """ return _get_qt_files(name, "PluginsPath", plugins) def copy_qt_files( finder: ModuleFinder, name: str, variable: str, arg: str ) -> None: """Helper function to copy Qt plugins, resources, translations, etc.""" for source_path, target_path in _get_qt_files(name, variable, arg): finder.include_files(source_path, target_path) def load_qt_phonon(finder: ModuleFinder, module: Module) -> None: """In Windows, phonon5.dll requires an additional dll phonon_ds94.dll to be present in the build directory inside a folder phonon_backend. """ if IS_WINDOWS or IS_MINGW: name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "phonon_backend") def load_qt_qt3dinput(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "3dinputdevices") def load_qt_qt3drender(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "sceneparsers") copy_qt_files(finder, name, "PluginsPath", "geometryloaders") copy_qt_files(finder, name, "PluginsPath", "renderplugins") copy_qt_files(finder, name, "PluginsPath", "renderers") def load_qt_qtbluetooth(finder: ModuleFinder, module: Module) -> None: """Include translations for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "TranslationsPath", "qtconnectivity_*.qm") def load_qt_qtcore(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) variable = "BinariesPath" if IS_WINDOWS else "LibrariesPath" for source, target in _get_qt_files(name, variable, "*"): finder.lib_files[source] = target def load_qt_qtdesigner(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "designer") copy_qt_files(finder, name, "TranslationsPath", "designer_*.qm") def load_qt_qtgui(finder: ModuleFinder, module: Module) -> None: """There is a chance that QtGui will use some image formats, then, add the image format plugins. """ name = _qt_implementation(module) for plugin_name in ( "accessiblebridge", "platforms", "platforms/darwin", "xcbglintegrations", "platformthemes", "platforminputcontexts", "generic", "iconengines", "imageformats", "egldeviceintegrations", "wayland-graphics-integration-client", "wayland-inputdevice-integration", "wayland-decoration-client", "wayland-shell-integration", "wayland-graphics-integration-server", "wayland-hardware-layer-integration", ): copy_qt_files(finder, name, "PluginsPath", plugin_name) copy_qt_files(finder, name, "TranslationsPath", "qt_??.qm") copy_qt_files(finder, name, "TranslationsPath", "qt_??_??.qm") copy_qt_files(finder, name, "TranslationsPath", "qtbase_*.qm") # old names? copy_qt_files(finder, name, "PluginsPath", "accessible") copy_qt_files(finder, name, "PluginsPath", "pictureformats") def load_qt_qtlocation(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "geoservices") copy_qt_files(finder, name, "TranslationsPath", "qtlocation_*.qm") def load_qt_qtmultimedia(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "multimedia") copy_qt_files(finder, name, "TranslationsPath", "qtmultimedia_*.qm") # ? copy_qt_files(finder, name, "PluginsPath", "audio") copy_qt_files(finder, name, "PluginsPath", "mediaservice") copy_qt_files(finder, name, "PluginsPath", "playlistformats") copy_qt_files(finder, name, "PluginsPath", "resourcepolicy") copy_qt_files(finder, name, "PluginsPath", "video") def load_qt_qtnetwork(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "networkaccess") copy_qt_files(finder, name, "PluginsPath", "networkinformation") copy_qt_files(finder, name, "PluginsPath", "tls") copy_qt_files(finder, name, "PluginsPath", "bearer") # ? def load_qt_qtpositioning(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "position") def load_qt_qtprintsupport(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "printsupport") if IS_WINDOWS: copy_qt_files(finder, name, "PrefixPath", "Qt?Pdf*.dll") def load_qt_qtqml(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "qmllint") # pyqt6 copy_qt_files(finder, name, "PluginsPath", "qmltooling") copy_qt_files(finder, name, "QmlImportsPath", "*") finder.include_module(f"{name}.QtQuick") def load_qt_qtquick(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "scenegraph") def load_qt_qtquick3d(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "assetimporters") def load_qt_qtscript(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "script") def load_qt_qtscxml(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "scxmldatamodel") def load_qt_qtsensors(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "sensors") copy_qt_files(finder, name, "PluginsPath", "sensorgestures") # pyqt6 def load_qt_qtserialbus(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "canbus") def load_qt_qtserialport(finder: ModuleFinder, module: Module) -> None: """Include translations for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "TranslationsPath", "qtserialport_*.qm") def load_qt_qtsql(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "sqldrivers") def load_qt_qttexttospeech(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "texttospeech") def load_qt_qtvirtualkeyboard(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "virtualkeyboard") def load_qt_qtwebenginecore(finder: ModuleFinder, module: Module) -> None: """Include module dependency and QtWebEngineProcess files.""" name = _qt_implementation(module) distribution = module.parent.distribution environment = (distribution and distribution.installer) or "pip" if IS_WINDOWS: for filename in ( "QtWebEngineProcess.exe", "d3dcompiler_47.dll", "libEGL.dll", "libGLESv2.dll", "opengl32sw.dll", ): # pyside2 - only QtWebEngineProcess is in LibraryExecutablesPath # pyside6 - like pyside2, but the two lib*.dll are missing copy_qt_files(finder, name, "ArchDataPath", filename) # pyqt5 - all files listed in LibraryExecutablesPath copy_qt_files(finder, name, "LibraryExecutablesPath", filename) elif IS_MACOS and environment == "pip": # pip wheels for macOS source_path, _ = get_qt_paths(name, "LibrariesPath") source_framework = source_path / "QtWebEngineCore.framework" # QtWebEngineProcess finder.include_files(source_framework / "Helpers", "share") # QtWebEngineCore resources source_resources = source_framework / "Resources" if source_resources.exists(): target_datapath = get_qt_paths(name, "DataPath")[1] for resource in source_resources.iterdir(): if resource.name == "Info.plist": continue if resource.name == "qtwebengine_locales": target = get_qt_paths(name, "TranslationsPath")[1] else: target = target_datapath / "resources" finder.include_files( resource, target / resource.name, copy_dependent_files=False, ) else: # wheels for Linux or conda-forge Linux and macOS copy_qt_files( finder, name, "LibraryExecutablesPath", "QtWebEngineProcess" ) if environment == "conda": # conda-forge Linux and macOS prefix = Path(sys.prefix) conda_meta = prefix / "conda-meta" pkg = next(conda_meta.glob("nss-*.json")) files = json.loads(pkg.read_text(encoding="utf_8"))["files"] for file in files: source = prefix / file if source.match("lib*.so") or source.match("lib*.dylib"): finder.include_files(source, f"lib/{source.name}") else: copy_qt_files(finder, name, "LibraryExecutablesPath", "libnss*.*") copy_qt_files(finder, name, "DataPath", "resources") copy_qt_files(finder, name, "TranslationsPath", "qtwebengine_*.qm") copy_qt_files(finder, name, "TranslationsPath", "qtwebengine_locales") def load_qt_qtwebenginewidgets(finder: ModuleFinder, module: Module) -> None: """Include data and plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "LibrariesPath", "*WebEngineWidgets.*") copy_qt_files(finder, name, "PluginsPath", "webview") def load_qt_qtwebsockets(finder: ModuleFinder, module: Module) -> None: """Include translations for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "TranslationsPath", "qtwebsockets_*.qm") def load_qt_qtwidgets(finder: ModuleFinder, module: Module) -> None: """Include plugins for the module.""" name = _qt_implementation(module) copy_qt_files(finder, name, "PluginsPath", "styles") def load_qt_uic(finder: ModuleFinder, module: Module) -> None: """The uic module makes use of "plugins" that need to be read directly and cannot be frozen; the PyQt5.QtWebKit and PyQt5.QtNetwork modules are also implicity loaded. """ name = _qt_implementation(module) source_dir = module.path[0] / "widget-plugins" if source_dir.exists(): finder.include_files(source_dir, f"{name}.uic.widget-plugins")