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)
|