mirror of https://github.com/qt/qtbase.git
wasm: Introduce wasmdeployqt
Introduce wasmdeployqt which is analogous to other deployment tools that we currently have like "windeployqt" or "androiddeployqt". Running wasmdeployqt makes it much easier to deploy and ship dynamically linked WebAssembly applications. In case of statically linked applications this tool does not provide any extra value as static apps are ready to be shipped by default. This commit also removes older python scripts which were supposed to serve this same goal as well as some cmake machinery arround it. Fixes: QTBUG-134580 Change-Id: Ib5c8ab128f49897f91c510bfec03b0b772e6548a Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io>
This commit is contained in:
parent
49c3cda61c
commit
b6cdc2a6ef
|
@ -437,21 +437,6 @@ elseif(WASM)
|
|||
"${QT_BUILD_DIR}/${INSTALL_LIBEXECDIR}/qt-wasmtestrunner.py" @ONLY)
|
||||
qt_install(PROGRAMS "${QT_BUILD_DIR}/${INSTALL_LIBEXECDIR}/qt-wasmtestrunner.py"
|
||||
DESTINATION "${INSTALL_LIBEXECDIR}")
|
||||
|
||||
if(QT_FEATURE_shared)
|
||||
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/util/wasm/preload/preload_qml_imports.py"
|
||||
"${QT_BUILD_DIR}/${INSTALL_LIBEXECDIR}/preload_qml_imports.py" COPYONLY)
|
||||
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/util/wasm/preload/preload_qt_plugins.py"
|
||||
"${QT_BUILD_DIR}/${INSTALL_LIBEXECDIR}/preload_qt_plugins.py" COPYONLY)
|
||||
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/util/wasm/preload/generate_default_preloads.sh.in"
|
||||
"${QT_BUILD_DIR}/${INSTALL_LIBEXECDIR}/generate_default_preloads.sh.in" COPYONLY)
|
||||
qt_install(PROGRAMS "${QT_BUILD_DIR}/${INSTALL_LIBEXECDIR}/preload_qml_imports.py"
|
||||
DESTINATION "${INSTALL_LIBEXECDIR}")
|
||||
qt_install(PROGRAMS "${QT_BUILD_DIR}/${INSTALL_LIBEXECDIR}/preload_qt_plugins.py"
|
||||
DESTINATION "${INSTALL_LIBEXECDIR}")
|
||||
qt_install(PROGRAMS "${QT_BUILD_DIR}/${INSTALL_LIBEXECDIR}/generate_default_preloads.sh.in"
|
||||
DESTINATION "${INSTALL_LIBEXECDIR}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Install CI support files to libexec.
|
||||
|
|
|
@ -91,15 +91,6 @@ function(_qt_internal_wasm_add_target_helpers target)
|
|||
${_target_directory}/qtloader.js COPYONLY)
|
||||
configure_file("${WASM_BUILD_DIR}/plugins/platforms/qtlogo.svg"
|
||||
${_target_directory}/qtlogo.svg COPYONLY)
|
||||
if(QT_FEATURE_shared)
|
||||
set(TARGET_DIR "${_target_directory}")
|
||||
set(SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
set(QT_HOST_DIR "${QT_HOST_PATH}")
|
||||
set(QT_WASM_DIR "${WASM_BUILD_DIR}")
|
||||
set(QT_INSTALL_DIR "${QT6_INSTALL_PREFIX}")
|
||||
configure_file("${WASM_BUILD_DIR}/libexec/generate_default_preloads.sh.in"
|
||||
"${_target_directory}/generate_default_preloads_for_${target}.sh" @ONLY)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
|
|
@ -190,7 +190,8 @@ async function qtLoad(config)
|
|||
const originalLocateFile = config.locateFile;
|
||||
config.locateFile = filename => {
|
||||
const originalLocatedFilename = originalLocateFile ? originalLocateFile(filename) : filename;
|
||||
if (originalLocatedFilename.startsWith('libQt6'))
|
||||
if (originalLocatedFilename.startsWith(
|
||||
'libQt6')) // wasmqtdeploy rely on this behavior, update both in case of change
|
||||
return `${config.qt.qtdir}/lib/${originalLocatedFilename}`;
|
||||
return originalLocatedFilename;
|
||||
}
|
||||
|
|
|
@ -28,6 +28,10 @@ if(QT_FEATURE_macdeployqt)
|
|||
add_subdirectory(macdeployqt)
|
||||
endif()
|
||||
|
||||
if(QT_FEATURE_wasmdeployqt)
|
||||
add_subdirectory(wasmdeployqt)
|
||||
endif()
|
||||
|
||||
if(QT_FEATURE_windeployqt)
|
||||
add_subdirectory(windeployqt)
|
||||
endif()
|
||||
|
|
|
@ -18,6 +18,12 @@ qt_feature("macdeployqt" PRIVATE
|
|||
AUTODETECT CMAKE_HOST_APPLE
|
||||
CONDITION MACOS AND QT_FEATURE_thread)
|
||||
|
||||
qt_feature("wasmdeployqt" PRIVATE
|
||||
SECTION "Deployment"
|
||||
LABEL "WebAssembly deployment tool"
|
||||
PURPOSE "The WebAssembly deployment tool is designed to automate the process of creating a deployable folder especially for dynamic linking case variant."
|
||||
CONDITION QT_FEATURE_process)
|
||||
|
||||
qt_feature("windeployqt" PRIVATE
|
||||
SECTION "Deployment"
|
||||
LABEL "Windows deployment tool"
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# Copyright (C) 2025 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
#####################################################################
|
||||
## wasmdeployqt Tool:
|
||||
#####################################################################
|
||||
|
||||
qt_get_tool_target_name(target_name wasmdeployqt)
|
||||
qt_internal_add_tool(${target_name}
|
||||
TOOLS_TARGET Core
|
||||
USER_FACING
|
||||
INSTALL_VERSIONED_LINK
|
||||
TARGET_DESCRIPTION "Qt WebAssembly Deployment Tool"
|
||||
SOURCES
|
||||
main.cpp wasmbinary.cpp jsontools.cpp
|
||||
LIBRARIES
|
||||
Qt::CorePrivate
|
||||
)
|
||||
qt_internal_return_unless_building_tools()
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#ifndef COMMON_H
|
||||
#define COMMON_H
|
||||
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
|
||||
struct PreloadEntry
|
||||
{
|
||||
QString source;
|
||||
QString destination;
|
||||
|
||||
bool operator==(const PreloadEntry &other) const
|
||||
{
|
||||
return source == other.source && destination == other.destination;
|
||||
}
|
||||
};
|
||||
|
||||
inline uint qHash(const PreloadEntry &key, uint seed = 0)
|
||||
{
|
||||
return qHash(key.source, seed) ^ qHash(key.destination, seed);
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,101 @@
|
|||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#include <QDir>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "jsontools.h"
|
||||
#include "common.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
|
||||
namespace JsonTools {
|
||||
|
||||
bool savePreloadFile(QSet<PreloadEntry> preload, QString destFile)
|
||||
{
|
||||
|
||||
QJsonArray jsonArray;
|
||||
for (const PreloadEntry &entry : preload) {
|
||||
QJsonObject obj;
|
||||
obj["source"] = entry.source;
|
||||
obj["destination"] = entry.destination;
|
||||
jsonArray.append(obj);
|
||||
}
|
||||
QJsonDocument doc(jsonArray);
|
||||
|
||||
QFile outFile(destFile);
|
||||
if (outFile.exists()) {
|
||||
if (!outFile.remove()) {
|
||||
std::cout << "ERROR: Failed to delete old file: " << outFile.fileName().toStdString()
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!outFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
std::cout << "ERROR: Failed to open file for writing:" << outFile.fileName().toStdString()
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
if (outFile.write(doc.toJson(QJsonDocument::Indented)) == -1) {
|
||||
std::cout << "ERROR: Failed writing into file :" << outFile.fileName().toStdString()
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
if (!outFile.flush()) {
|
||||
std::cout << "ERROR: Failed flushing the file :" << outFile.fileName().toStdString()
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
outFile.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<QSet<PreloadEntry>> getPreloadsFromQmlImportScannerOutput(QString output)
|
||||
{
|
||||
QString qtLibPath = "$QTDIR/lib";
|
||||
QString qtQmlPath = "$QTDIR/qml";
|
||||
QString qtDeployQmlPath = "/qt/qml";
|
||||
QSet<PreloadEntry> res;
|
||||
auto addImport = [&res](const PreloadEntry &entry) {
|
||||
// qDebug() << "adding " << entry.source << "" << entry.destination;
|
||||
res.insert(entry);
|
||||
};
|
||||
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(output.toUtf8(), &parseError);
|
||||
|
||||
if (parseError.error != QJsonParseError::NoError) {
|
||||
std::cout << "ERROR: QmlImport JSON parse error: " << parseError.errorString().toStdString()
|
||||
<< std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!doc.isArray()) {
|
||||
std::cout << "ERROR: QmlImport JSON is not an array." << std::endl;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QJsonArray jsonArray = doc.array();
|
||||
for (const QJsonValue &value : jsonArray) {
|
||||
if (value.isObject()) {
|
||||
QJsonObject obj = value.toObject();
|
||||
auto relativePath = obj["relativePath"].toString();
|
||||
auto plugin = obj["plugin"].toString();
|
||||
if (plugin.isEmpty() || relativePath.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
auto pluginFilename = "lib" + plugin + ".so";
|
||||
addImport(PreloadEntry{
|
||||
QDir::cleanPath(qtQmlPath + "/" + relativePath + "/" + pluginFilename),
|
||||
QDir::cleanPath(qtDeployQmlPath + "/" + relativePath + "/" + pluginFilename) });
|
||||
addImport(PreloadEntry{
|
||||
QDir::cleanPath(qtQmlPath + "/" + relativePath + "/" + "qmldir"),
|
||||
QDir::cleanPath(qtDeployQmlPath + "/" + relativePath + "/" + "qmldir") });
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
}; // namespace JsonTools
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#ifndef JSONTOOLS_H
|
||||
#define JSONTOOLS_H
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QSet>
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace JsonTools {
|
||||
bool savePreloadFile(QSet<PreloadEntry> preload, QString destFile);
|
||||
std::optional<QSet<PreloadEntry>> getPreloadsFromQmlImportScannerOutput(QString output);
|
||||
}; // namespace JsonTools
|
||||
|
||||
#endif
|
|
@ -0,0 +1,417 @@
|
|||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#include "common.h"
|
||||
#include "jsontools.h"
|
||||
#include "wasmbinary.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QDirListing>
|
||||
#include <QDirIterator>
|
||||
#include <QtGlobal>
|
||||
#include <QLibraryInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QStringList>
|
||||
#include <QtCore/QCommandLineOption>
|
||||
#include <QtCore/QCommandLineParser>
|
||||
#include <QtCore/QProcess>
|
||||
#include <QQueue>
|
||||
#include <QMap>
|
||||
#include <QSet>
|
||||
|
||||
#include <optional>
|
||||
#include <iostream>
|
||||
#include <ostream>
|
||||
|
||||
struct Parameters
|
||||
{
|
||||
std::optional<QString> argAppPath;
|
||||
QString appWasmPath;
|
||||
std::optional<QDir> qtHostDir;
|
||||
std::optional<QDir> qtWasmDir;
|
||||
QList<QDir> libPaths;
|
||||
std::optional<QDir> qmlRootPath;
|
||||
|
||||
QSet<QString> loadedQtLibraries;
|
||||
};
|
||||
|
||||
bool parseArguments(Parameters ¶ms)
|
||||
{
|
||||
QCoreApplication::setApplicationName("wasmdeployqt");
|
||||
QCoreApplication::setApplicationVersion("1.0");
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription(
|
||||
QStringLiteral("Qt for WebAssembly deployment tool \n\n"
|
||||
"Example:\n"
|
||||
"wasmdeployqt app.wasm --qml-root-path=repo/myapp "
|
||||
"--qt-wasm-dir=/home/user/qt/shared-qt-wasm/bin"));
|
||||
parser.addHelpOption();
|
||||
|
||||
QStringList args = QCoreApplication::arguments();
|
||||
|
||||
parser.addPositionalArgument("app", "Path to the application.");
|
||||
QCommandLineOption libPathOption("lib-path", "Colon-separated list of library directories.",
|
||||
"paths");
|
||||
parser.addOption(libPathOption);
|
||||
QCommandLineOption qtWasmDirOption("qt-wasm-dir", "Path to the Qt for WebAssembly directory.",
|
||||
"dir");
|
||||
parser.addOption(qtWasmDirOption);
|
||||
QCommandLineOption qtHostDirOption("qt-host-dir", "Path to the Qt host directory.", "dir");
|
||||
parser.addOption(qtHostDirOption);
|
||||
QCommandLineOption qmlRootPathOption("qml-root-path", "Root directory for QML files.", "dir");
|
||||
parser.addOption(qmlRootPathOption);
|
||||
parser.process(args);
|
||||
|
||||
const QStringList positionalArgs = parser.positionalArguments();
|
||||
if (positionalArgs.size() > 1) {
|
||||
std::cout << "ERROR: Expected only one positional argument with path to the app. Received: "
|
||||
<< positionalArgs.join(" ").toStdString() << std::endl;
|
||||
return false;
|
||||
}
|
||||
if (!positionalArgs.isEmpty()) {
|
||||
params.argAppPath = positionalArgs.first();
|
||||
}
|
||||
|
||||
if (parser.isSet(libPathOption)) {
|
||||
QStringList paths = parser.value(libPathOption).split(';', Qt::SkipEmptyParts);
|
||||
for (const QString &path : paths) {
|
||||
QDir dir(path);
|
||||
if (dir.exists()) {
|
||||
params.libPaths.append(dir);
|
||||
} else {
|
||||
std::cout << "ERROR: Directory does not exist: " << path.toStdString() << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parser.isSet(qtWasmDirOption)) {
|
||||
QDir dir(parser.value(qtWasmDirOption));
|
||||
if (dir.cdUp() && dir.exists())
|
||||
params.qtWasmDir = dir;
|
||||
else {
|
||||
std::cout << "ERROR: Directory does not exist: " << dir.absolutePath().toStdString()
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (parser.isSet(qtHostDirOption)) {
|
||||
QDir dir(parser.value(qtHostDirOption));
|
||||
if (dir.cdUp() && dir.exists())
|
||||
params.qtHostDir = dir;
|
||||
else {
|
||||
std::cout << "ERROR: Directory does not exist: " << dir.absolutePath().toStdString()
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (parser.isSet(qmlRootPathOption)) {
|
||||
QDir dir(parser.value(qmlRootPathOption));
|
||||
if (dir.exists()) {
|
||||
params.qmlRootPath = dir;
|
||||
} else {
|
||||
std::cout << "ERROR: Directory specified for qml-root-path does not exist: "
|
||||
<< dir.absolutePath().toStdString() << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<QString> detectAppName()
|
||||
{
|
||||
QDirIterator it(QDir::currentPath(), QStringList() << "*.html" << "*.wasm" << "*.js",
|
||||
QDir::NoFilter);
|
||||
QMap<QString, QSet<QString>> fileGroups;
|
||||
while (it.hasNext()) {
|
||||
QFileInfo fileInfo(it.next());
|
||||
QString baseName = fileInfo.completeBaseName();
|
||||
QString suffix = fileInfo.suffix();
|
||||
fileGroups[baseName].insert(suffix);
|
||||
}
|
||||
for (auto it = fileGroups.constBegin(); it != fileGroups.constEnd(); ++it) {
|
||||
const QSet<QString> &extensions = it.value();
|
||||
if (extensions.contains("html") && extensions.contains("js")
|
||||
&& extensions.contains("wasm")) {
|
||||
return it.key();
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool verifyPaths(Parameters ¶ms)
|
||||
{
|
||||
if (params.argAppPath) {
|
||||
QFileInfo fileInfo(*params.argAppPath);
|
||||
if (!fileInfo.exists()) {
|
||||
std::cout << "ERROR: Cannot find " << params.argAppPath->toStdString() << std::endl;
|
||||
std::cout << "Make sure that the path is valid." << std::endl;
|
||||
return false;
|
||||
}
|
||||
params.appWasmPath = fileInfo.absoluteFilePath();
|
||||
} else {
|
||||
auto appName = detectAppName();
|
||||
if (!appName) {
|
||||
std::cout << "ERROR: Cannot find the application in current directory. Specify the "
|
||||
"path as an argument:"
|
||||
"wasmdeployqt <path-to-app-wasm-binary>"
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
params.appWasmPath = QDir::current().filePath(*appName + ".wasm");
|
||||
std::cout << "Automatically detected " << params.appWasmPath.toStdString() << std::endl;
|
||||
}
|
||||
if (!params.qtWasmDir) {
|
||||
std::cout << "ERROR: Please set path to Qt WebAssembly installation as "
|
||||
"--qt-wasm-dir=<path_to_qt_wasm_bin>"
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
if (!params.qtHostDir) {
|
||||
auto qtHostPath = QLibraryInfo::path(QLibraryInfo::BinariesPath);
|
||||
if (qtHostPath.length() == 0) {
|
||||
std::cout << "ERROR: Cannot read Qt host path or detect it from environment. Please "
|
||||
"pass it explicitly with --qt-host-dir=<path>. "
|
||||
<< std::endl;
|
||||
} else {
|
||||
auto qtHostDir = QDir(qtHostPath);
|
||||
if (!qtHostDir.cdUp()) {
|
||||
std::cout << "ERROR: Invalid Qt host path: "
|
||||
<< qtHostDir.absolutePath().toStdString() << std::endl;
|
||||
return false;
|
||||
}
|
||||
params.qtHostDir = qtHostDir;
|
||||
}
|
||||
}
|
||||
params.libPaths.push_front(params.qtWasmDir->filePath("lib"));
|
||||
params.libPaths.push_front(*params.qtWasmDir);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool copyFile(QString srcPath, QString destPath)
|
||||
{
|
||||
auto file = QFile(destPath);
|
||||
if (file.exists()) {
|
||||
file.remove();
|
||||
}
|
||||
QFileInfo destInfo(destPath);
|
||||
if (!QDir().mkpath(destInfo.path())) {
|
||||
std::cout << "ERROR: Cannot create path " << destInfo.path().toStdString() << std::endl;
|
||||
return false;
|
||||
}
|
||||
if (!QFile::copy(srcPath, destPath)) {
|
||||
|
||||
std::cout << "ERROR: Failed to copy " << srcPath.toStdString() << " to "
|
||||
<< destPath.toStdString() << std::endl;
|
||||
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool copyDirectDependencies(QList<QString> dependencies, const Parameters ¶ms)
|
||||
{
|
||||
for (auto &&depFilename : dependencies) {
|
||||
if (params.loadedQtLibraries.contains(depFilename)) {
|
||||
continue; // dont copy library that has been already copied
|
||||
}
|
||||
|
||||
std::optional<QString> libPath;
|
||||
for (auto &&libDir : params.libPaths) {
|
||||
auto path = libDir.filePath(depFilename);
|
||||
QFileInfo file(path);
|
||||
if (file.exists()) {
|
||||
libPath = path;
|
||||
}
|
||||
}
|
||||
if (!libPath) {
|
||||
std::cout << "ERROR: Cannot find required library " << depFilename.toStdString()
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
if (!copyFile(*libPath, QDir::current().filePath(depFilename)))
|
||||
return false;
|
||||
}
|
||||
std::cout << "INFO: Succesfully copied direct dependencies." << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
QStringList findSoFiles(const QString &directory)
|
||||
{
|
||||
QStringList soFiles;
|
||||
QDir baseDir(directory);
|
||||
if (!baseDir.exists())
|
||||
return soFiles;
|
||||
|
||||
QDirIterator it(directory, QStringList() << "*.so", QDir::Files, QDirIterator::Subdirectories);
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
QString absPath = it.filePath();
|
||||
QString filePath = baseDir.relativeFilePath(absPath);
|
||||
soFiles.append(filePath);
|
||||
}
|
||||
return soFiles;
|
||||
}
|
||||
|
||||
bool copyQtLibs(Parameters ¶ms)
|
||||
{
|
||||
Q_ASSERT(params.qtWasmDir);
|
||||
auto qtLibDir = *params.qtWasmDir;
|
||||
if (!qtLibDir.cd("lib")) {
|
||||
std::cout << "ERROR: Cannot find lib directory in Qt installation." << std::endl;
|
||||
return false;
|
||||
}
|
||||
auto qtLibTargetDir = QDir(QDir(QDir::current().filePath("qt")).filePath("lib"));
|
||||
|
||||
auto soFiles = findSoFiles(qtLibDir.absolutePath());
|
||||
for (auto &&soFilePath : soFiles) {
|
||||
auto relativeFilePath = QDir("lib").filePath(soFilePath);
|
||||
auto srcPath = qtLibDir.absoluteFilePath(soFilePath);
|
||||
auto destPath = qtLibTargetDir.absoluteFilePath(soFilePath);
|
||||
if (!copyFile(srcPath, destPath))
|
||||
return false;
|
||||
params.loadedQtLibraries.insert(QFileInfo(srcPath).fileName());
|
||||
}
|
||||
std::cout << "INFO: Succesfully deployed qt lib shared objects." << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool copyPreloadPlugins(Parameters ¶ms)
|
||||
{
|
||||
Q_ASSERT(params.qtWasmDir);
|
||||
auto qtPluginsDir = *params.qtWasmDir;
|
||||
if (!qtPluginsDir.cd("plugins")) {
|
||||
std::cout << "ERROR: Cannot find plugins directory in Qt installation." << std::endl;
|
||||
return false;
|
||||
}
|
||||
auto qtPluginsTargetDir = QDir(QDir(QDir::current().filePath("qt")).filePath("plugins"));
|
||||
|
||||
// copy files
|
||||
auto soFiles = findSoFiles(qtPluginsDir.absolutePath());
|
||||
for (auto &&soFilePath : soFiles) {
|
||||
auto relativeFilePath = QDir("plugins").filePath(soFilePath);
|
||||
params.loadedQtLibraries.insert(QFileInfo(relativeFilePath).fileName());
|
||||
auto srcPath = qtPluginsDir.absoluteFilePath(soFilePath);
|
||||
auto destPath = qtPluginsTargetDir.absoluteFilePath(soFilePath);
|
||||
if (!copyFile(srcPath, destPath))
|
||||
return false;
|
||||
}
|
||||
|
||||
// qt_plugins.json
|
||||
QSet<PreloadEntry> preload{ { { "qt.conf" }, { "/qt.conf" } } };
|
||||
for (auto &&plugin : soFiles) {
|
||||
PreloadEntry entry;
|
||||
entry.source = QDir("$QTDIR").filePath("plugins") + QDir::separator()
|
||||
+ QDir(qtPluginsDir).relativeFilePath(plugin);
|
||||
entry.destination = "/qt/plugins/" + QDir(qtPluginsTargetDir).relativeFilePath(plugin);
|
||||
preload.insert(entry);
|
||||
}
|
||||
JsonTools::savePreloadFile(preload, QDir::current().filePath("qt_plugins.json"));
|
||||
|
||||
QString qtconfContent = "[Paths]\nPrefix = /qt\n";
|
||||
QString filePath = QDir::current().filePath("qt.conf");
|
||||
|
||||
QFile file(filePath);
|
||||
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
QTextStream out(&file);
|
||||
out << qtconfContent;
|
||||
if (!file.flush()) {
|
||||
std::cout << "ERROR: Failed flushing the file :" << file.fileName().toStdString()
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
file.close();
|
||||
} else {
|
||||
std::cout << "ERROR: Failed to write to qt.conf." << std::endl;
|
||||
return false;
|
||||
}
|
||||
std::cout << "INFO: Succesfully deployed qt plugins." << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool copyPreloadQmlImports(Parameters ¶ms)
|
||||
{
|
||||
Q_ASSERT(params.qtWasmDir);
|
||||
if (!params.qmlRootPath) {
|
||||
std::cout << "WARNING: qml-root-path not specified. Skipping generating preloads for QML "
|
||||
"imports."
|
||||
<< std::endl;
|
||||
std::cout << "WARNING: This may lead to erronous behaviour if applications requires QML "
|
||||
"imports."
|
||||
<< std::endl;
|
||||
QSet<PreloadEntry> preload;
|
||||
JsonTools::savePreloadFile(preload, QDir::current().filePath("qt_qml_imports.json"));
|
||||
return true;
|
||||
}
|
||||
auto qmlImportScannerPath = params.qtHostDir
|
||||
? QDir(params.qtHostDir->filePath("libexec")).filePath("qmlimportscanner")
|
||||
: "qmlimportscanner";
|
||||
QProcess process;
|
||||
auto qmlImportPath = *params.qtWasmDir;
|
||||
qmlImportPath.cd("qml");
|
||||
if (!qmlImportPath.exists()) {
|
||||
std::cout << "ERROR: Cannot find qml import path: "
|
||||
<< qmlImportPath.absolutePath().toStdString() << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
QStringList args{ "-rootPath", params.qmlRootPath->absolutePath(), "-importPath",
|
||||
qmlImportPath.absolutePath() };
|
||||
process.start(qmlImportScannerPath, args);
|
||||
if (!process.waitForFinished()) {
|
||||
std::cout << "ERROR: Failed to execute qmlImportScanner." << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
QString stdoutOutput = process.readAllStandardOutput();
|
||||
auto qmlImports = JsonTools::getPreloadsFromQmlImportScannerOutput(stdoutOutput);
|
||||
if (!qmlImports) {
|
||||
return false;
|
||||
}
|
||||
JsonTools::savePreloadFile(*qmlImports, QDir::current().filePath("qt_qml_imports.json"));
|
||||
for (const PreloadEntry &import : *qmlImports) {
|
||||
auto relativePath = import.source;
|
||||
relativePath.remove("$QTDIR/");
|
||||
|
||||
auto srcPath = params.qtWasmDir->absoluteFilePath(relativePath);
|
||||
auto destPath = QDir(QDir::current().filePath("qt")).absoluteFilePath(relativePath);
|
||||
if (!copyFile(srcPath, destPath))
|
||||
return false;
|
||||
}
|
||||
std::cout << "INFO: Succesfully deployed qml imports." << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
Parameters params;
|
||||
if (!parseArguments(params)) {
|
||||
return -1;
|
||||
}
|
||||
if (!verifyPaths(params)) {
|
||||
return -1;
|
||||
}
|
||||
std::cout << "INFO: Target: " << params.appWasmPath.toStdString() << std::endl;
|
||||
WasmBinary wasmBinary(params.appWasmPath);
|
||||
if (wasmBinary.type == WasmBinary::Type::INVALID) {
|
||||
return -1;
|
||||
} else if (wasmBinary.type == WasmBinary::Type::STATIC) {
|
||||
std::cout << "INFO: This is statically linked WebAssembly binary." << std::endl;
|
||||
std::cout << "INFO: No extra steps required!" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
std::cout << "INFO: Verified as shared module." << std::endl;
|
||||
|
||||
if (!copyQtLibs(params))
|
||||
return -1;
|
||||
if (!copyPreloadPlugins(params))
|
||||
return -1;
|
||||
if (!copyPreloadQmlImports(params))
|
||||
return -1;
|
||||
if (!copyDirectDependencies(wasmBinary.dependencies, params))
|
||||
return -1;
|
||||
|
||||
std::cout << "INFO: Deployment done!" << std::endl;
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#include "wasmbinary.h"
|
||||
|
||||
#include <QFile>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
WasmBinary::WasmBinary(QString filepath)
|
||||
{
|
||||
QFile file(filepath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
std::cout << "ERROR: Cannot open the file " << filepath.toStdString() << std::endl;
|
||||
std::cout << file.errorString().toStdString() << std::endl;
|
||||
type = WasmBinary::Type::INVALID;
|
||||
return;
|
||||
}
|
||||
auto bytes = file.readAll();
|
||||
if (!parsePreambule(bytes)) {
|
||||
type = WasmBinary::Type::INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
bool WasmBinary::parsePreambule(QByteArrayView data)
|
||||
{
|
||||
const auto preambuleSize = 24;
|
||||
if (data.size() < preambuleSize) {
|
||||
std::cout << "ERROR: Preambule of binary shorter than expected!" << std::endl;
|
||||
return false;
|
||||
}
|
||||
uint32_t int32View[6];
|
||||
std::memcpy(int32View, data.data(), sizeof(int32View));
|
||||
if (int32View[0] != 0x6d736100) {
|
||||
std::cout << "ERROR: Magic WASM number not found in binary. Binary corrupted?" << std::endl;
|
||||
return false;
|
||||
}
|
||||
if (data[8] != 0) {
|
||||
type = WasmBinary::Type::STATIC;
|
||||
return true;
|
||||
} else {
|
||||
type = WasmBinary::Type::SHARED;
|
||||
}
|
||||
const auto sectionStart = 9;
|
||||
size_t offset = sectionStart;
|
||||
auto sectionSize = getLeb(data, offset);
|
||||
auto sectionEnd = sectionStart + sectionSize;
|
||||
auto name = getString(data, offset);
|
||||
if (name != "dylink.0") {
|
||||
type = WasmBinary::Type::INVALID;
|
||||
std::cout << "ERROR: dylink.0 was not found in supposedly dynamically linked module"
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto WASM_DYLINK_NEEDED = 0x2;
|
||||
while (offset < sectionEnd) {
|
||||
auto subsectionType = data[offset++];
|
||||
auto subsectionSize = getLeb(data, offset);
|
||||
if (subsectionType == WASM_DYLINK_NEEDED) {
|
||||
auto neededDynlibsCount = getLeb(data, offset);
|
||||
while (neededDynlibsCount--) {
|
||||
dependencies.append(getString(data, offset));
|
||||
}
|
||||
} else {
|
||||
offset += subsectionSize;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t WasmBinary::getLeb(QByteArrayView data, size_t &offset)
|
||||
{
|
||||
auto ret = 0;
|
||||
auto mul = 1;
|
||||
while (true) {
|
||||
auto byte = data[offset++];
|
||||
ret += (byte & 0x7f) * mul;
|
||||
mul *= 0x80;
|
||||
if (!(byte & 0x80))
|
||||
break;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
QString WasmBinary::getString(QByteArrayView data, size_t &offset)
|
||||
{
|
||||
auto length = getLeb(data, offset);
|
||||
offset += length;
|
||||
return QString::fromUtf8(data.sliced(offset - length, length));
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#ifndef WASMBINARY_H
|
||||
#define WASMBINARY_H
|
||||
|
||||
#include <QString>
|
||||
#include <QList>
|
||||
|
||||
class WasmBinary
|
||||
{
|
||||
public:
|
||||
enum class Type { INVALID, STATIC, SHARED };
|
||||
WasmBinary(QString filepath);
|
||||
Type type;
|
||||
QList<QString> dependencies;
|
||||
|
||||
private:
|
||||
bool parsePreambule(QByteArrayView data);
|
||||
size_t getLeb(QByteArrayView data, size_t &offset);
|
||||
QString getString(QByteArrayView data, size_t &offset);
|
||||
};
|
||||
|
||||
#endif
|
|
@ -1,19 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
TARGET_DIR="@TARGET_DIR@"
|
||||
SOURCE_DIR="@SOURCE_DIR@"
|
||||
QT_HOST_DIR="@QT_HOST_DIR@"
|
||||
QT_WASM_DIR="@QT_WASM_DIR@"
|
||||
QT_INSTALL_DIR="@QT_INSTALL_DIR@"
|
||||
|
||||
python3 \
|
||||
"$QT_WASM_DIR/libexec/preload_qt_plugins.py" \
|
||||
"$QT_INSTALL_DIR" \
|
||||
"$TARGET_DIR"
|
||||
|
||||
python3 \
|
||||
"$QT_WASM_DIR/libexec/preload_qml_imports.py" \
|
||||
"$SOURCE_DIR" \
|
||||
"$QT_HOST_DIR" \
|
||||
"$QT_INSTALL_DIR" \
|
||||
"$TARGET_DIR"
|
|
@ -1,98 +0,0 @@
|
|||
#!/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
|
||||
|
||||
# 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 preload_file(source, destination):
|
||||
preload_files.append({"source": source, "destination": destination})
|
||||
|
||||
|
||||
def extract_preload_files_from_imports(imports):
|
||||
libraries = []
|
||||
for qml_import in imports:
|
||||
try:
|
||||
relative_path = qml_import["relativePath"]
|
||||
plugin = qml_import["plugin"]
|
||||
|
||||
# plugin .so
|
||||
plugin_filename = "lib" + plugin + ".so"
|
||||
so_plugin_source_path = os.path.join(
|
||||
qt_qml_path, relative_path, plugin_filename
|
||||
)
|
||||
so_plugin_destination_path = os.path.join(
|
||||
qt_deploy_qml_path, relative_path, plugin_filename
|
||||
)
|
||||
|
||||
preload_file(so_plugin_source_path, so_plugin_destination_path)
|
||||
so_plugin_qt_install_path = os.path.join(
|
||||
qt_wasm_path, "qml", relative_path, plugin_filename
|
||||
)
|
||||
|
||||
# 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:
|
||||
continue
|
||||
return libraries
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 5:
|
||||
print("Usage: python preload_qml_imports.py <qml-source-path> <qt-host-path> <qt-wasm-path> <output-dir>")
|
||||
sys.exit(1)
|
||||
|
||||
qml_source_path = sys.argv[1]
|
||||
qt_host_path = sys.argv[2]
|
||||
qt_wasm_path = sys.argv[3]
|
||||
output_dir = sys.argv[4]
|
||||
|
||||
qml_import_path = os.path.join(qt_wasm_path, "qml")
|
||||
qmlimportsscanner_path = os.path.join(qt_host_path, "libexec/qmlimportscanner")
|
||||
|
||||
command = [qmlimportsscanner_path, "-rootPath", qml_source_path, "-importPath", qml_import_path]
|
||||
result = subprocess.run(command, stdout=subprocess.PIPE)
|
||||
imports = json.loads(result.stdout)
|
||||
|
||||
preload_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)
|
||||
|
||||
with open(f"{output_dir}/qt_qml_imports.json", "w") as f:
|
||||
f.write(json.dumps(preload_files, indent=2))
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
#!/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) != 3:
|
||||
print("Usage: python preload_qt_plugins.py <qt-wasm-path> <output-dir>")
|
||||
sys.exit(1)
|
||||
|
||||
qt_wasm_path = sys.argv[1]
|
||||
output_dir = sys.argv[2]
|
||||
|
||||
# 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(f"{output_dir}/qt.conf", "w") as f:
|
||||
f.write(qtconf)
|
||||
preload.append({"source": "qt.conf", "destination": "/qt.conf"})
|
||||
|
||||
with open(f"{output_dir}/qt_plugins.json", "w") as f:
|
||||
f.write(json.dumps(preload, indent=2))
|
||||
|
Loading…
Reference in New Issue