diff options
-rw-r--r-- | doc/installerfw-using.qdoc | 14 | ||||
-rw-r--r-- | doc/installerfw.qdoc | 7 | ||||
-rw-r--r-- | doc/scripting-api/packagemanagercore.qdoc | 28 | ||||
-rw-r--r-- | src/libs/ifwtools/binarycreator.cpp | 25 | ||||
-rw-r--r-- | src/libs/ifwtools/repositorygen.cpp | 11 | ||||
-rw-r--r-- | src/libs/installer/commandlineparser.cpp | 7 | ||||
-rw-r--r-- | src/libs/installer/constants.h | 9 | ||||
-rw-r--r-- | src/libs/installer/fileutils.cpp | 95 | ||||
-rw-r--r-- | src/libs/installer/fileutils.h | 5 | ||||
-rw-r--r-- | src/libs/installer/packagemanagercore.cpp | 185 | ||||
-rw-r--r-- | src/libs/installer/packagemanagercore.h | 14 | ||||
-rw-r--r-- | src/libs/installer/packagemanagercore_p.cpp | 212 | ||||
-rw-r--r-- | src/libs/installer/packagemanagercore_p.h | 10 | ||||
-rw-r--r-- | src/libs/installer/packagemanagercoredata.cpp | 5 | ||||
-rw-r--r-- | src/libs/installer/packagemanagercoredata.h | 5 | ||||
-rw-r--r-- | src/libs/installer/settings.cpp | 2 | ||||
-rw-r--r-- | src/sdk/commandlineinterface.cpp | 31 | ||||
-rw-r--r-- | src/sdk/commandlineinterface.h | 1 | ||||
-rw-r--r-- | src/sdk/main.cpp | 3 | ||||
-rw-r--r-- | tests/auto/installer/cliinterface/tst_cliinterface.cpp | 8 |
20 files changed, 633 insertions, 44 deletions
diff --git a/doc/installerfw-using.qdoc b/doc/installerfw-using.qdoc index cb0e4d3ad..d4f4b6688 100644 --- a/doc/installerfw-using.qdoc +++ b/doc/installerfw-using.qdoc @@ -470,6 +470,20 @@ maintenancetool.exe purge \endcode + \section1 Creating Custom Offline Installers + + To create a personal offline installer containing a selected set of components from online repositories, + use the \c create-offline command. This is a useful feature for scenarios where the same installation + content will be deployed multiple times or on several machines, as it saves time spent on downloading + component archives and enables easily reproducible installations. The \c --offline-installer-name + option can be used to set a desired name for the generated offline installer. + + Note that you can create offline installers only from online installers with valid repositories enabled. + + \code + installer.exe --root "C:\TargetFolder" --offline-installer-name "MyInstaller" create-offline componentA componentB + \endcode + \section1 Unattended Usage By default, the generated installers may ask for additional information during installation, diff --git a/doc/installerfw.qdoc b/doc/installerfw.qdoc index ebc1e2044..ef7479a33 100644 --- a/doc/installerfw.qdoc +++ b/doc/installerfw.qdoc @@ -223,6 +223,10 @@ \li -t, --root <directory> \li [CLI] Set the installation root directory. \row + \li --oi, --offline-installer-name <filename> + \li [CLI] Set custom filename for the generated offline installer. Without this + the original filename is used with an added "_offline-yyyy-MM-dd" suffix. + \row \li -p, --platform <plugin> \li Use the specified platform plugin. \row @@ -316,6 +320,9 @@ \li se, search <regexp> \li Search available packages. If no search pattern is given, show all available packages. \row + \li co, create-offline <pkg ...> + \li Create offline installer from selected packages. + \row \li pr, purge \li Uninstall all packages and remove the program directory. \endtable diff --git a/doc/scripting-api/packagemanagercore.qdoc b/doc/scripting-api/packagemanagercore.qdoc index 74b430d2f..8a9d297a0 100644 --- a/doc/scripting-api/packagemanagercore.qdoc +++ b/doc/scripting-api/packagemanagercore.qdoc @@ -211,6 +211,22 @@ */ /*! + \qmlsignal installer::offlineGenerationStarted() + + Triggered when offline installer generation has started. + + \sa offlineGenerationFinished() +*/ + +/*! + \qmlsignal installer::offlineGenerationFinished + + Triggered when offline installer generation has finished. + + \sa offlineGenerationStarted() +*/ + +/*! \qmlsignal installer::titleMessageChanged(string title) Emitted when the text of the installer status (on the PerformInstallation page) changes to @@ -832,6 +848,12 @@ */ /*! + \qmlmethod boolean installer::isOfflineGenerator() + + Returns \c true if the application is executed in offline generation mode. +*/ + +/*! \qmlmethod boolean installer::isUserSetBinaryMarker() Returns \c true if the magic binary marker has been set by user. @@ -868,6 +890,12 @@ */ /*! + \qmlmethod boolean installer::runOfflineGenerator() + + Runs the offline generator. Returns \c true on success, \c false otherwise. +*/ + +/*! \qmlmethod void installer::languageChanged() Calls languangeChanged on all components. diff --git a/src/libs/ifwtools/binarycreator.cpp b/src/libs/ifwtools/binarycreator.cpp index 8fcc14ca3..deea01ed7 100644 --- a/src/libs/ifwtools/binarycreator.cpp +++ b/src/libs/ifwtools/binarycreator.cpp @@ -37,6 +37,7 @@ #include "repository.h" #include "settings.h" #include "utils.h" +#include "fileutils.h" #include <QDateTime> #include <QDirIterator> @@ -433,8 +434,13 @@ static int assemble(Input input, const QInstaller::Settings &settings, const QSt return EXIT_FAILURE; } +#ifdef Q_OS_MACOS + // installer executable + chmod755(input.outputPath); +#endif #ifndef Q_OS_WIN - chmod755(out.fileName()); + // installer executable on linux, installer.dat on macOS + chmod755(targetName); #endif QFile::remove(tempFile); @@ -580,6 +586,9 @@ void QInstallerTools::copyConfigData(const QString &configFile, const QString &t const QString targetConfigFile = targetDir + QLatin1String("/config.xml"); QInstallerTools::copyWithException(sourceConfigFile, targetConfigFile, QLatin1String("configuration")); + // Permissions might be set to bogus values + QInstaller::setDefaultFilePermissions(targetConfigFile, DefaultFilePermissions::NonExecutable); + QFile configXml(targetConfigFile); QInstaller::openForRead(&configXml); @@ -699,13 +708,19 @@ int QInstallerTools::createBinary(BinaryCreatorArgs args, QString &argumentError "contain any components apart from the root component."); return EXIT_FAILURE; } -#ifdef Q_OS_WIN - if (!args.templateBinary.endsWith(suffix)) - args.templateBinary = args.templateBinary + suffix; -#endif if (!QFileInfo(args.templateBinary).exists()) { +#ifdef Q_OS_WIN + if (!args.templateBinary.endsWith(suffix)) + args.templateBinary = args.templateBinary + suffix; + // Try again with added executable suffix + if (!QFileInfo(args.templateBinary).exists()) { + argumentError = QString::fromLatin1("Error: Template base binary not found at the specified location."); + return EXIT_FAILURE; + } +#else argumentError = QString::fromLatin1("Error: Template not found at the specified location."); return EXIT_FAILURE; +#endif } const QFileInfo fi(args.configFile); if (!fi.exists()) { diff --git a/src/libs/ifwtools/repositorygen.cpp b/src/libs/ifwtools/repositorygen.cpp index e30234f52..fbcf7b9f3 100644 --- a/src/libs/ifwtools/repositorygen.cpp +++ b/src/libs/ifwtools/repositorygen.cpp @@ -609,12 +609,19 @@ PackageInfoVector QInstallerTools::createListOfRepositoryPackages(const QStringL if (!hasUnifiedMetaFile) { const QDomElement sha1 = el.firstChildElement(QInstaller::scSHA1); if (!sha1.isNull()) { + // 1. First, try with normal repository structure QString metaFile = QString::fromLatin1("%1/%3%2").arg(info.directory, QString::fromLatin1("meta.7z"), info.version); if (!QFileInfo(metaFile).exists()) { - throw QInstaller::Error(QString::fromLatin1("Could not find meta archive for component " - "%1 %2 in repository %3.").arg(info.name, info.version, it->filePath())); + // 2. If that does not work, check for fetched temporary repository structure + metaFile = QString::fromLatin1("%1/%2-%3-%4").arg(it->filePath(), + info.name, info.version, QString::fromLatin1("meta.7z")); + + if (!QFileInfo(metaFile).exists()) { + throw QInstaller::Error(QString::fromLatin1("Could not find meta archive for component " + "%1 %2 in repository %3.").arg(info.name, info.version, it->filePath())); + } } info.metaFile = metaFile; } diff --git a/src/libs/installer/commandlineparser.cpp b/src/libs/installer/commandlineparser.cpp index 3ffbdb9b5..9812023cb 100644 --- a/src/libs/installer/commandlineparser.cpp +++ b/src/libs/installer/commandlineparser.cpp @@ -59,6 +59,8 @@ CommandLineParser::CommandLineParser() .arg(CommandLineOptions::scListShort, CommandLineOptions::scListLong) + indent + QString::fromLatin1("%1, %2 - search available packages - <regexp>\n") .arg(CommandLineOptions::scSearchShort, CommandLineOptions::scSearchLong) + + indent + QString::fromLatin1("%1, %2 - create offline installer from selected packages - <pkg ...>\n") + .arg(CommandLineOptions::scCreateOfflineShort, CommandLineOptions::scCreateOfflineLong) + indent + QString::fromLatin1("%1, %2 - uninstall all packages and remove entire program directory") .arg(CommandLineOptions::scPurgeShort, CommandLineOptions::scPurgeLong); @@ -129,6 +131,11 @@ CommandLineParser::CommandLineParser() QLatin1String("[CLI] Set installation root directory."), QLatin1String("directory"))); m_parser.addOption(QCommandLineOption(QStringList() + << CommandLineOptions::scOfflineInstallerNameShort << CommandLineOptions::scOfflineInstallerNameLong, + QLatin1String("[CLI] Set custom filename for the generated offline installer. Without this " + "the original filename is used with an added \"_offline-yyyy-MM-dd\" suffix."), + QLatin1String("filename"))); + m_parser.addOption(QCommandLineOption(QStringList() << CommandLineOptions::scPlatformShort << CommandLineOptions::scPlatformLong, QLatin1String("Use the specified platform plugin."), QLatin1String("plugin"))); diff --git a/src/libs/installer/constants.h b/src/libs/installer/constants.h index 7ecdf2b89..304efb772 100644 --- a/src/libs/installer/constants.h +++ b/src/libs/installer/constants.h @@ -64,6 +64,7 @@ static const QLatin1String scInstalledVersion("InstalledVersion"); static const QLatin1String scUncompressedSize("UncompressedSize"); static const QLatin1String scUncompressedSizeSum("UncompressedSizeSum"); static const QLatin1String scRequiresAdminRights("RequiresAdminRights"); +static const QLatin1String scOfflineBinaryName("OfflineBinaryName"); static const QLatin1String scSHA1("SHA1"); // constants used throughout the components class @@ -83,6 +84,8 @@ static const QLatin1String scTargetConfigurationFile("TargetConfigurationFile"); static const QLatin1String scAllowNonAsciiCharacters("AllowNonAsciiCharacters"); static const QLatin1String scDisableAuthorizationFallback("DisableAuthorizationFallback"); static const QLatin1String scDisableCommandLineInterface("DisableCommandLineInterface"); +static const QLatin1String scRemoteRepositories("RemoteRepositories"); +static const QLatin1String scRepositoryCategories("RepositoryCategories"); static const QLatin1String scRepositorySettingsPageVisible("RepositorySettingsPageVisible"); static const QLatin1String scAllowSpaceInPath("AllowSpaceInPath"); static const QLatin1String scWizardStyle("WizardStyle"); @@ -135,6 +138,8 @@ static const QLatin1String scListShort("li"); static const QLatin1String scListLong("list"); static const QLatin1String scSearchShort("se"); static const QLatin1String scSearchLong("search"); +static const QLatin1String scCreateOfflineShort("co"); +static const QLatin1String scCreateOfflineLong("create-offline"); static const QLatin1String scPurgeShort("pr"); static const QLatin1String scPurgeLong("purge"); @@ -178,6 +183,8 @@ static const QLatin1String scConfirmCommandLong("confirm-command"); // Misc installation options static const QLatin1String scRootShort("t"); static const QLatin1String scRootLong("root"); +static const QLatin1String scOfflineInstallerNameShort("oi"); +static const QLatin1String scOfflineInstallerNameLong("offline-installer-name"); static const QLatin1String scPlatformShort("p"); static const QLatin1String scPlatformLong("platform"); static const QLatin1String scNoForceInstallationShort("nf"); @@ -221,6 +228,8 @@ static const QStringList scCommandLineInterfaceOptions = { scListLong, scSearchShort, scSearchLong, + scCreateOfflineShort, + scCreateOfflineLong, scPurgeShort, scPurgeLong }; diff --git a/src/libs/installer/fileutils.cpp b/src/libs/installer/fileutils.cpp index 273901267..3c9f2ad1c 100644 --- a/src/libs/installer/fileutils.cpp +++ b/src/libs/installer/fileutils.cpp @@ -29,6 +29,7 @@ #include "globals.h" #include "constants.h" +#include "fileio.h" #include <errors.h> #include <QtCore/QDateTime> @@ -724,3 +725,97 @@ void QInstaller::replaceHighDpiImage(QString &imagePath) imagePath = highdpiPixmap; } } + +/*! + Copies an internal configuration file from \a source to \a target. The XML elements, + and their children, specified by \a elementsToRemoveTags will be removed from the \a target + file. All relative filenames referenced in the \a source configuration file will be + also copied to the location of the \a target file. + + Throws \c QInstaller::Error in case of failure. +*/ +void QInstaller::trimmedCopyConfigData(const QString &source, const QString &target, const QStringList &elementsToRemoveTags) +{ + qCDebug(QInstaller::lcDeveloperBuild) << "Copying configuration file and associated data."; + + const QString targetPath = QFileInfo(target).absolutePath(); + if (!QDir(targetPath).exists() && !QDir().mkpath(targetPath)) { + throw Error(QCoreApplication::translate("QInstaller", + "Cannot create directory \"%1\".").arg(targetPath)); + } + + QFile xmlFile(source); + if (!xmlFile.copy(target)) { + throw Error(QCoreApplication::translate("QInstaller", + "Cannot copy file \"%1\" to \"%2\": %3").arg(source, target, xmlFile.errorString())); + } + setDefaultFilePermissions(target, DefaultFilePermissions::NonExecutable); + + xmlFile.setFileName(target); + QInstaller::openForRead(&xmlFile); // throws in case of error + + QDomDocument dom; + dom.setContent(&xmlFile); + xmlFile.close(); + + foreach (auto elementTag, elementsToRemoveTags) { + QDomNodeList elementsToRemove = dom.elementsByTagName(elementTag); + for (int i = 0; i < elementsToRemove.length(); i++) { + QDomNode elementToRemove = elementsToRemove.item(i); + elementToRemove.parentNode().removeChild(elementToRemove); + + qCDebug(QInstaller::lcDeveloperBuild) << "Removed dom element from target file:" + << elementToRemove.toElement().text(); + } + } + + const QDomNodeList children = dom.documentElement().childNodes(); + copyConfigChildElements(dom, children, QFileInfo(source).absolutePath(), QFileInfo(target).absolutePath()); + + QInstaller::openForWrite(&xmlFile); // throws in case of error + QTextStream stream(&xmlFile); + dom.save(stream, 4); // use 4 as the amount of space for indentation + + qCDebug(QInstaller::lcDeveloperBuild) << "Finished copying configuration data."; +} + +/*! + \internal + + Recursively iterates over a list of QDomNode \a objects belonging to \a dom and their + children accordingly, searching for relative file names. Found files are copied from + \a sourceDir to \a targetDir. + + Throws \c QInstaller::Error in case of failure. +*/ +void QInstaller::copyConfigChildElements(QDomDocument &dom, const QDomNodeList &objects, + const QString &sourceDir, const QString &targetDir) +{ + for (int i = 0; i < objects.length(); i++) { + QDomElement domElement = objects.at(i).toElement(); + if (domElement.isNull()) + continue; + + // Iterate recursively over all child nodes + const QDomNodeList elementChildren = domElement.childNodes(); + QInstaller::copyConfigChildElements(dom, elementChildren, sourceDir, targetDir); + + // Filename may also contain a path relative to source directory but we + // copy it strictly into target directory without extra paths + const QString newName = domElement.text() + .replace(QRegExp(QLatin1String("\\\\|/|\\.|:")), QLatin1String("_")); + + const QString targetFile = targetDir + QDir::separator() + newName; + const QFileInfo elementFileInfo = QFileInfo(sourceDir, domElement.text()); + + if (!elementFileInfo.exists() || elementFileInfo.isDir()) + continue; + + domElement.replaceChild(dom.createTextNode(newName), domElement.firstChild()); + + if (!QFile::copy(elementFileInfo.absoluteFilePath(), targetFile)) { + throw Error(QCoreApplication::translate("QInstaller", + "Cannot copy file \"%1\" to \"%2\".").arg(elementFileInfo.absoluteFilePath(), targetFile)); + } + } +} diff --git a/src/libs/installer/fileutils.h b/src/libs/installer/fileutils.h index 101a6a1cc..ac3f95098 100644 --- a/src/libs/installer/fileutils.h +++ b/src/libs/installer/fileutils.h @@ -33,6 +33,8 @@ #include <QtCore/QSet> #include <QtCore/QString> #include <QtCore/QStringList> +#include <QtXml/QDomDocument> +#include <QtXml/QDomNodeList> QT_BEGIN_NAMESPACE class QFileInfo; @@ -94,6 +96,9 @@ private: QString replacePath(const QString &path, const QString &pathBefore, const QString &pathAfter); void replaceHighDpiImage(QString &imagePath); + void INSTALLER_EXPORT trimmedCopyConfigData(const QString &source, const QString &target, const QStringList &elementsToRemoveTags); + void copyConfigChildElements(QDomDocument &dom, const QDomNodeList &objects, const QString &sourceDir, const QString &targetDir); + #ifdef Q_OS_WIN QString INSTALLER_EXPORT getLongPathName(const QString &name); QString INSTALLER_EXPORT getShortPathName(const QString &name); diff --git a/src/libs/installer/packagemanagercore.cpp b/src/libs/installer/packagemanagercore.cpp index 8e205b7d7..ca128c6ea 100644 --- a/src/libs/installer/packagemanagercore.cpp +++ b/src/libs/installer/packagemanagercore.cpp @@ -317,6 +317,18 @@ using namespace QInstaller; */ /*! + \fn QInstaller::PackageManagerCore::offlineGenerationStarted() + + \sa {installer::offlineGenerationStarted()}{installer.offlineGenerationStarted()} +*/ + +/*! + \fn QInstaller::PackageManagerCore::offlineGenerationFinished() + + \sa {installer::offlineGenerationFinished()}{installer.offlineGenerationFinished()} +*/ + +/*! \fn QInstaller::PackageManagerCore::titleMessageChanged(const QString &title) Emitted when the text of the installer status (on the PerformInstallation page) changes to @@ -2249,6 +2261,47 @@ bool PackageManagerCore::componentUninstallableFromCommandLine(const QString &co } /*! + \internal + + Tries to set \c Qt::CheckStateRole to \c Qt::Checked for given \a components in the + default component model. Returns \c true if \a components contains at least one component + eligible for installation, otherwise returns \c false. An error message can be retrieved + with \a errorMessage. +*/ +bool PackageManagerCore::checkComponentsForInstallation(const QStringList &components, QString &errorMessage) +{ + bool installComponentsFound = false; + + ComponentModel *model = defaultComponentModel(); + foreach (const QString &name, components) { + const QModelIndex &idx = model->indexFromComponentName(name); + Component *component = componentByName(name); + if (component && idx.isValid()) { + if ((model->data(idx, Qt::CheckStateRole) == QVariant::Invalid) && !component->forcedInstallation()) { + // User cannot select the component, check why + if (component->autoDependencies().count() > 0) { + errorMessage.append(tr("Cannot install component %1. Component is installed only as automatic " + "dependency to %2.\n").arg(name, component->autoDependencies().join(QLatin1Char(',')))); + } else if (!component->isCheckable()) { + errorMessage.append(tr("Cannot install component %1. Component is not checkable meaning you " + "have to select one of the subcomponents.\n").arg(name)); + } + } else if (component->isInstalled()) { + errorMessage.append(tr("Component %1 already installed\n").arg(name)); + } else { + model->setData(idx, Qt::Checked, Qt::CheckStateRole); + installComponentsFound = true; + } + } else { // idx is invalid and component valid when we have invisible virtual component + component && component->isVirtual() + ? errorMessage.append(tr("Cannot install %1. Component is virtual.\n").arg(name)) + : errorMessage.append(tr("Cannot install %1. Component not found.\n").arg(name)); + } + } + return installComponentsFound; +} + +/*! Lists installed packages without GUI. List of packages can be filtered with \a regexp. */ void PackageManagerCore::listInstalledPackages(const QString ®exp) @@ -2451,6 +2504,33 @@ PackageManagerCore::Status PackageManagerCore::removeInstallationSilently() } /*! + Creates an offline installer from selected \a componentsToAdd without displaying + a user interface. Virtual components cannot be selected unless made visible with + --show-virtual-components as in installation. AutoDependOn nor non-checkable components + cannot be selected directly. Returns \c PackageManagerCore::Status. +*/ +PackageManagerCore::Status PackageManagerCore::createOfflineInstaller(const QStringList &componentsToAdd) +{ + setOfflineGenerator(); + // init default model before fetching remote packages tree + ComponentModel *model = defaultComponentModel(); + Q_UNUSED(model); + if (!fetchRemotePackagesTree()) + return status(); + + QString errorMessage; + if (checkComponentsForInstallation(componentsToAdd, errorMessage)) { + if (d->calculateComponentsAndRun()) { + qCDebug(QInstaller::lcInstallerInstallLog) + << "Created installer to:" << offlineBinaryName(); + } + } else { + qCDebug(QInstaller::lcInstallerInstallLog).noquote().nospace() << errorMessage; + } + return status(); +} + +/*! Installs the selected components \a components without displaying a user interface. Virtual components cannot be installed unless made visible with --show-virtual-components. AutoDependOn nor non-checkable components cannot @@ -2475,39 +2555,18 @@ PackageManagerCore::Status PackageManagerCore::installSelectedComponentsSilently } } + // init default model before fetching remote packages tree ComponentModel *model = defaultComponentModel(); + Q_UNUSED(model); if (!fetchRemotePackagesTree()) return status(); - bool installComponentsFound = false; - foreach (const QString &name, components){ - const QModelIndex &idx = model->indexFromComponentName(name); - Component *component = componentByName(name); - if (idx.isValid()) { - if ((model->data(idx, Qt::CheckStateRole) == QVariant::Invalid) && - !component->forcedInstallation()) { // User cannot select the component, check why - if (component && component->autoDependencies().count() > 0) - qCDebug(QInstaller::lcInstallerInstallLog).noquote().nospace() << "Cannot install component "<< name - << ". Component is installed only as automatic dependency to "<< component->autoDependencies().join(QLatin1Char(',')) << "."; - if (component && !component->isCheckable()) - qCDebug(QInstaller::lcInstallerInstallLog).noquote().nospace() << "Cannot install component "<< name - <<". Component is not checkable meaning you have to select one of the subcomponents."; - } else if (component->isInstalled()) { - qCDebug(QInstaller::lcInstallerInstallLog).noquote().nospace() << "Component " << name <<" already installed"; - } else { - model->setData(idx, Qt::Checked, Qt::CheckStateRole); - installComponentsFound = true; - } - } else { // idx is invalid and component valid when we have invisible virtual component - if (component && component->isVirtual()) - qCDebug(QInstaller::lcInstallerInstallLog).noquote().nospace() << "Cannot install " << name <<". Component is virtual."; - else - qCDebug(QInstaller::lcInstallerInstallLog).noquote().nospace() << "Cannot install " << name <<". Component not found."; - } - } - if (installComponentsFound) { + QString errorMessage; + if (checkComponentsForInstallation(components, errorMessage)) { if (d->calculateComponentsAndRun()) qCDebug(QInstaller::lcInstallerInstallLog) << "Components installed successfully"; + } else { + qCDebug(QInstaller::lcInstallerInstallLog).noquote().nospace() << errorMessage; } return status(); } @@ -3045,6 +3104,27 @@ void PackageManagerCore::setInstallerBaseBinary(const QString &path) } /*! + Sets the \c installerbase binary located at \a path to use when writing the + offline installer. Setting this makes it possible to run the offline generator + in cases where we are not running a real installer, i.e. when executing autotests. + + For normal runs, the executable segment of the running installer will be used. +*/ +void PackageManagerCore::setOfflineBaseBinary(const QString &path) +{ + d->m_offlineBaseBinaryUnreplaced = path; +} + +/*! + Adds the resource collection in \a rcPath to the list of resource files + to be included into the generated offline installer binary. +*/ +void PackageManagerCore::addResourcesForOfflineGeneration(const QString &rcPath) +{ + d->m_offlineGeneratorResourceCollections.append(rcPath); +} + +/*! Returns the installer value for \a key. If \a key is not known to the system, \a defaultValue is returned. Additionally, on Windows, \a key can be a registry key. @@ -3199,6 +3279,22 @@ QString PackageManagerCore::installerBinaryPath() const } /*! + Sets the \a name for the generated offline binary. +*/ +void PackageManagerCore::setOfflineBinaryName(const QString &name) +{ + setValue(scOfflineBinaryName, name); +} + +/*! + Returns the path set for the generated offline binary. +*/ +QString PackageManagerCore::offlineBinaryName() const +{ + return d->offlineBinaryName(); +} + +/*! \sa {installer::setInstaller}{installer.setInstaller} \sa isInstaller(), setUpdater(), setPackageManager() */ @@ -3293,6 +3389,24 @@ bool PackageManagerCore::isPackageManager() const } /*! + Sets current installer to be offline generator based on \a offlineGenerator. +*/ +void PackageManagerCore::setOfflineGenerator(bool offlineGenerator) +{ + d->m_offlineGenerator = offlineGenerator; +} + +/*! + Returns \c true if current installer is executed as offline generator. + + \sa {installer::isOfflineGenerator}{installer.isOfflineGenerator} +*/ +bool PackageManagerCore::isOfflineGenerator() const +{ + return d->isOfflineGenerator(); +} + +/*! Sets the installer magic binary marker based on \a magicMarker and userSetBinaryMarker to \c true. */ @@ -3380,6 +3494,16 @@ bool PackageManagerCore::runPackageUpdater() } /*! + Runs the offline generator. Returns \c true on success, \c false otherwise. + + \sa {installer::runOfflineGenerator}{installer.runOfflineGenerator} +*/ +bool PackageManagerCore::runOfflineGenerator() +{ + return d->runOfflineGenerator(); +} + +/*! \sa {installer::languageChanged}{installer.languageChanged} */ void PackageManagerCore::languageChanged() @@ -3389,17 +3513,20 @@ void PackageManagerCore::languageChanged() } /*! - Runs the installer, uninstaller, updater, or package manager, depending on + Runs the installer, uninstaller, updater, package manager, or offline generator depending on the type of this binary. Returns \c true on success, otherwise \c false. */ bool PackageManagerCore::run() { - if (isInstaller()) + if (isOfflineGenerator()) + return d->runOfflineGenerator(); + else if (isInstaller()) return d->runInstaller(); else if (isUninstaller()) return d->runUninstaller(); else if (isMaintainer()) return d->runPackageUpdater(); + return false; } diff --git a/src/libs/installer/packagemanagercore.h b/src/libs/installer/packagemanagercore.h index 5e6ab2867..70ec83f15 100644 --- a/src/libs/installer/packagemanagercore.h +++ b/src/libs/installer/packagemanagercore.h @@ -160,6 +160,9 @@ public: Q_INVOKABLE static QString findPath(const QString &name, const QStringList &paths = QStringList()); Q_INVOKABLE void setInstallerBaseBinary(const QString &path); + void setOfflineBaseBinary(const QString &path); + + void addResourcesForOfflineGeneration(const QString &rcPath); // parameter handling Q_INVOKABLE bool containsValue(const QString &key) const; @@ -179,6 +182,9 @@ public: QString maintenanceToolName() const; QString installerBinaryPath() const; + void setOfflineBinaryName(const QString &name); + QString offlineBinaryName() const; + bool testChecksum() const; void setTestChecksum(bool test); @@ -241,6 +247,7 @@ public: PackageManagerCore::Status installDefaultComponentsSilently(); PackageManagerCore::Status uninstallComponentsSilently(const QStringList& components); PackageManagerCore::Status removeInstallationSilently(); + PackageManagerCore::Status createOfflineInstaller(const QStringList &componentsToAdd); // convenience Q_INVOKABLE void setInstaller(); @@ -256,6 +263,9 @@ public: Q_INVOKABLE void setPackageManager(); Q_INVOKABLE bool isPackageManager() const; + void setOfflineGenerator(bool offlineGenerator = true); + Q_INVOKABLE bool isOfflineGenerator() const; + void setUserSetBinaryMarker(qint64 magicMarker); Q_INVOKABLE bool isUserSetBinaryMarker() const; @@ -323,6 +333,7 @@ public Q_SLOTS: bool runInstaller(); bool runUninstaller(); bool runPackageUpdater(); + bool runOfflineGenerator(); void interrupt(); void setCanceled(); void languageChanged(); @@ -360,6 +371,8 @@ Q_SIGNALS: void updateFinished(); void uninstallationStarted(); void uninstallationFinished(); + void offlineGenerationStarted(); + void offlineGenerationFinished(); void titleMessageChanged(const QString &title); void wizardPageInsertionRequested(QWidget *widget, QInstaller::PackageManagerCore::WizardPage page); @@ -399,6 +412,7 @@ private: bool fetchPackagesTree(const PackagesList &packages, const LocalPackagesHash installedPackages); bool componentUninstallableFromCommandLine(const QString &componentName); + bool checkComponentsForInstallation(const QStringList &components, QString &errorMessage); private: PackageManagerCorePrivate *const d; diff --git a/src/libs/installer/packagemanagercore_p.cpp b/src/libs/installer/packagemanagercore_p.cpp index b3c543758..e3dfe7f4f 100644 --- a/src/libs/installer/packagemanagercore_p.cpp +++ b/src/libs/installer/packagemanagercore_p.cpp @@ -48,6 +48,7 @@ #include "uninstallercalculator.h" #include "componentchecker.h" #include "globals.h" +#include "binarycreator.h" #include "selfrestarter.h" #include "filedownloaderfactory.h" @@ -238,6 +239,7 @@ PackageManagerCorePrivate::PackageManagerCorePrivate(PackageManagerCore *core) , m_autoAcceptLicenses(false) , m_disableWriteMaintenanceTool(false) , m_autoConfirmCommand(false) + , m_offlineGenerator(false) { } @@ -276,6 +278,7 @@ PackageManagerCorePrivate::PackageManagerCorePrivate(PackageManagerCore *core, q , m_autoAcceptLicenses(false) , m_disableWriteMaintenanceTool(false) , m_autoConfirmCommand(false) + , m_offlineGenerator(false) { foreach (const OperationBlob &operation, performedOperations) { QScopedPointer<QInstaller::Operation> op(KDUpdater::UpdateOperationFactory::instance() @@ -302,6 +305,10 @@ PackageManagerCorePrivate::PackageManagerCorePrivate(PackageManagerCore *core, q m_core, &PackageManagerCore::uninstallationStarted); connect(this, &PackageManagerCorePrivate::uninstallationFinished, m_core, &PackageManagerCore::uninstallationFinished); + connect(this, &PackageManagerCorePrivate::offlineGenerationStarted, + m_core, &PackageManagerCore::offlineGenerationStarted); + connect(this, &PackageManagerCorePrivate::offlineGenerationFinished, + m_core, &PackageManagerCore::offlineGenerationFinished); } PackageManagerCorePrivate::~PackageManagerCorePrivate() @@ -638,6 +645,10 @@ void PackageManagerCorePrivate::initialize(const QHash<QString, QString> ¶ms ProgressCoordinator::instance(), &ProgressCoordinator::reset); connect(this, &PackageManagerCorePrivate::uninstallationStarted, ProgressCoordinator::instance(), &ProgressCoordinator::reset); + disconnect(this, &PackageManagerCorePrivate::offlineGenerationStarted, + ProgressCoordinator::instance(), &ProgressCoordinator::reset); + connect(this, &PackageManagerCorePrivate::offlineGenerationStarted, + ProgressCoordinator::instance(), &ProgressCoordinator::reset); if (!isInstaller()) m_localPackageHub->setFileName(componentsXmlPath()); @@ -696,6 +707,11 @@ bool PackageManagerCorePrivate::isPackageManager() const return m_magicBinaryMarker == BinaryContent::MagicPackageManagerMarker; } +bool PackageManagerCorePrivate::isOfflineGenerator() const +{ + return m_offlineGenerator; +} + bool PackageManagerCorePrivate::statusCanceledOrFailed() const { return m_status == PackageManagerCore::Canceled || m_status == PackageManagerCore::Failure; @@ -758,6 +774,18 @@ QString PackageManagerCorePrivate::maintenanceToolName() const return QString::fromLatin1("%1/%2").arg(targetDir()).arg(filename); } +QString PackageManagerCorePrivate::offlineBinaryName() const +{ + QString filename = m_core->value(scOfflineBinaryName, qApp->applicationName() + + QLatin1String("_offline-") + QDate::currentDate().toString(Qt::ISODate)); +#if defined(Q_OS_WIN) + const QString suffix = QLatin1String(".exe"); + if (!filename.endsWith(suffix)) + filename += suffix; +#endif + return QString::fromLatin1("%1/%2").arg(targetDir()).arg(filename); +} + static QNetworkProxy readProxy(QXmlStreamReader &reader) { QNetworkProxy proxy(QNetworkProxy::HttpProxy); @@ -1477,6 +1505,58 @@ void PackageManagerCorePrivate::writeMaintenanceTool(OperationList performedOper m_needToWriteMaintenanceTool = false; } +void PackageManagerCorePrivate::writeOfflineBaseBinary() +{ + qint64 size; + QFile input(installerBinaryPath()); + + QInstaller::openForRead(&input); +#ifndef Q_OS_MACOS + BinaryLayout layout = BinaryContent::binaryLayout(&input, BinaryContent::MagicCookie); + size = layout.endOfExectuable; +#else + // On macOS the data is on a separate file so we can just get the size + size = input.size(); +#endif + + const QString offlineBinaryTempName = offlineBinaryName() + QLatin1String(".new"); + qCDebug(QInstaller::lcInstallerInstallLog) << "Writing offline base binary:" << offlineBinaryTempName; + ProgressCoordinator::instance()->emitLabelAndDetailTextChanged(tr("Writing offline base binary.")); + + QFile out(generateTemporaryFileName()); + QInstaller::openForWrite(&out); // throws an exception in case of error + + if (!input.seek(0)) + throw Error(tr("Failed to seek in file %1: %2").arg(input.fileName(), input.errorString())); + + QInstaller::appendData(&out, &input, size); + + { + // Check if we have an existing binary, for any reason + QFile dummy(offlineBinaryTempName); + if (dummy.exists() && !dummy.remove()) { + throw Error(tr("Cannot remove file \"%1\": %2").arg(dummy.fileName(), + dummy.errorString())); + } + // Offline binary name might contain non-existing leading directories + const QString offlineBinaryAbsolutePath = QFileInfo(offlineBinaryTempName).absolutePath(); + QDir dummyDir(offlineBinaryAbsolutePath); + if (!dummyDir.exists() && !dummyDir.mkpath(offlineBinaryAbsolutePath)) { + throw Error(tr("Cannot create directory \"%1\".").arg(dummyDir.absolutePath())); + } + } + + if (!out.copy(offlineBinaryTempName)) { + throw Error(tr("Cannot write offline binary to \"%1\": %2").arg(offlineBinaryTempName, + out.errorString())); + } + + if (out.exists() && !out.remove()) { + qCWarning(QInstaller::lcInstallerInstallLog) << tr("Cannot remove temporary file \"%1\": %2") + .arg(out.fileName(), out.errorString()); + } +} + QString PackageManagerCorePrivate::registerPath() { #ifdef Q_OS_WIN @@ -1930,6 +2010,138 @@ bool PackageManagerCorePrivate::runUninstaller() return success; } +bool PackageManagerCorePrivate::runOfflineGenerator() +{ + const QString offlineBinaryTempName = offlineBinaryName() + QLatin1String(".new"); + const QString tempSettingsFilePath = generateTemporaryFileName() + + QDir::separator() + QLatin1String("config.xml"); + + bool adminRightsGained = false; + try { + setStatus(PackageManagerCore::Running); + emit offlineGenerationStarted(); // Resets also the ProgressCoordninator + + // Never write the maintenance tool when generating offline installer + m_needToWriteMaintenanceTool = false; + + // Reserve some progress for the final writing, it should take + // only a fraction of time spent in the download part + ProgressCoordinator::instance()->addReservePercentagePoints(1); + + const QString target = QDir::cleanPath(targetDir().replace(QLatin1Char('\\'), QLatin1Char('/'))); + if (target.isEmpty()) + throw Error(tr("Variable 'TargetDir' not set.")); + + // Create target directory for installer to be generated + if (!QDir(target).exists()) { + if (!QDir().mkpath(target)) { + adminRightsGained = m_core->gainAdminRights(); + // Try again with admin privileges + if (!QDir().mkpath(target)) + throw Error(tr("Cannot create target directory for installer.")); + } + } else if (QDir(target).exists()) { + if (!directoryWritable(targetDir())) + adminRightsGained = m_core->gainAdminRights(); + } + setDefaultFilePermissions(target, DefaultFilePermissions::Executable); + + // Show that there was some work + ProgressCoordinator::instance()->addManualPercentagePoints(1); + ProgressCoordinator::instance()->emitLabelAndDetailTextChanged(tr("Preparing offline generation...")); + + const QList<Component*> componentsToInclude = m_core->orderedComponentsToInstall(); + qCDebug(QInstaller::lcInstallerInstallLog) << "Included components:" << componentsToInclude.size(); + + // Give full part progress size as this is the most time consuming step + m_core->downloadNeededArchives(double(1)); + + const QString installerBaseReplacement = replaceVariables(m_offlineBaseBinaryUnreplaced); + if (!installerBaseReplacement.isEmpty() && QFileInfo(installerBaseReplacement).exists()) { + qCDebug(QInstaller::lcInstallerInstallLog) << "Got a replacement installer base binary:" + << offlineBinaryTempName; + + if (!QFile::copy(installerBaseReplacement, offlineBinaryTempName)) { + qCWarning(QInstaller::lcInstallerInstallLog) << QString::fromLatin1("Cannot copy " + "replacemement binary to temporary location \"%1\" from \"%2\".") + .arg(offlineBinaryTempName, installerBaseReplacement); + } + } else { + writeOfflineBaseBinary(); + } + + ProgressCoordinator::instance()->emitLabelAndDetailTextChanged(tr("Preparing installer configuration...")); + + // Create copy of internal config file and data, remove repository related elements + QInstaller::trimmedCopyConfigData(m_data.settingsFilePath(), tempSettingsFilePath, + QStringList() << scRemoteRepositories << scRepositoryCategories); + + // Assemble final installer binary + QInstallerTools::BinaryCreatorArgs args; + args.target = offlineBinaryName(); +#ifdef Q_OS_MACOS + // Target is a disk image on macOS + args.target.append(QLatin1String(".dmg")); +#endif + args.templateBinary = offlineBinaryTempName; + args.offlineOnly = true; + args.configFile = tempSettingsFilePath; + args.ftype = QInstallerTools::Include; + // Add possible custom resources + if (!m_offlineGeneratorResourceCollections.isEmpty()) + args.resources = m_offlineGeneratorResourceCollections; + + foreach (auto component, componentsToInclude) { + args.filteredPackages.append(component->name()); + args.repositoryDirectories.append(component->localTempPath()); + } + args.repositoryDirectories.removeDuplicates(); + + ProgressCoordinator::instance()->emitLabelAndDetailTextChanged(tr("Creating the installer...")); + + QString errorMessage; + if (QInstallerTools::createBinary(args, errorMessage) == EXIT_FAILURE) { + throw Error(tr("Failed to create offline installer. %1").arg(errorMessage)); + } else { + setStatus(PackageManagerCore::Success); + } + + // Fake a possible wrong value to show a full progress + const int progress = ProgressCoordinator::instance()->progressInPercentage(); + // This should be only the reserved one (1) from the beginning + if (progress < 100) + ProgressCoordinator::instance()->addManualPercentagePoints(100 - progress); + + } catch (const Error &err) { + if (m_core->status() != PackageManagerCore::Canceled) { + setStatus(PackageManagerCore::Failure); + MessageBoxHandler::critical(MessageBoxHandler::currentBestSuitParent(), + QLatin1String("installationError"), tr("Error"), err.message()); + } + } + QFile tempBinary(offlineBinaryTempName); + if (tempBinary.exists() && !tempBinary.remove()) { + qCWarning(QInstaller::lcInstallerInstallLog) << tr("Cannot remove temporary file \"%1\": %2") + .arg(tempBinary.fileName(), tempBinary.errorString()); + } + + QDir tempSettingsDir(QFileInfo(tempSettingsFilePath).absolutePath()); + if (tempSettingsDir.exists() && !tempSettingsDir.removeRecursively()) { + qCWarning(QInstaller::lcInstallerInstallLog) << tr("Cannot remove temporary directory \"%1\".") + .arg(tempSettingsDir.path()); + } + + const bool success = m_core->status() == PackageManagerCore::Success; + if (adminRightsGained) + m_core->dropAdminRights(); + + ProgressCoordinator::instance()->emitLabelAndDetailTextChanged(QString::fromLatin1("\n%1").arg( + success ? tr("Offline generation completed successfully.") : tr("Offline generation aborted!"))); + + emit offlineGenerationFinished(); + return success; +} + void PackageManagerCorePrivate::installComponent(Component *component, double progressOperationSize, bool adminRightsGained) { diff --git a/src/libs/installer/packagemanagercore_p.h b/src/libs/installer/packagemanagercore_p.h index 8b24cf2d0..8983a95c2 100644 --- a/src/libs/installer/packagemanagercore_p.h +++ b/src/libs/installer/packagemanagercore_p.h @@ -96,11 +96,13 @@ public: QString maintenanceToolName() const; QString installerBinaryPath() const; + QString offlineBinaryName() const; void writeMaintenanceConfigFiles(); void readMaintenanceConfigFiles(const QString &targetDir); void writeMaintenanceTool(OperationList performedOperations); + void writeOfflineBaseBinary(); QString componentsXmlPath() const; QString configurationFileName() const; @@ -133,6 +135,9 @@ public: bool runPackageUpdater(); bool isPackageManager() const; + bool runOfflineGenerator(); + bool isOfflineGenerator() const; + QString replaceVariables(const QString &str) const; QByteArray replaceVariables(const QByteArray &str) const; @@ -171,6 +176,8 @@ signals: void installationFinished(); void uninstallationStarted(); void uninstallationFinished(); + void offlineGenerationStarted(); + void offlineGenerationFinished(); public: UpdateFinder *m_updateFinder; @@ -194,6 +201,8 @@ public: bool m_needToWriteMaintenanceTool; PackageManagerCoreData m_data; QString m_installerBaseBinaryUnreplaced; + QString m_offlineBaseBinaryUnreplaced; + QStringList m_offlineGeneratorResourceCollections; QList<QInstaller::Component*> m_rootComponents; QList<QInstaller::Component*> m_rootDependencyReplacements; @@ -263,6 +272,7 @@ private: qint64 m_magicBinaryMarker; bool m_componentsToInstallCalculated; bool m_foundEssentialUpdate; + bool m_offlineGenerator; mutable ScriptEngine *m_componentScriptEngine; mutable ScriptEngine *m_controlScriptEngine; diff --git a/src/libs/installer/packagemanagercoredata.cpp b/src/libs/installer/packagemanagercoredata.cpp index 9f8ec7014..149911d61 100644 --- a/src/libs/installer/packagemanagercoredata.cpp +++ b/src/libs/installer/packagemanagercoredata.cpp @@ -70,8 +70,9 @@ PackageManagerCoreData::PackageManagerCoreData(const QHash<QString, QString> &va // TODO: add more platforms as needed... #endif - m_settings = Settings::fromFileAndPrefix(QLatin1String(":/metadata/installer-config/config.xml"), - QLatin1String(":/metadata/installer-config/"), Settings::RelaxedParseMode); + m_settingsFilePath = QLatin1String(":/metadata/installer-config/config.xml"); + m_settings = Settings::fromFileAndPrefix(m_settingsFilePath, + QFileInfo(m_settingsFilePath).absolutePath(), Settings::RelaxedParseMode); // fill the variables defined in the settings m_variables.insert(QLatin1String("ProductName"), m_settings.applicationName()); diff --git a/src/libs/installer/packagemanagercoredata.h b/src/libs/installer/packagemanagercoredata.h index ec0d4c302..e112fea1c 100644 --- a/src/libs/installer/packagemanagercoredata.h +++ b/src/libs/installer/packagemanagercoredata.h @@ -45,6 +45,10 @@ public: Settings &settings() const; QStringList keys() const; + inline QString settingsFilePath() { + return m_settingsFilePath; + } + bool contains(const QString &key) const; bool setValue(const QString &key, const QString &normalizedValue); QVariant value(const QString &key, const QVariant &_default = QVariant()) const; @@ -54,6 +58,7 @@ public: private: mutable Settings m_settings; + QString m_settingsFilePath; QHash<QString, QString> m_variables; }; diff --git a/src/libs/installer/settings.cpp b/src/libs/installer/settings.cpp index 5d8bde36a..7c1400aed 100644 --- a/src/libs/installer/settings.cpp +++ b/src/libs/installer/settings.cpp @@ -66,8 +66,6 @@ static const QLatin1String scMaintenanceToolName("MaintenanceToolName"); static const QLatin1String scUserRepositories("UserRepositories"); static const QLatin1String scTmpRepositories("TemporaryRepositories"); static const QLatin1String scMaintenanceToolIniFile("MaintenanceToolIniFile"); -static const QLatin1String scRemoteRepositories("RemoteRepositories"); -static const QLatin1String scRepositoryCategories("RepositoryCategories"); static const QLatin1String scDependsOnLocalInstallerBinary("DependsOnLocalInstallerBinary"); static const QLatin1String scTranslations("Translations"); static const QLatin1String scCreateLocalRepository("CreateLocalRepository"); diff --git a/src/sdk/commandlineinterface.cpp b/src/sdk/commandlineinterface.cpp index f522f7df6..7bb76d2af 100644 --- a/src/sdk/commandlineinterface.cpp +++ b/src/sdk/commandlineinterface.cpp @@ -210,6 +210,37 @@ int CommandLineInterface::removeInstallation() } } +int CommandLineInterface::createOfflineInstaller() +{ + if (!initialize()) + return EXIT_FAILURE; + if (!m_core->isInstaller() || m_core->isOfflineOnly()) { + qCWarning(QInstaller::lcInstallerInstallLog) + << "Offline installer can only be created with an online installer."; + return EXIT_FAILURE; + } + if (m_positionalArguments.isEmpty()) { + qCWarning(QInstaller::lcInstallerInstallLog) << "Missing components argument."; + return EXIT_FAILURE; + } + if (!(setTargetDir() && checkLicense())) + return EXIT_FAILURE; + + if (m_parser.isSet(CommandLineOptions::scOfflineInstallerNameLong)) { + m_core->setOfflineBinaryName(m_parser.value(CommandLineOptions::scOfflineInstallerNameLong)); + } else { + const QString offlineName = m_core->offlineBinaryName(); + qCDebug(QInstaller::lcInstallerInstallLog) << "No filename specified for " + "the generated installer, using default name:" << offlineName; + } + try { + return m_core->createOfflineInstaller(m_positionalArguments); + } catch (const QInstaller::Error &err) { + qCCritical(QInstaller::lcInstallerInstallLog) << err.message(); + return EXIT_FAILURE; + } +} + bool CommandLineInterface::checkLicense() { const ProductKeyCheck *const productKeyCheck = ProductKeyCheck::instance(); diff --git a/src/sdk/commandlineinterface.h b/src/sdk/commandlineinterface.h index 9833ad871..2627bc6f8 100644 --- a/src/sdk/commandlineinterface.h +++ b/src/sdk/commandlineinterface.h @@ -48,6 +48,7 @@ public: int installPackages(); int uninstallPackages(); int removeInstallation(); + int createOfflineInstaller(); private: bool initialize(); diff --git a/src/sdk/main.cpp b/src/sdk/main.cpp index 8c21cc39a..076458173 100644 --- a/src/sdk/main.cpp +++ b/src/sdk/main.cpp @@ -263,6 +263,9 @@ int main(int argc, char *argv[]) } else if (parser.positionalArguments().contains(CommandLineOptions::scPurgeShort) || parser.positionalArguments().contains(CommandLineOptions::scPurgeLong)){ return CommandLineInterface(argc, argv).removeInstallation(); + } else if (parser.positionalArguments().contains(CommandLineOptions::scCreateOfflineShort) + || parser.positionalArguments().contains(CommandLineOptions::scCreateOfflineLong)) { + return CommandLineInterface(argc, argv).createOfflineInstaller(); } if (QInstaller::isVerbose()) { std::cout << VERSION << std::endl << BUILDDATE << std::endl << SHA << std::endl; diff --git a/tests/auto/installer/cliinterface/tst_cliinterface.cpp b/tests/auto/installer/cliinterface/tst_cliinterface.cpp index e19a37288..c3e1eff57 100644 --- a/tests/auto/installer/cliinterface/tst_cliinterface.cpp +++ b/tests/auto/installer/cliinterface/tst_cliinterface.cpp @@ -102,22 +102,22 @@ private slots: QLoggingCategory::setFilterRules(loggingRules); QTest::ignoreMessage(QtDebugMsg, "Preparing meta information download..."); - QTest::ignoreMessage(QtDebugMsg, "Cannot install component A. Component is installed only as automatic dependency to autoDep."); + QTest::ignoreMessage(QtDebugMsg, "Cannot install component A. Component is installed only as automatic dependency to autoDep.\n"); QCOMPARE(PackageManagerCore::Success, core->installSelectedComponentsSilently(QStringList() << QLatin1String("A"))); QTest::ignoreMessage(QtDebugMsg, "Preparing meta information download..."); - QTest::ignoreMessage(QtDebugMsg, "Cannot install component AB. Component is not checkable meaning you have to select one of the subcomponents."); + QTest::ignoreMessage(QtDebugMsg, "Cannot install component AB. Component is not checkable meaning you have to select one of the subcomponents.\n"); QCOMPARE(PackageManagerCore::Success, core->installSelectedComponentsSilently(QStringList() << QLatin1String("AB"))); QTest::ignoreMessage(QtDebugMsg, "Preparing meta information download..."); - QTest::ignoreMessage(QtDebugMsg, "Cannot install B. Component is virtual."); + QTest::ignoreMessage(QtDebugMsg, "Cannot install B. Component is virtual.\n"); QCOMPARE(PackageManagerCore::Success, core->installSelectedComponentsSilently(QStringList() << QLatin1String("B"))); QTest::ignoreMessage(QtDebugMsg, "Preparing meta information download..."); - QTest::ignoreMessage(QtDebugMsg, "Cannot install MissingComponent. Component not found."); + QTest::ignoreMessage(QtDebugMsg, "Cannot install MissingComponent. Component not found.\n"); QCOMPARE(PackageManagerCore::Success, core->installSelectedComponentsSilently(QStringList() << QLatin1String("MissingComponent"))); QCOMPARE(PackageManagerCore::Success, core->status()); |