diff --git a/util/wasm/preload/preload_qml_imports.py b/util/wasm/preload/preload_qml_imports.py new file mode 100755 index 00000000000..3e936dce223 --- /dev/null +++ b/util/wasm/preload/preload_qml_imports.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import subprocess +import json +import re + +# Paths to shared libraries and qml imports on the Qt installation on the web server. +# "$QTDIR" is replaced by qtloader.js at load time (defaults to "qt"), and makes +# possible to relocate the application build relative to the Qt build on the web server. +qt_lib_path = "$QTDIR/lib" +qt_qml_path = "$QTDIR/qml" + +# Path to QML imports on the in-memory file system provided by Emscripten. This script emits +# preload commands which copies QML imports to this directory. In addition, preload_qt_plugins.py +# creates (and preloads) a qt.conf file which makes Qt load QML plugins from this location. +qt_deploy_qml_path = "/qt/qml" + + +def eprint(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + + +def preload_file(source, destination): + preload_files.append({"source": source, "destination": destination}) + + +def find_dependencies(filepath): + # Very basic dependency finder which scans for ".so" strings in the file + try: + with open(filepath, "rb") as file: + content = file.read() + return [ + m.group(0).decode("utf-8") + for m in re.finditer(rb"[\w\-.]+\.so", content) + ] + except IOError as e: + eprint(f"Error: {e}") + return [] + + +def extract_preload_files_from_imports(imports): + libraries = [] + files = [] + for qml_import in imports: + try: + relative_path = qml_import["relativePath"] + plugin = qml_import["plugin"] + + # plugin .so + so_plugin_source_path = os.path.join( + qt_qml_path, relative_path, "lib" + plugin + ".so" + ) + so_plugin_destination_path = os.path.join( + qt_deploy_qml_path, relative_path, "lib" + plugin + ".so" + ) + + preload_file(so_plugin_source_path, so_plugin_destination_path) + so_plugin_qt_install_path = os.path.join( + qt_wasm_path, "qml", relative_path, "lib" + plugin + ".so" + ) + deps = find_dependencies(so_plugin_qt_install_path) + libraries.extend(deps) + + # qmldir file + qmldir_source_path = os.path.join(qt_qml_path, relative_path, "qmldir") + qmldir_destination_path = os.path.join( + qt_deploy_qml_path, relative_path, "qmldir" + ) + preload_file(qmldir_source_path, qmldir_destination_path) + except Exception as e: + eprint(e) + continue + return files, libraries + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: python make_qt_symlinks.py ") + sys.exit(1) + + qt_host_path = sys.argv[1] + qt_wasm_path = sys.argv[2] + + qml_import_path = os.path.join(qt_wasm_path, "qml") + qmlimportsscanner_path = os.path.join(qt_host_path, "libexec/qmlimportscanner") + + eprint("runing qmlimportsscanner") + result = subprocess.run( + [qmlimportsscanner_path, "-rootPath", ".", "-importPath", qml_import_path], + stdout=subprocess.PIPE, + ) + imports = json.loads(result.stdout) + + preload_files = [] + libraries = [] + files, libraries = extract_preload_files_from_imports(imports) + + # Deploy plugin dependencies, that is, shared libraries used by the plugins. + # Skip some of the obvious libraries which will be + skip_libraries = [ + "libQt6Core.so", + "libQt6Gui.so", + "libQt6Quick.so", + "libQt6Qml.so" "libQt6Network.so", + "libQt6OpenGL.so", + ] + + libraries = set(libraries) - set(skip_libraries) + for library in libraries: + source = os.path.join(qt_lib_path, library) + # Emscripten looks for shared libraries on "/", shared libraries + # most be deployed there instead of at /qt/lib + destination = os.path.join("/", library) + preload_file(source, destination) + + print(json.dumps(preload_files, indent=2)) diff --git a/util/wasm/preload/preload_qt_plugins.py b/util/wasm/preload/preload_qt_plugins.py new file mode 100755 index 00000000000..362d1297320 --- /dev/null +++ b/util/wasm/preload/preload_qt_plugins.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import json + +# Path to plugins on the Qt installation on the web server. "$QTPATH" is replaced by qtloader.js +# at load time (defaults to "qt"), which makes it possible to relocate the application build relative +# to the Qt build on the web server. +qt_plugins_path = "$QTDIR/plugins" + +# Path to plugins on the in-memory file system provided by Emscripten. This script emits +# preload commands which copies plugins to this directory. +qt_deploy_plugins_path = "/qt/plugins" + + +def find_so_files(directory): + so_files = [] + for root, dirs, files in os.walk(directory): + for file in files: + if file.endswith(".so"): + relative_path = os.path.relpath(os.path.join(root, file), directory) + so_files.append(relative_path) + return so_files + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python make_qt_symlinks.py ") + sys.exit(1) + + qt_wasm_path = sys.argv[1] + + # preload all plugins + plugins = find_so_files(os.path.join(qt_wasm_path, "plugins")) + preload = [ + { + "source": os.path.join(qt_plugins_path, plugin), + "destination": os.path.join(qt_deploy_plugins_path, plugin), + } + for plugin in plugins + ] + + # Create and preload qt.conf which will tell Qt to look for plugins + # and QML imports in /qt/plugins and /qt/qml. The qt.conf file is + # written to the current directory. + qtconf = "[Paths]\nPrefix = /qt\n" + with open("qt.conf", "w") as f: + f.write(qtconf) + preload.append({"source": "qt.conf", "destination": "/qt.conf"}) + + print(json.dumps(preload, indent=2))