diff options
-rw-r--r-- | sources/pyside-tools/deploy.py | 1 | ||||
-rw-r--r-- | sources/pyside-tools/deploy_lib/config.py | 28 | ||||
-rw-r--r-- | sources/pyside-tools/deploy_lib/default.spec | 3 | ||||
-rw-r--r-- | sources/pyside-tools/deploy_lib/dependency_util.py | 64 | ||||
-rw-r--r-- | sources/pyside-tools/deploy_lib/nuitka_helper.py | 33 | ||||
-rw-r--r-- | sources/pyside6/doc/_tags/android.rst | 21 | ||||
-rw-r--r-- | sources/pyside6/doc/_tags/tagsindex.rst | 12 | ||||
-rw-r--r-- | sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py | 68 |
8 files changed, 188 insertions, 42 deletions
diff --git a/sources/pyside-tools/deploy.py b/sources/pyside-tools/deploy.py index 6cb6d4d9c..b54943ddf 100644 --- a/sources/pyside-tools/deploy.py +++ b/sources/pyside-tools/deploy.py @@ -120,6 +120,7 @@ def main(main_file: Path = None, name: str = None, config_file: Path = None, ini command_str = nuitka.create_executable(source_file=config.source_file, extra_args=config.extra_args, qml_files=config.qml_files, + qt_plugins=config.qt_plugins, excluded_qml_plugins=config.excluded_qml_plugins, icon=config.icon, dry_run=dry_run) diff --git a/sources/pyside-tools/deploy_lib/config.py b/sources/pyside-tools/deploy_lib/config.py index 44b4ded06..f1c877cac 100644 --- a/sources/pyside-tools/deploy_lib/config.py +++ b/sources/pyside-tools/deploy_lib/config.py @@ -365,7 +365,7 @@ class DesktopConfig(Config): existing_config_file: bool = False, extra_ignore_dirs: List[str] = None): super().__init__(config_file, source_file, python_exe, dry_run, existing_config_file, extra_ignore_dirs) - + self.dependency_reader = QtDependencyReader(dry_run=self.dry_run) if self.get_value("qt", "modules"): self.modules = self.get_value("qt", "modules").split(",") else: @@ -373,20 +373,34 @@ class DesktopConfig(Config): self._find_and_set_qtquick_modules() self._find_dependent_qt_modules() + self._qt_plugins = [] + if self.get_value("qt", "plugins"): + self._qt_plugins = self.get_value("qt", "plugins").split(",") + else: + self.qt_plugins = self.dependency_reader.find_plugin_dependencies(self.modules) + + @property + def qt_plugins(self): + return self._qt_plugins + + @qt_plugins.setter + def qt_plugins(self, qt_plugins): + self._qt_plugins = qt_plugins + self.set_value("qt", "plugins", ",".join(qt_plugins)) + def _find_dependent_qt_modules(self): """ Given pysidedeploy_config.modules, find all the other dependent Qt modules. """ - dependency_reader = QtDependencyReader(dry_run=self.dry_run) all_modules = set(self.modules) - if not dependency_reader.lib_reader: - warnings.warn(f"[DEPLOY] Unable to find {dependency_reader.lib_reader_name}. This tool" - " helps to find the Qt module dependencies of the application. Skipping " - " checking for dependencies.", category=RuntimeWarning) + if not self.dependency_reader.lib_reader: + warnings.warn(f"[DEPLOY] Unable to find {self.dependency_reader.lib_reader_name}. This " + "tool helps to find the Qt module dependencies of the application. " + "Skipping checking for dependencies.", category=RuntimeWarning) return for module_name in self.modules: - dependency_reader.find_dependencies(module=module_name, used_modules=all_modules) + self.dependency_reader.find_dependencies(module=module_name, used_modules=all_modules) self.modules = list(all_modules) diff --git a/sources/pyside-tools/deploy_lib/default.spec b/sources/pyside-tools/deploy_lib/default.spec index 2276fa496..8c0697afd 100644 --- a/sources/pyside-tools/deploy_lib/default.spec +++ b/sources/pyside-tools/deploy_lib/default.spec @@ -44,6 +44,9 @@ excluded_qml_plugins = # Qt modules used. Comma separated modules = +# Qt plugins used by the application +plugins = + [android] # path to PySide wheel diff --git a/sources/pyside-tools/deploy_lib/dependency_util.py b/sources/pyside-tools/deploy_lib/dependency_util.py index c7821794f..53c12ad92 100644 --- a/sources/pyside-tools/deploy_lib/dependency_util.py +++ b/sources/pyside-tools/deploy_lib/dependency_util.py @@ -5,6 +5,7 @@ import ast import re import os import site +import json import warnings import logging import shutil @@ -15,25 +16,6 @@ from typing import List, Set from . import IMPORT_WARNING_PYSIDE, run_command -def get_qt_libs_dir(): - """ - Finds the path to the Qt libs directory inside PySide6 package installation - """ - pyside_install_dir = None - for possible_site_package in site.getsitepackages(): - if possible_site_package.endswith("site-packages"): - pyside_install_dir = Path(possible_site_package) / "PySide6" - - if not pyside_install_dir: - print("Unable to find site-packages. Exiting ...") - sys.exit(-1) - - if sys.platform == "win32": - return pyside_install_dir - - return pyside_install_dir / "Qt" / "lib" # for linux and macOS - - def find_pyside_modules(project_dir: Path, extra_ignore_dirs: List[Path] = None, project_data=None): """ @@ -164,9 +146,27 @@ class QtDependencyReader: print(f"[DEPLOY] Deployment on unsupported platfrom {sys.platform}") sys.exit(1) - self.qt_libs_dir = get_qt_libs_dir() + self.pyside_install_dir = None + self.qt_libs_dir = self.get_qt_libs_dir() self._lib_reader = shutil.which(self.lib_reader_name) + def get_qt_libs_dir(self): + """ + Finds the path to the Qt libs directory inside PySide6 package installation + """ + for possible_site_package in site.getsitepackages(): + if possible_site_package.endswith("site-packages"): + self.pyside_install_dir = Path(possible_site_package) / "PySide6" + + if not self.pyside_install_dir: + print("Unable to find site-packages. Exiting ...") + sys.exit(-1) + + if sys.platform == "win32": + return self.pyside_install_dir + + return self.pyside_install_dir / "Qt" / "lib" # for linux and macOS + @property def lib_reader(self): return self._lib_reader @@ -216,3 +216,27 @@ class QtDependencyReader: logging.info(f"[DEPLOY] Following dependencies found for {module}: {dependent_modules}") else: logging.info(f"[DEPLOY] No Qt dependencies found for {module}") + + def find_plugin_dependencies(self, used_modules: List[str]) -> List[str]: + """ + Given the modules used by the application, returns all the required plugins + """ + plugins = set() + pyside_mod_plugin_jsons = ["PySide6_Essentials.json", "PySide6_Addons.json"] + for pyside_mod_plugin_json_name in pyside_mod_plugin_jsons: + pyside_mod_plugin_json_file = self.pyside_install_dir / pyside_mod_plugin_json_name + if not pyside_mod_plugin_json_file.exists(): + warnings.warn(f"[DEPLOY] Unable to find {pyside_mod_plugin_json_file}.", + category=RuntimeWarning) + continue + + # convert the json to dict + pyside_mod_dict = {} + with open(pyside_mod_plugin_json_file) as pyside_json: + pyside_mod_dict = json.load(pyside_json) + + # find all the plugins in the modules + for module in used_modules: + plugins.update(pyside_mod_dict.get(module, [])) + + return list(plugins) diff --git a/sources/pyside-tools/deploy_lib/nuitka_helper.py b/sources/pyside-tools/deploy_lib/nuitka_helper.py index ae5834b6b..721701f70 100644 --- a/sources/pyside-tools/deploy_lib/nuitka_helper.py +++ b/sources/pyside-tools/deploy_lib/nuitka_helper.py @@ -17,6 +17,23 @@ class Nuitka: def __init__(self, nuitka): self.nuitka = nuitka + # plugins to ignore. The sensible plugins are include by default by Nuitka for PySide6 + # application deployment + self.qt_plugins_to_ignore = ["imageformats", # being Nuitka `sensible`` plugins + "iconengines", + "mediaservice", + "printsupport", + "platforms", + "platformthemes", + "styles", + "wayland-shell-integration", + "wayland-decoration-client", + "wayland-graphics-integration-client", + "egldeviceintegrations", + "xcbglintegrations", + "tls", # end Nuitka `sensible` plugins + "generic" # plugins that error with Nuitka + ] @staticmethod def icon_option(): @@ -28,11 +45,12 @@ class Nuitka: return "--macos-app-icon" def create_executable(self, source_file: Path, extra_args: str, qml_files: List[Path], - excluded_qml_plugins: List[str], icon: str, dry_run: bool): + qt_plugins: List[str], excluded_qml_plugins: List[str], icon: str, + dry_run: bool): + qt_plugins = [plugin for plugin in qt_plugins if plugin not in self.qt_plugins_to_ignore] extra_args = extra_args.split() qml_args = [] if qml_files: - qml_args.append("--include-qt-plugins=all") # This will generate options for each file using: # --include-data-files=ABSOLUTE_PATH_TO_FILE=RELATIVE_PATH_TO ROOT # for each file. This will preserve the directory structure of QML resources. @@ -41,6 +59,11 @@ class Nuitka: f"./{qml_file.resolve().relative_to(source_file.parent)}" for qml_file in qml_files] ) + # add qml plugin. The `qml`` plugin name is not present in the module json files shipped + # with Qt and hence not in `qt_plugins``. However, Nuitka uses the 'qml' plugin name to + # include the necessary qml plugins. There we have to add it explicitly for a qml + # application + qt_plugins.append("qml") if excluded_qml_plugins: prefix = "lib" if sys.platform != "win32" else "" @@ -59,8 +82,14 @@ class Nuitka: "--enable-plugin=pyside6", f"--output-dir={output_dir}", ] + command.extend(extra_args + qml_args) command.append(f"{self.__class__.icon_option()}={icon}") + if qt_plugins: + # sort qt_plugins so that the result is definitive when testing + qt_plugins.sort() + qt_plugins_str = ",".join(qt_plugins) + command.append(f"--include-qt-plugins={qt_plugins_str}") command_str, _ = run_command(command=command, dry_run=dry_run) return command_str diff --git a/sources/pyside6/doc/_tags/android.rst b/sources/pyside6/doc/_tags/android.rst new file mode 100644 index 000000000..08a30fc0f --- /dev/null +++ b/sources/pyside6/doc/_tags/android.rst @@ -0,0 +1,21 @@ +My tags: Android +################ + +.. toctree:: + :maxdepth: 1 + :caption: With this tag + + ../examples/example_bluetooth_heartrate_game.rst + ../examples/example_bluetooth_lowenergyscanner.rst + ../examples/example_location_mapviewer.rst + ../examples/example_multimedia_audiooutput.rst + ../examples/example_multimedia_audiosource.rst + ../examples/example_multimedia_camera.rst + ../examples/example_qml_editingmodel.rst + ../examples/example_qml_usingmodel.rst + ../examples/example_quick_models_objectlistmodel.rst + ../examples/example_quick_models_stringlistmodel.rst + ../examples/example_quick_painteditem.rst + ../examples/example_quickcontrols_contactslist.rst + ../examples/example_quickcontrols_gallery.rst + ../examples/example_widgets_widgets_digitalclock.rst diff --git a/sources/pyside6/doc/_tags/tagsindex.rst b/sources/pyside6/doc/_tags/tagsindex.rst new file mode 100644 index 000000000..be29337ec --- /dev/null +++ b/sources/pyside6/doc/_tags/tagsindex.rst @@ -0,0 +1,12 @@ +:orphan: + +.. _tagoverview: + +Tags overview +############# + +.. toctree:: + :caption: Tags + :maxdepth: 1 + + Android (14) <android.rst> diff --git a/sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py b/sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py index c79a633e1..db8813ccf 100644 --- a/sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py +++ b/sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py @@ -81,6 +81,7 @@ class DeployTestBase(LongSortedOptionTest): @unittest.skipIf(sys.platform == "darwin" and int(platform.mac_ver()[0].split('.')[0]) <= 11, "Test only works on macOS version 12+") +@patch("deploy_lib.config.QtDependencyReader.find_plugin_dependencies") class TestPySide6DeployWidgets(DeployTestBase): @classmethod def setUpClass(cls): @@ -94,10 +95,18 @@ class TestPySide6DeployWidgets(DeployTestBase): os.chdir(self.temp_example_widgets) self.main_file = self.temp_example_widgets / "tetrix.py" self.deployment_files = self.temp_example_widgets / "deployment" + # All the plugins included. This is different from plugins_nuitka, because Nuitka bundles + # some plugins by default + self.all_plugins = ["accessiblebridge", "egldeviceintegrations", "generic", "iconengines", + "imageformats", "platforminputcontexts", "platforms", + "platforms/darwin", "platformthemes", "styles", "xcbglintegrations"] + # Plugins that needs to be passed to Nuitka + plugins_nuitka = ("accessiblebridge,platforminputcontexts,platforms/darwin") self.expected_run_cmd = ( f"{sys.executable} -m nuitka {str(self.main_file)} --follow-imports --onefile" f" --enable-plugin=pyside6 --output-dir={str(self.deployment_files)} --quiet" f" --noinclude-qt-translations" + f" --include-qt-plugins={plugins_nuitka}" ) if sys.platform.startswith("linux"): self.expected_run_cmd += f" --linux-icon={str(self.linux_icon)}" @@ -110,16 +119,18 @@ class TestPySide6DeployWidgets(DeployTestBase): self.expected_run_cmd += " --static-libpython=no" self.config_file = self.temp_example_widgets / "pysidedeploy.spec" - def testWidgetDryRun(self): + def testWidgetDryRun(self, mock_plugins): + mock_plugins.return_value = self.all_plugins # Checking for dry run commands is equivalent to mocking the # subprocess.check_call() in commands.py as the the dry run command # is the command being run. original_output = self.deploy.main(self.main_file, dry_run=True, force=True) self.assertEqual(original_output, self.expected_run_cmd) - @patch("deploy_lib.dependency_util.get_qt_libs_dir") - def testWidgetConfigFile(self, mock_sitepackages): + @patch("deploy_lib.dependency_util.QtDependencyReader.get_qt_libs_dir") + def testWidgetConfigFile(self, mock_sitepackages, mock_plugins): mock_sitepackages.return_value = Path(_get_qt_lib_dir()) + mock_plugins.return_value = self.all_plugins # includes both dry run and config_file tests # init init_result = self.deploy.main(self.main_file, init=True, force=True) @@ -146,9 +157,12 @@ class TestPySide6DeployWidgets(DeployTestBase): expected_modules.add("DBus") obtained_modules = set(config_obj.get_value("qt", "modules").split(",")) self.assertEqual(obtained_modules, expected_modules) + obtained_qt_plugins = config_obj.get_value("qt", "plugins").split(",") + self.assertEqual(obtained_qt_plugins.sort(), self.all_plugins.sort()) self.config_file.unlink() - def testErrorReturns(self): + def testErrorReturns(self, mock_plugins): + mock_plugins.return_value = self.all_plugins # main file and config file does not exists fake_main_file = self.main_file.parent / "main.py" with self.assertRaises(RuntimeError) as context: @@ -158,6 +172,7 @@ class TestPySide6DeployWidgets(DeployTestBase): @unittest.skipIf(sys.platform == "darwin" and int(platform.mac_ver()[0].split('.')[0]) <= 11, "Test only works on macOS version 12+") +@patch("deploy_lib.config.QtDependencyReader.find_plugin_dependencies") class TestPySide6DeployQml(DeployTestBase): @classmethod def setUpClass(cls): @@ -173,13 +188,24 @@ class TestPySide6DeployQml(DeployTestBase): self.deployment_files = self.temp_example_qml / "deployment" self.first_qml_file = "main.qml" self.second_qml_file = "MovingRectangle.qml" + # All the plugins included. This is different from plugins_nuitka, because Nuitka bundles + # some plugins by default + self.all_plugins = ["accessiblebridge", "egldeviceintegrations", "generic", "iconengines", + "imageformats", "networkaccess", "networkinformation", + "platforminputcontexts", "platforms", "platforms/darwin", + "platformthemes", "qmltooling", "scenegraph", "tls", + "xcbglintegrations"] + # Plugins that needs to be passed to Nuitka + plugins_nuitka = ("accessiblebridge,networkaccess,networkinformation,platforminputcontexts," + "platforms/darwin,qml,qmltooling,scenegraph") self.expected_run_cmd = ( f"{sys.executable} -m nuitka {str(self.main_file)} --follow-imports --onefile" f" --enable-plugin=pyside6 --output-dir={str(self.deployment_files)} --quiet" - f" --noinclude-qt-translations --include-qt-plugins=all" + f" --noinclude-qt-translations" + f" --include-qt-plugins={plugins_nuitka}" f" --include-data-files={str(self.temp_example_qml / self.first_qml_file)}=" f"./main.qml --include-data-files=" - f"{str(self.temp_example_qml /self.second_qml_file)}=./MovingRectangle.qml" + f"{str(self.temp_example_qml / self.second_qml_file)}=./MovingRectangle.qml" ) if sys.platform != "win32": @@ -206,9 +232,10 @@ class TestPySide6DeployQml(DeployTestBase): self.expected_run_cmd += " --static-libpython=no" self.config_file = self.temp_example_qml / "pysidedeploy.spec" - @patch("deploy_lib.dependency_util.get_qt_libs_dir") - def testQmlConfigFile(self, mock_sitepackages): + @patch("deploy_lib.dependency_util.QtDependencyReader.get_qt_libs_dir") + def testQmlConfigFile(self, mock_sitepackages, mock_plugins): mock_sitepackages.return_value = Path(_get_qt_lib_dir()) + mock_plugins.return_value = self.all_plugins # create config file with patch("deploy_lib.config.run_qmlimportscanner") as mock_qmlimportscanner: mock_qmlimportscanner.return_value = ["QtQuick"] @@ -235,16 +262,20 @@ class TestPySide6DeployQml(DeployTestBase): expected_modules.add("DBus") obtained_modules = set(config_obj.get_value("qt", "modules").split(",")) self.assertEqual(obtained_modules, expected_modules) + obtained_qt_plugins = config_obj.get_value("qt", "plugins").split(",") + self.assertEqual(obtained_qt_plugins.sort(), self.all_plugins.sort()) self.config_file.unlink() - def testQmlDryRun(self): + def testQmlDryRun(self, mock_plugins): + mock_plugins.return_value = self.all_plugins with patch("deploy_lib.config.run_qmlimportscanner") as mock_qmlimportscanner: mock_qmlimportscanner.return_value = ["QtQuick"] original_output = self.deploy.main(self.main_file, dry_run=True, force=True) self.assertEqual(original_output, self.expected_run_cmd) self.assertEqual(mock_qmlimportscanner.call_count, 1) - def testMainFileDryRun(self): + def testMainFileDryRun(self, mock_plugins): + mock_plugins.return_value = self.all_plugins with patch("deploy_lib.config.run_qmlimportscanner") as mock_qmlimportscanner: mock_qmlimportscanner.return_value = ["QtQuick"] original_output = self.deploy.main(Path.cwd() / "main.py", dry_run=True, force=True) @@ -263,14 +294,24 @@ class TestPySide6DeployWebEngine(DeployTestBase): shutil.copytree(example_webenginequick, Path(cls.temp_dir) / "nanobrowser") ).resolve() - @patch("deploy_lib.dependency_util.get_qt_libs_dir") - def testWebEngineQuickDryRun(self, mock_sitepackages): + @patch("deploy_lib.config.QtDependencyReader.find_plugin_dependencies") + @patch("deploy_lib.dependency_util.QtDependencyReader.get_qt_libs_dir") + def testWebEngineQuickDryRun(self, mock_sitepackages, mock_plugins): mock_sitepackages.return_value = Path(_get_qt_lib_dir()) + all_plugins = ["accessiblebridge", "egldeviceintegrations", "generic", "iconengines", + "imageformats", "networkaccess", "networkinformation", + "platforminputcontexts", "platforms", "platforms/darwin", + "platformthemes", "qmltooling", "scenegraph", "tls", + "xcbglintegrations"] + mock_plugins.return_value = all_plugins # this test case retains the QtWebEngine dlls # setup os.chdir(self.temp_example_webenginequick) main_file = self.temp_example_webenginequick / "quicknanobrowser.py" deployment_files = self.temp_example_webenginequick / "deployment" + # Plugins that needs to be passed to Nuitka + plugins_nuitka = ("accessiblebridge,networkaccess,networkinformation,platforminputcontexts," + "platforms/darwin,qml,qmltooling,scenegraph") qml_files = [ "ApplicationRoot.qml", "BrowserDialog.qml", @@ -281,7 +322,7 @@ class TestPySide6DeployWebEngine(DeployTestBase): ] data_files_cmd = " ".join( [ - f"--include-data-files={str(self.temp_example_webenginequick/file)}=./{file}" + f"--include-data-files={str(self.temp_example_webenginequick / file)}=./{file}" for file in qml_files ] ) @@ -290,6 +331,7 @@ class TestPySide6DeployWebEngine(DeployTestBase): f" --enable-plugin=pyside6 --output-dir={str(deployment_files)} --quiet" f" --noinclude-qt-translations --include-qt-plugins=all" f" {data_files_cmd}" + f" --include-qt-plugins={plugins_nuitka}" ) if sys.platform != "win32": |