aboutsummaryrefslogtreecommitdiffstats
path: root/sources/pyside-tools/deploy_lib/config.py
diff options
context:
space:
mode:
Diffstat (limited to 'sources/pyside-tools/deploy_lib/config.py')
-rw-r--r--sources/pyside-tools/deploy_lib/config.py368
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