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