diff options
Diffstat (limited to 'sources/pyside-tools/project.py')
-rw-r--r-- | sources/pyside-tools/project.py | 472 |
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) |