fix: Reinstall doesn't remove files deleted by the new version — install merges into the existing directory

fixes #313
This commit is contained in:
Khushboo Mehta
2026-08-19 14:55:29 +02:00
committed by Khushboo-dev-cpp
parent 9118071484
commit d7c2f6e355
3 changed files with 90 additions and 26 deletions
@@ -78,11 +78,11 @@ Item {
LogosButton {
Layout.fillWidth: true
Layout.minimumWidth: 100
Layout.preferredWidth: 130
Layout.maximumWidth: 130
Layout.preferredWidth: implicitWidth
Layout.maximumWidth: implicitWidth
Layout.preferredHeight: 40
radius: Theme.spacing.radiusLarge
text: qsTr("Repositories")
text: qsTr("Manage Repositories")
onClicked: root.repositoriesClicked()
}
+77 -23
View File
@@ -604,30 +604,8 @@ QVariantMap PackageCoordinator::buildPlanPayload(const QStringList& batch,
// Cascade confirmation — triggered from QML once the user OKs the dialog.
// ---------------------------------------------------------------------------
void PackageCoordinator::confirmUninstallCascade(const QString& moduleName)
void PackageCoordinator::cascadeUnloadForPackage(const QString& moduleName)
{
if ((m_pendingAction.op != PendingOp::UninstallCascade &&
m_pendingAction.op != PendingOp::UpgradeCascade)
|| m_pendingAction.name != moduleName) {
qWarning() << "confirmUninstallCascade for" << moduleName
<< "but pending action is" << m_pendingAction.name;
return;
}
// Snapshot before clearing — the callbacks below capture by value.
const bool isUpgrade = (m_pendingAction.op == PendingOp::UpgradeCascade);
const QString releaseTag = m_pendingAction.releaseTag;
m_pendingAction = {};
// Defer the cascade body off the QML click stack: unloadModuleWithDependents
// spins a nested event loop, and running that under the dialog's onClicked
// handler trips a QQmlData::destroyed qFatal. Pending state is cleared above;
// queue the rest so the click handler unwinds first.
QPointer<PackageCoordinator> selfDefer(this);
QMetaObject::invokeMethod(this,
[this, selfDefer, moduleName, isUpgrade, releaseTag]() {
if (!selfDefer) return;
// Snapshot the loaded-dependents list BEFORE the cascade — once
// unloadModuleWithDependents returns, the target is off the loaded-
// modules list and the filter would come up empty. UI-plugin dependents
@@ -670,6 +648,29 @@ void PackageCoordinator::confirmUninstallCascade(const QString& moduleName)
}
m_uiPluginManager->teardownUiPluginWidget(moduleName);
}
}
void PackageCoordinator::confirmUninstallCascade(const QString& moduleName)
{
if ((m_pendingAction.op != PendingOp::UninstallCascade &&
m_pendingAction.op != PendingOp::UpgradeCascade)
|| m_pendingAction.name != moduleName) {
qWarning() << "confirmUninstallCascade for" << moduleName
<< "but pending action is" << m_pendingAction.name;
return;
}
// Snapshot before clearing — the callbacks below capture by value.
const bool isUpgrade = (m_pendingAction.op == PendingOp::UpgradeCascade);
const QString releaseTag = m_pendingAction.releaseTag;
m_pendingAction = {};
QPointer<PackageCoordinator> selfDefer(this);
QMetaObject::invokeMethod(this,
[this, selfDefer, moduleName, isUpgrade, releaseTag]() {
if (!selfDefer) return;
cascadeUnloadForPackage(moduleName);
// Hand the actual package-lifecycle work back to the module.
if (!m_logosAPI) return;
@@ -1928,6 +1929,59 @@ void PackageCoordinator::installOnePackage(const QVariantMap& dl,
return;
}
if (!m_logosAPI) {
if (onDone) onDone(false, QStringLiteral("package_manager not connected"));
return;
}
const bool alreadyInstalled = m_installedNameSet.contains(packageName);
const bool isEmbedded =
m_installTypeByModule.value(packageName) == QLatin1String("embedded");
// Never tear down or remove our own UI — same guard uninstallUiModule
// carries, for the same reason: it would brick Basecamp mid-install.
// AppsFilterProxy::excludeMainUi only hides it from the list; it is not a
// safety gate, and a resolver result can name it as a transitive entry.
// Falling through installs over it, which is the old merge behaviour —
// strictly better than deleting the running UI.
const bool isSelf = (packageName == QStringLiteral("main_ui"));
if (isSelf && alreadyInstalled) {
qWarning() << "Refusing to remove main_ui before install; "
"installing over it instead";
}
if (alreadyInstalled && !isEmbedded && !isSelf) {
cascadeUnloadForPackage(packageName);
LogosModules logos(m_logosAPI);
QPointer<PackageCoordinator> self(this);
logos.package_manager.uninstallPackageAsync(packageName,
[self, dl, packageName, onDone](QVariantMap uninstallResult) {
if (!self) return;
if (!uninstallResult.value("success", false).toBool()) {
const QString err = uninstallResult.value("error").toString();
qWarning() << "Pre-install removal of" << packageName
<< "failed, aborting install:" << err;
if (onDone)
onDone(false, err.isEmpty()
? QStringLiteral("Could not remove the installed version")
: err);
return;
}
self->installDownloadedFile(dl, onDone);
});
return;
}
installDownloadedFile(dl, onDone);
}
void PackageCoordinator::installDownloadedFile(const QVariantMap& dl,
std::function<void(bool, const QString&)> onDone)
{
const QString packageName = dl.value("name").toString();
const QString filePath = dl.value("path").toString();
LogosModules logos(m_logosAPI);
QPointer<PackageCoordinator> self(this);
// Installing was left on the default 20 s IPC deadline while DOWNLOADING
+10
View File
@@ -370,8 +370,18 @@ private:
const QString& topLevelName,
int index,
QStringList failures = QStringList{});
// Unload/tear down a package and its loaded dependents ahead of any
// destructive package-lifecycle work. Shared by confirmUninstallCascade
// (the gated uninstall/upgrade flow) and by the App Manager's
// replace-before-install step, so both run identical teardown.
void cascadeUnloadForPackage(const QString& moduleName);
// installOnePackage tears down + removes an already-installed package
// before handing off to installDownloadedFile, the bare installPlugin call.
void installOnePackage(const QVariantMap& downloadResult,
std::function<void(bool, const QString&)> onDone);
void installDownloadedFile(const QVariantMap& downloadResult,
std::function<void(bool, const QString&)> onDone);
// Drive the in-flight registry. setOpStage updates the InstallRegistry entry
// and emits catalogInstallStageChanged.