class NuitkaPluginQtBindingsPluginBase(NuitkaPluginBase): # For overload in the derived bindings plugin. binding_name = None def __init__(self, qt_plugins): self.webengine_done = False self.qt_plugins_dirs = None self.qt_plugins = OrderedSet(x.strip().lower() for x in qt_plugins.split(",")) # Allow to specify none. if self.qt_plugins == set(["none"]): self.qt_plugins = set() @classmethod def addPluginCommandLineOptions(cls, group): group.add_option( "--include-qt-plugins", action="store", dest="qt_plugins", default="sensible", help="""\ Which Qt plugins to include. These can be big with dependencies, so by default only the sensible ones are included, but you can also put "all" or list them individually. If you specify something that does not exist, a list of all available will be given.""", ) @abstractmethod def _getQmlTargetDir(self, target_plugin_dir): """ Where does the bindings package expect the QML files. """ def getQtPluginsSelected(self): # Resolve "sensible on first use" if "sensible" in self.qt_plugins: # Most used ones with low dependencies. self.qt_plugins.update( tuple( family for family in ( "imageformats", "iconengines", "mediaservice", "printsupport", "platforms", ) if self.hasPluginFamily(family) ) ) self.qt_plugins.remove("sensible") # Make sure the above didn't detect nothing, which would be # indicating the check to be bad. assert self.qt_plugins return self.qt_plugins def getQtPluginDirs(self): if self.qt_plugins_dirs is not None: return self.qt_plugins_dirs command = """\ from __future__ import print_function from __future__ import absolute_import import %(binding_name)s.QtCore for v in %(binding_name)s.QtCore.QCoreApplication.libraryPaths(): print(v) import os # Standard CPython has installations like this. guess_path = os.path.join(os.path.dirname(%(binding_name)s.__file__), "plugins") if os.path.exists(guess_path): print("GUESS:", guess_path) # Anaconda has this, but it seems not automatic. guess_path = os.path.join(os.path.dirname(%(binding_name)s.__file__), "..", "..", "..", "Library", "plugins") if os.path.exists(guess_path): print("GUESS:", guess_path) """ % { "binding_name": self.binding_name } output = Execution.check_output([sys.executable, "-c", command]) # May not be good for everybody, but we cannot have bytes in paths, or # else working with them breaks down. if str is not bytes: output = output.decode("utf-8") result = [] for line in output.replace("\r", "").split("\n"): if not line: continue # Take the guessed path only if necessary. if line.startswith("GUESS: "): if result: continue line = line[len("GUESS: ") :] result.append(os.path.normpath(line)) # Avoid duplicates. result = tuple(sorted(set(result))) self.qt_plugins_dirs = result return result def _getQtBinDirs(self): for plugin_dir in self.getQtPluginDirs(): qt_bin_dir = os.path.normpath(os.path.join(plugin_dir, "..", "bin")) if os.path.isdir(qt_bin_dir): yield qt_bin_dir def hasPluginFamily(self, family): for plugin_dir in self.getQtPluginDirs(): if os.path.isdir(os.path.join(plugin_dir, family)): return True # TODO: Special case "xml". return False def copyQmlFiles(self, full_name, target_plugin_dir): for plugin_dir in self.getQtPluginDirs(): qml_plugin_dir = os.path.normpath(os.path.join(plugin_dir, "..", "qml")) if os.path.exists(qml_plugin_dir): break else: self.sysexit("Error, no such Qt plugin family: qml") qml_target_dir = os.path.normpath(self._getQmlTargetDir(target_plugin_dir)) self.info("Copying Qt plug-ins 'qml' to '%s'." % (qml_target_dir)) copyTree(qml_plugin_dir, qml_target_dir) # We try to filter here, not for DLLs. return [ ( filename, os.path.join(qml_target_dir, os.path.relpath(filename, qml_plugin_dir)), full_name, ) for filename in getFileList(qml_plugin_dir) if not filename.endswith( ( ".qml", ".qmlc", ".qmltypes", ".js", ".jsc", ".png", ".ttf", ".metainfo", ) ) if not os.path.isdir(filename) if not os.path.basename(filename) == "qmldir" ] def findDLLs(self, full_name, target_plugin_dir): # TODO: Change this to modern DLL entry points. return [ ( filename, os.path.join(target_plugin_dir, os.path.relpath(filename, plugin_dir)), full_name, ) for plugin_dir in self.getQtPluginDirs() for filename in getFileList(plugin_dir) if not filename.endswith(".qml") if not filename.endswith(".mesh") if os.path.exists( os.path.join(target_plugin_dir, os.path.relpath(filename, plugin_dir)) ) ] def createPostModuleLoadCode(self, module): """Create code to load after a module was successfully imported. For Qt we need to set the library path to the distribution folder we are running from. The code is immediately run after the code and therefore makes sure it's updated properly. """ # Only in standalone mode, this will be needed. if not Options.isStandaloneMode(): return full_name = module.getFullName() if full_name == "%s.QtCore" % self.binding_name: code = """\ from __future__ import absolute_import from %(package_name)s import QCoreApplication import os QCoreApplication.setLibraryPaths( [ os.path.join( os.path.dirname(__file__), "qt-plugins" ) ] ) """ % { "package_name": full_name } return ( code, """\ Setting Qt library path to distribution folder. We need to avoid loading target system Qt plug-ins, which may be from another Qt version.""", ) @staticmethod def createPreModuleLoadCode(module): """Method called when a module is being imported. Notes: If full name equals to the binding we insert code to include the dist folder in the 'PATH' environment variable (on Windows only). Args: module: the module object Returns: Code to insert and descriptive text (tuple), or (None, None). """ if ( not isWin32Windows() or not Options.isStandaloneMode() ): # we are only relevant on standalone mode for Windows return None if module.getFullName() != "PyQt5": return None code = """import os path = os.environ.get("PATH", "") if not path.startswith(__nuitka_binary_dir): os.environ["PATH"] = __nuitka_binary_dir + ";" + path """ return ( code, "Adding binary folder to runtime 'PATH' environment variable for proper loading.", ) def considerExtraDlls(self, dist_dir, module): # pylint: disable=too-many-branches,too-many-locals,too-many-statements full_name = module.getFullName() if full_name == self.binding_name: if not self.getQtPluginDirs(): self.sysexit( "Error, failed to detect %r plugin directories." % self.binding_name ) target_plugin_dir = os.path.join(dist_dir, full_name.asPath(), "qt-plugins") self.info( "Copying Qt plug-ins '%s' to '%s'." % ( ",".join( sorted(x for x in self.getQtPluginsSelected() if x != "xml") ), target_plugin_dir, ) ) # TODO: Change this to filtering copyTree while it's doing it. for plugin_dir in self.getQtPluginDirs(): copyTree(plugin_dir, target_plugin_dir) if "all" not in self.getQtPluginsSelected(): for plugin_candidate in getSubDirectories(target_plugin_dir): if ( os.path.basename(plugin_candidate) not in self.getQtPluginsSelected() ): removeDirectory(plugin_candidate, ignore_errors=False) for plugin_candidate in self.getQtPluginsSelected(): if plugin_candidate == "qml": continue if not os.path.isdir( os.path.join(target_plugin_dir, plugin_candidate) ): self.sysexit( "Error, no such Qt plugin family: %s" % plugin_candidate ) result = self.findDLLs( full_name=full_name, target_plugin_dir=target_plugin_dir, ) if isWin32Windows(): # Those 2 vars will be used later, just saving some resources # by caching the files list qt_bin_files = sum( (getFileList(qt_bin_dir) for qt_bin_dir in self._getQtBinDirs()), [], ) self.info("Copying OpenSSL DLLs to %r." % dist_dir) for filename in qt_bin_files: basename = os.path.basename(filename).lower() if basename in ("libeay32.dll", "ssleay32.dll"): shutil.copy(filename, os.path.join(dist_dir, basename)) if ( "qml" in self.getQtPluginsSelected() or "all" in self.getQtPluginsSelected() ): result += self.copyQmlFiles( full_name=full_name, target_plugin_dir=target_plugin_dir, ) # Also copy required OpenGL DLLs on Windows if isWin32Windows(): opengl_dlls = ("libegl.dll", "libglesv2.dll", "opengl32sw.dll") self.info("Copying OpenGL DLLs to %r." % dist_dir) for filename in qt_bin_files: basename = os.path.basename(filename).lower() if basename in opengl_dlls or basename.startswith( "d3dcompiler_" ): shutil.copy(filename, os.path.join(dist_dir, basename)) return result elif full_name == self.binding_name + ".QtNetwork": if not isWin32Windows(): dll_path = locateDLL("crypto") if dll_path is None: dist_dll_path = os.path.join(dist_dir, os.path.basename(dll_path)) shutil.copy(dll_path, dist_dll_path) dll_path = locateDLL("ssl") if dll_path is not None: dist_dll_path = os.path.join(dist_dir, os.path.basename(dll_path)) shutil.copy(dll_path, dist_dll_path) elif ( full_name in ( self.binding_name + ".QtWebEngine", self.binding_name + ".QtWebEngineCore", self.binding_name + ".QtWebEngineWidgets", ) and not self.webengine_done ): self.webengine_done = True # prevent multiple copies self.info("Copying QtWebEngine components") plugin_parent = os.path.dirname(self.getQtPluginDirs()[0]) if isWin32Windows(): bin_dir = os.path.join(plugin_parent, "bin") else: # TODO verify this for non-Windows! bin_dir = os.path.join(plugin_parent, "libexec") target_bin_dir = os.path.join(dist_dir) for f in os.listdir(bin_dir): if f.startswith("QtWebEngineProcess"): shutil.copy(os.path.join(bin_dir, f), target_bin_dir) resources_dir = os.path.join(plugin_parent, "resources") target_resources_dir = os.path.join(dist_dir) for f in os.listdir(resources_dir): shutil.copy(os.path.join(resources_dir, f), target_resources_dir) translations_dir = os.path.join(plugin_parent, "translations") pos = len(translations_dir) + 1 target_translations_dir = os.path.join( dist_dir, full_name.getTopLevelPackageName().asPath(), "Qt", "translations", ) for f in getFileList(translations_dir): tar_f = os.path.join(target_translations_dir, f[pos:]) makePath(os.path.dirname(tar_f)) shutil.copyfile(f, tar_f) return () def removeDllDependencies(self, dll_filename, dll_filenames): for value in self.getQtPluginDirs(): # TODO: That is not a proper check if a file is below that. if dll_filename.startswith(value): for sub_dll_filename in dll_filenames: for badword in ( "libKF5", "libkfontinst", "libkorganizer", "libplasma", "libakregator", "libdolphin", "libnoteshared", "libknotes", "libsystemsettings", "libkerfuffle", "libkaddressbook", "libkworkspace", "libkmail", "libmilou", "libtaskmanager", "libkonsole", "libgwenview", "libweather_ion", ): if os.path.basename(sub_dll_filename).startswith(badword): yield sub_dll_filename
class NuitkaPluginQtBindingsPluginBase(NuitkaPluginBase): # For overload in the derived bindings plugin. binding_name = None def __init__(self, qt_plugins): self.webengine_done = False self.qt_plugins_dirs = None self.qt_plugins = OrderedSet(x.strip().lower() for x in qt_plugins.split(",")) # Allow to specify none. if self.qt_plugins == set(["none"]): self.qt_plugins = set() @classmethod def addPluginCommandLineOptions(cls, group): group.add_option( "--include-qt-plugins", action="store", dest="qt_plugins", default="sensible", help="""\ Which Qt plugins to include. These can be big with dependencies, so by default only the sensible ones are included, but you can also put "all" or list them individually. If you specify something that does not exist, a list of all available will be given.""", ) @abstractmethod def _getQmlTargetDir(self, target_plugin_dir): """ Where does the bindings package expect the QML files. """ def getQtPluginsSelected(self): # Resolve "sensible on first use" if "sensible" in self.qt_plugins: # Most used ones with low dependencies. self.qt_plugins.update( tuple( family for family in ( "imageformats", "iconengines", "mediaservice", "printsupport", "platforms", "styles", ) if self.hasPluginFamily(family) ) ) self.qt_plugins.remove("sensible") # Make sure the above didn't detect nothing, which would be # indicating the check to be bad. assert self.qt_plugins return self.qt_plugins def _getQtInformation(self): # This is generic, and therefore needs to apply this to a lot of strings. def applyBindingName(template): return template % {"binding_name": self.binding_name} setup_codes = applyBindingName( r"""\ import os import %(binding_name)s.QtCore """ ) return self.queryRuntimeInformationMultiple( info_name=applyBindingName("%(binding_name)s_info"), setup_codes=setup_codes, values=( ( "library_paths", applyBindingName( "%(binding_name)s.QtCore.QCoreApplication.libraryPaths()" ), ), ( "guess_path1", applyBindingName( "os.path.join(os.path.dirname(%(binding_name)s.__file__), 'plugins')" ), ), ( "guess_path2", applyBindingName( "os.path.join(os.path.dirname(%(binding_name)s.__file__), '..', '..', '..', 'Library', 'plugins')" ), ), ( "version", applyBindingName( "%(binding_name)s.__version_info__" if "PySide" in self.binding_name else "%(binding_name)s.QtCore.PYQT_VERSION_STR" ), ), ( "nuitka_patch_level", applyBindingName( "getattr(%(binding_name)s, '_nuitka_patch_level', 0)" ), ), ), ) def _getBindingVersion(self): """ Get the version of the binding in tuple digit form, e.g. (6,0,3) """ return self._getQtInformation().version def _getNuitkaPatchLevel(self): """ Does it include the Nuitka patch, i.e. is a self-built one with it applied. """ return self._getQtInformation().nuitka_patch_level def getQtPluginDirs(self): if self.qt_plugins_dirs is not None: return self.qt_plugins_dirs qt_info = self._getQtInformation() self.qt_plugins_dirs = qt_info.library_paths if not self.qt_plugins_dirs and os.path.exists(qt_info.guess_path1): self.qt_plugins_dirs.append(qt_info.guess_path1) if not self.qt_plugins_dirs and os.path.exists(qt_info.guess_path2): self.qt_plugins_dirs.append(qt_info.guess_path2) # Avoid duplicates. self.qt_plugins_dirs = tuple(sorted(set(self.qt_plugins_dirs))) if not self.qt_plugins_dirs: self.warning("Couldn't detect Qt plugin directories.") return self.qt_plugins_dirs def _getQtBinDirs(self): for plugin_dir in self.getQtPluginDirs(): qt_bin_dir = os.path.normpath(os.path.join(plugin_dir, "..", "bin")) if os.path.isdir(qt_bin_dir): yield qt_bin_dir def hasPluginFamily(self, family): for plugin_dir in self.getQtPluginDirs(): if os.path.isdir(os.path.join(plugin_dir, family)): return True # TODO: Special case "xml". return False def copyQmlFiles(self, full_name, target_plugin_dir): for plugin_dir in self.getQtPluginDirs(): qml_plugin_dir = os.path.normpath(os.path.join(plugin_dir, "..", "qml")) if os.path.exists(qml_plugin_dir): break else: self.sysexit("Error, no such Qt plugin family: qml") qml_target_dir = os.path.normpath(self._getQmlTargetDir(target_plugin_dir)) self.info("Copying Qt plug-ins 'qml' to '%s'." % (qml_target_dir)) copyTree(qml_plugin_dir, qml_target_dir) # We try to filter here, not for DLLs. return [ ( filename, os.path.join(qml_target_dir, os.path.relpath(filename, qml_plugin_dir)), full_name, ) for filename in getFileList(qml_plugin_dir) if not filename.endswith( ( ".qml", ".qmlc", ".qmltypes", ".js", ".jsc", ".png", ".ttf", ".metainfo", ) ) if not os.path.isdir(filename) if not os.path.basename(filename) == "qmldir" ] def findDLLs(self, full_name, target_plugin_dir): # TODO: Change this to modern DLL entry points. return [ ( filename, os.path.join(target_plugin_dir, os.path.relpath(filename, plugin_dir)), full_name, ) for plugin_dir in self.getQtPluginDirs() for filename in getFileList(plugin_dir) if not filename.endswith(".qml") if not filename.endswith(".mesh") if os.path.exists( os.path.join(target_plugin_dir, os.path.relpath(filename, plugin_dir)) ) ] def _getChildNamed(self, *child_names): for child_name in child_names: return ModuleName(self.binding_name).getChildNamed(child_name) def getImplicitImports(self, module): # Way too many indeed, pylint: disable=too-many-branches full_name = module.getFullName() top_level_package_name, child_name = full_name.splitPackageName() if top_level_package_name != self.binding_name: return # These are alternatives depending on PyQt5 version if child_name == "QtCore" and "PyQt" in self.binding_name: if python_version < 0x300: yield "atexit" yield "sip" yield self._getChildNamed("sip") if child_name in ( "QtGui", "QtAssistant", "QtDBus", "QtDeclarative", "QtSql", "QtDesigner", "QtHelp", "QtNetwork", "QtScript", "QtQml", "QtGui", "QtScriptTools", "QtSvg", "QtTest", "QtWebKit", "QtOpenGL", "QtXml", "QtXmlPatterns", "QtPrintSupport", "QtNfc", "QtWebKitWidgets", "QtBluetooth", "QtMultimediaWidgets", "QtQuick", "QtWebChannel", "QtWebSockets", "QtX11Extras", "_QOpenGLFunctions_2_0", "_QOpenGLFunctions_2_1", "_QOpenGLFunctions_4_1_Core", ): yield self._getChildNamed("QtCore") if child_name in ( "QtDeclarative", "QtWebKit", "QtXmlPatterns", "QtQml", "QtPrintSupport", "QtWebKitWidgets", "QtMultimedia", "QtMultimediaWidgets", "QtQuick", "QtQuickWidgets", "QtWebSockets", "QtWebEngineWidgets", ): yield self._getChildNamed("QtNetwork") if child_name == "QtWebEngineWidgets": yield self._getChildNamed("QtWebEngineCore") yield self._getChildNamed("QtWebChannel") yield self._getChildNamed("QtPrintSupport") elif child_name == "QtScriptTools": yield self._getChildNamed("QtScript") elif child_name in ( "QtWidgets", "QtDeclarative", "QtDesigner", "QtHelp", "QtScriptTools", "QtSvg", "QtTest", "QtWebKit", "QtPrintSupport", "QtWebKitWidgets", "QtMultimedia", "QtMultimediaWidgets", "QtOpenGL", "QtQuick", "QtQuickWidgets", "QtSql", "_QOpenGLFunctions_2_0", "_QOpenGLFunctions_2_1", "_QOpenGLFunctions_4_1_Core", ): yield self._getChildNamed("QtGui") if child_name in ( "QtDesigner", "QtHelp", "QtTest", "QtPrintSupport", "QtSvg", "QtOpenGL", "QtWebKitWidgets", "QtMultimediaWidgets", "QtQuickWidgets", "QtSql", ): yield self._getChildNamed("QtWidgets") if child_name in ("QtPrintSupport",): yield self._getChildNamed("QtSvg") if child_name in ("QtWebKitWidgets",): yield self._getChildNamed("QtWebKit") yield self._getChildNamed("QtPrintSupport") if child_name in ("QtMultimediaWidgets",): yield self._getChildNamed("QtMultimedia") if child_name in ("QtQuick", "QtQuickWidgets"): yield self._getChildNamed("QtQml") if child_name in ("QtQuickWidgets", "QtQml"): yield self._getChildNamed("QtQuick") if child_name == "Qt": yield self._getChildNamed("QtCore") yield self._getChildNamed("QtDBus") yield self._getChildNamed("QtGui") yield self._getChildNamed("QtNetwork") yield self._getChildNamed("QtNetworkAuth") yield self._getChildNamed("QtSensors") yield self._getChildNamed("QtSerialPort") yield self._getChildNamed("QtMultimedia") yield self._getChildNamed("QtQml") yield self._getChildNamed("QtWidgets") # TODO: Questionable if this still exists in newer PySide. if child_name == "QtUiTools": yield self._getChildNamed("QtGui") yield self._getChildNamed("QtXml") # TODO: Questionable if this still exists in newer PySide. if full_name == "phonon": yield self._getChildNamed("QtGui") def createPostModuleLoadCode(self, module): """Create code to load after a module was successfully imported. For Qt we need to set the library path to the distribution folder we are running from. The code is immediately run after the code and therefore makes sure it's updated properly. """ # Only in standalone mode, this will be needed. if not Options.isStandaloneMode(): return full_name = module.getFullName() if full_name == "%s.QtCore" % self.binding_name: code = """\ from __future__ import absolute_import from %(package_name)s import QCoreApplication import os QCoreApplication.setLibraryPaths( [ os.path.join( os.path.dirname(__file__), "qt-plugins" ) ] ) """ % { "package_name": full_name } return ( code, """\ Setting Qt library path to distribution folder. We need to avoid loading target system Qt plug-ins, which may be from another Qt version.""", ) def createPreModuleLoadCode(self, module): """Method called when a module is being imported. Notes: If full name equals to the binding we insert code to include the dist folder in the 'PATH' environment variable (on Windows only). Args: module: the module object Returns: Code to insert and descriptive text (tuple), or (None, None). """ # This isonly relevant on standalone mode for Windows if not isWin32Windows() or not Options.isStandaloneMode(): return None full_name = module.getFullName() if full_name == self.binding_name: code = """import os path = os.environ.get("PATH", "") if not path.startswith(__nuitka_binary_dir): os.environ["PATH"] = __nuitka_binary_dir + ";" + path """ return ( code, "Adding binary folder to runtime 'PATH' environment variable for proper loading.", ) def considerExtraDlls(self, dist_dir, module): # pylint: disable=too-many-branches,too-many-locals,too-many-statements full_name = module.getFullName() if full_name == self.binding_name: if not self.getQtPluginDirs(): self.sysexit( "Error, failed to detect %r plugin directories." % self.binding_name ) target_plugin_dir = os.path.join(dist_dir, full_name.asPath(), "qt-plugins") self.info( "Copying Qt plug-ins '%s' to '%s'." % ( ",".join( sorted(x for x in self.getQtPluginsSelected() if x != "xml") ), target_plugin_dir, ) ) # TODO: Change this to filtering copyTree while it's doing it. for plugin_dir in self.getQtPluginDirs(): copyTree(plugin_dir, target_plugin_dir) if "all" not in self.getQtPluginsSelected(): for plugin_candidate in getSubDirectories(target_plugin_dir): if ( os.path.basename(plugin_candidate) not in self.getQtPluginsSelected() ): removeDirectory(plugin_candidate, ignore_errors=False) for plugin_candidate in self.getQtPluginsSelected(): if plugin_candidate == "qml": continue if not os.path.isdir( os.path.join(target_plugin_dir, plugin_candidate) ): self.sysexit( "Error, no such Qt plugin family: %s" % plugin_candidate ) result = self.findDLLs( full_name=full_name, target_plugin_dir=target_plugin_dir, ) if isWin32Windows(): # Those 2 vars will be used later, just saving some resources # by caching the files list qt_bin_files = sum( (getFileList(qt_bin_dir) for qt_bin_dir in self._getQtBinDirs()), [], ) self.info("Copying OpenSSL DLLs to %r." % dist_dir) for filename in qt_bin_files: basename = os.path.basename(filename).lower() if basename in ("libeay32.dll", "ssleay32.dll"): shutil.copy(filename, os.path.join(dist_dir, basename)) if ( "qml" in self.getQtPluginsSelected() or "all" in self.getQtPluginsSelected() ): result += self.copyQmlFiles( full_name=full_name, target_plugin_dir=target_plugin_dir, ) # Also copy required OpenGL DLLs on Windows if isWin32Windows(): opengl_dlls = ("libegl.dll", "libglesv2.dll", "opengl32sw.dll") self.info("Copying OpenGL DLLs to %r." % dist_dir) for filename in qt_bin_files: basename = os.path.basename(filename).lower() if basename in opengl_dlls or basename.startswith( "d3dcompiler_" ): shutil.copy(filename, os.path.join(dist_dir, basename)) return result elif full_name == self.binding_name + ".QtNetwork": if not isWin32Windows(): dll_path = locateDLL("crypto") if dll_path is not None: dist_dll_path = os.path.join(dist_dir, os.path.basename(dll_path)) shutil.copy(dll_path, dist_dll_path) dll_path = locateDLL("ssl") if dll_path is not None: dist_dll_path = os.path.join(dist_dir, os.path.basename(dll_path)) shutil.copy(dll_path, dist_dll_path) elif ( full_name in ( self.binding_name + ".QtWebEngine", self.binding_name + ".QtWebEngineCore", self.binding_name + ".QtWebEngineWidgets", ) and not self.webengine_done ): self.webengine_done = True # prevent multiple copies self.info("Copying QtWebEngine components") plugin_parent = os.path.dirname(self.getQtPluginDirs()[0]) if isWin32Windows(): bin_dir = os.path.join(plugin_parent, "bin") else: # TODO verify this for non-Windows! bin_dir = os.path.join(plugin_parent, "libexec") target_bin_dir = os.path.join(dist_dir) for f in os.listdir(bin_dir): if f.startswith("QtWebEngineProcess"): shutil.copy(os.path.join(bin_dir, f), target_bin_dir) resources_dir = os.path.join(plugin_parent, "resources") target_resources_dir = os.path.join(dist_dir) for f in os.listdir(resources_dir): shutil.copy(os.path.join(resources_dir, f), target_resources_dir) translations_dir = os.path.join(plugin_parent, "translations") pos = len(translations_dir) + 1 target_translations_dir = os.path.join( dist_dir, full_name.getTopLevelPackageName().asPath(), "Qt", "translations", ) for f in getFileList(translations_dir): tar_f = os.path.join(target_translations_dir, f[pos:]) makePath(os.path.dirname(tar_f)) shutil.copyfile(f, tar_f) return () def removeDllDependencies(self, dll_filename, dll_filenames): for value in self.getQtPluginDirs(): # TODO: That is not a proper check if a file is below that. if dll_filename.startswith(value): for sub_dll_filename in dll_filenames: for badword in ( "libKF5", "libkfontinst", "libkorganizer", "libplasma", "libakregator", "libdolphin", "libnoteshared", "libknotes", "libsystemsettings", "libkerfuffle", "libkaddressbook", "libkworkspace", "libkmail", "libmilou", "libtaskmanager", "libkonsole", "libgwenview", "libweather_ion", ): if os.path.basename(sub_dll_filename).startswith(badword): yield sub_dll_filename
class NuitkaPluginQtBindingsPluginBase(NuitkaPluginBase): # For overload in the derived bindings plugin. binding_name = None def __init__(self, qt_plugins, no_qt_translations): self.qt_plugins = OrderedSet(x.strip().lower() for x in qt_plugins.split(",")) self.no_qt_translations = no_qt_translations self.webengine_done_binaries = False self.webengine_done_data = False self.qt_plugins_dirs = None self.binding_package_name = ModuleName(self.binding_name) # Allow to specify none. if self.qt_plugins == set(["none"]): self.qt_plugins = set() # Prevent the list of binding names from being incomplete, it's used for conflicts. assert self.binding_name in _qt_binding_names, self.binding_name # Also lets have consistency in naming. assert self.plugin_name in getQtPluginNames() active_qt_plugin_name = getActiveQtPlugin() if active_qt_plugin_name is not None: self.sysexit( "Error, confliciting plugin '%s', you can only have one enabled." % active_qt_plugin_name) self.warned_about = set() @classmethod def addPluginCommandLineOptions(cls, group): group.add_option( "--include-qt-plugins", action="store", dest="qt_plugins", default="sensible", help="""\ Which Qt plugins to include. These can be big with dependencies, so by default only the sensible ones are included, but you can also put "all" or list them individually. If you specify something that does not exist, a list of all available will be given.""", ) group.add_option( "--noinclude-qt-translations", action="store", dest="no_qt_translations", default=False, help="""\ Include Qt translations with QtWebEngine if used. These can be a lot of files that you may not want to be included.""", ) def _getQmlTargetDir(self): """Where does the Qt bindings package expect the QML files.""" return os.path.join(self.binding_name, "qml") def _getResourcesTargetDir(self): """Where does the Qt bindings package expect the resources files.""" if isMacOS(): return "Content/Resources" elif isWin32Windows(): if self.binding_name in ("PySide2", "PyQt5"): return "resources" else: # While PyQt6/PySide6 complains about these, they are not working # return os.path.join(self.binding_name, "resources") return "." else: if self.binding_name in ("PySide2", "PySide6", "PyQt6"): return "." elif self.binding_name == "PyQt5": return "resources" else: assert False def _getTranslationsTargetDir(self): """Where does the Qt bindings package expect the translation files.""" if isMacOS(): return "Content/Resources" elif isWin32Windows(): if self.binding_name in ("PySide2", "PyQt5"): return "translations" elif self.binding_name == "PyQt6": # TODO: PyQt6 is complaining about not being in "translations", but ignores it there. return "." else: return os.path.join(self.binding_name, "translations") else: if self.binding_name in ("PySide2", "PySide6", "PyQt6"): return "." elif self.binding_name == "PyQt5": return "translations" else: assert False @staticmethod def _getWebEngineTargetDir(): """Where does the Qt bindings package expect the web process executable.""" return "Helpers" if isMacOS() else "." def getQtPluginsSelected(self): # Resolve "sensible on first use" if "sensible" in self.qt_plugins: # Most used ones with low dependencies. self.qt_plugins.update( tuple(family for family in ( "imageformats", "iconengines", "mediaservice", "printsupport", "platforms", "platformthemes", "styles", # Wayland on Linux needs these "wayland-shell-integration", "wayland-decoration-client", "wayland-graphics-integration-client", "egldeviceintegrations", # OpenGL rendering, maybe should be something separate. "xcbglintegrations", ) if self.hasPluginFamily(family))) self.qt_plugins.remove("sensible") # Make sure the above didn't detect nothing, which would be # indicating the check to be bad. assert self.qt_plugins return self.qt_plugins def hasQtPluginSelected(self, plugin_name): selected = self.getQtPluginsSelected() return "all" in selected or plugin_name in selected def _getQtInformation(self): # This is generic, and therefore needs to apply this to a lot of strings. def applyBindingName(template): return template % {"binding_name": self.binding_name} def getLocationQueryCode(path_name): if self.binding_name == "PyQt6": template = """\ %(binding_name)s.QtCore.QLibraryInfo.path(%(binding_name)s.QtCore.QLibraryInfo.LibraryPath.%(path_name)s)""" else: template = """\ %(binding_name)s.QtCore.QLibraryInfo.location(%(binding_name)s.QtCore.QLibraryInfo.%(path_name)s)""" return template % { "binding_name": self.binding_name, "path_name": path_name, } setup_codes = applyBindingName(r""" import os import %(binding_name)s.QtCore """) info = self.queryRuntimeInformationMultiple( info_name=applyBindingName("%(binding_name)s_info"), setup_codes=setup_codes, values=( ( "library_paths", applyBindingName( "%(binding_name)s.QtCore.QCoreApplication.libraryPaths()" ), ), ( "guess_path1", applyBindingName( "os.path.join(os.path.dirname(%(binding_name)s.__file__), 'plugins')" ), ), ( "guess_path2", applyBindingName( "os.path.join(os.path.dirname(%(binding_name)s.__file__), '..', '..', '..', 'Library', 'plugins')" ), ), ( "version", applyBindingName( "%(binding_name)s.__version_info__" if "PySide" in self.binding_name else "%(binding_name)s.QtCore.PYQT_VERSION_STR"), ), ( "nuitka_patch_level", applyBindingName( "getattr(%(binding_name)s, '_nuitka_patch_level', 0)"), ), ("translations_path", getLocationQueryCode("TranslationsPath")), ( "library_executables_path", getLocationQueryCode("LibraryExecutablesPath"), ), ("data_path", getLocationQueryCode("DataPath")), ), ) if info is None: self.sysexit("Error, it seems '%s' is not installed." % self.binding_name) return info def _getBindingVersion(self): """Get the version of the binding in tuple digit form, e.g. (6,0,3)""" return self._getQtInformation().version def _getNuitkaPatchLevel(self): """Does it include the Nuitka patch, i.e. is a self-built one with it applied.""" return self._getQtInformation().nuitka_patch_level def _getTranslationsPath(self): """Get the path to the Qt translations.""" return self._getQtInformation().translations_path def _getResourcesPath(self): """Get the path to the Qt webengine resources.""" return os.path.join(self._getQtInformation().data_path, "resources") def _getLibraryExecutablePath(self): """Get the patch to Qt binaries.""" return self._getQtInformation().library_executables_path def getQtPluginDirs(self): if self.qt_plugins_dirs is not None: return self.qt_plugins_dirs qt_info = self._getQtInformation() self.qt_plugins_dirs = qt_info.library_paths if not self.qt_plugins_dirs and os.path.exists(qt_info.guess_path1): self.qt_plugins_dirs.append(qt_info.guess_path1) if not self.qt_plugins_dirs and os.path.exists(qt_info.guess_path2): self.qt_plugins_dirs.append(qt_info.guess_path2) # Avoid duplicates. self.qt_plugins_dirs = [ os.path.normpath(dirname) for dirname in self.qt_plugins_dirs ] self.qt_plugins_dirs = tuple(sorted(set(self.qt_plugins_dirs))) if not self.qt_plugins_dirs: self.warning("Couldn't detect Qt plugin directories.") return self.qt_plugins_dirs def _getQtBinDirs(self): for plugin_dir in self.getQtPluginDirs(): if "PyQt" in self.binding_name: qt_bin_dir = os.path.normpath( os.path.join(plugin_dir, "..", "bin")) if os.path.isdir(qt_bin_dir): yield qt_bin_dir else: qt_bin_dir = os.path.normpath(os.path.join(plugin_dir, "..")) yield qt_bin_dir def hasPluginFamily(self, family): return any( os.path.isdir(os.path.join(plugin_dir, family)) for plugin_dir in self.getQtPluginDirs()) def _getQmlDirectory(self): for plugin_dir in self.getQtPluginDirs(): qml_plugin_dir = os.path.normpath( os.path.join(plugin_dir, "..", "qml")) if os.path.exists(qml_plugin_dir): return qml_plugin_dir self.sysexit("Error, no such Qt plugin family: qml") def _getQmlFileList(self, dlls): qml_plugin_dir = self._getQmlDirectory() # List all file types of the QML plugin folder that are datafiles and not DLLs. datafile_suffixes = ( ".qml", ".qmlc", ".qmltypes", ".js", ".jsc", ".png", ".ttf", ".metainfo", ".mesh", ".frag", "qmldir", ) if dlls: ignore_suffixes = datafile_suffixes only_suffixes = () else: ignore_suffixes = () only_suffixes = datafile_suffixes return getFileList( qml_plugin_dir, ignore_suffixes=ignore_suffixes, only_suffixes=only_suffixes, ) def _findQtPluginDLLs(self): for qt_plugins_dir in self.getQtPluginDirs(): for filename in getFileList(qt_plugins_dir): filename_relative = os.path.relpath(filename, start=qt_plugins_dir) qt_plugin_name = filename_relative.split(os.path.sep, 1)[0] if not self.hasQtPluginSelected(qt_plugin_name): continue yield self.makeDllEntryPoint( source_path=filename, dest_path=os.path.join( self.binding_name, "qt-plugins", filename_relative, ), package_name=self.binding_package_name, ) def _getChildNamed(self, *child_names): for child_name in child_names: return ModuleName(self.binding_name).getChildNamed(child_name) def getImplicitImports(self, module): # Way too many indeed, pylint: disable=too-many-branches,too-many-statements full_name = module.getFullName() top_level_package_name, child_name = full_name.splitPackageName() if top_level_package_name != self.binding_name: return # These are alternatives depending on PyQt5 version if child_name == "QtCore" and "PyQt" in self.binding_name: if python_version < 0x300: yield "atexit" yield "sip" yield self._getChildNamed("sip") if child_name in ( "QtGui", "QtAssistant", "QtDBus", "QtDeclarative", "QtSql", "QtDesigner", "QtHelp", "QtNetwork", "QtScript", "QtQml", "QtGui", "QtScriptTools", "QtSvg", "QtTest", "QtWebKit", "QtOpenGL", "QtXml", "QtXmlPatterns", "QtPrintSupport", "QtNfc", "QtWebKitWidgets", "QtBluetooth", "QtMultimediaWidgets", "QtQuick", "QtWebChannel", "QtWebSockets", "QtX11Extras", "_QOpenGLFunctions_2_0", "_QOpenGLFunctions_2_1", "_QOpenGLFunctions_4_1_Core", ): yield self._getChildNamed("QtCore") if child_name in ( "QtDeclarative", "QtWebKit", "QtXmlPatterns", "QtQml", "QtPrintSupport", "QtWebKitWidgets", "QtMultimedia", "QtMultimediaWidgets", "QtQuick", "QtQuickWidgets", "QtWebSockets", "QtWebEngineWidgets", ): yield self._getChildNamed("QtNetwork") if child_name == "QtWebEngineWidgets": yield self._getChildNamed("QtWebEngineCore") yield self._getChildNamed("QtWebChannel") yield self._getChildNamed("QtPrintSupport") elif child_name == "QtScriptTools": yield self._getChildNamed("QtScript") elif child_name in ( "QtWidgets", "QtDeclarative", "QtDesigner", "QtHelp", "QtScriptTools", "QtSvg", "QtTest", "QtWebKit", "QtPrintSupport", "QtWebKitWidgets", "QtMultimedia", "QtMultimediaWidgets", "QtOpenGL", "QtQuick", "QtQuickWidgets", "QtSql", "_QOpenGLFunctions_2_0", "_QOpenGLFunctions_2_1", "_QOpenGLFunctions_4_1_Core", ): yield self._getChildNamed("QtGui") if child_name in ( "QtDesigner", "QtHelp", "QtTest", "QtPrintSupport", "QtSvg", "QtOpenGL", "QtWebKitWidgets", "QtMultimediaWidgets", "QtQuickWidgets", "QtSql", ): yield self._getChildNamed("QtWidgets") if child_name in ("QtPrintSupport", ): yield self._getChildNamed("QtSvg") if child_name in ("QtWebKitWidgets", ): yield self._getChildNamed("QtWebKit") yield self._getChildNamed("QtPrintSupport") if child_name in ("QtMultimediaWidgets", ): yield self._getChildNamed("QtMultimedia") if child_name in ("QtQuick", "QtQuickWidgets"): yield self._getChildNamed("QtQml") yield self._getChildNamed("QtOpenGL") if child_name in ("QtQuickWidgets", "QtQml", "QtQuickControls2"): yield self._getChildNamed("QtQuick") if child_name == "Qt": yield self._getChildNamed("QtCore") yield self._getChildNamed("QtDBus") yield self._getChildNamed("QtGui") yield self._getChildNamed("QtNetwork") yield self._getChildNamed("QtNetworkAuth") yield self._getChildNamed("QtSensors") yield self._getChildNamed("QtSerialPort") yield self._getChildNamed("QtMultimedia") yield self._getChildNamed("QtQml") yield self._getChildNamed("QtWidgets") # TODO: Questionable if this still exists in newer PySide. if child_name == "QtUiTools": yield self._getChildNamed("QtGui") yield self._getChildNamed("QtXml") # TODO: Questionable if this still exists in newer PySide. if full_name == "phonon": yield self._getChildNamed("QtGui") def createPostModuleLoadCode(self, module): """Create code to load after a module was successfully imported. For Qt we need to set the library path to the distribution folder we are running from. The code is immediately run after the code and therefore makes sure it's updated properly. """ # Only in standalone mode, this will be needed. if not isStandaloneMode(): return full_name = module.getFullName() if full_name == "%s.QtCore" % self.binding_name: code = """\ from __future__ import absolute_import from %(package_name)s import QCoreApplication import os QCoreApplication.setLibraryPaths( [ os.path.join( os.path.dirname(__file__), "qt-plugins" ) ] ) os.environ["QML2_IMPORT_PATH"] = os.path.join( os.path.dirname(__file__), "qml" ) """ % { "package_name": full_name } yield ( code, """\ Setting Qt library path to distribution folder. We need to avoid loading target system Qt plugins, which may be from another Qt version.""", ) def isQtWebEngineModule(self, full_name): return full_name in ( self.binding_name + ".QtWebEngine", self.binding_name + ".QtWebEngineCore", self.binding_name + ".QtWebEngineWidgets", ) def createPreModuleLoadCode(self, module): """Method called when a module is being imported. Notes: If full name equals to the binding we insert code to include the dist folder in the 'PATH' environment variable (on Windows only). Args: module: the module object Returns: Code to insert and descriptive text (tuple), or (None, None). """ # This is only relevant on standalone mode for Windows if not isStandaloneMode(): return full_name = module.getFullName() if full_name == self.binding_name and isWin32Windows(): code = """import os path = os.environ.get("PATH", "") if not path.startswith(__nuitka_binary_dir): os.environ["PATH"] = __nuitka_binary_dir + ";" + path """ yield ( code, "Adding binary folder to runtime 'PATH' environment variable for proper Qt loading.", ) def considerDataFiles(self, module): full_name = module.getFullName() if full_name == self.binding_name and ( "qml" in self.getQtPluginsSelected() or "all" in self.getQtPluginsSelected()): qml_plugin_dir = self._getQmlDirectory() qml_target_dir = self._getQmlTargetDir() self.info("Including Qt plugins 'qml' below '%s'." % qml_target_dir) for filename in self._getQmlFileList(dlls=False): filename_relative = os.path.relpath(filename, qml_plugin_dir) yield self.makeIncludedDataFile( source_path=filename, dest_path=os.path.join( qml_target_dir, filename_relative, ), reason="Qt QML datafile", tags="qml", ) elif self.isQtWebEngineModule( full_name) and not self.webengine_done_data: self.webengine_done_data = True # TODO: This is probably wrong/not needed on macOS if not isMacOS(): yield self.makeIncludedGeneratedDataFile( data="""\ [Paths] Prefix = . """, dest_path="qt6.conf" if "6" in self.binding_name else "qt.conf", reason="QtWebEngine needs Qt configuration file", ) resources_dir = self._getResourcesPath() for filename, filename_relative in listDir(resources_dir): yield self.makeIncludedDataFile( source_path=filename, dest_path=os.path.join(self._getResourcesTargetDir(), filename_relative), reason="Qt resources", ) if not self.no_qt_translations: translations_path = self._getTranslationsPath() for filename in getFileList(translations_path): filename_relative = os.path.relpath( filename, translations_path) dest_path = self._getTranslationsTargetDir() yield self.makeIncludedDataFile( source_path=filename, dest_path=os.path.join(dest_path, filename_relative), reason="Qt translation", tags="translation", ) def getExtraDlls(self, module): # pylint: disable=too-many-branches full_name = module.getFullName() if full_name == self.binding_name: if not self.getQtPluginDirs(): self.sysexit( "Error, failed to detect '%s' plugin directories." % self.binding_name) target_plugin_dir = os.path.join(full_name.asPath(), "qt-plugins") self.info("Including Qt plugins '%s' below '%s'." % ( ",".join( sorted( x for x in self.getQtPluginsSelected() if x != "xml")), target_plugin_dir, )) # TODO: Yielding a generator should become OK too. for r in self._findQtPluginDLLs(): yield r if isWin32Windows(): # Those 2 vars will be used later, just saving some resources # by caching the files list qt_bin_files = sum( (getFileList(qt_bin_dir) for qt_bin_dir in self._getQtBinDirs()), [], ) count = 0 for filename in qt_bin_files: basename = os.path.basename(filename).lower() if basename in ("libeay32.dll", "ssleay32.dll"): yield self.makeDllEntryPoint( source_path=filename, dest_path=basename, package_name=full_name, ) count += 1 self.reportFileCount(full_name, count, section="OpenSSL") if ("qml" in self.getQtPluginsSelected() or "all" in self.getQtPluginsSelected()): qml_plugin_dir = self._getQmlDirectory() qml_target_dir = self._getQmlTargetDir() for filename in self._getQmlFileList(dlls=True): filename_relative = os.path.relpath( filename, qml_plugin_dir) yield self.makeDllEntryPoint(source_path=filename, dest_path=os.path.join( qml_target_dir, filename_relative, ), package_name=full_name # reason="Qt QML plugin DLL", ) # Also copy required OpenGL DLLs on Windows if isWin32Windows(): opengl_dlls = ("libegl.dll", "libglesv2.dll", "opengl32sw.dll") count = 0 for filename in qt_bin_files: basename = os.path.basename(filename).lower() if basename in opengl_dlls or basename.startswith( "d3dcompiler_"): yield self.makeDllEntryPoint( source_path=filename, dest_path=basename, package_name=full_name, ) self.reportFileCount(full_name, count, section="OpenGL") elif full_name == self.binding_name + ".QtNetwork": if not isWin32Windows(): dll_path = self.locateDLL("crypto") if dll_path is not None: yield self.makeDllEntryPoint( source_path=dll_path, dest_path=os.path.basename(dll_path), package_name=full_name, ) dll_path = self.locateDLL("ssl") if dll_path is not None: yield self.makeDllEntryPoint( source_path=dll_path, dest_path=os.path.basename(dll_path), package_name=full_name, ) elif self.isQtWebEngineModule( full_name) and not self.webengine_done_binaries: self.webengine_done_binaries = True # prevent multiple copies self.info("Including QtWebEngine executable.") qt_web_engine_dir = self._getLibraryExecutablePath() for filename, filename_relative in listDir(qt_web_engine_dir): if filename_relative.startswith("QtWebEngineProcess"): yield makeExeEntryPoint( source_path=filename, dest_path=os.path.join(self._getWebEngineTargetDir(), filename_relative), package_name=full_name, ) break else: self.sysexit( "Error, cannot locate QtWebEngineProcess executable at '%s'." % qt_web_engine_dir) def removeDllDependencies(self, dll_filename, dll_filenames): for value in self.getQtPluginDirs(): # TODO: That is not a proper check if a file is below that. if dll_filename.startswith(value): for sub_dll_filename in dll_filenames: for badword in ( "libKF5", "libkfontinst", "libkorganizer", "libplasma", "libakregator", "libdolphin", "libnoteshared", "libknotes", "libsystemsettings", "libkerfuffle", "libkaddressbook", "libkworkspace", "libkmail", "libmilou", "libtaskmanager", "libkonsole", "libgwenview", "libweather_ion", ): if os.path.basename(sub_dll_filename).startswith( badword): yield sub_dll_filename def onModuleEncounter(self, module_filename, module_name, module_kind): top_package_name = module_name.getTopLevelPackageName() if isStandaloneMode(): if (top_package_name in _qt_binding_names and top_package_name != self.binding_name): if top_package_name not in self.warned_about: self.info( """\ Unwanted import of '%(unwanted)s' that conflicts with '%(binding_name)s' encountered, preventing its use. As a result an "ImportError" might be given at run time. Uninstall it for full compatible behavior with the uncompiled code to debug it.""" % { "unwanted": top_package_name, "binding_name": self.binding_name, }) self.warned_about.add(top_package_name) return ( False, "Not included due to potentially conflicting Qt versions with selected Qt binding '%s'." % self.binding_name, ) def onModuleCompleteSet(self, module_set): for module in module_set: module_name = module.getFullName() if module_name in _qt_binding_names and module_name != self.binding_name: self.warning("""\ Unwanted import of '%(unwanted)s' that conflicts with '%(binding_name)s' encountered. Use \ '--nofollow-import-to=%(unwanted)s' or uninstall it.""" % { "unwanted": module_name, "binding_name": self.binding_name }) if module_name in _other_gui_binding_names: self.warning("""\ Unwanted import of '%(unwanted)s' that conflicts with '%(binding_name)s' encountered. Use \ '--nofollow-import-to=%(unwanted)s' or uninstall it.""" % { "unwanted": module_name, "binding_name": self.binding_name }) def onModuleSourceCode(self, module_name, source_code): """Third party packages that make binding selections.""" if module_name.hasNamespace("pyqtgraph"): # TODO: Add a mechanism to force all variable references of a name to something # during tree building, that would cover all uses in a nicer way. source_code = source_code.replace("{QT_LIB.lower()}", self.binding_name.lower()) source_code = source_code.replace("QT_LIB.lower()", repr(self.binding_name.lower())) return source_code