diff --git a/README.md b/README.md index 4b406aa..ab3af1a 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Options: -executable= : Let the given executable use the deployed libraries too -qmldir= : Scan for QML imports in the given path -always-overwrite : Copy files even if the target file exists + -no-translations : Skip deployment of translations linuxdeployqt takes an application as input and makes it self-contained by copying in the Qt libraries and plugins that diff --git a/linuxdeployqt/main.cpp b/linuxdeployqt/main.cpp index 1c1ab2b..0bde0c2 100644 --- a/linuxdeployqt/main.cpp +++ b/linuxdeployqt/main.cpp @@ -55,6 +55,7 @@ int main(int argc, char **argv) qDebug() << " -executable= : Let the given executable use the deployed libraries too"; qDebug() << " -qmldir= : Scan for QML imports in the given path"; qDebug() << " -always-overwrite : Copy files even if the target file exists"; + qDebug() << " -no-translations : Skip deployment of translations."; qDebug() << ""; qDebug() << "linuxdeployqt takes an application as input and makes it"; qDebug() << "self-contained by copying in the Qt libraries and plugins that"; @@ -168,6 +169,7 @@ int main(int argc, char **argv) extern QStringList librarySearchPath; QStringList additionalExecutables; bool qmldirArgumentUsed = false; + bool skipTranslations = false; QStringList qmlDirs; /* FHS-like mode is for an application that has been installed to a $PREFIX which is otherwise empty, e.g., /path/to/usr. @@ -358,6 +360,9 @@ int main(int argc, char **argv) } else if (argument == QByteArray("-always-overwrite")) { LogDebug() << "Argument found:" << argument; alwaysOwerwriteEnabled = true; + } else if (argument == QByteArray("-no-translations")) { + LogDebug() << "Argument found:" << argument; + skipTranslations = true; } else if (argument.startsWith("-")) { LogError() << "Unknown argument" << argument << "\n"; return 1; @@ -391,6 +396,9 @@ int main(int argc, char **argv) deploymentInfo.deployedLibraries = deploymentInfo.deployedLibraries.toSet().toList(); } + deploymentInfo.usedModulesMask = 0; + findUsedModules(deploymentInfo); + if (plugins && !deploymentInfo.qtPath.isEmpty()) { if (deploymentInfo.pluginPath.isEmpty()) deploymentInfo.pluginPath = QDir::cleanPath(deploymentInfo.qtPath + "/../plugins"); @@ -401,6 +409,10 @@ int main(int argc, char **argv) if (runStripEnabled) stripAppBinary(appDirPath); + if (!skipTranslations) { + deployTranslations(appDirPath, deploymentInfo.usedModulesMask); + } + if (appimage) { int result = createAppImage(appDirPath); LogDebug() << "result:" << result; diff --git a/shared/shared.cpp b/shared/shared.cpp index 2291d71..20f127a 100644 --- a/shared/shared.cpp +++ b/shared/shared.cpp @@ -62,6 +62,122 @@ using std::endl; QMap qtToBeBundledInfo; +enum QtModule +#if defined(Q_COMPILER_CLASS_ENUM) || defined(Q_CC_MSVC) + : quint64 +#endif +{ + QtBluetoothModule = 0x0000000000000001, + QtCLuceneModule = 0x0000000000000002, + QtConcurrentModule = 0x0000000000000004, + QtCoreModule = 0x0000000000000008, + QtDeclarativeModule = 0x0000000000000010, + QtDesignerComponents = 0x0000000000000020, + QtDesignerModule = 0x0000000000000040, + QtGuiModule = 0x0000000000000080, + QtCluceneModule = 0x0000000000000100, + QtHelpModule = 0x0000000000000200, + QtMultimediaModule = 0x0000000000000400, + QtMultimediaWidgetsModule = 0x0000000000000800, + QtMultimediaQuickModule = 0x0000000000001000, + QtNetworkModule = 0x0000000000002000, + QtNfcModule = 0x0000000000004000, + QtOpenGLModule = 0x0000000000008000, + QtPositioningModule = 0x0000000000010000, + QtPrintSupportModule = 0x0000000000020000, + QtQmlModule = 0x0000000000040000, + QtQuickModule = 0x0000000000080000, + QtQuickParticlesModule = 0x0000000000100000, + QtScriptModule = 0x0000000000200000, + QtScriptToolsModule = 0x0000000000400000, + QtSensorsModule = 0x0000000000800000, + QtSerialPortModule = 0x0000000001000000, + QtSqlModule = 0x0000000002000000, + QtSvgModule = 0x0000000004000000, + QtTestModule = 0x0000000008000000, + QtWidgetsModule = 0x0000000010000000, + QtWinExtrasModule = 0x0000000020000000, + QtXmlModule = 0x0000000040000000, + QtXmlPatternsModule = 0x0000000080000000, + QtWebKitModule = 0x0000000100000000, + QtWebKitWidgetsModule = 0x0000000200000000, + QtQuickWidgetsModule = 0x0000000400000000, + QtWebSocketsModule = 0x0000000800000000, + QtEnginioModule = 0x0000001000000000, + QtWebEngineCoreModule = 0x0000002000000000, + QtWebEngineModule = 0x0000004000000000, + QtWebEngineWidgetsModule = 0x0000008000000000, + QtQmlToolingModule = 0x0000010000000000, + Qt3DCoreModule = 0x0000020000000000, + Qt3DRendererModule = 0x0000040000000000, + Qt3DQuickModule = 0x0000080000000000, + Qt3DQuickRendererModule = 0x0000100000000000, + Qt3DInputModule = 0x0000200000000000, + QtLocationModule = 0x0000400000000000, + QtWebChannelModule = 0x0000800000000000, + QtTextToSpeechModule = 0x0001000000000000, + QtSerialBusModule = 0x0002000000000000 +}; + +struct QtModuleEntry { + quint64 module; + const char *option; + const char *libraryName; + const char *translation; +}; + +static QtModuleEntry qtModuleEntries[] = { + { QtBluetoothModule, "bluetooth", "Qt5Bluetooth", 0 }, + { QtCLuceneModule, "clucene", "Qt5CLucene", "qt_help" }, + { QtConcurrentModule, "concurrent", "Qt5Concurrent", "qtbase" }, + { QtCoreModule, "core", "Qt5Core", "qtbase" }, + { QtDeclarativeModule, "declarative", "Qt5Declarative", "qtquick1" }, + { QtDesignerModule, "designer", "Qt5Designer", 0 }, + { QtDesignerComponents, "designercomponents", "Qt5DesignerComponents", 0 }, + { QtEnginioModule, "enginio", "Enginio", 0 }, + { QtGuiModule, "gui", "Qt5Gui", "qtbase" }, + { QtHelpModule, "qthelp", "Qt5Help", "qt_help" }, + { QtMultimediaModule, "multimedia", "Qt5Multimedia", "qtmultimedia" }, + { QtMultimediaWidgetsModule, "multimediawidgets", "Qt5MultimediaWidgets", "qtmultimedia" }, + { QtMultimediaQuickModule, "multimediaquick", "Qt5MultimediaQuick_p", "qtmultimedia" }, + { QtNetworkModule, "network", "Qt5Network", "qtbase" }, + { QtNfcModule, "nfc", "Qt5Nfc", 0 }, + { QtOpenGLModule, "opengl", "Qt5OpenGL", 0 }, + { QtPositioningModule, "positioning", "Qt5Positioning", 0 }, + { QtPrintSupportModule, "printsupport", "Qt5PrintSupport", 0 }, + { QtQmlModule, "qml", "Qt5Qml", "qtdeclarative" }, + { QtQmlToolingModule, "qmltooling", "qmltooling", 0 }, + { QtQuickModule, "quick", "Qt5Quick", "qtdeclarative" }, + { QtQuickParticlesModule, "quickparticles", "Qt5QuickParticles", 0 }, + { QtQuickWidgetsModule, "quickwidgets", "Qt5QuickWidgets", 0 }, + { QtScriptModule, "script", "Qt5Script", "qtscript" }, + { QtScriptToolsModule, "scripttools", "Qt5ScriptTools", "qtscript" }, + { QtSensorsModule, "sensors", "Qt5Sensors", 0 }, + { QtSerialPortModule, "serialport", "Qt5SerialPort", "qtserialport" }, + { QtSqlModule, "sql", "Qt5Sql", "qtbase" }, + { QtSvgModule, "svg", "Qt5Svg", 0 }, + { QtTestModule, "test", "Qt5Test", "qtbase" }, + { QtWebKitModule, "webkit", "Qt5WebKit", 0 }, + { QtWebKitWidgetsModule, "webkitwidgets", "Qt5WebKitWidgets", 0 }, + { QtWebSocketsModule, "websockets", "Qt5WebSockets", "qtwebsockets" }, + { QtWidgetsModule, "widgets", "Qt5Widgets", "qtbase" }, + { QtWinExtrasModule, "winextras", "Qt5WinExtras", 0 }, + { QtXmlModule, "xml", "Qt5Xml", "qtbase" }, + { QtXmlPatternsModule, "xmlpatterns", "Qt5XmlPatterns", "qtxmlpatterns" }, + { QtWebEngineCoreModule, "webenginecore", "Qt5WebEngineCore", 0 }, + { QtWebEngineModule, "webengine", "Qt5WebEngine", "qtwebengine" }, + { QtWebEngineWidgetsModule, "webenginewidgets", "Qt5WebEngineWidgets", 0 }, + { Qt3DCoreModule, "3dcore", "Qt53DCore", 0 }, + { Qt3DRendererModule, "3drenderer", "Qt53DRenderer", 0 }, + { Qt3DQuickModule, "3dquick", "Qt53DQuick", 0 }, + { Qt3DQuickRendererModule, "3dquickrenderer", "Qt53DQuickRenderer", 0 }, + { Qt3DInputModule, "3dinput", "Qt53DInput", 0 }, + { QtLocationModule, "geoservices", "Qt5Location", 0 }, + { QtWebChannelModule, "webchannel", "Qt5WebChannel", 0 }, + { QtTextToSpeechModule, "texttospeech", "Qt5TextToSpeech", 0 }, + { QtSerialBusModule, "serialbus", "Qt5SerialBus", 0 } +}; + bool operator==(const LibraryInfo &a, const LibraryInfo &b) { return ((a.libraryPath == b.libraryPath) && (a.binaryPath == b.binaryPath)); @@ -1472,3 +1588,136 @@ int createAppImage(const QString &appDirPath) LogNormal() << "WEXITSTATUS(ret)" << WEXITSTATUS(ret); return WEXITSTATUS(ret); } + +void findUsedModules(DeploymentInfo &info) +{ + LogDebug() << "Creating mask of used modules"; + + const QStringList &libraries = info.deployedLibraries; + + const size_t qtModulesCount = sizeof(qtModuleEntries)/sizeof(QtModuleEntry); + for (size_t i = 0; i < qtModulesCount; ++i) { + QtModuleEntry &entry = qtModuleEntries[i]; + const QString name = QLatin1String(qtModuleEntries[i].libraryName); + + bool found = false; + foreach (const QString &library, libraries) { + if (library.contains(name, Qt::CaseInsensitive)) { + LogDebug() << "Found dependency:" << name; + found = true; + break; + } + } + + if (found) { + info.usedModulesMask |= entry.module; + } + } +} + +void deployTranslations(const QString &appDirPath, quint64 usedQtModules) +{ + LogDebug() << "Deploying translations..."; + QString qtTranslationsPath = qtToBeBundledInfo.value("QT_INSTALL_TRANSLATIONS"); + if (qtTranslationsPath.isEmpty() || !QFile::exists(qtTranslationsPath)) { + LogError() << "Qt translations path could not be determined"; + return; + } + + QString translationsDirPath = appDirPath + QStringLiteral("/translations"); + LogDebug() << "Using" << translationsDirPath << "as translations directory for App"; + LogDebug() << "Using" << qtTranslationsPath << " to search for Qt translations"; + + QFileInfo fi(translationsDirPath); + if (!fi.isDir()) { + if (!QDir().mkpath(translationsDirPath)) { + LogError() << "Failed to create translations directory"; + } + } else { + LogDebug() << "Translations directory already exists"; + } + + if (!deployTranslations(qtTranslationsPath, translationsDirPath, usedQtModules)) { + LogError() << "Failed to copy translations"; + } +} + +QStringList translationNameFilters(quint64 modules, const QString &prefix) +{ + QStringList result; + const size_t qtModulesCount = sizeof(qtModuleEntries)/sizeof(QtModuleEntry); + for (size_t i = 0; i < qtModulesCount; ++i) { + if ((qtModuleEntries[i].module & modules) && qtModuleEntries[i].translation) { + const QString name = QLatin1String(qtModuleEntries[i].translation) + + QLatin1Char('_') + prefix + QStringLiteral(".qm"); + if (!result.contains(name)) + result.push_back(name); + } + } + LogDebug() << "Translation name filters:" << result; + return result; +} + +bool deployTranslations(const QString &sourcePath, const QString &target, quint64 usedQtModules) +{ + LogDebug() << "Translations target is" << target; + + // Find available languages prefixes by checking on qtbase. + QStringList prefixes; + QDir sourceDir(sourcePath); + const QStringList qmFilter = QStringList(QStringLiteral("qtbase_*.qm")); + foreach (QString qmFile, sourceDir.entryList(qmFilter)) { + qmFile.chop(3); + qmFile.remove(0, 7); + prefixes.push_back(qmFile); + } + if (prefixes.isEmpty()) { + LogError() << "Could not find any translations in " + << sourcePath << " (developer build?)"; + return true; + } + // Run lconvert to concatenate all files into a single named "qt_.qm" in the application folder + // Use QT_INSTALL_TRANSLATIONS as working directory to keep the command line short. + const QString absoluteTarget = QFileInfo(target).absoluteFilePath(); + + QString lconvertPath = QDir::cleanPath(qtToBeBundledInfo.value("QT_INSTALL_BINS")) + "/lconvert"; + LogDebug() << "Looking for lconvert at" << lconvertPath; + + // Fallback: Look relative to the linuxdeployqt binary + if (!QFile(lconvertPath).exists()){ + lconvertPath = QCoreApplication::applicationDirPath() + "/lconvert"; + LogDebug() << "Fallback, looking for lconvert at" << lconvertPath; + } + + // Verify that we found a lconvert binary + if (!QFile(lconvertPath).exists()) { + LogError() << "lconvert not found at" << lconvertPath; + return false; + } + + LogNormal() << "Found lconvert at" << lconvertPath; + + QStringList arguments; + foreach (const QString &prefix, prefixes) { + arguments.clear(); + const QString targetFile = QStringLiteral("qt_") + prefix + QStringLiteral(".qm"); + arguments.append(QStringLiteral("-o")); + const QString currentTargetFile = absoluteTarget + QLatin1Char('/') + targetFile; + arguments.append(currentTargetFile); + + foreach (const QFileInfo &qmFileInfo, sourceDir.entryInfoList(translationNameFilters(usedQtModules, prefix))) + arguments.append(qmFileInfo.absoluteFilePath()); + + LogNormal() << "Creating " << currentTargetFile << "..."; + LogDebug() << "lconvert arguments:" << arguments; + + QProcess lconvert; + lconvert.start(lconvertPath, arguments); + lconvert.waitForFinished(); + + if (lconvert.exitStatus() != QProcess::NormalExit) { + LogError() << "Fail in lconvert on file" << currentTargetFile; + } + } // for prefixes. + return true; +} diff --git a/shared/shared.h b/shared/shared.h index 273b837..f50f4c4 100644 --- a/shared/shared.h +++ b/shared/shared.h @@ -96,6 +96,7 @@ public: QString qtPath; QString pluginPath; QStringList deployedLibraries; + quint64 usedModulesMask; QSet rpathsUsed; bool useLoaderPath; bool isLibrary; @@ -128,5 +129,8 @@ QStringList findAppLibraries(const QString &appDirPath); bool patchQtCore(const QString &path, const QString &variable, const QString &value); int createAppImage(const QString &appBundlePath); bool checkAppImagePrerequisites(const QString &appBundlePath); +void findUsedModules(DeploymentInfo &info); +void deployTranslations(const QString &appDirPath, quint64 usedQtModules); +bool deployTranslations(const QString &sourcePath, const QString &target, quint64 usedQtModules); #endif