From ad5a96d95b5db71fbe5db75825297802643694f0 Mon Sep 17 00:00:00 2001 From: bbedward Date: Fri, 17 Jul 2026 10:18:51 -0400 Subject: [PATCH] sysupdate: disallow partial updates on arch, add ignored to popout port 1.5 (cherry picked from commit b17a90a4123df81edc82edfc52cbe4ce449a746c) --- .../server/sysupdate/backend_pacman.go | 39 +++++- core/internal/server/sysupdate/manager.go | 3 + .../server/sysupdate/upgrade_commands_test.go | 16 ++- .../DankBar/Popouts/SystemUpdatePopout.qml | 122 +++++++++++++++++- quickshell/Services/SystemUpdateService.qml | 31 +++-- 5 files changed, 194 insertions(+), 17 deletions(-) diff --git a/core/internal/server/sysupdate/backend_pacman.go b/core/internal/server/sysupdate/backend_pacman.go index 09070e522..17ddb59ef 100644 --- a/core/internal/server/sysupdate/backend_pacman.go +++ b/core/internal/server/sysupdate/backend_pacman.go @@ -50,11 +50,42 @@ func (b pacmanBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine } func pacmanUpgradeArgv(opts UpgradeOptions) []string { - argv := []string{"pacman", "-Syu", "--noconfirm", "--needed"} - if len(opts.Ignored) > 0 { - argv = append(argv, "--ignore", strings.Join(opts.Ignored, ",")) + return privilegedArgv(opts, "pacman", "-Syu", "--noconfirm", "--needed") +} + +// Dont allow partial updates on arch, if they wanna break their system they can do it outside of DMS: +// https://wiki.archlinux.org/title/System_maintenance#Partial_upgrades_are_unsupported +// AUR packages are exempt — holding those cannot break the repo dependency graph. +func dropPacmanRepoIgnores(ignored []string, pending []Package) []string { + if len(ignored) == 0 { + return ignored + } + repoPending := make(map[string]bool, len(pending)) + for _, p := range pending { + if p.Repo == RepoSystem { + repoPending[p.Name] = true + } + } + out := make([]string, 0, len(ignored)) + for _, name := range ignored { + if repoPending[name] { + continue + } + out = append(out, name) + } + return out +} + +func isPacmanFamily(b Backend) bool { + if b == nil { + return false + } + switch b.ID() { + case "pacman", "paru", "yay": + return true + default: + return false } - return privilegedArgv(opts, argv...) } type archHelperBackend struct { diff --git a/core/internal/server/sysupdate/manager.go b/core/internal/server/sysupdate/manager.go index f4fcedfad..811168ebf 100644 --- a/core/internal/server/sysupdate/manager.go +++ b/core/internal/server/sysupdate/manager.go @@ -345,6 +345,9 @@ func (m *Manager) runUpgrade(ctx context.Context, opts UpgradeOptions) { opts.Targets = append([]Package(nil), m.state.Packages...) m.mu.RUnlock() } + if isPacmanFamily(m.selection.System) { + opts.Ignored = dropPacmanRepoIgnores(opts.Ignored, opts.Targets) + } opts.Targets = dropIgnoredTargets(opts.Targets, opts.Ignored) backends := upgradeBackends(m.selection, opts) diff --git a/core/internal/server/sysupdate/upgrade_commands_test.go b/core/internal/server/sysupdate/upgrade_commands_test.go index df5e8a694..257022ee9 100644 --- a/core/internal/server/sysupdate/upgrade_commands_test.go +++ b/core/internal/server/sysupdate/upgrade_commands_test.go @@ -50,9 +50,9 @@ func TestUpgradeCommandBuilders(t *testing.T) { want: []string{"paru", "-Syu", "--noconfirm", "--needed", "--ignore", "linux,discord"}, }, { - name: "pacman with ignored packages", + name: "pacman never passes --ignore", got: pacmanUpgradeArgv(UpgradeOptions{Ignored: []string{"linux"}}), - want: []string{"pkexec", "pacman", "-Syu", "--noconfirm", "--needed", "--ignore", "linux"}, + want: []string{"pkexec", "pacman", "-Syu", "--noconfirm", "--needed"}, }, { name: "dnf with ignored packages", @@ -106,6 +106,18 @@ func TestUpgradeCommandBuilders(t *testing.T) { } } +func TestDropPacmanRepoIgnoresKeepsAURHolds(t *testing.T) { + pending := []Package{ + {Name: "linux", Repo: RepoSystem}, + {Name: "librewolf", Repo: RepoAUR}, + } + got := dropPacmanRepoIgnores([]string{"linux", "librewolf", "not-pending"}, pending) + want := []string{"librewolf", "not-pending"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("ignored = %#v, want %#v", got, want) + } +} + func TestAptUpgradeArgvHoldsIgnored(t *testing.T) { argv := aptUpgradeArgv("apt-get", UpgradeOptions{Ignored: []string{"linux-image-generic", "bad;name"}}) if len(argv) < 2 || argv[len(argv)-2] != "-c" { diff --git a/quickshell/Modules/DankBar/Popouts/SystemUpdatePopout.qml b/quickshell/Modules/DankBar/Popouts/SystemUpdatePopout.qml index 673f41d42..1153eef72 100644 --- a/quickshell/Modules/DankBar/Popouts/SystemUpdatePopout.qml +++ b/quickshell/Modules/DankBar/Popouts/SystemUpdatePopout.qml @@ -336,7 +336,10 @@ DankPopout { StyledText { id: statusText - anchors.fill: parent + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: ignoredSection.top anchors.margins: Theme.spacingM horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter @@ -361,7 +364,10 @@ DankPopout { DankListView { id: packagesList - anchors.fill: parent + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: ignoredSection.top anchors.margins: Theme.spacingS visible: !SystemUpdateService.isUpgrading && SystemUpdateService.updateCount > 0 && !SystemUpdateService.hasError && !SystemUpdateService.isChecking clip: true @@ -467,13 +473,123 @@ DankPopout { iconName: "visibility_off" iconSize: 16 iconColor: Theme.surfaceVariantText - visible: rowHoverHandler.hovered + visible: rowHoverHandler.hovered && SystemUpdateService.canIgnorePackage(packageRow.modelData) tooltipText: I18n.tr("Ignore this package") onClicked: SystemUpdateService.ignorePackage(packageRow.modelData.name) } } } + Column { + id: ignoredSection + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Theme.spacingS + spacing: Theme.spacingXS + + readonly property var ignoredNames: SettingsData.updaterIgnoredPackages || [] + readonly property bool shown: ignoredNames.length > 0 && !SystemUpdateService.isUpgrading && !SystemUpdateService.isChecking + property bool expanded: false + + visible: shown + height: shown ? implicitHeight : 0 + + Rectangle { + id: ignoredToggle + width: parent.width + height: 32 + radius: Theme.cornerRadius + color: ignoredToggleArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.surfaceContainer, 0.5) + + DankIcon { + id: ignoredToggleIcon + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + name: "visibility_off" + size: 16 + color: Theme.surfaceVariantText + } + + StyledText { + anchors.left: ignoredToggleIcon.right + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("Ignored (%1)").arg(ignoredSection.ignoredNames.length) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + + DankIcon { + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + name: ignoredSection.expanded ? "expand_less" : "expand_more" + size: 16 + color: Theme.surfaceVariantText + } + + MouseArea { + id: ignoredToggleArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: ignoredSection.expanded = !ignoredSection.expanded + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + DankListView { + width: parent.width + height: ignoredSection.expanded ? Math.min(contentHeight, 150) : 0 + visible: ignoredSection.expanded + clip: true + spacing: Theme.spacingXS + model: ignoredSection.ignoredNames + + delegate: Rectangle { + id: ignoredRow + width: ListView.view.width + height: 32 + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainer, 0.5) + + required property string modelData + + StyledText { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.right: restoreButton.left + anchors.verticalCenter: parent.verticalCenter + text: ignoredRow.modelData + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + elide: Text.ElideRight + } + + DankActionButton { + id: restoreButton + anchors.right: parent.right + anchors.rightMargin: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + buttonSize: 24 + iconName: "visibility" + iconSize: 16 + iconColor: Theme.surfaceVariantText + tooltipText: I18n.tr("Stop ignoring %1").arg(ignoredRow.modelData) + onClicked: SystemUpdateService.unignorePackage(ignoredRow.modelData) + } + } + } + } + Column { anchors.fill: parent anchors.margins: Theme.spacingM diff --git a/quickshell/Services/SystemUpdateService.qml b/quickshell/Services/SystemUpdateService.qml index b7dc5ba2f..a8ed949d5 100644 --- a/quickshell/Services/SystemUpdateService.qml +++ b/quickshell/Services/SystemUpdateService.qml @@ -35,6 +35,17 @@ Singleton { readonly property bool helperAvailable: sysupdateAvailable && backends.length > 0 readonly property bool useCustomCommand: SettingsData.updaterUseCustomCommand && (SettingsData.updaterCustomCommand || "").trim().length > 0 + // Dont allow partial updates on arch, if they wanna break their system they can do it outside of DMS: + // https://wiki.archlinux.org/title/System_maintenance#Partial_upgrades_are_unsupported + // AUR/Flatpak packages stay ignorable — holding those cannot break the repo dependency graph. + readonly property bool systemHoldsAllowed: !["pacman", "paru", "yay"].includes(pkgManager) + + function canIgnorePackage(pkg) { + if (!pkg) + return false; + return systemHoldsAllowed || pkg.repo !== "system"; + } + Connections { target: DMSService function onCapabilitiesReceived() { @@ -108,9 +119,11 @@ Singleton { if (!data) { return; } + backends = data.backends || []; + const systemBackend = backends.find(b => b.repo === "system" || b.repo === "ostree"); + pkgManager = systemBackend ? systemBackend.id : (backends.length > 0 ? backends[0].id : ""); _rawUpdates = data.packages || []; availableUpdates = _filterUpdates(_rawUpdates); - backends = data.backends || []; distribution = data.distro || ""; distributionPretty = data.distroPretty || ""; distributionSupported = (backends.length > 0); @@ -145,13 +158,6 @@ Singleton { errorCode = ""; errorHint = ""; } - - if (backends.length > 0) { - const sys = backends.find(b => b.repo === "system" || b.repo === "ostree"); - pkgManager = sys ? sys.id : backends[0].id; - } else { - pkgManager = ""; - } } function _filterUpdates(pkgs) { @@ -159,6 +165,8 @@ Singleton { return (pkgs || []).filter(p => { if (!SettingsData.updaterAllowAUR && p.repo === "aur") return false; + if (!canIgnorePackage(p)) + return true; return ignored.indexOf(p.name) === -1; }); } @@ -177,6 +185,13 @@ Singleton { SettingsData.set("updaterIgnoredPackages", list); } + function unignorePackage(name) { + if (!name) + return; + const list = (SettingsData.updaterIgnoredPackages || []).filter(p => p !== name); + SettingsData.set("updaterIgnoredPackages", list); + } + function checkForUpdates() { DMSService.sysupdateRefresh(false, null); }