mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-01 19:18:28 -04:00
feat(dms updater): add support for ignoring specific packages during system updates
- Added UI component & popout settings to manage ignored packages - Added CLI support available in danklinux docs Fixes: #2827 Closes: #2344, #1741 Port 1.5
This commit is contained in:
@@ -47,6 +47,7 @@ var (
|
||||
sysUpdateJSON bool
|
||||
sysUpdateNoFlatpak bool
|
||||
sysUpdateNoAUR bool
|
||||
sysUpdateIgnore []string
|
||||
sysUpdateIntervalS int
|
||||
sysUpdateListPmTime = 5 * time.Minute
|
||||
)
|
||||
@@ -58,6 +59,7 @@ func init() {
|
||||
systemUpdateCmd.Flags().BoolVar(&sysUpdateJSON, "json", false, "Output as JSON (with --check)")
|
||||
systemUpdateCmd.Flags().BoolVar(&sysUpdateNoFlatpak, "no-flatpak", false, "Skip the Flatpak overlay")
|
||||
systemUpdateCmd.Flags().BoolVar(&sysUpdateNoAUR, "no-aur", false, "Skip the AUR (paru/yay only)")
|
||||
systemUpdateCmd.Flags().StringSliceVar(&sysUpdateIgnore, "ignore", nil, "Skip specific packages (repeatable or comma-separated)")
|
||||
systemUpdateCmd.Flags().IntVar(&sysUpdateIntervalS, "interval", -1, "Set the DMS server poll interval in seconds and exit (requires running server)")
|
||||
|
||||
systemCmd.AddCommand(systemUpdateCmd)
|
||||
@@ -192,6 +194,7 @@ func runSystemUpdateApply() {
|
||||
Targets: pkgs,
|
||||
IncludeFlatpak: !sysUpdateNoFlatpak,
|
||||
IncludeAUR: !sysUpdateNoAUR,
|
||||
Ignored: sysUpdateIgnore,
|
||||
DryRun: sysUpdateDry,
|
||||
UseSudo: true,
|
||||
}
|
||||
@@ -234,12 +237,19 @@ func collectUpdates(ctx context.Context, backends []sysupdate.Backend) ([]sysupd
|
||||
}
|
||||
|
||||
func filterUpdateTargets(pkgs []sysupdate.Package) []sysupdate.Package {
|
||||
if !sysUpdateNoAUR {
|
||||
if !sysUpdateNoAUR && len(sysUpdateIgnore) == 0 {
|
||||
return pkgs
|
||||
}
|
||||
ignored := make(map[string]bool, len(sysUpdateIgnore))
|
||||
for _, name := range sysUpdateIgnore {
|
||||
ignored[name] = true
|
||||
}
|
||||
out := pkgs[:0]
|
||||
for _, p := range pkgs {
|
||||
if p.Repo == sysupdate.RepoAUR {
|
||||
if sysUpdateNoAUR && p.Repo == sysupdate.RepoAUR {
|
||||
continue
|
||||
}
|
||||
if ignored[p.Name] {
|
||||
continue
|
||||
}
|
||||
out = append(out, p)
|
||||
|
||||
@@ -2,6 +2,7 @@ package sysupdate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -31,7 +32,36 @@ func (aptBackend) CheckUpdates(ctx context.Context) ([]Package, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseAptUpgradable(string(out)), nil
|
||||
return filterAptHeld(parseAptUpgradable(string(out)), aptHeldPackages(ctx)), nil
|
||||
}
|
||||
|
||||
// aptHeldPackages returns held packages, which apt-get upgrade never applies.
|
||||
func aptHeldPackages(ctx context.Context) map[string]bool {
|
||||
out, err := exec.CommandContext(ctx, "apt-mark", "showhold").Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
held := make(map[string]bool)
|
||||
for line := range strings.SplitSeq(string(out), "\n") {
|
||||
if name := strings.TrimSpace(line); name != "" {
|
||||
held[name] = true
|
||||
}
|
||||
}
|
||||
return held
|
||||
}
|
||||
|
||||
func filterAptHeld(pkgs []Package, held map[string]bool) []Package {
|
||||
if len(held) == 0 {
|
||||
return pkgs
|
||||
}
|
||||
out := pkgs[:0]
|
||||
for _, p := range pkgs {
|
||||
if held[p.Name] {
|
||||
continue
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (aptBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error {
|
||||
@@ -52,7 +82,22 @@ func (aptBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(
|
||||
}
|
||||
|
||||
func aptUpgradeArgv(bin string, opts UpgradeOptions) []string {
|
||||
return privilegedArgv(opts, "env", "DEBIAN_FRONTEND=noninteractive", "LC_ALL=C", bin, "upgrade", "-y")
|
||||
ignored := shellSafeNames(opts.Ignored)
|
||||
if len(ignored) == 0 {
|
||||
return privilegedArgv(opts, "env", "DEBIAN_FRONTEND=noninteractive", "LC_ALL=C", bin, "upgrade", "-y")
|
||||
}
|
||||
return privilegedArgv(opts, "env", "DEBIAN_FRONTEND=noninteractive", "LC_ALL=C", "sh", "-c", aptHoldScript(bin, ignored))
|
||||
}
|
||||
|
||||
// aptHoldScript holds ignored packages only for the upgrade, leaving pre-existing user holds untouched.
|
||||
func aptHoldScript(bin string, ignored []string) string {
|
||||
names := strings.Join(ignored, " ")
|
||||
return fmt.Sprintf(
|
||||
`new=""; for p in %s; do apt-mark showhold | grep -qx "$p" || new="$new $p"; done; `+
|
||||
`[ -n "$new" ] && apt-mark hold $new; `+
|
||||
`%s upgrade -y; rc=$?; `+
|
||||
`[ -n "$new" ] && apt-mark unhold $new; exit $rc`,
|
||||
names, bin)
|
||||
}
|
||||
|
||||
func parseAptUpgradable(text string) []Package {
|
||||
|
||||
@@ -70,3 +70,22 @@ libsdl2-2.0-0/stable 2.30.0+dfsg-1 amd64 [upgradable from: 2.28.5+dfsg-1]`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterAptHeld(t *testing.T) {
|
||||
pkgs := []Package{
|
||||
{Name: "bash", Repo: RepoSystem, Backend: "apt"},
|
||||
{Name: "linux-image-generic", Repo: RepoSystem, Backend: "apt"},
|
||||
{Name: "zsh", Repo: RepoSystem, Backend: "apt"},
|
||||
}
|
||||
|
||||
got := filterAptHeld(append([]Package(nil), pkgs...), map[string]bool{"linux-image-generic": true})
|
||||
want := []Package{pkgs[0], pkgs[2]}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("filterAptHeld() = %#v\nwant %#v", got, want)
|
||||
}
|
||||
|
||||
unfiltered := filterAptHeld(append([]Package(nil), pkgs...), nil)
|
||||
if !reflect.DeepEqual(unfiltered, pkgs) {
|
||||
t.Errorf("filterAptHeld(nil held) = %#v\nwant %#v", unfiltered, pkgs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package sysupdate
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
@@ -52,7 +53,11 @@ func (b dnfBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine fun
|
||||
}
|
||||
|
||||
func dnfUpgradeArgv(bin string, opts UpgradeOptions) []string {
|
||||
return privilegedArgv(opts, bin, "upgrade", "--refresh", "-y")
|
||||
argv := []string{bin, "upgrade", "--refresh", "-y"}
|
||||
if len(opts.Ignored) > 0 {
|
||||
argv = append(argv, "--exclude="+strings.Join(opts.Ignored, ","))
|
||||
}
|
||||
return privilegedArgv(opts, argv...)
|
||||
}
|
||||
|
||||
func dnfListUpgrades(ctx context.Context, bin string) (string, error) {
|
||||
@@ -65,9 +70,22 @@ func dnfListUpgrades(ctx context.Context, bin string) (string, error) {
|
||||
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok && exitErr.ExitCode() == 100 {
|
||||
return string(out), nil
|
||||
}
|
||||
if detail := lastNonEmptyLine(string(out)); detail != "" {
|
||||
return "", fmt.Errorf("%w: %s", err, detail)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func lastNonEmptyLine(text string) string {
|
||||
lines := strings.Split(text, "\n")
|
||||
for i := len(lines) - 1; i >= 0; i-- {
|
||||
if line := strings.TrimSpace(lines[i]); line != "" {
|
||||
return line
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func dnfCheckUpdatesArgv(bin string) []string {
|
||||
subcommand := "check-update"
|
||||
if bin == "dnf5" {
|
||||
|
||||
@@ -95,11 +95,21 @@ func (flatpakBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine f
|
||||
if !BackendHasTargets(flatpakBackend{}, opts.Targets, opts.IncludeAUR, opts.IncludeFlatpak) {
|
||||
return nil
|
||||
}
|
||||
return Run(ctx, flatpakUpgradeArgv(), RunOptions{OnLine: onLine})
|
||||
return Run(ctx, flatpakUpgradeArgv(opts), RunOptions{OnLine: onLine})
|
||||
}
|
||||
|
||||
func flatpakUpgradeArgv() []string {
|
||||
return []string{"flatpak", "update", "-y", "--noninteractive"}
|
||||
func flatpakUpgradeArgv(opts UpgradeOptions) []string {
|
||||
argv := []string{"flatpak", "update", "-y", "--noninteractive"}
|
||||
if len(opts.Ignored) == 0 {
|
||||
return argv
|
||||
}
|
||||
// No exclude flag; update the already-filtered refs explicitly.
|
||||
for _, p := range opts.Targets {
|
||||
if p.Repo == RepoFlatpak && p.Ref != "" {
|
||||
argv = append(argv, p.Ref)
|
||||
}
|
||||
}
|
||||
return argv
|
||||
}
|
||||
|
||||
func parseFlatpakUpdateOutput(text string, installed map[string]flatpakInstalledEntry) []Package {
|
||||
|
||||
@@ -50,7 +50,11 @@ func (b pacmanBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine
|
||||
}
|
||||
|
||||
func pacmanUpgradeArgv(opts UpgradeOptions) []string {
|
||||
return privilegedArgv(opts, "pacman", "-Syu", "--noconfirm", "--needed")
|
||||
argv := []string{"pacman", "-Syu", "--noconfirm", "--needed"}
|
||||
if len(opts.Ignored) > 0 {
|
||||
argv = append(argv, "--ignore", strings.Join(opts.Ignored, ","))
|
||||
}
|
||||
return privilegedArgv(opts, argv...)
|
||||
}
|
||||
|
||||
type archHelperBackend struct {
|
||||
@@ -99,23 +103,27 @@ func (b archHelperBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onL
|
||||
return nil
|
||||
}
|
||||
if os.Getenv("DMS_FORCE_PKEXEC") == "1" {
|
||||
argv := append([]string{"pkexec"}, archHelperUpgradeArgv(b.id, opts.IncludeAUR)...)
|
||||
argv := append([]string{"pkexec"}, archHelperUpgradeArgv(b.id, opts.IncludeAUR, opts.Ignored)...)
|
||||
return Run(ctx, argv, RunOptions{OnLine: onLine, AttachStdio: opts.AttachStdio})
|
||||
}
|
||||
term := findTerminal(opts.Terminal)
|
||||
if term == "" {
|
||||
return fmt.Errorf("no terminal found (pick one in DMS settings, set $TERMINAL, or install kitty/ghostty/foot/alacritty)")
|
||||
}
|
||||
cmd := strings.Join(archHelperUpgradeArgv(b.id, opts.IncludeAUR), " ")
|
||||
cmd := strings.Join(archHelperUpgradeArgv(b.id, opts.IncludeAUR, opts.Ignored), " ")
|
||||
title := fmt.Sprintf("DMS — System Update (%s)", b.id)
|
||||
return Run(ctx, wrapInTerminal(term, title, cmd), RunOptions{OnLine: onLine})
|
||||
}
|
||||
|
||||
func archHelperUpgradeArgv(id string, includeAUR bool) []string {
|
||||
func archHelperUpgradeArgv(id string, includeAUR bool, ignored []string) []string {
|
||||
argv := []string{id, "-Syu", "--noconfirm", "--needed"}
|
||||
if !includeAUR {
|
||||
argv = append(argv, "--repo")
|
||||
}
|
||||
ignored = shellSafeNames(ignored)
|
||||
if len(ignored) > 0 {
|
||||
argv = append(argv, "--ignore", strings.Join(ignored, ","))
|
||||
}
|
||||
return argv
|
||||
}
|
||||
|
||||
@@ -248,6 +256,10 @@ func parseArchUpdates(text, backendID string, repo RepoKind) []Package {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
// pacman -Qu / paru -Qua flag IgnorePkg entries with a trailing marker
|
||||
if strings.HasSuffix(line, "[ignored]") {
|
||||
continue
|
||||
}
|
||||
m := archUpdateLine.FindStringSubmatch(line)
|
||||
if m == nil {
|
||||
continue
|
||||
|
||||
@@ -92,6 +92,17 @@ foo`,
|
||||
{Name: "bat", Repo: RepoSystem, Backend: "pacman", FromVersion: "0.26.0-1", ToVersion: "0.26.1-2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "skips IgnorePkg entries",
|
||||
input: `bat 0.26.0-1 -> 0.26.1-2
|
||||
linux 6.18.0-1 -> 6.18.1-1 [ignored]
|
||||
discord 0.0.108-1 -> 0.0.109-1 [ignored]`,
|
||||
backendID: "pacman",
|
||||
repo: RepoSystem,
|
||||
want: []Package{
|
||||
{Name: "bat", Repo: RepoSystem, Backend: "pacman", FromVersion: "0.26.0-1", ToVersion: "0.26.1-2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "extra whitespace tolerated",
|
||||
input: " bat 0.26.0-1 -> 0.26.1-2 ",
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -81,5 +83,20 @@ func (zypperBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine fu
|
||||
}
|
||||
|
||||
func zypperUpgradeArgv(opts UpgradeOptions) []string {
|
||||
return privilegedArgv(opts, "zypper", "--non-interactive", "update")
|
||||
ignored := shellSafeNames(opts.Ignored)
|
||||
if len(ignored) == 0 {
|
||||
return privilegedArgv(opts, "zypper", "--non-interactive", "update")
|
||||
}
|
||||
return privilegedArgv(opts, "sh", "-c", zypperLockScript(ignored))
|
||||
}
|
||||
|
||||
// zypperLockScript locks ignored packages only for the update, leaving pre-existing user locks untouched.
|
||||
func zypperLockScript(ignored []string) string {
|
||||
names := strings.Join(ignored, " ")
|
||||
return fmt.Sprintf(
|
||||
`new=""; for p in %s; do grep -qsE "^solvable_name:[[:space:]]*$p$" /etc/zypp/locks || new="$new $p"; done; `+
|
||||
`[ -n "$new" ] && zypper --non-interactive al $new; `+
|
||||
`zypper --non-interactive update; rc=$?; `+
|
||||
`[ -n "$new" ] && zypper --non-interactive rl $new; exit $rc`,
|
||||
names)
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ func handleUpgrade(conn net.Conn, req models.Request, m *Manager) {
|
||||
DryRun: params.BoolOpt(req.Params, "dry", false),
|
||||
CustomCommand: params.StringOpt(req.Params, "customCommand", ""),
|
||||
Terminal: params.StringOpt(req.Params, "terminal", ""),
|
||||
Ignored: stringSliceOpt(req.Params, "ignored"),
|
||||
}
|
||||
if err := m.Upgrade(opts); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
@@ -53,3 +54,21 @@ func handleUpgrade(conn net.Conn, req models.Request, m *Manager) {
|
||||
}
|
||||
models.Respond(conn, req.ID, m.GetState())
|
||||
}
|
||||
|
||||
func stringSliceOpt(p map[string]any, key string) []string {
|
||||
val, ok := params.Any(p, key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
arr, ok := val.([]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(arr))
|
||||
for _, v := range arr {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ const (
|
||||
minIntervalSeconds = 5 * 60
|
||||
recentLogCapacity = 200
|
||||
checkTimeout = 5 * time.Minute
|
||||
retryIntervalSeconds = 5 * 60
|
||||
upgradeTimeout = 30 * time.Minute
|
||||
postUpgradeCompleteDelay = 3 * time.Second
|
||||
)
|
||||
@@ -150,7 +151,7 @@ func (m *Manager) Refresh(opts RefreshOptions) {
|
||||
m.refreshSerial.Unlock()
|
||||
return
|
||||
}
|
||||
m.runRefresh(context.Background())
|
||||
m.runRefresh(context.Background(), true)
|
||||
}
|
||||
|
||||
func (m *Manager) Upgrade(opts UpgradeOptions) error {
|
||||
@@ -237,12 +238,12 @@ func (m *Manager) scheduler() {
|
||||
case <-m.wakeSched:
|
||||
t.Stop()
|
||||
case <-t.C:
|
||||
m.runRefresh(context.Background())
|
||||
m.runRefresh(context.Background(), false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) runRefresh(parent context.Context) {
|
||||
func (m *Manager) runRefresh(parent context.Context, manual bool) {
|
||||
m.refreshSerial.Lock()
|
||||
defer m.refreshSerial.Unlock()
|
||||
|
||||
@@ -284,27 +285,43 @@ func (m *Manager) runRefresh(parent context.Context) {
|
||||
now := time.Now().Unix()
|
||||
m.mu.Lock()
|
||||
m.state.LastCheckUnix = now
|
||||
m.state.Packages = m.state.Packages[:0]
|
||||
prev := m.state.Packages
|
||||
next := make([]Package, 0, len(prev))
|
||||
var firstErr error
|
||||
for i, r := range results {
|
||||
if r.err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = fmt.Errorf("%s: %w", backends[i].ID(), r.err)
|
||||
}
|
||||
// Retain a failed backend's last known packages so a transient failure doesn't wipe the list.
|
||||
for _, p := range prev {
|
||||
if p.Backend == backends[i].ID() {
|
||||
next = append(next, p)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
m.state.Packages = append(m.state.Packages, r.pkgs...)
|
||||
next = append(next, r.pkgs...)
|
||||
}
|
||||
m.state.Count = len(m.state.Packages)
|
||||
m.state.Packages = next
|
||||
m.state.Count = len(next)
|
||||
m.state.NextCheckUnix = now + int64(m.state.IntervalSeconds)
|
||||
if firstErr != nil {
|
||||
m.state.Phase = PhaseError
|
||||
m.state.Error = &ErrorInfo{Code: ErrCodeBackendFailed, Message: firstErr.Error()}
|
||||
} else {
|
||||
switch {
|
||||
case firstErr == nil:
|
||||
m.state.Phase = PhaseIdle
|
||||
m.state.LastSuccessUnix = now
|
||||
case manual:
|
||||
m.state.Phase = PhaseError
|
||||
m.state.Error = &ErrorInfo{Code: ErrCodeBackendFailed, Message: firstErr.Error()}
|
||||
default:
|
||||
// Background checks fail silently and retry sooner; only manual refreshes surface errors.
|
||||
m.state.Phase = PhaseIdle
|
||||
retry := min(int64(m.state.IntervalSeconds), retryIntervalSeconds)
|
||||
m.state.NextCheckUnix = now + retry
|
||||
log.Warnf("[sysupdate] background check failed, retrying in %ds: %v", retry, firstErr)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
m.wake()
|
||||
m.markDirty()
|
||||
}
|
||||
|
||||
@@ -328,10 +345,15 @@ func (m *Manager) runUpgrade(ctx context.Context, opts UpgradeOptions) {
|
||||
opts.Targets = append([]Package(nil), m.state.Packages...)
|
||||
m.mu.RUnlock()
|
||||
}
|
||||
opts.Targets = dropIgnoredTargets(opts.Targets, opts.Ignored)
|
||||
|
||||
backends := upgradeBackends(m.selection, opts)
|
||||
if len(backends) == 0 {
|
||||
m.setError(ErrCodeNoBackend, "no backend selected for upgrade")
|
||||
if len(opts.Targets) > 0 {
|
||||
m.setError(ErrCodeNoBackend, "all pending updates are excluded by current settings (AUR/Flatpak disabled)")
|
||||
} else {
|
||||
m.setError(ErrCodeNoBackend, "no backend selected for upgrade")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -429,6 +451,24 @@ func (m *Manager) finishSuccessfulUpgrade(clearPackages bool) {
|
||||
m.markDirty()
|
||||
}
|
||||
|
||||
func dropIgnoredTargets(targets []Package, ignored []string) []Package {
|
||||
if len(ignored) == 0 {
|
||||
return targets
|
||||
}
|
||||
skip := make(map[string]bool, len(ignored))
|
||||
for _, name := range ignored {
|
||||
skip[name] = true
|
||||
}
|
||||
out := targets[:0]
|
||||
for _, p := range targets {
|
||||
if skip[p.Name] {
|
||||
continue
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func upgradeBackends(sel Selection, opts UpgradeOptions) []Backend {
|
||||
var out []Backend
|
||||
if sel.System != nil {
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
package sysupdate
|
||||
|
||||
import "regexp"
|
||||
|
||||
var safePkgName = regexp.MustCompile(`^[A-Za-z0-9@._+:-]+$`)
|
||||
|
||||
// shellSafeNames drops names unsafe to interpolate into the apt/zypper sh -c scripts.
|
||||
func shellSafeNames(names []string) []string {
|
||||
out := make([]string, 0, len(names))
|
||||
for _, n := range names {
|
||||
if safePkgName.MatchString(n) {
|
||||
out = append(out, n)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func BackendHasTargets(b Backend, targets []Package, includeAUR, includeFlatpak bool) bool {
|
||||
if b == nil || len(targets) == 0 {
|
||||
return false
|
||||
|
||||
@@ -81,6 +81,7 @@ type UpgradeOptions struct {
|
||||
CustomCommand string
|
||||
Terminal string
|
||||
Targets []Package
|
||||
Ignored []string
|
||||
}
|
||||
|
||||
type RefreshOptions struct {
|
||||
|
||||
@@ -2,6 +2,7 @@ package sysupdate
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -35,19 +36,55 @@ func TestUpgradeCommandBuilders(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "aur helper full update with aur",
|
||||
got: archHelperUpgradeArgv("paru", true),
|
||||
got: archHelperUpgradeArgv("paru", true, nil),
|
||||
want: []string{"paru", "-Syu", "--noconfirm", "--needed"},
|
||||
},
|
||||
{
|
||||
name: "aur helper repo-only full update",
|
||||
got: archHelperUpgradeArgv("yay", false),
|
||||
got: archHelperUpgradeArgv("yay", false, nil),
|
||||
want: []string{"yay", "-Syu", "--noconfirm", "--needed", "--repo"},
|
||||
},
|
||||
{
|
||||
name: "aur helper with ignored packages",
|
||||
got: archHelperUpgradeArgv("paru", true, []string{"linux", "bad;name", "discord"}),
|
||||
want: []string{"paru", "-Syu", "--noconfirm", "--needed", "--ignore", "linux,discord"},
|
||||
},
|
||||
{
|
||||
name: "pacman with ignored packages",
|
||||
got: pacmanUpgradeArgv(UpgradeOptions{Ignored: []string{"linux"}}),
|
||||
want: []string{"pkexec", "pacman", "-Syu", "--noconfirm", "--needed", "--ignore", "linux"},
|
||||
},
|
||||
{
|
||||
name: "dnf with ignored packages",
|
||||
got: dnfUpgradeArgv("dnf5", UpgradeOptions{Ignored: []string{"kernel", "mesa"}}),
|
||||
want: []string{"pkexec", "dnf5", "upgrade", "--refresh", "-y", "--exclude=kernel,mesa"},
|
||||
},
|
||||
{
|
||||
name: "apt without ignored uses plain upgrade",
|
||||
got: aptUpgradeArgv("apt-get", UpgradeOptions{}),
|
||||
want: []string{"pkexec", "env", "DEBIAN_FRONTEND=noninteractive", "LC_ALL=C", "apt-get", "upgrade", "-y"},
|
||||
},
|
||||
{
|
||||
name: "zypper without ignored uses plain update",
|
||||
got: zypperUpgradeArgv(UpgradeOptions{}),
|
||||
want: []string{"pkexec", "zypper", "--non-interactive", "update"},
|
||||
},
|
||||
{
|
||||
name: "flatpak full update",
|
||||
got: flatpakUpgradeArgv(),
|
||||
got: flatpakUpgradeArgv(UpgradeOptions{}),
|
||||
want: []string{"flatpak", "update", "-y", "--noninteractive"},
|
||||
},
|
||||
{
|
||||
name: "flatpak update with ignored targets refs",
|
||||
got: flatpakUpgradeArgv(UpgradeOptions{
|
||||
Ignored: []string{"org.mozilla.firefox"},
|
||||
Targets: []Package{
|
||||
{Name: "Discord", Repo: RepoFlatpak, Ref: "com.discordapp.Discord//stable"},
|
||||
{Name: "bash", Repo: RepoSystem, Backend: "apt"},
|
||||
},
|
||||
}),
|
||||
want: []string{"flatpak", "update", "-y", "--noninteractive", "com.discordapp.Discord//stable"},
|
||||
},
|
||||
{
|
||||
name: "rpm-ostree upgrade",
|
||||
got: rpmOstreeUpgradeArgv(UpgradeOptions{}),
|
||||
@@ -69,6 +106,45 @@ func TestUpgradeCommandBuilders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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" {
|
||||
t.Fatalf("expected sh -c script, got %#v", argv)
|
||||
}
|
||||
script := argv[len(argv)-1]
|
||||
if !strings.Contains(script, "apt-mark hold") || !strings.Contains(script, "apt-mark unhold") {
|
||||
t.Fatalf("hold script missing hold/unhold: %q", script)
|
||||
}
|
||||
if !strings.Contains(script, "linux-image-generic") {
|
||||
t.Fatalf("hold script missing ignored package: %q", script)
|
||||
}
|
||||
if strings.Contains(script, "bad;name") {
|
||||
t.Fatalf("hold script must drop unsafe name: %q", script)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZypperUpgradeArgvLocksIgnored(t *testing.T) {
|
||||
argv := zypperUpgradeArgv(UpgradeOptions{Ignored: []string{"kernel-default"}})
|
||||
if len(argv) < 2 || argv[len(argv)-2] != "-c" {
|
||||
t.Fatalf("expected sh -c script, got %#v", argv)
|
||||
}
|
||||
script := argv[len(argv)-1]
|
||||
if !strings.Contains(script, "zypper --non-interactive al") || !strings.Contains(script, "zypper --non-interactive rl") {
|
||||
t.Fatalf("lock script missing add/remove lock: %q", script)
|
||||
}
|
||||
if !strings.Contains(script, "kernel-default") {
|
||||
t.Fatalf("lock script missing ignored package: %q", script)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellSafeNames(t *testing.T) {
|
||||
got := shellSafeNames([]string{"linux", "gtk+", "bad name", "rm -rf /", "org.mozilla.firefox", "a;b"})
|
||||
want := []string{"linux", "gtk+", "org.mozilla.firefox"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("shellSafeNames() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackendHasTargetsRespectsBackendAndOptions(t *testing.T) {
|
||||
targets := []Package{
|
||||
{Name: "bash.x86_64", Repo: RepoSystem, Backend: "dnf5"},
|
||||
|
||||
Reference in New Issue
Block a user