aboutsummaryrefslogtreecommitdiffstats
path: root/sources/pyside-tools/project.py
diff options
context:
space:
mode:
Diffstat (limited to 'sources/pyside-tools/project.py')
-rw-r--r--sources/pyside-tools/project.py472
1 files changed, 141 insertions, 331 deletions
diff --git a/sources/pyside-tools/project.py b/sources/pyside-tools/project.py
index 5ffdf0d21..3706a2985 100644
--- a/sources/pyside-tools/project.py
+++ b/sources/pyside-tools/project.py
@@ -5,7 +5,9 @@
"""
Builds a '.pyproject' file
-Builds Qt Designer forms, resource files and QML type files.
+Builds Qt Designer forms, resource files and QML type files
+
+Deploys the application by creating an executable for the corresponding platform
For each entry in a '.pyproject' file:
- <name>.pyproject: Recurse to handle subproject
@@ -17,294 +19,78 @@ created and populated with .qmltypes and qmldir files for use by code analysis
tools. Currently, only one QML module consisting of several classes can be
handled per project file.
"""
-
-import json
-import os
-import subprocess
import sys
-
-from argparse import ArgumentParser, RawTextHelpFormatter
+import os
+from typing import List, Tuple, Optional
from pathlib import Path
-from typing import Dict, List, Optional, Tuple
+from argparse import ArgumentParser, RawTextHelpFormatter
+from project import (QmlProjectData, check_qml_decorators, is_python_file,
+ QMLDIR_FILE, MOD_CMD, METATYPES_JSON_SUFFIX,
+ SHADER_SUFFIXES, TRANSLATION_SUFFIX,
+ requires_rebuild, run_command, remove_path,
+ ProjectData, resolve_project_file, new_project,
+ ProjectType, ClOptions)
MODE_HELP = """build Builds the project
-run Builds the project and runs the first file")
-clean Cleans the build artifacts")
-qmllint Runs the qmllint tool"""
-
-
-opt_quiet = False
-opt_dry_run = False
-opt_force = False
-opt_qml_module = False
-
+run Builds the project and runs the first file")
+clean Cleans the build artifacts")
+qmllint Runs the qmllint tool
+deploy Deploys the application
+lupdate Updates translation (.ts) files
+new-ui Creates a new QtWidgets project with a Qt Designer-based main window
+new-widget Creates a new QtWidgets project with a main window
+new-quick Creates a new QtQuick project
+"""
UIC_CMD = "pyside6-uic"
RCC_CMD = "pyside6-rcc"
-MOD_CMD = "pyside6-metaobjectdump"
+LRELEASE_CMD = "pyside6-lrelease"
+LUPDATE_CMD = "pyside6-lupdate"
QMLTYPEREGISTRAR_CMD = "pyside6-qmltyperegistrar"
QMLLINT_CMD = "pyside6-qmllint"
-QTPATHS_CMD = "qtpaths6"
-
-
-PROJECT_FILE_SUFFIX = ".pyproject"
-QMLDIR_FILE = "qmldir"
-
-
-QML_IMPORT_NAME = "QML_IMPORT_NAME"
-QML_IMPORT_MAJOR_VERSION = "QML_IMPORT_MAJOR_VERSION"
-QML_IMPORT_MINOR_VERSION = "QML_IMPORT_MINOR_VERSION"
-QT_MODULES = "QT_MODULES"
-
-
-METATYPES_JSON_SUFFIX = "_metatypes.json"
-
-
-def run_command(command: List[str], cwd: str = None, ignore_fail: bool = False):
- """Run a command observing quiet/dry run"""
- if not opt_quiet or opt_dry_run:
- print(" ".join(command))
- if not opt_dry_run:
- ex = subprocess.call(command, cwd=cwd)
- if ex != 0 and not ignore_fail:
- sys.exit(ex)
-
-
-def requires_rebuild(sources: List[Path], artifact: Path) -> bool:
- """Returns whether artifact needs to be rebuilt depending on sources"""
- if not artifact.is_file():
- return True
- artifact_mod_time = artifact.stat().st_mtime
- for source in sources:
- if source.stat().st_mtime > artifact_mod_time:
- return True
- return False
-
-
-def _remove_path_recursion(path: Path):
- """Recursion to remove a file or directory."""
- if path.is_file():
- path.unlink()
- elif path.is_dir():
- for item in path.iterdir():
- _remove_path_recursion(item)
- path.rmdir()
-
-
-def remove_path(path: Path):
- """Remove path (file or directory) observing opt_dry_run."""
- if not path.exists():
- return
- if not opt_quiet:
- print(f"Removing {path.name}...")
- if opt_dry_run:
- return
- _remove_path_recursion(path)
+QSB_CMD = "pyside6-qsb"
+DEPLOY_CMD = "pyside6-deploy"
+NEW_PROJECT_TYPES = {"new-quick": ProjectType.QUICK,
+ "new-ui": ProjectType.WIDGET_FORM,
+ "new-widget": ProjectType.WIDGET}
-def package_dir() -> Path:
- """Return the PySide6 root."""
- return Path(__file__).resolve().parents[1]
+def _sort_sources(files: List[Path]) -> List[Path]:
+ """Sort the sources for building, ensure .qrc is last since it might depend
+ on generated files."""
-_qtpaths_info: Dict[str, str] = {}
+ def key_func(p: Path):
+ return p.suffix if p.suffix != ".qrc" else ".zzzz"
-
-def qtpaths() -> Dict[str, str]:
- """Run qtpaths and return a dict of values."""
- global _qtpaths_info
- if not _qtpaths_info:
- output = subprocess.check_output([QTPATHS_CMD, "--query"])
- for line in output.decode("utf-8").split("\n"):
- tokens = line.strip().split(":")
- if len(tokens) == 2:
- _qtpaths_info[tokens[0]] = tokens[1]
- return _qtpaths_info
-
-
-_qt_metatype_json_dir: Optional[Path] = None
-
-
-def qt_metatype_json_dir() -> Path:
- """Return the location of the Qt QML metatype files."""
- global _qt_metatype_json_dir
- if not _qt_metatype_json_dir:
- qt_dir = package_dir()
- if sys.platform != "win32":
- qt_dir /= "Qt"
- metatypes_dir = qt_dir / "lib" / "metatypes"
- if metatypes_dir.is_dir(): # Fully installed case
- _qt_metatype_json_dir = metatypes_dir
- else:
- # Fallback for distro builds/development.
- print(f"Falling back to {QTPATHS_CMD} to determine metatypes directory.",
- file=sys.stderr)
- _qt_metatype_json_dir = Path(qtpaths()["QT_INSTALL_LIBS"]) / "metatypes"
- return _qt_metatype_json_dir
-
-
-class QmlProjectData:
- """QML relevant project data."""
-
- def __init__(self):
- self._import_name: str = ""
- self._import_major_version: int = 0
- self._import_minor_version: int = 0
- self._qt_modules: List[str] = []
-
- def registrar_options(self):
- result = ["--import-name", self._import_name,
- "--major-version", str(self._import_major_version),
- "--minor-version", str(self._import_minor_version)]
- if self._qt_modules:
- # Add Qt modules as foreign types
- foreign_files: List[str] = []
- meta_dir = qt_metatype_json_dir()
- for mod in self._qt_modules:
- mod_id = mod[2:].lower()
- pattern = f"qt6{mod_id}_*{METATYPES_JSON_SUFFIX}"
- for f in meta_dir.glob(pattern):
- foreign_files.append(os.fspath(f))
- break
- list = ",".join(foreign_files)
- result.append(f"--foreign-types={list}")
- return result
-
- @property
- def import_name(self):
- return self._import_name
-
- @import_name.setter
- def import_name(self, n):
- self._import_name = n
-
- @property
- def import_major_version(self):
- return self._import_major_version
-
- @import_major_version.setter
- def import_major_version(self, v):
- self._import_major_version = v
-
- @property
- def import_minor_version(self):
- return self._import_minor_version
-
- @import_minor_version.setter
- def import_minor_version(self, v):
- self._import_minor_version = v
-
- @property
- def qt_modules(self):
- return self._qt_modules
-
- @qt_modules.setter
- def qt_modules(self, v):
- self._qt_modules = v
-
- def __str__(self) -> str:
- vmaj = self._import_major_version
- vmin = self._import_minor_version
- return f'"{self._import_name}" v{vmaj}.{vmin}'
-
- def __bool__(self) -> bool:
- return len(self._import_name) > 0 and self._import_major_version > 0
-
-
-def _has_qml_decorated_class(class_list: List) -> bool:
- """Check for QML-decorated classes in the moc json output."""
- for d in class_list:
- class_infos = d.get("classInfos")
- if class_infos:
- for e in class_infos:
- if "QML" in e["name"]:
- return True
- return False
-
-
-def _check_qml_decorators(py_file: Path) -> Tuple[bool, QmlProjectData]:
- """Check if a Python file has QML-decorated classes by running a moc check
- and return whether a class was found and the QML data."""
- data = None
- try:
- cmd = [MOD_CMD, "--quiet", os.fspath(py_file)]
- with subprocess.Popen(cmd, stdout=subprocess.PIPE) as proc:
- data = json.load(proc.stdout)
- proc.wait()
- except Exception as e:
- t = type(e).__name__
- print(f"{t}: running {MOD_CMD} on {py_file}: {e}", file=sys.stderr)
- sys.exit(1)
-
- qml_project_data = QmlProjectData()
- if not data:
- return (False, qml_project_data) # No classes in file
-
- first = data[0]
- class_list = first["classes"]
- has_class = _has_qml_decorated_class(class_list)
- if has_class:
- v = first.get(QML_IMPORT_NAME)
- if v:
- qml_project_data.import_name = v
- v = first.get(QML_IMPORT_MAJOR_VERSION)
- if v:
- qml_project_data.import_major_version = v
- qml_project_data.import_minor_version = first.get(QML_IMPORT_MINOR_VERSION)
- v = first.get(QT_MODULES)
- if v:
- qml_project_data.qt_modules = v
- return (has_class, qml_project_data)
+ return sorted(files, key=key_func)
class Project:
-
+ """
+ Class to wrap the various operations on Project
+ """
def __init__(self, project_file: Path):
- """Parse the project."""
- self._project_file = project_file
-
- # All sources except subprojects
- self._files: List[Path] = []
- # QML files
- self._qml_files: List[Path] = []
- self._sub_projects: List[Project] = []
+ self.project = ProjectData(project_file=project_file)
+ self.cl_options = ClOptions()
# Files for QML modules using the QmlElement decorators
self._qml_module_sources: List[Path] = []
self._qml_module_dir: Optional[Path] = None
self._qml_dir_file: Optional[Path] = None
self._qml_project_data = QmlProjectData()
-
- with project_file.open("r") as pyf:
- pyproject = json.load(pyf)
- for f in pyproject["files"]:
- file = Path(project_file.parent / f)
- if file.suffix == PROJECT_FILE_SUFFIX:
- self._sub_projects.append(Project(file))
- else:
- self._files.append(file)
- if file.suffix == ".qml":
- self._qml_files.append(file)
self._qml_module_check()
- @property
- def project_file(self):
- return self._project_file
-
- @property
- def files(self):
- return self._files
-
def _qml_module_check(self):
"""Run a pre-check on Python source files and find the ones with QML
- decorators (representing a QML module)."""
+ decorators (representing a QML module)."""
# Quick check for any QML files (to avoid running moc for no reason).
- if not opt_qml_module and not self._qml_files:
+ if not self.cl_options.qml_module and not self.project.qml_files:
return
- for file in self.files:
- if file.suffix == ".py":
- has_class, data = _check_qml_decorators(file)
+ for file in self.project.files:
+ if is_python_file(file):
+ has_class, data = check_qml_decorators(file)
if has_class:
self._qml_module_sources.append(file)
if data:
@@ -313,53 +99,63 @@ class Project:
if not self._qml_module_sources:
return
if not self._qml_project_data:
- print("Detected QML-decorated files, "
- "but was unable to detect QML_IMPORT_NAME")
+ print("Detected QML-decorated files, " "but was unable to detect QML_IMPORT_NAME")
sys.exit(1)
- self._qml_module_dir = self._project_file.parent
+ self._qml_module_dir = self.project.project_file.parent
for uri_dir in self._qml_project_data.import_name.split("."):
self._qml_module_dir /= uri_dir
print(self._qml_module_dir)
self._qml_dir_file = self._qml_module_dir / QMLDIR_FILE
- if not opt_quiet:
+ if not self.cl_options.quiet:
count = len(self._qml_module_sources)
- print(f"{self._project_file.name}, {count} QML file(s), {self._qml_project_data}")
+ print(f"{self.project.project_file.name}, {count} QML file(s),"
+ f" {self._qml_project_data}")
- def _get_artifact(self, file: Path) -> Tuple[Optional[Path], Optional[List[str]]]:
+ def _get_artifacts(self, file: Path) -> Tuple[List[Path], Optional[List[str]]]:
"""Return path and command for a file's artifact"""
if file.suffix == ".ui": # Qt form files
py_file = f"{file.parent}/ui_{file.stem}.py"
- return (Path(py_file), [UIC_CMD, os.fspath(file), "-o", py_file])
+ return ([Path(py_file)], [UIC_CMD, os.fspath(file), "--rc-prefix", "-o", py_file])
if file.suffix == ".qrc": # Qt resources
py_file = f"{file.parent}/rc_{file.stem}.py"
- return (Path(py_file), [RCC_CMD, os.fspath(file), "-o", py_file])
+ return ([Path(py_file)], [RCC_CMD, os.fspath(file), "-o", py_file])
# generate .qmltypes from sources with Qml decorators
if file.suffix == ".py" and file in self._qml_module_sources:
- assert(self._qml_module_dir)
+ assert self._qml_module_dir
qml_module_dir = os.fspath(self._qml_module_dir)
json_file = f"{qml_module_dir}/{file.stem}{METATYPES_JSON_SUFFIX}"
- return (Path(json_file), [MOD_CMD, "-o", json_file,
- os.fspath(file)])
+ return ([Path(json_file)], [MOD_CMD, "-o", json_file, os.fspath(file)])
# Run qmltyperegistrar
if file.name.endswith(METATYPES_JSON_SUFFIX):
- assert(self._qml_module_dir)
- stem = file.name[:len(file.name) - len(METATYPES_JSON_SUFFIX)]
+ assert self._qml_module_dir
+ stem = file.name[: len(file.name) - len(METATYPES_JSON_SUFFIX)]
qmltypes_file = self._qml_module_dir / f"{stem}.qmltypes"
+ cpp_file = self._qml_module_dir / f"{stem}_qmltyperegistrations.cpp"
cmd = [QMLTYPEREGISTRAR_CMD, "--generate-qmltypes",
- os.fspath(qmltypes_file), "-o", os.devnull, os.fspath(file)]
+ os.fspath(qmltypes_file), "-o", os.fspath(cpp_file),
+ os.fspath(file)]
cmd.extend(self._qml_project_data.registrar_options())
- return (qmltypes_file, cmd)
+ return ([qmltypes_file, cpp_file], cmd)
+
+ if file.name.endswith(TRANSLATION_SUFFIX):
+ qm_file = f"{file.parent}/{file.stem}.qm"
+ cmd = [LRELEASE_CMD, os.fspath(file), "-qm", qm_file]
+ return ([Path(qm_file)], cmd)
+
+ if file.suffix in SHADER_SUFFIXES:
+ qsb_file = f"{file.parent}/{file.stem}.qsb"
+ cmd = [QSB_CMD, "-o", qsb_file, os.fspath(file)]
+ return ([Path(qsb_file)], cmd)
- return (None, None)
+ return ([], None)
def _regenerate_qmldir(self):
"""Regenerate the 'qmldir' file."""
- if opt_dry_run or not self._qml_dir_file:
+ if self.cl_options.dry_run or not self._qml_dir_file:
return
- if opt_force or requires_rebuild(self._qml_module_sources,
- self._qml_dir_file):
+ if self.cl_options.force or requires_rebuild(self._qml_module_sources, self._qml_dir_file):
with self._qml_dir_file.open("w") as qf:
qf.write(f"module {self._qml_project_data.import_name}\n")
for f in self._qml_module_dir.glob("*.qmltypes"):
@@ -367,111 +163,121 @@ class Project:
def _build_file(self, source: Path):
"""Build an artifact."""
- artifact, command = self._get_artifact(source)
- if not artifact:
- return
- if opt_force or requires_rebuild([source], artifact):
- run_command(command, cwd=self._project_file.parent)
- self._build_file(artifact) # Recurse for QML (json->qmltypes)
+ artifacts, command = self._get_artifacts(source)
+ for artifact in artifacts:
+ if self.cl_options.force or requires_rebuild([source], artifact):
+ run_command(command, cwd=self.project.project_file.parent)
+ self._build_file(artifact) # Recurse for QML (json->qmltypes)
def build(self):
"""Build."""
- for sub_project in self._sub_projects:
- sub_project.build()
+ for sub_project_file in self.project.sub_projects_files:
+ Project(project_file=sub_project_file).build()
if self._qml_module_dir:
self._qml_module_dir.mkdir(exist_ok=True, parents=True)
- for file in self._files:
+ for file in _sort_sources(self.project.files):
self._build_file(file)
self._regenerate_qmldir()
def run(self):
- """Runs the project (first .py file)."""
+ """Runs the project"""
self.build()
- if self.files:
- for file in self._files:
- if file.suffix == ".py":
- cmd = [sys.executable, os.fspath(file)]
- run_command(cmd, cwd=self._project_file.parent)
- break
+ cmd = [sys.executable, str(self.project.main_file)]
+ run_command(cmd, cwd=self.project.project_file.parent)
def _clean_file(self, source: Path):
"""Clean an artifact."""
- artifact, command = self._get_artifact(source)
- if artifact and artifact.is_file():
+ artifacts, command = self._get_artifacts(source)
+ for artifact in artifacts:
remove_path(artifact)
self._clean_file(artifact) # Recurse for QML (json->qmltypes)
def clean(self):
"""Clean build artifacts."""
- for sub_project in self._sub_projects:
- sub_project.clean()
- for file in self._files:
+ for sub_project_file in self.project.sub_projects_files:
+ Project(project_file=sub_project_file).clean()
+ for file in self.project.files:
self._clean_file(file)
if self._qml_module_dir and self._qml_module_dir.is_dir():
remove_path(self._qml_module_dir)
# In case of a dir hierarchy ("a.b" -> a/b), determine and delete
# the root directory
- if self._qml_module_dir.parent != self._project_file.parent:
- project_dir_parts = len(self._project_file.parent.parts)
+ if self._qml_module_dir.parent != self.project.project_file.parent:
+ project_dir_parts = len(self.project.project_file.parent.parts)
first_module_dir = self._qml_module_dir.parts[project_dir_parts]
- remove_path(self._project_file.parent / first_module_dir)
+ remove_path(self.project.project_file.parent / first_module_dir)
def _qmllint(self):
"""Helper for running qmllint on .qml files (non-recursive)."""
- if not self._qml_files:
- print(f"{self._project_file.name}: No QML files found",
- file=sys.stderr)
+ if not self.project.qml_files:
+ print(f"{self.project.project_file.name}: No QML files found", file=sys.stderr)
return
cmd = [QMLLINT_CMD]
if self._qml_dir_file:
cmd.extend(["-i", os.fspath(self._qml_dir_file)])
- for f in self._qml_files:
+ for f in self.project.qml_files:
cmd.append(os.fspath(f))
- run_command(cmd, cwd=self._project_file.parent, ignore_fail=True)
+ run_command(cmd, cwd=self.project.project_file.parent, ignore_fail=True)
def qmllint(self):
"""Run qmllint on .qml files."""
self.build()
- for sub_project in self._sub_projects:
- sub_project._qmllint()
+ for sub_project_file in self.project.sub_projects_files:
+ Project(project_file=sub_project_file)._qmllint()
self._qmllint()
+ def deploy(self):
+ """Deploys the application"""
+ cmd = [DEPLOY_CMD]
+ cmd.extend([str(self.project.main_file), "-f"])
+ run_command(cmd, cwd=self.project.project_file.parent)
+
+ def lupdate(self):
+ for sub_project_file in self.project.sub_projects_files:
+ Project(project_file=sub_project_file).lupdate()
-def resolve_project_file(cmdline: str) -> Optional[Path]:
- """Return the project file from the command line value, either
- from the file argument or directory"""
- project_file = Path(cmdline).resolve() if cmdline else Path.cwd()
- if project_file.is_file():
- return project_file
- if project_file.is_dir():
- for m in project_file.glob(f"*{PROJECT_FILE_SUFFIX}"):
- return m
- return None
+ if not self.project.ts_files:
+ print(f"{self.project.project_file.name}: No .ts file found.",
+ file=sys.stderr)
+ return
+
+ source_files = self.project.python_files + self.project.ui_files
+ cmd_prefix = [LUPDATE_CMD] + [p.name for p in source_files]
+ cmd_prefix.append("-ts")
+ for ts_file in self.project.ts_files:
+ if requires_rebuild(source_files, ts_file):
+ cmd = cmd_prefix
+ cmd.append(ts_file.name)
+ run_command(cmd, cwd=self.project.project_file.parent)
if __name__ == "__main__":
- parser = ArgumentParser(description=__doc__,
- formatter_class=RawTextHelpFormatter)
+ parser = ArgumentParser(description=__doc__, formatter_class=RawTextHelpFormatter)
parser.add_argument("--quiet", "-q", action="store_true", help="Quiet")
- parser.add_argument("--dry-run", "-n", action="store_true",
- help="Only print commands")
- parser.add_argument("--force", "-f", action="store_true",
- help="Force rebuild")
+ parser.add_argument("--dry-run", "-n", action="store_true", help="Only print commands")
+ parser.add_argument("--force", "-f", action="store_true", help="Force rebuild")
parser.add_argument("--qml-module", "-Q", action="store_true",
help="Perform check for QML module")
- parser.add_argument("mode",
- choices=["build", "run", "clean", "qmllint"],
- default="build", type=str, help=MODE_HELP)
+ mode_choices = ["build", "run", "clean", "qmllint", "deploy", "lupdate"]
+ mode_choices.extend(NEW_PROJECT_TYPES.keys())
+ parser.add_argument("mode", choices=mode_choices, default="build",
+ type=str, help=MODE_HELP)
parser.add_argument("file", help="Project file", nargs="?", type=str)
options = parser.parse_args()
- opt_quiet = options.quiet
- opt_dry_run = options.dry_run
- opt_force = options.force
- opt_qml_module = options.qml_module
+ cl_options = ClOptions(dry_run=options.dry_run, quiet=options.quiet, force=options.force,
+ qml_module=options.qml_module)
+
mode = options.mode
+ new_project_type = NEW_PROJECT_TYPES.get(mode)
+ if new_project_type:
+ if not options.file:
+ print(f"{mode} requires a directory name.", file=sys.stderr)
+ sys.exit(1)
+ sys.exit(new_project(options.file, new_project_type))
+
project_file = resolve_project_file(options.file)
if not project_file:
print(f"Cannot determine project_file {options.file}", file=sys.stderr)
@@ -485,6 +291,10 @@ if __name__ == "__main__":
project.clean()
elif mode == "qmllint":
project.qmllint()
+ elif mode == "deploy":
+ project.deploy()
+ elif mode == "lupdate":
+ project.lupdate()
else:
print(f"Invalid mode {mode}", file=sys.stderr)
sys.exit(1)