Skip to content

Extensions: Added uninstall and update process #27768

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 7, 2025
5 changes: 1 addition & 4 deletions src/appshell/internal/applicationactioncontroller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,7 @@ bool ApplicationActionController::onDropEvent(QDropEvent* event)
case DragTarget::Extension: {
muse::io::path_t filePath = url.toLocalFile();
async::Async::call(this, [this, filePath]() {
Ret ret = extensionInstaller()->installExtension(filePath);
if (!ret) {
LOGE() << ret.toString();
}
extensionInstaller()->installExtension(filePath);
});
} break;
case DragTarget::Unknown:
Expand Down
2 changes: 2 additions & 0 deletions src/framework/extensions/extensionstypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ struct Manifest {
};

Uri uri;
io::path_t path;
Type type = Type::Undefined;
String title;
String description;
Expand All @@ -226,6 +227,7 @@ struct Manifest {
String version;
int apiversion = DEFAULT_API_VERSION;
bool legacyPlugin = false;
bool isRemovable = false;

std::vector<Action> actions;

Expand Down
8 changes: 5 additions & 3 deletions src/framework/extensions/iextensioninstaller.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

#include "modularity/imoduleinterface.h"

#include "global/types/ret.h"
#include "global/io/path.h"
#include "global/types/ret.h"
#include "global/types/uri.h"

namespace muse::extensions {
class IExtensionInstaller : MODULE_EXPORT_INTERFACE
Expand All @@ -13,7 +14,8 @@ class IExtensionInstaller : MODULE_EXPORT_INTERFACE

virtual ~IExtensionInstaller() = default;

virtual Ret isFileSupported(const io::path_t path) const = 0;
virtual Ret installExtension(const io::path_t path) = 0;
virtual Ret isFileSupported(const io::path_t& path) const = 0;
virtual void installExtension(const io::path_t& path) = 0;
virtual void uninstallExtension(const Uri& uri) = 0;
};
}
129 changes: 96 additions & 33 deletions src/framework/extensions/internal/extensioninstaller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@

#include "global/io/fileinfo.h"
#include "global/serialization/zipreader.h"
#include "global/translation.h"
#include "global/uuid.h"

#include "../extensionstypes.h"
#include "../extensionserrors.h"
#include "extensionsloader.h"

#include "log.h"

using namespace muse;
using namespace muse::extensions;

