diff options
Diffstat (limited to 'tools/sync_examples.py')
-rw-r--r-- | tools/sync_examples.py | 199 |
1 files changed, 199 insertions, 0 deletions
diff --git a/tools/sync_examples.py b/tools/sync_examples.py new file mode 100644 index 000000000..dda86998b --- /dev/null +++ b/tools/sync_examples.py @@ -0,0 +1,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 == "qmldir": + 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) |