diff --git a/src/app/main.cpp b/src/app/main.cpp index 05e441a8801..4af2efe4482 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -839,11 +839,17 @@ int main(int argc, char **argv) // Make sure we honor the system's proxy settings QNetworkProxyFactory::setUseSystemConfiguration(true); + // We need to install plugins before we scan for them. + PluginManager::installPluginsAfterRestart(); + // Load const QStringList pluginPaths = installPluginPaths + options.customPluginPaths; PluginManager::setPluginPaths( getPluginPaths() + Utils::transform(pluginPaths, &FilePath::fromUserInput)); + // We need to remove plugins once we have scanned for them. + PluginManager::removePluginsAfterRestart(); + QMap foundAppOptions; if (pluginArguments.size() > 1) { QMap appOptions; diff --git a/src/libs/extensionsystem/pluginmanager.cpp b/src/libs/extensionsystem/pluginmanager.cpp index 1f2b7d2eef0..67743aea2cd 100644 --- a/src/libs/extensionsystem/pluginmanager.cpp +++ b/src/libs/extensionsystem/pluginmanager.cpp @@ -347,6 +347,27 @@ void PluginManager::addPlugins(const PluginSpecs &specs) d->addPlugins(specs); } +void PluginManager::removePluginsAfterRestart() +{ + d->removePluginsAfterRestart(); +} + +void PluginManager::installPluginsAfterRestart() +{ + d->installPluginsAfterRestart(); +} + +void PluginManager::removePluginOnRestart(const QString &id) +{ + d->removePluginOnNextRestart(id); +} + +void PluginManager::installPluginOnRestart( + const Utils::FilePath &source, const Utils::FilePath &destination) +{ + d->installPluginOnNextRestart(source, destination); +} + /*! Returns \c true if any plugin has errors even though it is enabled. Most useful to call after loadPlugins(). @@ -1689,6 +1710,11 @@ PluginSpec *PluginManager::specForPlugin(IPlugin *plugin) return findOrDefault(d->pluginSpecs, equal(&PluginSpec::plugin, plugin)); } +bool PluginManager::takePluginIdForRemoval(const QString &id) +{ + return d->m_pluginsToRemove.remove(id); +} + static QString pluginListString(const QSet &plugins) { QStringList names = Utils::transform(plugins, &PluginSpec::name); @@ -1899,6 +1925,36 @@ static const FilePaths pluginFiles(const FilePaths &pluginPaths) return pluginFiles; } +bool PluginManagerPrivate::removePlugin(const QString &pluginId) +{ + PluginSpec *existingSpec + = Utils::findOrDefault(pluginSpecs, Utils::equal(&PluginSpec::id, pluginId)); + + if (existingSpec) { + QTC_ASSERT(existingSpec->state() == PluginSpec::State::Resolved, return false); + + const Result removeResult = existingSpec->removePluginFiles(); + if (!removeResult) { + qCWarning(pluginLog) << "Failed to remove plugin files for" << pluginId << ":" + << removeResult.error(); + return false; + } + + for (QList &category : pluginCategories) { + category.removeOne(existingSpec); + } + + if (pluginSpecs.removeOne(existingSpec)) { + delete existingSpec; + return true; + } + } + + // This is used by other plugin managers like Lua that is not loaded yet. + m_pluginsToRemove << pluginId; + return true; +} + void PluginManagerPrivate::addPlugins(const PluginSpecs &specs) { pluginSpecs += specs; @@ -1928,6 +1984,108 @@ void PluginManagerPrivate::addPlugins(const PluginSpecs &specs) emit q->pluginsChanged(); } +static const char PLUGINS_TO_INSTALL_KEY[] = "PluginsToInstall"; +static const char PLUGINS_TO_REMOVE_KEY[] = "PluginsToRemove"; + +void PluginManagerPrivate::removePluginOnNextRestart(const QString &pluginId) +{ + settings->setValue( + PLUGINS_TO_REMOVE_KEY, settings->value(PLUGINS_TO_REMOVE_KEY).toStringList() << pluginId); + settings->sync(); +} + +static QList> readPluginInstallList(QtcSettings *settings) +{ + int size = settings->beginReadArray(PLUGINS_TO_INSTALL_KEY); + + QList> installList; + for (int i = 0; i < size; ++i) { + settings->setArrayIndex(i); + installList.append( + {FilePath::fromVariant(settings->value("src")), + FilePath::fromVariant(settings->value("dest"))}); + } + settings->endArray(); + return installList; +} + +void PluginManagerPrivate::installPluginOnNextRestart( + const Utils::FilePath &src, const Utils::FilePath &dest) +{ + const QList> list = readPluginInstallList(settings) + << qMakePair(src, dest); + + settings->beginWriteArray(PLUGINS_TO_INSTALL_KEY); + for (int i = 0; i < list.size(); ++i) { + settings->setArrayIndex(i); + settings->setValue("src", list.at(i).first.toVariant()); + settings->setValue("dest", list.at(i).second.toVariant()); + } + settings->endArray(); + + settings->sync(); +} + +void PluginManagerPrivate::removePluginsAfterRestart() +{ + const QStringList removeList = settings->value(PLUGINS_TO_REMOVE_KEY).toStringList(); + for (const QString &pluginId : removeList) + removePlugin(pluginId); + + settings->remove(PLUGINS_TO_REMOVE_KEY); +} + +void PluginManagerPrivate::installPluginsAfterRestart() +{ + QTC_CHECK(pluginSpecs.isEmpty()); + + QList> installList = readPluginInstallList(settings); + const Utils::FilePaths pluginPaths = PluginManager::pluginPaths(); + + for (const auto &[src, dest] : installList) { + if (!src.exists()) { + qCWarning(pluginLog()) << "Cannot install source " << src << ", it does not exist"; + continue; + } + + if (dest.isDir()) { + if (auto result = dest.removeRecursively(); !result) { + qCWarning(pluginLog()) << "Failed to remove" << dest << ":" << result.error(); + continue; + } + } else if (dest.isFile()) { + if (const Result result = dest.removeFile(); !result) { + qCWarning(pluginLog()) << "Failed to remove" << dest << ":" << result.error(); + continue; + } + } + + if (src.isFile()) { + if (!dest.createDir()) { + qCWarning(pluginLog()) << "Cannot install file" << src << "to" << dest + << "because the destination directory cannot be created"; + continue; + } + } + + Utils::Result result = src.isDir() ? src.copyRecursively(dest) + : src.copyFile(dest / src.fileName()); + + if (!result) { + qCWarning(pluginLog()) + << "Failed to install" << src << "to" << dest << ":" << result.error(); + continue; + } + + result = src.isDir() ? src.removeRecursively() : src.removeFile(); + if (!result) + qCWarning(pluginLog()) + << "Failed to remove the source file in" << src << ":" << result.error(); + } + + settings->remove(PLUGINS_TO_INSTALL_KEY); +} + /*! \internal */ @@ -1954,6 +2112,13 @@ void PluginManagerPrivate::readPluginPaths() newSpecs.append(spec->release()); } + newSpecs = Utils::filtered(newSpecs, [this](PluginSpec *spec) { + return pluginById(spec->id()) == nullptr; + }); + + if (newSpecs.empty()) + return; + addPlugins(newSpecs); } diff --git a/src/libs/extensionsystem/pluginmanager.h b/src/libs/extensionsystem/pluginmanager.h index 1fbb266c24a..7ea388b3ce4 100644 --- a/src/libs/extensionsystem/pluginmanager.h +++ b/src/libs/extensionsystem/pluginmanager.h @@ -80,9 +80,19 @@ public: static const QSet pluginsRequiredByPlugin(PluginSpec *spec); static void checkForProblematicPlugins(); static PluginSpec *specForPlugin(IPlugin *plugin); + static bool takePluginIdForRemoval(const QString &id); static void addPlugins(const QVector &specs); + static void reInstallPlugins(); + + static void removePluginOnRestart(const QString &id); + static void installPluginOnRestart( + const Utils::FilePath &source, const Utils::FilePath &destination); + + static void removePluginsAfterRestart(); + static void installPluginsAfterRestart(); + // UI static std::optional> askForEnablingPlugins( QWidget *dialogParent, const QSet &plugins, bool enable); diff --git a/src/libs/extensionsystem/pluginmanager_p.h b/src/libs/extensionsystem/pluginmanager_p.h index cdbd7ee52e1..3855b3c016b 100644 --- a/src/libs/extensionsystem/pluginmanager_p.h +++ b/src/libs/extensionsystem/pluginmanager_p.h @@ -52,6 +52,8 @@ public: void loadPluginsAtRuntime(const QSet &plugins); void addPlugins(const QVector &specs); + bool removePlugin(const QString &pluginId); + void shutdown(); void setPluginPaths(const Utils::FilePaths &paths); const QVector loadQueue(); @@ -70,6 +72,13 @@ public: bool acceptTermsAndConditions(PluginSpec *spec); void setAcceptTermsAndConditionsCallback(const std::function &callback); + void readPluginPaths(); + + void removePluginsAfterRestart(); + void installPluginsAfterRestart(); + + void removePluginOnNextRestart(const QString &pluginId); + void installPluginOnNextRestart(const Utils::FilePath &src, const Utils::FilePath &dest); class TestSpec { public: @@ -143,12 +152,13 @@ public: PluginManager::ProcessData m_creatorProcessData; + QSet m_pluginsToRemove; + private: PluginManager *q; void startDelayedInitialize(); - void readPluginPaths(); bool loadQueue(PluginSpec *spec, QVector &queue, QVector &circularityCheckQueue); diff --git a/src/libs/extensionsystem/pluginspec.cpp b/src/libs/extensionsystem/pluginspec.cpp index 6b501f2cc89..7cc3221312a 100644 --- a/src/libs/extensionsystem/pluginspec.cpp +++ b/src/libs/extensionsystem/pluginspec.cpp @@ -1453,4 +1453,33 @@ QList pluginSpecsFromArchive(const Utils::FilePath &path) return results; } +Utils::Result PluginSpec::removePluginFiles() const +{ + if (isSystemPlugin()) + return Result::Error(Tr::tr("Cannot remove system plugins.")); + + // Try to figure out where we are ... + const Utils::FilePaths pluginPaths = PluginManager::pluginPaths(); + + for (const FilePath &pluginPath : pluginPaths) { + if (location().isChildOf(pluginPath)) { + const FilePath rootFolder = location().relativeChildPath(pluginPath); + if (rootFolder.isEmpty()) + return Result::Error(Tr::tr("Could not determine root folder.")); + + const FilePath pathToDelete = pluginPath + / rootFolder.pathComponents().first().toString(); + return pathToDelete.removeRecursively(); + } + } + + return filePath().removeFile(); +} + +bool PluginSpec::isSystemPlugin() const +{ + return !filePath().isChildOf(appInfo().userPluginsRoot) + && !filePath().isChildOf(appInfo().userLuaPlugins); +} + } // namespace ExtensionSystem diff --git a/src/libs/extensionsystem/pluginspec.h b/src/libs/extensionsystem/pluginspec.h index dfa7274f0c7..39717f0ed61 100644 --- a/src/libs/extensionsystem/pluginspec.h +++ b/src/libs/extensionsystem/pluginspec.h @@ -161,6 +161,9 @@ public: virtual Utils::FilePath installLocation(bool inUserFolder) const = 0; + virtual Utils::Result removePluginFiles() const; + virtual bool isSystemPlugin() const; + protected: virtual void setEnabledByDefault(bool value); virtual void setEnabledIndirectly(bool value); diff --git a/src/plugins/coreplugin/plugininstallwizard.cpp b/src/plugins/coreplugin/plugininstallwizard.cpp index c11862b2bad..03f77b22ca7 100644 --- a/src/plugins/coreplugin/plugininstallwizard.cpp +++ b/src/plugins/coreplugin/plugininstallwizard.cpp @@ -51,6 +51,7 @@ struct Data bool installIntoApplication = false; std::unique_ptr pluginSpec = nullptr; bool loadImmediately = false; + bool prepareForUpdate = false; }; static bool hasLibSuffix(const FilePath &path) @@ -127,13 +128,20 @@ public: using CheckResult = expected_str; -static Result checkPlugin(PluginSpec *spec) +static Result checkPlugin(PluginSpec *spec, bool update) { - if (Utils::anyOf(PluginManager::plugins(), [spec](PluginSpec *other) { - return other->id() == spec->id(); - })) + const bool pluginAlreadyExists + = Utils::anyOf(PluginManager::plugins(), [spec](PluginSpec *other) { + return other->id() == spec->id(); + }); + + if (!update && pluginAlreadyExists) { return Result::Error( Tr::tr("A plugin with ID \"%1\" is already installed.").arg(spec->id())); + } else if (update && !pluginAlreadyExists) { + return Result::Error(Tr::tr("No plugin with ID \"%1\" is installed.").arg(spec->id())); + } + if (!spec->resolveDependencies(PluginManager::plugins())) { return Result::Error( Tr::tr("Plugin failed to resolve dependencies:") + " " + spec->errorString()); @@ -142,18 +150,18 @@ static Result checkPlugin(PluginSpec *spec) } static expected_str> checkPlugin( - expected_str> spec) + expected_str> spec, bool update) { if (!spec) return spec; - const Result ok = checkPlugin(spec->get()); + const Result ok = checkPlugin(spec->get(), update); if (ok) return spec; return Utils::make_unexpected(ok.error()); } // Async. Result is set if any issue was found. -void checkContents(QPromise &promise, const FilePath &tempDir) +void checkContents(QPromise &promise, const FilePath &tempDir, bool update) { QList plugins = pluginSpecsFromArchive(tempDir); if (plugins.isEmpty()) { @@ -167,7 +175,7 @@ void checkContents(QPromise &promise, const FilePath &tempDir) } PluginSpec *plugin = plugins.front(); - const Result ok = checkPlugin(plugin); + const Result ok = checkPlugin(plugin, update); if (!ok) { promise.addResult(Utils::make_unexpected(ok.error())); delete plugin; @@ -214,8 +222,8 @@ public: emit completeChanged(); if (hasLibSuffix(m_data->sourcePath)) { m_cancelButton->setVisible(false); - expected_str> spec = checkPlugin( - readCppPluginSpec(m_data->sourcePath)); + expected_str> spec + = checkPlugin(readCppPluginSpec(m_data->sourcePath), m_data->prepareForUpdate); if (!spec) { m_label->setType(InfoLabel::Error); m_label->setText(spec.error()); @@ -228,8 +236,18 @@ public: return; } - m_tempDir = std::make_unique("plugininstall"); - m_data->extractedPath = m_tempDir->path(); + FilePath tmpDirBase; + if (m_data->prepareForUpdate) + tmpDirBase = appInfo().userResources / "install-prep-area"; + else + tmpDirBase = TemporaryDirectory::masterDirectoryFilePath(); + + tmpDirBase.ensureWritableDir(); + + m_tempDir = std::make_unique((tmpDirBase / "plugininstall").path()); + m_tempDir->setAutoRemove(false); + + m_data->extractedPath = FilePath::fromString(m_tempDir->path()); m_label->setText(Tr::tr("Checking archive...")); m_label->setType(InfoLabel::None); @@ -244,7 +262,7 @@ public: const auto onUnarchiverSetup = [this, sourceAndCommand](Unarchiver &unarchiver) { unarchiver.setSourceAndCommand(*sourceAndCommand); - unarchiver.setDestDir(m_tempDir->path()); + unarchiver.setDestDir(m_data->extractedPath); connect(&unarchiver, &Unarchiver::outputReceived, this, [this](const QString &output) { m_output->append(output); }); @@ -255,10 +273,11 @@ public: }; const auto onCheckerSetup = [this](Async &async) { - if (!m_tempDir) + if (!m_data->extractedPath.exists()) return SetupResult::StopWithError; - async.setConcurrentCallData(checkContents, m_tempDir->path()); + async.setConcurrentCallData( + checkContents, m_data->extractedPath, m_data->prepareForUpdate); return SetupResult::Continue; }; const auto onCheckerDone = [this](const Async &async) { @@ -289,7 +308,10 @@ public: { // back button pressed m_taskTreeRunner.reset(); - m_tempDir.reset(); + if (m_tempDir) { + m_tempDir->remove(); + m_tempDir.reset(); + } } bool isComplete() const final { return m_isComplete; } @@ -301,7 +323,7 @@ public: return WizardPage::nextId(); } - std::unique_ptr m_tempDir; + std::unique_ptr m_tempDir; TaskTreeRunner m_taskTreeRunner; InfoLabel *m_label = nullptr; QPushButton *m_cancelButton = nullptr; @@ -489,7 +511,13 @@ static bool copyPluginFile(const FilePath &src, const FilePath &dest) return false; destFile.removeFile(); } - dest.parentDir().ensureWritableDir(); + if (!destFile.parentDir().ensureWritableDir()) { + QMessageBox::warning( + ICore::dialogParent(), + Tr::tr("Failed to Write File"), + Tr::tr("Failed to create directory \"%1\".").arg(dest.toUserOutput())); + return false; + } if (!src.copyFile(destFile)) { QMessageBox::warning(ICore::dialogParent(), Tr::tr("Failed to Write File"), @@ -500,12 +528,18 @@ static bool copyPluginFile(const FilePath &src, const FilePath &dest) return true; } -InstallResult executePluginInstallWizard(const FilePath &archive) +QString extensionId(PluginSpec *spec) +{ + return spec->vendorId() + "." + spec->id(); +} + +InstallResult executePluginInstallWizard(const FilePath &archive, bool prepareForUpdate) { Wizard wizard; wizard.setWindowTitle(Tr::tr("Install Plugin")); Data data; + data.prepareForUpdate = prepareForUpdate; if (archive.isEmpty()) { auto filePage = new SourcePage(&data, &wizard); @@ -526,10 +560,23 @@ InstallResult executePluginInstallWizard(const FilePath &archive) auto summaryPage = new SummaryPage(&data, &wizard); wizard.addPage(summaryPage); - auto install = [&wizard, &data]() { + auto install = [&wizard, &data, prepareForUpdate]() { if (wizard.exec()) { const FilePath installPath = data.pluginSpec->installLocation( - !data.installIntoApplication); + !data.installIntoApplication) + / extensionId(data.pluginSpec.get()); + + if (prepareForUpdate) { + if (hasLibSuffix(data.sourcePath)) { + ExtensionSystem::PluginManager::installPluginOnRestart( + data.pluginSpec->filePath(), installPath); + } else { + ExtensionSystem::PluginManager::installPluginOnRestart( + data.extractedPath, installPath); + } + return true; + } + if (hasLibSuffix(data.sourcePath)) { if (!copyPluginFile(data.sourcePath, installPath)) return false; @@ -568,6 +615,9 @@ InstallResult executePluginInstallWizard(const FilePath &archive) // so we can safely set them as accepted here. PluginManager::instance()->setTermsAndConditionsAccepted(data.pluginSpec.get()); + if (prepareForUpdate) + return InstallResult::NeedsRestart; + auto spec = data.pluginSpec.release(); PluginManager::addPlugins({spec}); diff --git a/src/plugins/coreplugin/plugininstallwizard.h b/src/plugins/coreplugin/plugininstallwizard.h index 85b703619fb..648248c69d1 100644 --- a/src/plugins/coreplugin/plugininstallwizard.h +++ b/src/plugins/coreplugin/plugininstallwizard.h @@ -17,6 +17,7 @@ enum class InstallResult { NeedsRestart, }; -CORE_EXPORT InstallResult executePluginInstallWizard(const Utils::FilePath &archive = {}); +CORE_EXPORT InstallResult +executePluginInstallWizard(const Utils::FilePath &archive = {}, bool prepareForUpdate = false); } // namespace Core diff --git a/src/plugins/extensionmanager/extensionmanagerwidget.cpp b/src/plugins/extensionmanager/extensionmanagerwidget.cpp index 59b72eb0165..246fd516ea2 100644 --- a/src/plugins/extensionmanager/extensionmanagerwidget.cpp +++ b/src/plugins/extensionmanager/extensionmanagerwidget.cpp @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -87,6 +88,21 @@ static QWidget *toScrollableColumn(QWidget *widget) return scrollArea; }; +const char kRestartSetting[] = "RestartAfterPluginEnabledChanged"; + +static void requestRestart() +{ + if (ICore::infoBar()->canInfoBeAdded(kRestartSetting)) { + Utils::InfoBarEntry + info(kRestartSetting, Core::Tr::tr("Plugin changes will take effect after restart.")); + info.addCustomButton(Tr::tr("Restart Now"), [] { + ICore::infoBar()->removeInfo(kRestartSetting); + QTimer::singleShot(0, ICore::instance(), &ICore::restart); + }); + ICore::infoBar()->addInfo(info); + } +} + class CollapsingWidget : public QWidget { public: @@ -153,6 +169,24 @@ public: installButton = new Button(Tr::tr("Install..."), Button::LargePrimary); installButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); installButton->hide(); + connect( + installButton, + &QAbstractButton::pressed, + this, + &HeadingWidget::pluginInstallationRequested); + + removeButton = new Button(Tr::tr("Remove ..."), Button::SmallSecondary); + removeButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); + removeButton->hide(); + connect(removeButton, &QAbstractButton::pressed, this, [this]() { + ExtensionSystem::PluginManager::removePluginOnRestart(m_currentPluginId); + requestRestart(); + }); + + updateButton = new Button(Tr::tr("Update ..."), Button::LargePrimary); + updateButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); + updateButton->hide(); + connect(updateButton, &QAbstractButton::pressed, this, &HeadingWidget::pluginUpdateRequested); using namespace Layouting; Row { @@ -181,6 +215,8 @@ public: }, Column { installButton, + updateButton, + removeButton, st, }, noMargin, spacing(SpacingTokens::ExPaddingGapL), @@ -189,8 +225,6 @@ public: setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum); m_dlCountItems->setVisible(false); - connect(installButton, &QAbstractButton::pressed, - this, &HeadingWidget::pluginInstallationRequested); connect(m_vendor, &QAbstractButton::pressed, this, [this]() { emit vendorClicked(m_currentVendor); }); @@ -203,6 +237,8 @@ public: if (!current.isValid()) return; + m_currentPluginId = current.data(RoleId).toString(); + m_icon->setPixmap(itemIcon(current, SizeBig)); const QString name = current.data(RoleName).toString(); @@ -220,17 +256,27 @@ public: const QString description = current.data(RoleDescriptionShort).toString(); m_details->setText(description); + ExtensionSystem::PluginSpec *pluginSpec = pluginSpecForId(current.data(RoleId).toString()); + const ItemType itemType = current.data(RoleItemType).value(); const bool isPack = itemType == ItemTypePack; - const bool isRemotePlugin = !(isPack || pluginSpecForId(current.data(RoleId).toString())); + const bool isRemotePlugin = !(isPack || pluginSpec); const QString downloadUrl = current.data(RoleDownloadUrl).toString(); + removeButton->setVisible(!isRemotePlugin && pluginSpec && !pluginSpec->isSystemPlugin()); installButton->setVisible(isRemotePlugin && !downloadUrl.isEmpty()); if (installButton->isVisible()) installButton->setToolTip(downloadUrl); + + updateButton->setVisible( + pluginSpec + && ExtensionSystem::PluginSpec::versionCompare( + pluginSpec->version(), current.data(RoleVersion).toString()) + < 0); } signals: void pluginInstallationRequested(); + void pluginUpdateRequested(); void vendorClicked(const QString &vendor); private: @@ -243,24 +289,12 @@ private: QWidget *m_dlCountItems; QLabel *m_details; QAbstractButton *installButton; + QAbstractButton *removeButton; + QAbstractButton *updateButton; QString m_currentVendor; + QString m_currentPluginId; }; -const char kRestartSetting[] = "RestartAfterPluginEnabledChanged"; - -static void requestRestart() -{ - if (ICore::infoBar()->canInfoBeAdded(kRestartSetting)) { - Utils::InfoBarEntry - info(kRestartSetting, Core::Tr::tr("Plugin changes will take effect after restart.")); - info.addCustomButton(Tr::tr("Restart Now"), [] { - ICore::infoBar()->removeInfo(kRestartSetting); - QTimer::singleShot(0, ICore::instance(), &ICore::restart); - }); - ICore::infoBar()->addInfo(info); - } -} - class PluginStatusWidget : public QWidget { public: @@ -394,7 +428,7 @@ public: private: void updateView(const QModelIndex ¤t); - void fetchAndInstallPlugin(const QUrl &url, const QString &id); + void fetchAndInstallPlugin(const QUrl &url, const QString &id, bool update); QString m_currentItemName; ExtensionsModel *m_extensionModel; @@ -562,7 +596,10 @@ ExtensionManagerWidget::ExtensionManagerWidget() m_secondaryDescriptionWidget->setWidth(secondaryDescriptionWidth); }); connect(m_headingWidget, &HeadingWidget::pluginInstallationRequested, this, [this]() { - fetchAndInstallPlugin(QUrl::fromUserInput(m_currentDownloadUrl), m_currentId); + fetchAndInstallPlugin(QUrl::fromUserInput(m_currentDownloadUrl), m_currentId, false); + }); + connect(m_headingWidget, &HeadingWidget::pluginUpdateRequested, this, [this]() { + fetchAndInstallPlugin(QUrl::fromUserInput(m_currentDownloadUrl), m_currentId, true); }); connect(m_tags, &TagList::tagSelected, m_extensionBrowser, &ExtensionsBrowser::setFilter); connect(m_headingWidget, &HeadingWidget::vendorClicked, @@ -649,7 +686,7 @@ void ExtensionManagerWidget::updateView(const QModelIndex ¤t) } } -void ExtensionManagerWidget::fetchAndInstallPlugin(const QUrl &url, const QString &id) +void ExtensionManagerWidget::fetchAndInstallPlugin(const QUrl &url, const QString &id, bool update) { using namespace Tasking; @@ -720,7 +757,7 @@ void ExtensionManagerWidget::fetchAndInstallPlugin(const QUrl &url, const QStrin } }; - const auto onPluginInstallation = [storage]() { + const auto onPluginInstallation = [storage, update]() { if (storage->packageData.isEmpty()) return false; const FilePath source = FilePath::fromUrl(storage->url); @@ -730,7 +767,7 @@ void ExtensionManagerWidget::fetchAndInstallPlugin(const QUrl &url, const QStrin saver.write(storage->packageData); if (saver.finalize(ICore::dialogParent())) { - auto result = executePluginInstallWizard(saver.filePath()); + auto result = executePluginInstallWizard(saver.filePath(), update); switch (result) { case InstallResult::Success: return true; diff --git a/src/plugins/extensionmanager/extensionsmodel.cpp b/src/plugins/extensionmanager/extensionsmodel.cpp index bea38dd368f..a624aa9beb1 100644 --- a/src/plugins/extensionmanager/extensionsmodel.cpp +++ b/src/plugins/extensionmanager/extensionsmodel.cpp @@ -48,7 +48,7 @@ public: void ExtensionsModelPrivate::addUnlistedLocalPlugins() { - QStringList responseExtensions; + QSet responseExtensions; for (const QJsonValueConstRef &responseItem : qAsConst(responseItems)) responseExtensions << responseItem.toObject().value("id").toString(); @@ -124,6 +124,8 @@ QVariant ExtensionsModelPrivate::dataFromRemotePlugin(const QJsonObject &json, i } break; } + case RoleVersion: + return metaData.value("Version"); case RoleItemType: return ItemTypeExtension; case RoleDescriptionLong: { diff --git a/src/plugins/lua/luaplugin.cpp b/src/plugins/lua/luaplugin.cpp index e679516147e..27107364b09 100644 --- a/src/plugins/lua/luaplugin.cpp +++ b/src/plugins/lua/luaplugin.cpp @@ -416,9 +416,25 @@ public: FilePaths folders = path.dirEntries(FileFilter({}, QDir::Dirs | QDir::NoDotAndDotDot)); for (const FilePath &folder : folders) { - const FilePath script = folder / (folder.baseName() + ".lua"); - if (!script.exists()) + FilePath script = folder / (folder.baseName() + ".lua"); + if (!script.exists()) { + FilePaths contents = folder.dirEntries(QDir::Dirs | QDir::NoDotAndDotDot); + if (contents.empty()) + continue; + + for (const FilePath &subfolder : contents) { + script = subfolder / (subfolder.baseName() + ".lua"); + if (!script.exists()) { + script.clear(); + continue; + } + break; + } + } + + if (script.isEmpty()) { continue; + } const expected_str result = loadPlugin(script); @@ -430,6 +446,19 @@ public: continue; } + if (PluginManager::takePluginIdForRemoval((*result)->id())) { + auto removeResult = (*result)->removePluginFiles(); + if (!removeResult) { + qWarning() << "Failed to remove plugin files" << script << ":" + << removeResult.error(); + MessageManager::writeFlashing( + Tr::tr("Failed to remove plugin files of %1: %2") + .arg((*result)->id()) + .arg(removeResult.error())); + } + continue; + } + plugins.insert(*result); } }