aboutsummaryrefslogtreecommitdiffstats
path: root/sources/pyside-tools/project.py
blob: 3706a29859300032b1e14f4a8ed6fe5ef164d5f9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# 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


"""
Builds a '.pyproject' file

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
- <name>.qrc      : Runs the resource compiler to create a file rc_<name>.py
- <name>.ui       : Runs the user interface compiler to create a file ui_<name>.py

For a Python file declaring a QML module, a directory matching the URI is
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 sys
import os
from typing import List, Tuple, Optional
from pathlib import Path
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
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"
LRELEASE_CMD = "pyside6-lrelease"
LUPDATE_CMD = "pyside6-lupdate"
QMLTYPEREGISTRAR_CMD = "pyside6-qmltyperegistrar"
QMLLINT_CMD = "pyside6-qmllint"
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 _sort_sources(files: List[Path]) -> List[Path]:
    """Sort the sources for building, ensure .qrc is last since it might depend
       on generated files."""

    def key_func(p: Path):
        return p.suffix if p.suffix != ".qrc" else ".zzzz"

    return sorted(files, key=key_func)


class Project:
    """
    Class to wrap the various operations on Project
    """
    def __init__(self, project_file: Path):
        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()
        self._qml_module_check()

    def _qml_module_check(self):
        """Run a pre-check on Python source files and find the ones with QML
        decorators (representing a QML module)."""
        # Quick check for any QML files (to avoid running moc for no reason).
        if not self.cl_options.qml_module and not self.project.qml_files:
            return
        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:
                        self._qml_project_data = data

        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")
            sys.exit(1)

        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 self.cl_options.quiet:
            count = len(self._qml_module_sources)
            print(f"{self.project.project_file.name}, {count} QML file(s),"
                  f" {self._qml_project_data}")

    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), "--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])
        # generate .qmltypes from sources with Qml decorators
        if file.suffix == ".py" and file in self._qml_module_sources:
            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)])
        # Run qmltyperegistrar
        if file.name.endswith(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.fspath(cpp_file),
                   os.fspath(file)]
            cmd.extend(self._qml_project_data.registrar_options())
            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)

    def _regenerate_qmldir(self):
        """Regenerate the 'qmldir' file."""
        if self.cl_options.dry_run or not self._qml_dir_file:
            return
        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"):
                    qf.write(f"typeinfo {f.name}\n")

    def _build_file(self, source: Path):
        """Build an artifact."""
        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_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 _sort_sources(self.project.files):
            self._build_file(file)
        self._regenerate_qmldir()

    def run(self):
        """Runs the project"""
        self.build()
        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."""
        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_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.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.project_file.parent / first_module_dir)

    def _qmllint(self):
        """Helper for running qmllint on .qml files (non-recursive)."""
        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.project.qml_files:
            cmd.append(os.fspath(f))
        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_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()

        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.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("--qml-module", "-Q", action="store_true",
                        help="Perform check for QML module")
    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()
    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)
        sys.exit(1)
    project = Project(project_file)
    if mode == "build":
        project.build()
    elif mode == "run":
        project.run()
    elif mode == "clean":
        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)