Ret ExtensionInstaller::isFileSupported(const io::path_t path) const
Ret ExtensionInstaller::isFileSupported(const io::path_t& path) const
{
bool isExt = io::FileInfo(path).suffix() == SINGLE_FILE_EXT;
if (isExt) {
Expand All @@ -22,47 +22,110 @@ Ret ExtensionInstaller::isFileSupported(const io::path_t path) const
return muse::make_ret(Ret::Code::NotSupported);
}

Ret ExtensionInstaller::installExtension(const io::path_t srcPath)
void ExtensionInstaller::installExtension(const io::path_t& srcPath)
{
// check manifest and duplication
{
ZipReader zip(srcPath);
ByteArray data = zip.fileData("manifest.json");
if (data.empty()) {
LOGE() << "not found manifest.json in: " << srcPath;
return make_ret(Err::ExtBadFormat);
}

ExtensionsLoader loader;
Manifest m = loader.parseManifest(data);
const ZipReader zip(srcPath);
const ByteArray data = zip.fileData("manifest.json");
if (data.empty()) {
LOGE() << "not found manifest.json in: " << srcPath;

bool hasSame = provider()->manifest(m.uri).isValid();
if (hasSame) {
LOGI() << "already installed: " << m.uri;
return make_ok();
}
interactive()->error(trc("extensions", "Failed to install extension"),
trc("extensions", "The extension does not contain a valid manifest file."),
{ interactive()->buttonData(IInteractive::Button::Ok) });

return;
}

// unpack
Ret ret;
{
io::path_t dstPath = configuration()->userPath() + "/"
+ io::FileInfo(srcPath).baseName() + "_"
+ Uuid::gen();

ZipUnpack zip;
ret = zip.unpack(srcPath, dstPath);
if (!ret) {
LOGE() << "failed unpack from: " << srcPath << ", to: " << dstPath << ", err: " << ret.toString();
} else {
LOGI() << "success unpack from: " << srcPath << ", to: " << dstPath;
const ExtensionsLoader loader;
const Manifest m = loader.parseManifest(data);

const Manifest existingManifest = provider()->manifest(m.uri);
const bool alreadyInstalled = existingManifest.isValid();

if (!alreadyInstalled) {
doInstallExtension(srcPath);
return;
}

if (existingManifest.version == m.version) {
LOGI() << "already installed: " << m.uri;

interactive()->info(trc("extensions", "The extension is already installed."), std::string(),
{ interactive()->buttonData(IInteractive::Button::Ok) });

return;
}

if (!existingManifest.isRemovable) {
interactive()->error(trc("extensions", "This extension cannot be updated."),
trc("extensions", "The currently installed version cannot be uninstalled."),
{ interactive()->buttonData(IInteractive::Button::Ok) });

return;
}

const std::string text = qtrc("extensions", "Another version of the extension “%1” is already installed (version %2). "
"Do you want to replace it with version %3?")
.arg(existingManifest.title, existingManifest.version, m.version).toStdString();

interactive()->question(trc("extensions", "Update extension"),
text,
{ IInteractive::Button::Cancel, IInteractive::Button::Ok })
.onResolve(this, [this, existingManifest, srcPath](const IInteractive::Result& result) {
if (result.isButton(IInteractive::Button::Ok)) {
uninstallExtension(existingManifest.uri);
doInstallExtension(srcPath);
}
});
}

void ExtensionInstaller::doInstallExtension(const io::path_t& srcPath)
{
// unpack
const io::path_t dstPath = configuration()->userPath() + "/"
+ io::FileInfo(srcPath).baseName() + "_"
+ Uuid::gen();

ZipUnpack zip;
Ret ret = zip.unpack(srcPath, dstPath);
if (!ret) {
LOGE() << "failed unpack from: " << srcPath << ", to: " << dstPath << ", err: " << ret.toString();

interactive()->error(trc("extensions", "Failed to install extension"),
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
qtrc("extensions", "Error code: %1").arg(ret.toString()).toStdString(),
#else
qtrc("extensions", "Error code: %1").arg(QString::fromStdString(ret.toString())).toStdString(),
#endif
{ interactive()->buttonData(IInteractive::Button::Ok) });

return;
}

LOGI() << "success unpack from: " << srcPath << ", to: " << dstPath;

// reload
if (ret) {
provider()->reloadExtensions();
provider()->reloadExtensions();
}

void ExtensionInstaller::uninstallExtension(const Uri& uri)
{
Manifest manifest = provider()->manifest(uri);
if (!manifest.isValid()) {
LOGE() << "Manifest not found for URI: " << uri.toString();
return;
} else if (!manifest.isRemovable) {
LOGE() << "Extension is not removable: " << manifest.uri.toString();
return;
}

Ret ret = fileSystem()->remove(io::dirpath(manifest.path));
if (!ret) {
LOGE() << "Failed to delete the folder: " << manifest.path << ", err: " << ret.toString();
return;
}

return ret;
provider()->reloadExtensions();
}
15 changes: 12 additions & 3 deletions src/framework/extensions/internal/extensioninstaller.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,29 @@

#include "../iextensioninstaller.h"

#include "async/asyncable.h"
#include "modularity/ioc.h"
#include "../iextensionsconfiguration.h"
#include "../iextensionsprovider.h"
#include "global/iinteractive.h"
#include "io/ifilesystem.h"

namespace muse::extensions {
class ExtensionInstaller : public IExtensionInstaller
class ExtensionInstaller : public IExtensionInstaller, public async::Asyncable
{
muse::GlobalInject<IExtensionsConfiguration> configuration;
muse::GlobalInject<IExtensionsProvider> provider;
muse::GlobalInject<muse::IInteractive> interactive;
muse::GlobalInject<io::IFileSystem> fileSystem;

public:
ExtensionInstaller() = default;

Ret isFileSupported(const io::path_t path) const override;
Ret installExtension(const io::path_t path) override;
Ret isFileSupported(const io::path_t& path) const override;
void installExtension(const io::path_t& path) override;
void uninstallExtension(const Uri& uri) override;

private:
void doInstallExtension(const io::path_t& srcPath);
};
}
4 changes: 3 additions & 1 deletion src/framework/extensions/internal/extensionsloader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ ManifestList ExtensionsLoader::loadManifestList(const io::path_t& defPath, const
retList.push_back(m);
}

for (const Manifest& m : externalManifests) {
for (Manifest& m : externalManifests) {
if (!m.isValid()) {
continue;
}
Expand All @@ -67,6 +67,7 @@ ManifestList ExtensionsLoader::loadManifestList(const io::path_t& defPath, const
continue;
}

m.isRemovable = true;
retList.push_back(m);
}

Expand All @@ -80,6 +81,7 @@ ManifestList ExtensionsLoader::manifestList(const io::path_t& rootPath) const
for (const io::path_t& path : paths) {
LOGD() << "parsing manifest: " << path;
Manifest manifest = parseManifest(path);
manifest.path = path;
resolvePaths(manifest, io::FileInfo(path).dirPath());
manifests.push_back(manifest);
}
Expand Down
1 change: 1 addition & 0 deletions src/framework/extensions/internal/extensionsprovider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

#include "global/containers.h"
#include "global/async/async.h"
#include "global/io/path.h"

#include "extensionsloader.h"
#include "legacy/extpluginsloader.h"
Expand Down
2 changes: 2 additions & 0 deletions src/framework/extensions/internal/extensionsprovider.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@
#include "../iextensionsprovider.h"
#include "../iextensionsexecpointsregister.h"
#include "global/iinteractive.h"
#include "io/ifilesystem.h"

namespace muse::extensions {
class ExtensionsProvider : public IExtensionsProvider, public Injectable, public async::Asyncable
{
Inject<IExtensionsConfiguration> configuration = { this };
Inject<IExtensionsExecPointsRegister> execPointsRegister = { this };
Inject<IInteractive> interactive = { this };
Inject<io::IFileSystem> fileSystem = { this };

public:
ExtensionsProvider(const modularity::ContextPtr& iocCtx)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,11 @@ ManifestList ExtPluginsLoader::loadManifestList(const io::path_t& defPath, const
retList.push_back(m);
}

for (const Manifest& m : externalManifests) {
for (Manifest& m : externalManifests) {
if (!m.isValid()) {
continue;
}
m.isRemovable = true;
retList.push_back(m);
}

Expand All @@ -95,6 +96,7 @@ ManifestList ExtPluginsLoader::manifestList(const io::path_t& rootPath) const
if (!manifest.isValid()) {
continue;
}
manifest.path = path;
resolvePaths(manifest, io::FileInfo(path).dirPath());
manifests.push_back(manifest);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ Item {
background: flickable

isEnabled: Boolean(selectedPlugin) ? selectedPlugin.enabled : false
isRemovable: Boolean(selectedPlugin) ? selectedPlugin.isRemovable : false

additionalInfoModel: [
{"title": qsTrc("extensions", "Version:"), "value": Boolean(selectedPlugin) ? selectedPlugin.version : "" },
Expand All @@ -198,6 +199,12 @@ Item {
extensionsModel.selectExecPoint(selectedPlugin.uri, index)
}

onRemoveRequest: function() {
extensionsModel.removeExtension(selectedPlugin.uri)
prv.resetSelectedPlugin()
panel.close()
}

onEditShortcutRequested: {
Qt.callLater(extensionsModel.editShortcut, selectedPlugin.uri)
panel.close()
Expand Down
Loading
Loading