diff options
| -rw-r--r-- | sources/pyside-tools/deploy.py | 309 | ||||
| -rw-r--r-- | sources/pyside-tools/deploy/__init__.py | 7 | ||||
| -rw-r--r-- | sources/pyside-tools/deploy/commands.py | 31 | ||||
| -rw-r--r-- | sources/pyside-tools/deploy/config.py | 177 | ||||
| -rw-r--r-- | sources/pyside-tools/deploy/nuitka_helper.py | 48 | ||||
| -rw-r--r-- | sources/pyside-tools/deploy/python_helper.py | 82 |
6 files changed, 347 insertions, 307 deletions
diff --git a/sources/pyside-tools/deploy.py b/sources/pyside-tools/deploy.py index cfa35ebcf..2dde34ad3 100644 --- a/sources/pyside-tools/deploy.py +++ b/sources/pyside-tools/deploy.py @@ -25,324 +25,19 @@ 2. pyside6-deploy -c /path/to/config_file """ -import configparser -import subprocess import argparse import logging import sys -import os from pathlib import Path -from configparser import ConfigParser import shutil -from importlib import util import traceback +from deploy import Config, PythonExecutable + MAJOR_VERSION = 6 EXE_FORMAT = ".exe" if sys.platform == "win32" else ".bin" -def run_command(command, dry_run: bool): - command_str = " ".join([str(cmd) for cmd in command]) - try: - if not dry_run: - subprocess.check_call(command, shell=(sys.platform == "win32")) - else: - print(command_str + "\n") - except FileNotFoundError as error: - logging.exception(f"[DEPLOY]: {error.filename} not found") - raise - except subprocess.CalledProcessError as error: - logging.exception( - f"[DEPLOY]: Command {command_str} failed with error {error} and return_code" - f"{error.returncode}" - ) - raise - except Exception as error: - logging.exception(f"[DEPLOY]: Command {command_str} failed with error {error}") - raise - - -class Nuitka: - """ - Wrapper class around the nuitka executable, enabling its usage through python code - """ - - def __init__(self, nuitka): - self.nuitka = nuitka - - def create_executable( - self, source_file: Path, extra_args: str, qml_files: list[Path], dry_run: bool - ): - extra_args = extra_args.split() - qml_args = [] - if qml_files: - # this includes "all" the plugins - # FIXME: adding the "qml" plugin is equivalent to "all" because of dependencies - # Ideally it should only add the specific qml plugins. eg: quick window, quick controls - qml_args.append("--include-qt-plugins=all") - qml_args.extend( - [f"--include-data-files={qml_file}=./{qml_file.name}" for qml_file in qml_files] - ) - - output_dir = source_file.parent / "deployment" - if not dry_run: - output_dir.mkdir(parents=True, exist_ok=True) - print("[DEPLOY] Running Nuitka") - command = self.nuitka + [ - source_file, - "--follow-imports", - "--onefile", - "--enable-plugin=pyside6", - f"--output-dir={output_dir}", - ] - command.extend(extra_args + qml_args) - - if sys.platform == "linux": - linux_icon = str(Path(__file__).parent / "deploy" / "pyside_icon.jpg") - command.append(f"--linux-onefile-icon={linux_icon}") - - run_command(command=command, dry_run=dry_run) - - -class Config: - """ - Wrapper class around config file, whose options are used to control the executable creation - """ - - def __init__(self, config_file: Path) -> 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 / "deploy" / "default.spec", self.config_file) - else: - print(f"Using existing config file {config_file}") - self.parser.read(self.config_file) - - self.project_dir = None - if self.get_value("app", "project_dir"): - self.project_dir = Path(self.get_value("app", "project_dir")).absolute() - - self.qml_files = [] - config_qml_files = self.get_value("qt", "qml_files") - if config_qml_files and self.project_dir: - self.qml_files = [Path(self.project_dir) / file for file in config_qml_files.split(",")] - - def update_config(self): - logging.info("[DEPLOY] Creating {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): - 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") - - def set_or_fetch(self, config_property_val, config_property_key, config_property_group="app"): - """ - Write to config_file if 'config_property_key' is known without config_file - Fetch and return from config_file if 'config_property_key' is unknown, but - config_file exists - Otherwise, raise an exception - """ - if config_property_val: - self.set_value(config_property_group, config_property_key, str(config_property_val)) - return config_property_val - elif self.get_value(config_property_group, config_property_key): - return self.get_value(config_property_group, config_property_key) - else: - logging.exception( - f"[DEPLOY]: No {config_property_key} specified in config file or as cli option" - ) - raise - - @property - def qml_files(self): - return self._qml_files - - @qml_files.setter - def qml_files(self, qml_files): - self._qml_files = qml_files - - @property - def project_dir(self): - return self._project_dir - - @project_dir.setter - def project_dir(self, project_dir): - self._project_dir = project_dir - - 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""" - - if self.project_dir: - qml_files_str = self.get_value("qt", "qml_files") - self.qml_files = [] - for file in qml_files_str.split(","): - if file: - self.qml_files.append(Path(self.project_dir) / file) - 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 not self.qml_files: - qml_files_temp = list(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: - # python venv path is inside the main source dir - qml_files_temp = list( - set(qml_files_temp) - set(python_exe.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?" - ) - else: - logging.warning( - "You seem to include a lot of QML files. \ - Are they intended?" - ) - - if qml_files_temp: - extra_qml_files = [Path(file) for file in qml_files_temp] - self.qml_files.extend(extra_qml_files) - self.set_value( - "qt", "qml_files", ",".join([str(file) for file in self.qml_files]) - ) - logging.info("[DEPLOY] QML files identified and set in config_file") - - def find_and_set_project_dir(self): - source_file = ( - Path(self.get_value("app", "input_file")) - if self.get_value("app", "input_file") - else None - ) - - if self.qml_files: - paths = self.qml_files.copy() - paths.append(source_file.absolute()) - self.project_dir = Path(os.path.commonpath(paths=paths)) - - # update all qml paths - logging.info("[DEPLOY] Update QML files paths to relative paths") - qml_relative_paths = ",".join( - [str(qml_file.relative_to(self.project_dir)) for qml_file in self.qml_files] - ) - self.set_value("qt", "qml_files", qml_relative_paths) - else: - self.project_dir = source_file.parent - - # update input_file path - logging.info("[DEPLOY] Update input_file path") - self.set_value("app", "input_file", str(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()))) - - -class PythonExecutable: - """ - Wrapper class around Python executable - """ - - def __init__(self, python_path=None, create_venv=False, dry_run=False): - self.exe = python_path if python_path else Path(sys.executable) - self.dry_run = dry_run - if create_venv: - self.__create_venv() - self.nuitka = Nuitka(nuitka=[self.exe, "-m", "nuitka"]) - - @property - def exe(self): - return Path(self._exe) - - @exe.setter - def exe(self, exe): - self._exe = exe - - @staticmethod - def is_venv(): - venv = os.environ.get("VIRTUAL_ENV") - return True if venv else False - - def __create_venv(self): - self.install("virtualenv") - if not self.is_venv(): - run_command( - command=[self.exe, "-m", "venv", Path.cwd() / "deployment" / "venv"], - dry_run=self.dry_run, - ) - venv_path = Path(os.environ["VIRTUAL_ENV"]) - if sys.platform == "win32": - self.exe = venv_path / "Scripts" / "python.exe" - elif sys.platform in ["linux", "darwin"]: - self.exe = venv_path / "bin" / "python" - else: - logging.info("[DEPLOY]: You are already in virtual environment!") - - def install(self, packages: list = None): - if packages: - for package in packages: - if not self.is_installed(package=package): - logging.info(f"[DEPLOY]: Installing package: {package}") - run_command( - command=[self.exe, "-m", "pip", "install", package], - dry_run=self.dry_run, - ) - else: - logging.info(f"[DEPLOY]: Upgrading package: {package}") - run_command( - command=[self.exe, "-m", "pip", "install", "--upgrade", package], - dry_run=self.dry_run, - ) - - def is_installed(self, package): - return bool(util.find_spec(package)) - - def create_executable(self, source_file: Path, extra_args: str, config: Config): - if config.qml_files: - logging.info(f"[DEPLOY]: Included QML files: {config.qml_files}") - - self.nuitka.create_executable( - source_file=source_file, - extra_args=extra_args, - qml_files=config.qml_files, - dry_run=self.dry_run, - ) - - def config_option_exists(): return True if any(item in sys.argv for item in ["--config-file", "-c"]) else False diff --git a/sources/pyside-tools/deploy/__init__.py b/sources/pyside-tools/deploy/__init__.py new file mode 100644 index 000000000..b94bc665b --- /dev/null +++ b/sources/pyside-tools/deploy/__init__.py @@ -0,0 +1,7 @@ +# 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 .commands import run_command +from .nuitka_helper import Nuitka +from .config import Config +from .python_helper import PythonExecutable diff --git a/sources/pyside-tools/deploy/commands.py b/sources/pyside-tools/deploy/commands.py new file mode 100644 index 000000000..92745367f --- /dev/null +++ b/sources/pyside-tools/deploy/commands.py @@ -0,0 +1,31 @@ +# 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 + +import subprocess +import sys +import logging + +""" +All utility functions for deployment +""" + + +def run_command(command, dry_run: bool): + command_str = " ".join([str(cmd) for cmd in command]) + try: + if not dry_run: + subprocess.check_call(command, shell=(sys.platform == "win32")) + else: + print(command_str + "\n") + except FileNotFoundError as error: + logging.exception(f"[DEPLOY]: {error.filename} not found") + raise + except subprocess.CalledProcessError as error: + logging.exception( + f"[DEPLOY]: Command {command_str} failed with error {error} and return_code" + f"{error.returncode}" + ) + raise + except Exception as error: + logging.exception(f"[DEPLOY]: Command {command_str} failed with error {error}") + raise diff --git a/sources/pyside-tools/deploy/config.py b/sources/pyside-tools/deploy/config.py new file mode 100644 index 000000000..3e1c72abe --- /dev/null +++ b/sources/pyside-tools/deploy/config.py @@ -0,0 +1,177 @@ +# 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 configparser +from configparser import ConfigParser +import shutil +import logging +import os + + +class Config: + """ + Wrapper class around config file, whose options are used to control the executable creation + """ + + def __init__(self, config_file: Path) -> 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.parser.read(self.config_file) + + self.project_dir = None + if self.get_value("app", "project_dir"): + self.project_dir = Path(self.get_value("app", "project_dir")).absolute() + + self.qml_files = [] + config_qml_files = self.get_value("qt", "qml_files") + if config_qml_files and self.project_dir: + self.qml_files = [Path(self.project_dir) / file for file in config_qml_files.split(",")] + + def update_config(self): + logging.info("[DEPLOY] Creating {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): + 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") + + def set_or_fetch(self, config_property_val, config_property_key, config_property_group="app"): + """ + Write to config_file if 'config_property_key' is known without config_file + Fetch and return from config_file if 'config_property_key' is unknown, but + config_file exists + Otherwise, raise an exception + """ + if config_property_val: + self.set_value(config_property_group, config_property_key, str(config_property_val)) + return config_property_val + elif self.get_value(config_property_group, config_property_key): + return self.get_value(config_property_group, config_property_key) + else: + logging.exception( + f"[DEPLOY]: No {config_property_key} specified in config file or as cli option" + ) + raise + + @property + def qml_files(self): + return self._qml_files + + @qml_files.setter + def qml_files(self, qml_files): + self._qml_files = qml_files + + @property + def project_dir(self): + return self._project_dir + + @project_dir.setter + def project_dir(self, project_dir): + self._project_dir = project_dir + + 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""" + + if self.project_dir: + qml_files_str = self.get_value("qt", "qml_files") + self.qml_files = [] + for file in qml_files_str.split(","): + if file: + self.qml_files.append(Path(self.project_dir) / file) + 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 not self.qml_files: + qml_files_temp = list(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: + # python venv path is inside the main source dir + qml_files_temp = list( + set(qml_files_temp) - set(python_exe.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?" + ) + else: + logging.warning( + "You seem to include a lot of QML files. \ + Are they intended?" + ) + + if qml_files_temp: + extra_qml_files = [Path(file) for file in qml_files_temp] + self.qml_files.extend(extra_qml_files) + self.set_value( + "qt", "qml_files", ",".join([str(file) for file in self.qml_files]) + ) + logging.info("[DEPLOY] QML files identified and set in config_file") + + def find_and_set_project_dir(self): + source_file = ( + Path(self.get_value("app", "input_file")) + if self.get_value("app", "input_file") + else None + ) + + if self.qml_files and source_file: + paths = self.qml_files.copy() + paths.append(source_file.absolute()) + self.project_dir = Path(os.path.commonpath(paths=paths)) + + # update all qml paths + logging.info("[DEPLOY] Update QML files paths to relative paths") + qml_relative_paths = ",".join( + [str(qml_file.relative_to(self.project_dir)) for qml_file in self.qml_files] + ) + self.set_value("qt", "qml_files", qml_relative_paths) + else: + self.project_dir = source_file.parent + + # update input_file path + logging.info("[DEPLOY] Update input_file path") + self.set_value("app", "input_file", str(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()))) diff --git a/sources/pyside-tools/deploy/nuitka_helper.py b/sources/pyside-tools/deploy/nuitka_helper.py new file mode 100644 index 000000000..a6b3f79d6 --- /dev/null +++ b/sources/pyside-tools/deploy/nuitka_helper.py @@ -0,0 +1,48 @@ +# 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 + +import sys +from pathlib import Path +from .utils import run_command + + +class Nuitka: + """ + Wrapper class around the nuitka executable, enabling its usage through python code + """ + + def __init__(self, nuitka): + self.nuitka = nuitka + + def create_executable( + self, source_file: Path, extra_args: str, qml_files: list[Path], dry_run: bool + ): + extra_args = extra_args.split() + qml_args = [] + if qml_files: + # this includes "all" the plugins + # FIXME: adding the "qml" plugin is equivalent to "all" because of dependencies + # Ideally it should only add the specific qml plugins. eg: quick window, quick controls + qml_args.append("--include-qt-plugins=all") + qml_args.extend( + [f"--include-data-files={qml_file}=./{qml_file.name}" for qml_file in qml_files] + ) + + output_dir = source_file.parent / "deployment" + if not dry_run: + output_dir.mkdir(parents=True, exist_ok=True) + print("[DEPLOY] Running Nuitka") + command = self.nuitka + [ + source_file, + "--follow-imports", + "--onefile", + "--enable-plugin=pyside6", + f"--output-dir={output_dir}", + ] + command.extend(extra_args + qml_args) + + if sys.platform == "linux": + linux_icon = str(Path(__file__).parent / "pyside_icon.jpg") + command.append(f"--linux-onefile-icon={linux_icon}") + + run_command(command=command, dry_run=dry_run) diff --git a/sources/pyside-tools/deploy/python_helper.py b/sources/pyside-tools/deploy/python_helper.py new file mode 100644 index 000000000..3831b7dde --- /dev/null +++ b/sources/pyside-tools/deploy/python_helper.py @@ -0,0 +1,82 @@ +# 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 + +import sys +import os +import logging +from importlib import util +from pathlib import Path + +from . import Nuitka, run_command, Config + + +class PythonExecutable: + """ + Wrapper class around Python executable + """ + + def __init__(self, python_path=None, create_venv=False, dry_run=False): + self.exe = python_path if python_path else Path(sys.executable) + self.dry_run = dry_run + if create_venv: + self.__create_venv() + self.nuitka = Nuitka(nuitka=[self.exe, "-m", "nuitka"]) + + @property + def exe(self): + return Path(self._exe) + + @exe.setter + def exe(self, exe): + self._exe = exe + + @staticmethod + def is_venv(): + venv = os.environ.get("VIRTUAL_ENV") + return True if venv else False + + def __create_venv(self): + self.install("virtualenv") + if not self.is_venv(): + run_command( + command=[self.exe, "-m", "venv", Path.cwd() / "deployment" / "venv"], + dry_run=self.dry_run, + ) + venv_path = Path(os.environ["VIRTUAL_ENV"]) + if sys.platform == "win32": + self.exe = venv_path / "Scripts" / "python.exe" + elif sys.platform in ["linux", "darwin"]: + self.exe = venv_path / "bin" / "python" + else: + logging.info("[DEPLOY]: You are already in virtual environment!") + + def install(self, packages: list = None): + if packages: + for package in packages: + if not self.is_installed(package=package): + logging.info(f"[DEPLOY]: Installing package: {package}") + run_command( + command=[self.exe, "-m", "pip", "install", package], + dry_run=self.dry_run, + ) + else: + logging.info(f"[DEPLOY]: Upgrading package: {package}") + run_command( + command=[self.exe, "-m", "pip", "install", "--upgrade", package], + dry_run=self.dry_run, + ) + + def is_installed(self, package): + return bool(util.find_spec(package)) + + def create_executable(self, source_file: Path, extra_args: str, config: Config): + if config.qml_files: + logging.info(f"[DEPLOY]: Included QML files: {config.qml_files}") + + self.nuitka.create_executable( + source_file=source_file, + extra_args=extra_args, + qml_files=config.qml_files, + dry_run=self.dry_run, + ) + |
