240 lines
6.0 KiB
Go
240 lines
6.0 KiB
Go
package updates
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type PackageUpdate struct {
|
|
Name string
|
|
CurrentVersion string
|
|
NewVersion string
|
|
}
|
|
|
|
func detectPM() string {
|
|
for _, pm := range []string{"apt-get", "dnf", "yum", "pacman", "zypper", "apk"} {
|
|
if _, err := exec.LookPath(pm); err == nil {
|
|
if pm == "apt-get" {
|
|
return "apt"
|
|
}
|
|
return pm
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// CheckAvailable returns the list of packages with available upgrades.
|
|
// Returns nil, nil when no supported package manager is found.
|
|
func CheckAvailable() ([]PackageUpdate, error) {
|
|
switch detectPM() {
|
|
case "apt":
|
|
return checkApt()
|
|
case "dnf":
|
|
return checkDnfYum("dnf")
|
|
case "yum":
|
|
return checkDnfYum("yum")
|
|
case "pacman":
|
|
return checkPacman()
|
|
case "zypper":
|
|
return checkZypper()
|
|
case "apk":
|
|
return checkApk()
|
|
default:
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
// ApplyAll runs a full non-interactive upgrade using the detected package manager.
|
|
func ApplyAll() error {
|
|
switch detectPM() {
|
|
case "apt":
|
|
// Refresh lists first, then upgrade.
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
defer cancel()
|
|
if err := exec.CommandContext(ctx, "apt-get", "update", "-qq").Run(); err != nil {
|
|
return err
|
|
}
|
|
return exec.CommandContext(ctx, "apt-get", "upgrade", "-y").Run()
|
|
case "dnf":
|
|
return exec.Command("dnf", "upgrade", "-y").Run()
|
|
case "yum":
|
|
return exec.Command("yum", "upgrade", "-y").Run()
|
|
case "pacman":
|
|
return exec.Command("pacman", "-Syu", "--noconfirm").Run()
|
|
case "zypper":
|
|
return exec.Command("zypper", "update", "-y").Run()
|
|
case "apk":
|
|
return exec.Command("apk", "upgrade").Run()
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func checkApt() ([]PackageUpdate, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
defer cancel()
|
|
// Best-effort refresh; ignore errors (cached data is fine).
|
|
exec.CommandContext(ctx, "apt-get", "update", "-qq").Run() //nolint:errcheck
|
|
|
|
out, err := exec.Command("apt", "list", "--upgradable").Output()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var updates []PackageUpdate
|
|
scanner := bufio.NewScanner(bytes.NewReader(out))
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
// Format: package/suite version arch [upgradable from: old-ver]
|
|
if !strings.Contains(line, "[upgradable from:") {
|
|
continue
|
|
}
|
|
parts := strings.Fields(line)
|
|
if len(parts) < 2 {
|
|
continue
|
|
}
|
|
name := strings.SplitN(parts[0], "/", 2)[0]
|
|
newVer := parts[1]
|
|
oldVer := ""
|
|
if idx := strings.Index(line, "upgradable from: "); idx != -1 {
|
|
rest := line[idx+len("upgradable from: "):]
|
|
oldVer = strings.TrimSuffix(strings.TrimSpace(rest), "]")
|
|
}
|
|
updates = append(updates, PackageUpdate{Name: name, CurrentVersion: oldVer, NewVersion: newVer})
|
|
}
|
|
return updates, nil
|
|
}
|
|
|
|
func checkDnfYum(pm string) ([]PackageUpdate, error) {
|
|
cmd := exec.Command(pm, "check-update")
|
|
out, err := cmd.Output()
|
|
// Exit code 100 means updates are available — not an error.
|
|
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 100 {
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var updates []PackageUpdate
|
|
pastHeader := false
|
|
scanner := bufio.NewScanner(bytes.NewReader(out))
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if !pastHeader {
|
|
if strings.TrimSpace(line) == "" {
|
|
pastHeader = true
|
|
}
|
|
continue
|
|
}
|
|
parts := strings.Fields(line)
|
|
if len(parts) < 2 {
|
|
continue
|
|
}
|
|
// name.arch new-version repo
|
|
name := strings.SplitN(parts[0], ".", 2)[0]
|
|
updates = append(updates, PackageUpdate{Name: name, NewVersion: parts[1]})
|
|
}
|
|
return updates, nil
|
|
}
|
|
|
|
func checkPacman() ([]PackageUpdate, error) {
|
|
out, _ := exec.Command("pacman", "-Qu").Output()
|
|
var updates []PackageUpdate
|
|
scanner := bufio.NewScanner(bytes.NewReader(out))
|
|
for scanner.Scan() {
|
|
parts := strings.Fields(scanner.Text())
|
|
// Format: package old-version -> new-version
|
|
if len(parts) < 4 {
|
|
continue
|
|
}
|
|
updates = append(updates, PackageUpdate{Name: parts[0], CurrentVersion: parts[1], NewVersion: parts[3]})
|
|
}
|
|
return updates, nil
|
|
}
|
|
|
|
func checkZypper() ([]PackageUpdate, error) {
|
|
out, err := exec.Command("zypper", "list-updates").Output()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var updates []PackageUpdate
|
|
scanner := bufio.NewScanner(bytes.NewReader(out))
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
// Data rows start with "v |" (available) or "i |" (installed but updatable).
|
|
if !strings.HasPrefix(line, "v |") && !strings.HasPrefix(line, "i |") {
|
|
continue
|
|
}
|
|
parts := strings.Split(line, "|")
|
|
if len(parts) < 5 {
|
|
continue
|
|
}
|
|
updates = append(updates, PackageUpdate{
|
|
Name: strings.TrimSpace(parts[2]),
|
|
CurrentVersion: strings.TrimSpace(parts[3]),
|
|
NewVersion: strings.TrimSpace(parts[4]),
|
|
})
|
|
}
|
|
return updates, nil
|
|
}
|
|
|
|
func checkApk() ([]PackageUpdate, error) {
|
|
out, err := exec.Command("apk", "list", "--upgradable").Output()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var updates []PackageUpdate
|
|
scanner := bufio.NewScanner(bytes.NewReader(out))
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if !strings.Contains(line, "[upgradable") {
|
|
continue
|
|
}
|
|
parts := strings.Fields(line)
|
|
if len(parts) < 1 {
|
|
continue
|
|
}
|
|
pkgVer := parts[0]
|
|
name := apkName(pkgVer)
|
|
newVer := apkVersion(pkgVer)
|
|
oldVer := ""
|
|
if idx := strings.Index(line, "upgradable from:"); idx != -1 {
|
|
rest := strings.TrimSpace(line[idx+len("upgradable from:"):])
|
|
rest = strings.TrimSuffix(rest, "]")
|
|
oldVer = apkVersion(strings.TrimSpace(rest))
|
|
}
|
|
updates = append(updates, PackageUpdate{Name: name, CurrentVersion: oldVer, NewVersion: newVer})
|
|
}
|
|
return updates, nil
|
|
}
|
|
|
|
func apkName(pkgVer string) string {
|
|
parts := strings.Split(pkgVer, "-")
|
|
var name []string
|
|
for _, p := range parts {
|
|
if len(p) > 0 && p[0] >= '0' && p[0] <= '9' {
|
|
break
|
|
}
|
|
name = append(name, p)
|
|
}
|
|
return strings.Join(name, "-")
|
|
}
|
|
|
|
func apkVersion(pkgVer string) string {
|
|
parts := strings.Split(pkgVer, "-")
|
|
var ver []string
|
|
inVer := false
|
|
for _, p := range parts {
|
|
if !inVer && len(p) > 0 && p[0] >= '0' && p[0] <= '9' {
|
|
inVer = true
|
|
}
|
|
if inVer {
|
|
ver = append(ver, p)
|
|
}
|
|
}
|
|
return strings.Join(ver, "-")
|
|
}
|