aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShyamnath Premnadh <Shyamnath.Premnadh@qt.io>2024-09-20 17:00:39 +0200
committerShyamnath Premnadh <Shyamnath.Premnadh@qt.io>2024-10-07 13:54:03 +0200
commit1e5fe140b485209a30d7dd38b3334440c58cbb05 (patch)
tree43511980e289c57b6208ad0bf9b0c09ea58a483e
parent9a10caa1e22773ce7563f82680c67596293178e4 (diff)
Deployment: Support Design Studio projects
- new class 'DesignStudio' to handle Design Studio projects. - Currently uses a way of monkey patching to override the 'main.py' to use 'main_patch.py' which has the same content but with 'app_dir' set to the parent of `main.py``. The reason for doing this is that Nuitka requires the `main.py` to be in the same directory as other resources required for the project. Once the corresponding patch, to alternate between evaluating 'app_dir' based on whether the application is deployed or called through the Python interpreter, is merged then this temporary fix of creating 'main_patch.py' can be removed. - Add tests. Pick-to: 6.7 Change-Id: I79e6572bdbbf4576fbdd9039a4922997a22139f8 Reviewed-by: Friedemann Kleint <Friedemann.Kleint@qt.io>
-rw-r--r--sources/pyside-tools/deploy_lib/__init__.py1
-rw-r--r--sources/pyside-tools/deploy_lib/config.py57
-rw-r--r--sources/pyside-tools/deploy_lib/design_studio.py57
-rw-r--r--sources/pyside-tools/deploy_lib/nuitka_helper.py27
-rw-r--r--sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py92
5 files changed, 200 insertions, 34 deletions
diff --git a/sources/pyside-tools/deploy_lib/__init__.py b/sources/pyside-tools/deploy_lib/__init__.py
index 98179ecf0..3417f7a29 100644
--- a/sources/pyside-tools/deploy_lib/__init__.py
+++ b/sources/pyside-tools/deploy_lib/__init__.py
@@ -54,6 +54,7 @@ def get_all_pyside_modules():
return [module[2:] for module in PySide6.__all__]
+from .design_studio import DesignStudio
from .commands import run_command, run_qmlimportscanner
from .dependency_util import find_pyside_modules, find_permission_categories, QtDependencyReader
from .nuitka_helper import Nuitka
diff --git a/sources/pyside-tools/deploy_lib/config.py b/sources/pyside-tools/deploy_lib/config.py
index c400348ec..d34489b97 100644
--- a/sources/pyside-tools/deploy_lib/config.py
+++ b/sources/pyside-tools/deploy_lib/config.py
@@ -12,7 +12,7 @@ from pathlib import Path
from enum import Enum
from project import ProjectData
-from . import (DEFAULT_APP_ICON, DEFAULT_IGNORE_DIRS, find_pyside_modules,
+from . import (DEFAULT_APP_ICON, DEFAULT_IGNORE_DIRS, DesignStudio, find_pyside_modules,
find_permission_categories, QtDependencyReader, run_qmlimportscanner)
# Some QML plugins like QtCore are excluded from this list as they don't contribute much to
@@ -165,7 +165,10 @@ class Config(BaseConfig):
else:
self.excluded_qml_plugins = self._find_excluded_qml_plugins()
- self._generated_files_path = self.project_dir / "deployment"
+ if DesignStudio.isDSProject(self.source_file):
+ self._generated_files_path = self.project_dir / "Python" / "deployment"
+ else:
+ self._generated_files_path = self.project_dir / "deployment"
self.modules = []
@@ -208,9 +211,10 @@ class Config(BaseConfig):
@qml_files.setter
def qml_files(self, qml_files):
self._qml_files = qml_files
- self.set_value("qt", "qml_files",
- ",".join([str(file.absolute().relative_to(self.project_dir.absolute()))
- for file in self.qml_files]))
+ qml_files = [str(file.absolute().relative_to(self.project_dir.absolute()))
+ if file.absolute().is_relative_to(self.project_dir) else str(file.absolute())
+ for file in self.qml_files]
+ self.set_value("qt", "qml_files", ",".join(qml_files))
@property
def project_dir(self):
@@ -254,6 +258,11 @@ class Config(BaseConfig):
@source_file.setter
def source_file(self, source_file: Path):
self._source_file = source_file
+ # FIXME: Remove when new DS is released
+ # for DS project, set self._source_file to main_patch.py, but don't change the value
+ # in the config file as main_patch.py is a temporary file
+ if DesignStudio.isDSProject(source_file):
+ self._source_file = DesignStudio(source_file).ds_source_file
self.set_value("app", "input_file", str(source_file))
@property
@@ -314,30 +323,28 @@ class Config(BaseConfig):
qml_files.extend([self.project_dir / str(qml_file) for qml_file in
ProjectData(project_file=sub_project_file).qml_files])
else:
- qml_files_temp = None
- if self.source_file and self.python_path:
- if not self.qml_files:
- # filter out files from DEFAULT_IGNORE_DIRS
- qml_files_temp = [file for file in self.source_file.parent.glob("**/*.qml")
- if all(part not in file.parts for part in
- DEFAULT_IGNORE_DIRS)]
-
- if len(qml_files_temp) > 500:
- warnings.warn(
- "You seem to include a lot of QML files. This can lead to errors in "
- "deployment."
- )
-
- if qml_files_temp:
- extra_qml_files = [Path(file) for file in qml_files_temp]
- qml_files.extend(extra_qml_files)
+ # Filter out files from DEFAULT_IGNORE_DIRS
+ qml_files = [
+ file for file in self.project_dir.glob("**/*.qml")
+ if all(part not in file.parts for part in DEFAULT_IGNORE_DIRS)
+ ]
+
+ if len(qml_files) > 500:
+ warnings.warn(
+ "You seem to include a lot of QML files from "
+ f"{self.project_dir}. This can lead to errors in deployment."
+ )
return qml_files
def _find_project_dir(self) -> Path:
- # there is no other way to find the project_dir than assume it is the parent directory
- # of source_file
- project_dir = self.source_file.parent
+ if DesignStudio.isDSProject(self.source_file):
+ ds = DesignStudio(self.source_file)
+ project_dir = ds.project_dir
+ else:
+ # there is no other way to find the project_dir than assume it is the parent directory
+ # of source_file
+ project_dir = self.source_file.parent
return project_dir
def _find_project_file(self) -> Path:
diff --git a/sources/pyside-tools/deploy_lib/design_studio.py b/sources/pyside-tools/deploy_lib/design_studio.py
new file mode 100644
index 000000000..1fc1a4cc8
--- /dev/null
+++ b/sources/pyside-tools/deploy_lib/design_studio.py
@@ -0,0 +1,57 @@
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+import logging
+import atexit
+from pathlib import Path
+
+# FIXME: Remove this idea of creating main_patch.py once the corresponding changes are
+# made in Design Studio main.py file:
+# if '__compiled__' in globals(): #nuitka
+# app_dir = Path(__file__).parent
+# else:
+# app_dir = Path(__file__).parent.parent
+
+
+class DesignStudio:
+ """
+ Class to handle Design Studio projects
+ """
+
+ def __init__(self, main_file: Path):
+ self.ds_project_dir = main_file.parent.parent
+ self.current_main_file = main_file
+ self.new_main_file = main_file.parent / 'main_patch.py'
+ self._create_new_main_file()
+ atexit.register(self._delete_main_patch_file)
+
+ def _create_new_main_file(self):
+ # read the content of main file
+ content = ""
+ with open(self.current_main_file, 'r', encoding='utf-8') as main_file:
+ content = main_file.read()
+
+ # replace app_dir
+ content = content.replace("app_dir = Path(__file__).parent.parent", # old value
+ "app_dir = Path(__file__).parent") # new value
+
+ # write the content to new main file
+ with open(self.new_main_file, 'w', encoding="utf-8") as main_file:
+ main_file.write(content)
+
+ def _delete_main_patch_file(self):
+ if self.new_main_file.exists():
+ logging.info(f"[DEPLOY] Removing {self.new_main_file}")
+ self.new_main_file.unlink()
+
+ @staticmethod
+ def isDSProject(main_file: Path) -> bool:
+ return (main_file.parent / 'autogen/settings.py').exists()
+
+ @property
+ def project_dir(self) -> str:
+ return self.ds_project_dir
+
+ @property
+ def ds_source_file(self) -> Path:
+ return self.new_main_file
diff --git a/sources/pyside-tools/deploy_lib/nuitka_helper.py b/sources/pyside-tools/deploy_lib/nuitka_helper.py
index f0acfb720..2d46dcf8a 100644
--- a/sources/pyside-tools/deploy_lib/nuitka_helper.py
+++ b/sources/pyside-tools/deploy_lib/nuitka_helper.py
@@ -11,7 +11,7 @@ import shlex
import sys
from pathlib import Path
-from . import MAJOR_VERSION, run_command
+from . import MAJOR_VERSION, DesignStudio, run_command
from .config import DesktopConfig
@@ -104,14 +104,23 @@ class Nuitka:
qml_args = []
if qml_files:
- # 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.
- qml_args.extend(
- [f"--include-data-files={qml_file.resolve()}="
- f"./{qml_file.resolve().relative_to(source_file.resolve().parent)}"
- for qml_file in qml_files]
- )
+ if DesignStudio.isDSProject(source_file):
+ ds = DesignStudio(source_file)
+ # include all subdirectories of ds.project_directory as data directories
+ # this will contain all the qml files and other resources
+ for subdir in ds.project_dir.iterdir():
+ if subdir.is_dir():
+ extra_args.append(f"--include-data-dir={subdir}="
+ f"./{subdir.name}")
+ else:
+ # 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.
+ qml_args.extend(
+ [f"--include-data-files={qml_file.resolve()}="
+ f"./{qml_file.resolve().relative_to(source_file.resolve().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
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 9bf917b32..9d98a2b73 100644
--- a/sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py
+++ b/sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py
@@ -444,5 +444,97 @@ class TestLongCommand(DeployTestBase):
if line.startswith("# nuitka-project:")]), 517)
+@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 DSProjectTest(DeployTestBase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ # setup a test DS Python project
+ base_path = Path(cls.temp_dir) / "PythonProject"
+
+ files = [
+ base_path / "Python" / "autogen" / "settings.py",
+ base_path / "Python" / "main.py",
+ base_path / "PythonProject" / "test.qml",
+ base_path / "PythonProjectContent" / "test.qml"
+ ]
+
+ # Create the files
+ for file in files:
+ file.parent.mkdir(parents=True, exist_ok=True)
+ file.touch(exist_ok=True)
+
+ cls.temp_example = base_path
+
+ def setUp(self):
+ os.chdir(self.temp_example)
+ self.temp_example = self.temp_example.resolve()
+ self.main_file = self.temp_example / "Python" / "main.py"
+ self.main_patch_file = self.temp_example / "Python" / "main_patch.py"
+ self.deployment_files = self.temp_example / "Python" / "deployment"
+
+ self.expected_run_cmd = (
+ f"{sys.executable} -m nuitka {self.main_patch_file} --follow-imports"
+ f" --enable-plugin=pyside6 --output-dir={self.deployment_files} --quiet"
+ f" --noinclude-qt-translations"
+ f" --include-data-dir={self.temp_example / 'PythonProjectContent'}="
+ "./PythonProjectContent"
+ f" --include-data-dir={self.temp_example / 'Python'}=./Python"
+ f" --include-data-dir={self.temp_example / 'PythonProject'}=./PythonProject"
+ f" {self.dlls_ignore_nuitka}"
+ f" --noinclude-dlls=*/qml/QtQuickEffectMaker/*"
+ f" --include-qt-plugins=qml"
+ )
+
+ if sys.platform != "win32":
+ self.expected_run_cmd += (
+ " --noinclude-dlls=libQt6Charts* --noinclude-dlls=libQt6Quick*"
+ " --noinclude-dlls=libQt6Quick3D* --noinclude-dlls=libQt6Sensors*"
+ " --noinclude-dlls=libQt6Test* --noinclude-dlls=libQt6WebEngine*"
+ )
+ else:
+ self.expected_run_cmd += (
+ " --noinclude-dlls=Qt6Charts* --noinclude-dlls=Qt6Quick*"
+ " --noinclude-dlls=Qt6Quick3D* --noinclude-dlls=Qt6Sensors*"
+ " --noinclude-dlls=Qt6Test* --noinclude-dlls=Qt6WebEngine*"
+ )
+
+ if sys.platform.startswith("linux"):
+ self.expected_run_cmd += f" --linux-icon={str(self.linux_icon)} --onefile"
+ elif sys.platform == "darwin":
+ self.expected_run_cmd += (f" --macos-app-icon={str(self.macos_icon)}"
+ " --macos-create-app-bundle --standalone")
+ elif sys.platform == "win32":
+ self.expected_run_cmd += f" --windows-icon-from-ico={str(self.win_icon)} --onefile"
+
+ if is_pyenv_python():
+ self.expected_run_cmd += " --static-libpython=no"
+
+ self.config_file = self.temp_example / "Python" / "pysidedeploy.spec"
+
+ def testDryRun(self, mock_plugins):
+ with patch("deploy_lib.config.run_qmlimportscanner") as mock_qmlimportscanner: # noqa: F841
+ 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.QtDependencyReader.get_qt_libs_dir")
+ def testConfigFile(self, mock_sitepackages, mock_plugins):
+ mock_sitepackages.return_value = Path(_get_qt_lib_dir())
+ # create config file
+ with patch("deploy_lib.config.run_qmlimportscanner") as mock_qmlimportscanner: # noqa: F841
+ init_result = self.deploy.main(self.main_file, init=True, force=True)
+ self.assertEqual(init_result, None)
+
+ # test config file contents
+ config_obj = self.deploy_lib.BaseConfig(config_file=self.config_file)
+
+ self.assertTrue(config_obj.get_value("app", "input_file").endswith("main.py"))
+ self.assertTrue(config_obj.get_value("app", "project_dir").endswith("PythonProject"))
+ self.config_file.unlink()
+
+
if __name__ == "__main__":
unittest.main()