aboutsummaryrefslogtreecommitdiffstats
path: root/tools/sync_examples.py
blob: c2715f4174d20c1e504aa9a33e3f12279f1327e4 (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
# Copyright (C) 2025 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 os
import shutil
import sys
from pathlib import Path
from argparse import ArgumentParser, RawDescriptionHelpFormatter

USAGE = """
Updates example images, shaders, *.qml, *.ui, *.qrc and qmldir files from
a Qt source tree.

Check the diffs produced with care ("prefer" in qmldir, QML module
definitions).
"""

BINARY_SUFFIXES = ["jpg", "png", "svgz", "webp"]
TEXT_SUFFIXES = ["frag", "qrc", "qml", "svg", "ui", "vert"]
SUFFIXES = BINARY_SUFFIXES + TEXT_SUFFIXES


QML_SIMPLE_TUTORIAL_NAMES = ["chapter1-basics", "chapter2-methods",
                             "chapter3-bindings", "chapter4-customPropertyTypes",
                             "chapter5-listproperties", "chapter6-plugins"]
QML_SIMPLE_TUTORIALS = ["qml/tutorials/extending-qml/" + n for n in QML_SIMPLE_TUTORIAL_NAMES]

QML_ADVANCED_TUTORIAL_NAMES = ["advanced1-Base-project", "advanced2-Inheritance-and-coercion",
                               "advanced3-Default-properties", "advanced4-Grouped-properties",
                               "advanced5-Attached-properties", "advanced6-Property-value-source"]
QML_ADVANCED_TUTORIALS = ["qml/tutorials/extending-qml-advanced/" + n
                          for n in QML_ADVANCED_TUTORIAL_NAMES]

EXAMPLE_MAPPING = {
    "qtbase": ["corelib/ipc/sharedmemory", "gui/rhiwindow", "sql/books",
               "widgets/animation/easing", "widgets/rhi/simplerhiwidget"],
    "qtconnectivity": ["bluetooth/heartrate_game", "bluetooth/lowenergyscanner"],
    "qtdeclarative": (QML_SIMPLE_TUTORIALS + QML_ADVANCED_TUTORIALS
                      + ["quick/models/stringlistmodel", "quick/models/objectlistmodel",
                         "quick/window",
                         "quick/rendercontrol/rendercontrol_opengl",
                         "quick/scenegraph/openglunderqml",
                         "quick/scenegraph/scenegraph_customgeometry",
                         "quick/customitems/painteditem",
                         "quickcontrols/filesystemexplorer", "quickcontrols/gallery"]),
    "qtgraphs": ["graphs/2d/hellographs", "graphs/3d/bars", "graphs/3d/widgetgraphgallery"],
    "qtlocation": ["location/mapviewer"],
    "qtmultimedia": ["multimedia/camera"],
    "qtquick3d": ["quick3d/customgeometry", "quick3d/intro", "quick3d/proceduraltexture"],
    "qtserialbus": ["serialbus/can", "serialbus/modbus/modbusclient"],
    "qtserialport": ["serialport/terminal"],
    "qtspeech": ["speech/hello_speak"],
    "qtwebchannel": ["webchannel/standalone"],
    "qtwebengine": ["pdfwidgets/pdfviewer", "webenginequick/nanobrowser",
                    "webenginewidgets/notifications", "webenginewidgets/simplebrowser"],
    "qtwebview": ["webview/minibrowser"],
}


file_count = 0
updated_file_count = 0
new_file_count = 0
warnings_count = 0


def pyside_2_qt_example(e):
    """Fix some example names differing in PySide."""
    if "heartrate" in e:
        return e.replace("heartrate_", "heartrate-")
    if e == "webenginequick/nanobrowser":
        return "webenginequick/quicknanobrowser"
    if e.endswith("scenegraph_customgeometry"):
        return e.replace("scenegraph_customgeometry", "customgeometry")
    if e.endswith("modbusclient"):
        return e.replace("modbusclient", "client")
    return e


def files_differ(p1, p2):
    return (p1.stat().st_size != p2.stat().st_size
            or p1.read_bytes() != p2.read_bytes())


def use_file(path):
    """Exclude C++ docs and Qt Creator builds."""
    path_str = os.fspath(path)
    return "/doc/" not in path_str and "_install_" not in path_str


def example_sources(qt_example):
    """Retrieve all update-able files of a Qt C++ example."""
    result = []
    for suffix in SUFFIXES:
        for file in qt_example.glob(f"**/*.{suffix}"):
            if use_file(file):
                result.append(file)
    for file in qt_example.glob("**/qmldir*"):
        if use_file(file):
            result.append(file)
    return result


def detect_qml_module(pyside_example, sources):
    """Detect the directory of a QML module of a PySide example.
       While in Qt C++, the QML module's .qml files are typically
       located in the example root, PySide has an additional directory
       since it loads the QML files from the file system.
       Read the qmldir file and check whether a module directory exists."""
    qml_dir_file = None
    for source in sources:
        if source.name.startswith("qmldir"):  # "qmldir"/"qmldir.in"
            qml_dir_file = source
            break
    if not qml_dir_file:
        return None
    for line in qml_dir_file.read_text(encoding="utf-8").split("\n"):
        if line.startswith("module "):
            module = line[7:].strip()
            if (pyside_example / module).is_dir():
                return module
            break
    return None


def sync_example(pyside_example, qt_example, dry_run):
    """Update files of a PySide example."""
    global file_count, updated_file_count, new_file_count, warnings_count
    sources = example_sources(qt_example)
    source_count = len(sources)
    if source_count == 0:
        print(f"No sources found in {qt_example}", file=sys.stderr)
        return
    count = 0
    qml_module = detect_qml_module(pyside_example, sources)
    for source in sources:
        rel_source = source.relative_to(qt_example)
        target = pyside_example / rel_source
        if qml_module and not target.is_file():
            target = pyside_example / qml_module / rel_source
        if target.is_file():
            if files_differ(source, target):
                if not dry_run:
                    shutil.copy(source, target)
                count += 1
        else:
            print(f"{qt_example.name}: {rel_source} does not have an equivalent "
                  "PySide file, skipping", file=sys.stderr)
            warnings_count += 1
            new_file_count += 1
    if count > 0:
        print(f"  {qt_example.name:<30}: Updated {count}/{source_count} files(s)")
    else:
        print(f"  {qt_example.name:<30}: Unchanged, {source_count} files(s)")
    file_count += source_count
    updated_file_count += count


def main():
    global warnings_count
    parser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter,
                            description=USAGE)
    parser.add_argument("--dry-run", action="store_true", help="show the files to be updated")
    parser.add_argument('qtsource', nargs=1)
    args = parser.parse_args()
    dry_run = args.dry_run
    qt_source = Path(args.qtsource[0])
    if not qt_source.is_dir():
        raise Exception(f"{qt_source} is not a directory")

    pyside_examples = Path(__file__).parents[1].resolve() / "examples"
    print(qt_source, '->', pyside_examples)

    for qt_module, example_list in EXAMPLE_MAPPING.items():
        for example in example_list:
            pyside_example = pyside_examples / example
            qt_example = (qt_source / qt_module / "examples"
                          / pyside_2_qt_example(example))
            if pyside_example.is_dir() and qt_example.is_dir():
                sync_example(pyside_example, qt_example, dry_run)
            else:
                print(f"Invalid mapping {qt_example} -> {pyside_example}",
                      file=sys.stderr)
                warnings_count += 1
    msg = f"Updated {updated_file_count}/{file_count} file(s)"
    if new_file_count:
        msg += f", {new_file_count} new files(s)"
    if warnings_count:
        msg += f", {warnings_count} warning(s)"
    print(f"\n{msg}.\n")
    return 0


if __name__ == "__main__":
    r = -1
    try:
        r = main()
    except Exception as e:
        print(str(e), file=sys.stderr)
    sys.exit(r)