diff options
Diffstat (limited to 'sources/pyside-tools/deploy_lib/config.py')
-rw-r--r-- | sources/pyside-tools/deploy_lib/config.py | 368 |
1 files changed, 296 insertions, 72 deletions
diff --git a/sources/pyside-tools/deploy_lib/config.py b/sources/pyside-tools/deploy_lib/config.py index ba165d20b..5d5070bef 100644 --- a/sources/pyside-tools/deploy_lib/config.py +++ b/sources/pyside-tools/deploy_lib/config.py @@ -1,40 +1,90 @@ # Copyright (C) 2022 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 -from pathlib import Path +import sys import configparser -from configparser import ConfigParser -import shutil import logging +import warnings +from configparser import ConfigParser +from typing import List +from pathlib import Path +from enum import Enum from project import ProjectData -from .commands import run_qmlimportscanner +from . import (DEFAULT_APP_ICON, 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 # executable size. Excluding them saves the extra processing of checking for them in files EXCLUDED_QML_PLUGINS = {"QtQuick", "QtQuick3D", "QtCharts", "QtWebEngine", "QtTest", "QtSensors"} +PERMISSION_MAP = {"Bluetooth": "NSBluetoothAlwaysUsageDescription:BluetoothAccess", + "Camera": "NSCameraUsageDescription:CameraAccess", + "Microphone": "NSMicrophoneUsageDescription:MicrophoneAccess", + "Contacts": "NSContactsUsageDescription:ContactsAccess", + "Calendar": "NSCalendarsUsageDescription:CalendarAccess", + # for iOS NSLocationWhenInUseUsageDescription and + # NSLocationAlwaysAndWhenInUseUsageDescription are also required. + "Location": "NSLocationUsageDescription:LocationAccess", + } -class Config: - """ - Wrapper class around config file, whose options are used to control the executable creation - """ - def __init__(self, config_file: Path, source_file: Path, python_exe: Path, dry_run: bool): +class BaseConfig: + """Wrapper class around any .spec file with function to read and set values for the .spec file + """ + def __init__(self, config_file: Path, comment_prefixes: str = "/", + existing_config_file: bool = False) -> None: self.config_file = config_file - self.parser = ConfigParser(comment_prefixes="/", allow_no_value=True) - if not self.config_file.exists(): - logging.info(f"[DEPLOY] Creating config file {self.config_file}") - shutil.copy(Path(__file__).parent / "default.spec", self.config_file) - else: - print(f"Using existing config file {config_file}") + self.existing_config_file = existing_config_file + self.parser = ConfigParser(comment_prefixes=comment_prefixes, strict=False, + allow_no_value=True) self.parser.read(self.config_file) - self.dry_run = dry_run + def update_config(self): + logging.info(f"[DEPLOY] Creating {self.config_file}") + with open(self.config_file, "w+") as config_file: + self.parser.write(config_file, space_around_delimiters=True) + + def set_value(self, section: str, key: str, new_value: str, raise_warning: bool = True): + try: + current_value = self.get_value(section, key, ignore_fail=True) + if current_value != new_value: + self.parser.set(section, key, new_value) + except configparser.NoOptionError: + if raise_warning: + logging.warning(f"[DEPLOY] Key {key} does not exist") + except configparser.NoSectionError: + if raise_warning: + logging.warning(f"[DEPLOY] Section {section} does not exist") + + def get_value(self, section: str, key: str, ignore_fail: bool = False): + try: + return self.parser.get(section, key) + except configparser.NoOptionError: + if not ignore_fail: + logging.warning(f"[DEPLOY] Key {key} does not exist") + except configparser.NoSectionError: + if not ignore_fail: + logging.warning(f"[DEPLOY] Section {section} does not exist") + + +class Config(BaseConfig): + """ + Wrapper class around pysidedeploy.spec file, whose options are used to control the executable + creation + """ + + def __init__(self, config_file: Path, source_file: Path, python_exe: Path, dry_run: bool, + existing_config_file: bool = False, extra_ignore_dirs: List[str] = None): + super().__init__(config_file=config_file, existing_config_file=existing_config_file) + + self.extra_ignore_dirs = extra_ignore_dirs + self._dry_run = dry_run + self.qml_modules = set() # set source_file self.source_file = Path( self.set_or_fetch(config_property_val=source_file, config_property_key="input_file") - ) + ).resolve() # set python path self.python_path = Path( @@ -45,12 +95,27 @@ class Config: ) ) + self.title = self.get_value("app", "title") + + # set application icon + config_icon = self.get_value("app", "icon") + if config_icon: + self.icon = str(Path(config_icon).resolve()) + else: + self.icon = DEFAULT_APP_ICON + self.project_dir = None if self.get_value("app", "project_dir"): self.project_dir = Path(self.get_value("app", "project_dir")).absolute() else: self._find_and_set_project_dir() + self.exe_dir = None + if self.get_value("app", "exec_directory"): + self.exe_dir = Path(self.get_value("app", "exec_directory")).absolute() + else: + self._find_and_set_exe_dir() + self.project_data: ProjectData = None if self.get_value("app", "project_file"): project_file = Path(self.get_value("app", "project_file")).absolute() @@ -60,39 +125,20 @@ class Config: self.qml_files = [] config_qml_files = self.get_value("qt", "qml_files") - if config_qml_files and self.project_dir: + if config_qml_files and self.project_dir and self.existing_config_file: self.qml_files = [Path(self.project_dir) / file for file in config_qml_files.split(",")] else: self._find_and_set_qml_files() self.excluded_qml_plugins = [] - if self.get_value("qt", "excluded_qml_plugins"): + if self.get_value("qt", "excluded_qml_plugins") and self.existing_config_file: self.excluded_qml_plugins = self.get_value("qt", "excluded_qml_plugins").split(",") else: self._find_and_set_excluded_qml_plugins() - def update_config(self): - logging.info(f"[DEPLOY] Creating {self.config_file}") - with open(self.config_file, "w+") as config_file: - self.parser.write(config_file, space_around_delimiters=True) + self._generated_files_path = self.project_dir / "deployment" - def set_value(self, section: str, key: str, new_value: str): - try: - current_value = self.get_value(section, key) - if current_value != new_value: - self.parser.set(section, key, new_value) - except configparser.NoOptionError: - logging.warning(f"[DEPLOY] key {key} does not exist") - except configparser.NoSectionError: - logging.warning(f"[DEPLOY] section {section} does not exist") - - def get_value(self, section: str, key: str): - try: - return self.parser.get(section, key) - except configparser.NoOptionError: - logging.warning(f"[DEPLOY] key {key} does not exist") - except configparser.NoSectionError: - logging.warning(f"[DEPLOY] section {section} does not exist") + self.modules = [] def set_or_fetch(self, config_property_val, config_property_key, config_property_group="app"): """ @@ -107,10 +153,17 @@ class Config: elif self.get_value(config_property_group, config_property_key): return self.get_value(config_property_group, config_property_key) else: - logging.exception( + raise RuntimeError( f"[DEPLOY] No {config_property_key} specified in config file or as cli option" ) - raise + + @property + def dry_run(self): + return self._dry_run + + @property + def generated_files_path(self): + return self._generated_files_path @property def qml_files(self): @@ -129,11 +182,29 @@ class Config: self._project_dir = project_dir @property + def title(self): + return self._title + + @title.setter + def title(self, title): + self._title = title + self.set_value("app", "title", title) + + @property + def icon(self): + return self._icon + + @icon.setter + def icon(self, icon): + self._icon = icon + self.set_value("app", "icon", icon) + + @property def source_file(self): return self._source_file @source_file.setter - def source_file(self, source_file): + def source_file(self, source_file: Path): self._source_file = source_file @property @@ -141,10 +212,18 @@ class Config: return self._python_path @python_path.setter - def python_path(self, python_path): + def python_path(self, python_path: Path): self._python_path = python_path @property + def extra_args(self): + return self.get_value("nuitka", "extra_args") + + @extra_args.setter + def extra_args(self, extra_args): + self.set_value("nuitka", "extra_args", extra_args) + + @property def excluded_qml_plugins(self): return self._excluded_qml_plugins @@ -152,6 +231,23 @@ class Config: def excluded_qml_plugins(self, excluded_qml_plugins): self._excluded_qml_plugins = excluded_qml_plugins + @property + def exe_dir(self): + return self._exe_dir + + @exe_dir.setter + def exe_dir(self, exe_dir: Path): + self._exe_dir = exe_dir + + @property + def modules(self): + return self._modules + + @modules.setter + def modules(self, modules): + self._modules = modules + self.set_value("qt", "modules", ",".join(modules)) + def _find_and_set_qml_files(self): """Fetches all the qml_files in the folder and sets them if the field qml_files is empty in the config_dir""" @@ -163,39 +259,29 @@ class Config: self.qml_files = qml_files else: qml_files_temp = None - source_file = ( - Path(self.get_value("app", "input_file")) - if self.get_value("app", "input_file") - else None - ) - python_exe = ( - Path(self.get_value("python", "python_path")) - if self.get_value("python", "python_path") - else None - ) - if source_file and python_exe: + if self.source_file and self.python_path: if not self.qml_files: - qml_files_temp = list(source_file.parent.glob("**/*.qml")) + qml_files_temp = list(self.source_file.parent.glob("**/*.qml")) # add all QML files, excluding the ones shipped with installed PySide6 # The QML files shipped with PySide6 gets added if venv is used, # because of recursive glob - if python_exe.parent.parent == source_file.parent: + if self.python_path.parent.parent == self.source_file.parent: # python venv path is inside the main source dir qml_files_temp = list( - set(qml_files_temp) - set(python_exe.parent.parent.rglob("*.qml")) + set(qml_files_temp) - set(self.python_path.parent.parent.rglob("*.qml")) ) if len(qml_files_temp) > 500: if "site-packages" in str(qml_files_temp[-1]): - logging.warning( - "You seem to include a lot of QML files from a \ - local virtual env. Are they intended?" + raise RuntimeError( + "You are including a lot of QML files from a local virtual env." + " This can lead to errors in deployment." ) else: - logging.warning( - "You seem to include a lot of QML files. \ - Are they intended?" + warnings.warn( + "You seem to include a lot of QML files. This can lead to errors in " + "deployment." ) if qml_files_temp: @@ -205,7 +291,8 @@ class Config: self.set_value( "qt", "qml_files", - ",".join([str(file.relative_to(self.project_dir)) for file in self.qml_files]), + ",".join([str(file.absolute().relative_to(self.project_dir)) + for file in self.qml_files]), ) logging.info("[DEPLOY] QML files identified and set in config_file") @@ -215,18 +302,14 @@ class Config: self.project_dir = self.source_file.parent # update input_file path - logging.info("[DEPLOY] Update input_file path") self.set_value("app", "input_file", str(self.source_file.relative_to(self.project_dir))) - logging.info("[DEPLOY] Update project_dir path") if self.project_dir != Path.cwd(): self.set_value("app", "project_dir", str(self.project_dir)) else: self.set_value("app", "project_dir", str(self.project_dir.relative_to(Path.cwd()))) def _find_and_set_project_file(self): - logging.info("[DEPLOY] Searching for .pyproject file") - if self.project_dir: files = list(self.project_dir.glob("*.pyproject")) else: @@ -245,12 +328,153 @@ class Config: def _find_and_set_excluded_qml_plugins(self): if self.qml_files: - included_qml_modules = set(run_qmlimportscanner(qml_files=self.qml_files, - dry_run=self.dry_run)) - self.excluded_qml_plugins = EXCLUDED_QML_PLUGINS.difference(included_qml_modules) + self.qml_modules = set(run_qmlimportscanner(qml_files=self.qml_files, + dry_run=self.dry_run)) + self.excluded_qml_plugins = EXCLUDED_QML_PLUGINS.difference(self.qml_modules) # needed for dry_run testing self.excluded_qml_plugins = sorted(self.excluded_qml_plugins) if self.excluded_qml_plugins: self.set_value("qt", "excluded_qml_plugins", ",".join(self.excluded_qml_plugins)) + + def _find_and_set_exe_dir(self): + if self.project_dir == Path.cwd(): + self.exe_dir = self.project_dir.relative_to(Path.cwd()) + else: + self.exe_dir = self.project_dir + self.exe_dir = Path( + self.set_or_fetch( + config_property_val=self.exe_dir, config_property_key="exec_directory" + ) + ).absolute() + + def _find_and_set_pysidemodules(self): + self.modules = find_pyside_modules(project_dir=self.project_dir, + extra_ignore_dirs=self.extra_ignore_dirs, + project_data=self.project_data) + logging.info("The following PySide modules were found from the Python files of " + f"the project {self.modules}") + + def _find_and_set_qtquick_modules(self): + """Identify if QtQuick is used in QML files and add them as dependency + """ + extra_modules = [] + if not self.qml_modules: + self.qml_modules = set(run_qmlimportscanner(qml_files=self.qml_files, + dry_run=self.dry_run)) + + if "QtQuick" in self.qml_modules: + extra_modules.append("Quick") + + if "QtQuick.Controls" in self.qml_modules: + extra_modules.append("QuickControls2") + + self.modules += extra_modules + + +class DesktopConfig(Config): + """Wrapper class around pysidedeploy.spec, but specific to Desktop deployment + """ + class NuitkaMode(Enum): + ONEFILE = "onefile" + STANDALONE = "standalone" + + def __init__(self, config_file: Path, source_file: Path, python_exe: Path, dry_run: bool, + existing_config_file: bool = False, extra_ignore_dirs: List[str] = None, + mode: str = "onefile"): + 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: + self._find_and_set_pysidemodules() + 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, + python_exe) + + self._permissions = [] + if sys.platform == "darwin": + nuitka_macos_permissions = self.get_value("nuitka", "macos.permissions") + if nuitka_macos_permissions: + self._permissions = nuitka_macos_permissions.split(",") + else: + self._find_and_set_permissions() + + self._mode = self.NuitkaMode.ONEFILE + if self.get_value("nuitka", "mode") == self.NuitkaMode.STANDALONE.value: + self._mode = self.NuitkaMode.STANDALONE + elif mode == self.NuitkaMode.STANDALONE.value: + self.mode = self.NuitkaMode.STANDALONE + + @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)) + + @property + def permissions(self): + return self._permissions + + @permissions.setter + def permissions(self, permissions): + self._permissions = permissions + self.set_value("nuitka", "macos.permissions", ",".join(permissions)) + + @property + def mode(self): + return self._mode + + @mode.setter + def mode(self, mode: NuitkaMode): + self._mode = mode + self.set_value("nuitka", "mode", mode.value) + + def _find_dependent_qt_modules(self): + """ + Given pysidedeploy_config.modules, find all the other dependent Qt modules. + """ + all_modules = set(self.modules) + + 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: + self.dependency_reader.find_dependencies(module=module_name, used_modules=all_modules) + + self.modules = list(all_modules) + + def _find_and_set_permissions(self): + """ + Finds and sets the usage description string required for each permission requested by the + macOS application. + """ + permissions = [] + perm_categories = find_permission_categories(project_dir=self.project_dir, + extra_ignore_dirs=self.extra_ignore_dirs, + project_data=self.project_data) + + perm_categories_str = ",".join(perm_categories) + logging.info(f"[DEPLOY] Usage descriptions for the {perm_categories_str} will be added to " + "the Info.plist file of the macOS application bundle") + + # handling permissions + for perm_category in perm_categories: + if perm_category in PERMISSION_MAP: + permissions.append(PERMISSION_MAP[perm_category]) + + self.permissions = permissions |