mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-15 16:52:47 -04:00
refactor(SysUpdate): Flatpak & Cli command handling
This commit is contained in:
@@ -2,6 +2,7 @@ package sysupdate
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -20,17 +21,44 @@ func (flatpakBackend) RunsInTerminal() bool { return false }
|
|||||||
func (flatpakBackend) IsAvailable(_ context.Context) bool { return commandExists("flatpak") }
|
func (flatpakBackend) IsAvailable(_ context.Context) bool { return commandExists("flatpak") }
|
||||||
|
|
||||||
func (flatpakBackend) CheckUpdates(ctx context.Context) ([]Package, error) {
|
func (flatpakBackend) CheckUpdates(ctx context.Context) ([]Package, error) {
|
||||||
cmd := exec.CommandContext(ctx, "flatpak", "remote-ls", "--updates", "--columns=application,version,branch,commit,name")
|
// Run `flatpak update`
|
||||||
|
cmd := exec.CommandContext(ctx, "flatpak", "update")
|
||||||
|
cmd.Stdin = strings.NewReader("n\nn\n") // decline up to 2 installation prompts
|
||||||
out, err := cmd.Output()
|
out, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok && exitErr.ExitCode() == 1 && len(out) > 0 {
|
||||||
|
} else if len(out) == 0 {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
installed := flatpakInstalled(ctx)
|
installed := flatpakInstalled(ctx)
|
||||||
return parseFlatpakUpdates(string(out), installed), nil
|
return parseFlatpakUpdateOutput(string(out), installed), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type flatpakInstalledEntry struct {
|
||||||
|
version string
|
||||||
|
branch string
|
||||||
}
|
}
|
||||||
|
|
||||||
func flatpakInstalled(ctx context.Context) map[string]flatpakInstalledEntry {
|
func flatpakInstalled(ctx context.Context) map[string]flatpakInstalledEntry {
|
||||||
out, err := exec.CommandContext(ctx, "flatpak", "list", "--columns=application,version,branch,active").Output()
|
m := flatpakListInstalled(ctx, false)
|
||||||
|
if m == nil {
|
||||||
|
m = make(map[string]flatpakInstalledEntry)
|
||||||
|
}
|
||||||
|
for k, v := range flatpakListInstalled(ctx, true) {
|
||||||
|
if _, exists := m[k]; !exists {
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func flatpakListInstalled(ctx context.Context, system bool) map[string]flatpakInstalledEntry {
|
||||||
|
args := []string{"flatpak", "list", "--columns=application,version,branch"}
|
||||||
|
if system {
|
||||||
|
args = append(args, "--system")
|
||||||
|
}
|
||||||
|
out, err := exec.CommandContext(ctx, args[0], args[1:]...).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -51,9 +79,6 @@ func flatpakInstalled(ctx context.Context) map[string]flatpakInstalledEntry {
|
|||||||
if len(fields) > 2 {
|
if len(fields) > 2 {
|
||||||
entry.branch = fields[2]
|
entry.branch = fields[2]
|
||||||
}
|
}
|
||||||
if len(fields) > 3 {
|
|
||||||
entry.commit = fields[3]
|
|
||||||
}
|
|
||||||
key := appID
|
key := appID
|
||||||
if entry.branch != "" {
|
if entry.branch != "" {
|
||||||
key = appID + "//" + entry.branch
|
key = appID + "//" + entry.branch
|
||||||
@@ -63,12 +88,6 @@ func flatpakInstalled(ctx context.Context) map[string]flatpakInstalledEntry {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
type flatpakInstalledEntry struct {
|
|
||||||
version string
|
|
||||||
branch string
|
|
||||||
commit string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (flatpakBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error {
|
func (flatpakBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error {
|
||||||
if opts.DryRun {
|
if opts.DryRun {
|
||||||
return Run(ctx, []string{"flatpak", "update", "--no-deploy", "-y"}, RunOptions{OnLine: onLine})
|
return Run(ctx, []string{"flatpak", "update", "--no-deploy", "-y"}, RunOptions{OnLine: onLine})
|
||||||
@@ -83,74 +102,71 @@ func flatpakUpgradeArgv() []string {
|
|||||||
return []string{"flatpak", "update", "-y", "--noninteractive"}
|
return []string{"flatpak", "update", "-y", "--noninteractive"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFlatpakUpdates(text string, installed map[string]flatpakInstalledEntry) []Package {
|
func parseFlatpakUpdateOutput(text string, installed map[string]flatpakInstalledEntry) []Package {
|
||||||
if text == "" {
|
var pkgs []Package
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for line := range strings.SplitSeq(text, "\n") {
|
||||||
|
p := parseFlatpakUpdateRow(strings.TrimRight(line, "\r"), installed)
|
||||||
|
if p == nil || seen[p.Ref] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[p.Ref] = true
|
||||||
|
pkgs = append(pkgs, *p)
|
||||||
|
}
|
||||||
|
return pkgs
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFlatpakUpdateRow(line string, installed map[string]flatpakInstalledEntry) *Package {
|
||||||
|
// Row format: " N.\t<name>\t<appID>\t<branch>\t<op>\t<remote>\t<size>"
|
||||||
|
fields := strings.Split(line, "\t")
|
||||||
|
if len(fields) < 5 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var pkgs []Package
|
// First field must look like " N." (optional whitespace, digits, period)
|
||||||
for line := range strings.SplitSeq(text, "\n") {
|
rowField := strings.TrimSpace(fields[0])
|
||||||
if line == "" {
|
if len(rowField) < 2 || rowField[len(rowField)-1] != '.' {
|
||||||
continue
|
return nil
|
||||||
}
|
}
|
||||||
fields := strings.Split(line, "\t")
|
for _, c := range rowField[:len(rowField)-1] {
|
||||||
if len(fields) == 0 || fields[0] == "" {
|
if c < '0' || c > '9' {
|
||||||
continue
|
return nil
|
||||||
}
|
}
|
||||||
appID := fields[0]
|
|
||||||
version, branch, commit := "", "", ""
|
|
||||||
if len(fields) > 1 {
|
|
||||||
version = fields[1]
|
|
||||||
}
|
|
||||||
if len(fields) > 2 {
|
|
||||||
branch = fields[2]
|
|
||||||
}
|
|
||||||
if len(fields) > 3 {
|
|
||||||
commit = fields[3]
|
|
||||||
}
|
|
||||||
display := appID
|
|
||||||
if len(fields) > 4 && fields[4] != "" {
|
|
||||||
display = fields[4]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
key := appID
|
appID := strings.TrimSpace(fields[2])
|
||||||
if branch != "" {
|
branch := strings.TrimSpace(fields[3])
|
||||||
key = appID + "//" + branch
|
op := strings.TrimSpace(fields[4])
|
||||||
|
if appID == "" || op == "" {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
inst := installed[key]
|
switch op {
|
||||||
|
case "i", "u", "r": // install, update, reinstall
|
||||||
if inst.commit != "" && commit != "" && strings.HasPrefix(commit, inst.commit) {
|
default:
|
||||||
continue
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
from, to := flatpakVersionPair(inst.version, inst.commit, version, commit)
|
|
||||||
|
|
||||||
ref := appID
|
ref := appID
|
||||||
if branch != "" {
|
if branch != "" {
|
||||||
ref = appID + "//" + branch
|
ref = appID + "//" + branch
|
||||||
}
|
}
|
||||||
|
|
||||||
pkgs = append(pkgs, Package{
|
name := strings.TrimSpace(fields[1])
|
||||||
Name: display,
|
if name == "" {
|
||||||
|
name = appID
|
||||||
|
}
|
||||||
|
|
||||||
|
var from string
|
||||||
|
if op != "i" {
|
||||||
|
if inst, ok := installed[ref]; ok {
|
||||||
|
from = inst.version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Package{
|
||||||
|
Name: name,
|
||||||
Repo: RepoFlatpak,
|
Repo: RepoFlatpak,
|
||||||
Backend: "flatpak",
|
Backend: "flatpak",
|
||||||
FromVersion: from,
|
FromVersion: from,
|
||||||
ToVersion: to,
|
|
||||||
Ref: ref,
|
Ref: ref,
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return pkgs
|
|
||||||
}
|
|
||||||
|
|
||||||
func flatpakVersionPair(installedVer, installedCommit, remoteVer, remoteCommit string) (from, to string) {
|
|
||||||
if remoteVer != "" {
|
|
||||||
return installedVer, remoteVer
|
|
||||||
}
|
|
||||||
return shortCommit(installedCommit), shortCommit(remoteCommit)
|
|
||||||
}
|
|
||||||
|
|
||||||
func shortCommit(c string) string {
|
|
||||||
if len(c) > 8 {
|
|
||||||
return c[:8]
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseFlatpakUpdates(t *testing.T) {
|
func TestParseFlatpakUpdateOutput(t *testing.T) {
|
||||||
|
realOutput := "Looking for updates…\n\n\n 1.\t \torg.gtk.Gtk3theme.adw-gtk3-dark\t3.22\ti\tflathub\t< 131.4 kB\n\nProceed with these changes to the system installation? [Y/n]: n\n"
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
input string
|
input string
|
||||||
@@ -13,137 +15,92 @@ func TestParseFlatpakUpdates(t *testing.T) {
|
|||||||
want []Package
|
want []Package
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty",
|
name: "empty output",
|
||||||
input: "",
|
input: "",
|
||||||
want: nil,
|
want: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "real flathub-style row with empty version, falls back to commit",
|
name: "nothing to do",
|
||||||
// columns: application,version,branch,commit,name
|
input: "Looking for updates…\n\nNothing to do.\n",
|
||||||
input: "com.discordapp.Discord\t\tstable\t43a1e5d2d3a446919356fd86d9f984ad7c6a0e20f109250d9d868223f26ca586\tDiscord",
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "real flatpak update output — new install",
|
||||||
|
input: realOutput,
|
||||||
|
want: []Package{
|
||||||
|
{
|
||||||
|
Name: "org.gtk.Gtk3theme.adw-gtk3-dark",
|
||||||
|
Repo: RepoFlatpak,
|
||||||
|
Backend: "flatpak",
|
||||||
|
FromVersion: "",
|
||||||
|
Ref: "org.gtk.Gtk3theme.adw-gtk3-dark//3.22",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update with installed version",
|
||||||
|
input: "Looking for updates…\n\n 1.\tSlack\tcom.slack.Slack\tstable\tu\tflathub\t< 5.2 MB\n\nProceed? [Y/n]: n\n",
|
||||||
installed: map[string]flatpakInstalledEntry{
|
installed: map[string]flatpakInstalledEntry{
|
||||||
"com.discordapp.Discord//stable": {commit: "8b16fa1a9b2aa189302c2428c8a7bb33dd050faf7e535dd1d975044cb0986855"},
|
"com.slack.Slack//stable": {version: "4.40.0"},
|
||||||
},
|
},
|
||||||
want: []Package{
|
want: []Package{
|
||||||
{
|
{
|
||||||
Name: "Discord",
|
Name: "Slack",
|
||||||
Repo: RepoFlatpak,
|
Repo: RepoFlatpak,
|
||||||
Backend: "flatpak",
|
Backend: "flatpak",
|
||||||
FromVersion: "8b16fa1a",
|
FromVersion: "4.40.0",
|
||||||
ToVersion: "43a1e5d2",
|
Ref: "com.slack.Slack//stable",
|
||||||
Ref: "com.discordapp.Discord//stable",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "remote provides version, installed version known",
|
name: "reinstall op included",
|
||||||
input: "com.example.App\t1.5.0\tstable\tdeadbeefcafe\tExample App",
|
input: " 1.\t\torg.freedesktop.Platform\t25.08\tr\tflathub\t< 100 MB\n",
|
||||||
installed: map[string]flatpakInstalledEntry{
|
|
||||||
"com.example.App//stable": {version: "1.4.2"},
|
|
||||||
},
|
|
||||||
want: []Package{
|
want: []Package{
|
||||||
{
|
{
|
||||||
Name: "Example App",
|
Name: "org.freedesktop.Platform",
|
||||||
|
Repo: RepoFlatpak,
|
||||||
|
Backend: "flatpak",
|
||||||
|
Ref: "org.freedesktop.Platform//25.08",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown op excluded",
|
||||||
|
input: " 1.\t\torg.freedesktop.Platform\t25.08\te\tflathub\t0\n",
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deduplicates same ref",
|
||||||
|
input: " 1.\t\tcom.example.App\tstable\ti\tflathub\t< 1 MB\n 2.\t\tcom.example.App\tstable\ti\tflathub\t< 1 MB\n",
|
||||||
|
want: []Package{
|
||||||
|
{
|
||||||
|
Name: "com.example.App",
|
||||||
Repo: RepoFlatpak,
|
Repo: RepoFlatpak,
|
||||||
Backend: "flatpak",
|
Backend: "flatpak",
|
||||||
FromVersion: "1.4.2",
|
|
||||||
ToVersion: "1.5.0",
|
|
||||||
Ref: "com.example.App//stable",
|
Ref: "com.example.App//stable",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no installed entry, remote has no version, falls back to commit on both sides",
|
name: "non-table lines ignored",
|
||||||
input: "org.gnome.Platform\t\t49\tbadcd4afb1fe\tgnome platform",
|
input: "Looking for updates…\nSome warning line\nID\tBranch\tOp\n 1.\t\tcom.example.App\tstable\ti\tflathub\t< 1 MB\nProceed? [Y/n]: n\n",
|
||||||
installed: nil,
|
|
||||||
want: []Package{
|
want: []Package{
|
||||||
{
|
{
|
||||||
Name: "gnome platform",
|
Name: "com.example.App",
|
||||||
Repo: RepoFlatpak,
|
Repo: RepoFlatpak,
|
||||||
Backend: "flatpak",
|
Backend: "flatpak",
|
||||||
FromVersion: "",
|
Ref: "com.example.App//stable",
|
||||||
ToVersion: "badcd4af",
|
|
||||||
Ref: "org.gnome.Platform//49",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "missing display name falls back to application id",
|
|
||||||
input: "com.example.NoName\t2.0\tstable\tabcdef123456\t",
|
|
||||||
want: []Package{
|
|
||||||
{
|
|
||||||
Name: "com.example.NoName",
|
|
||||||
Repo: RepoFlatpak,
|
|
||||||
Backend: "flatpak",
|
|
||||||
FromVersion: "",
|
|
||||||
ToVersion: "2.0",
|
|
||||||
Ref: "com.example.NoName//stable",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "skips blank lines and rows with empty application id",
|
|
||||||
input: "\n\t\t\t\t\norg.real.App\t1.0\tstable\tdeadbeef\tReal App",
|
|
||||||
want: []Package{
|
|
||||||
{
|
|
||||||
Name: "Real App",
|
|
||||||
Repo: RepoFlatpak,
|
|
||||||
Backend: "flatpak",
|
|
||||||
FromVersion: "",
|
|
||||||
ToVersion: "1.0",
|
|
||||||
Ref: "org.real.App//stable",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "skips phantom updates where remote commit matches installed",
|
|
||||||
input: "com.phantom.App\t\tstable\tabc12345deadbeef\tPhantom",
|
|
||||||
installed: map[string]flatpakInstalledEntry{
|
|
||||||
"com.phantom.App//stable": {commit: "abc12345"},
|
|
||||||
},
|
|
||||||
want: nil,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got := parseFlatpakUpdates(tt.input, tt.installed)
|
got := parseFlatpakUpdateOutput(tt.input, tt.installed)
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
t.Errorf("parseFlatpakUpdates() = %#v\nwant %#v", got, tt.want)
|
t.Errorf("parseFlatpakUpdateOutput() = %#v\nwant %#v", got, tt.want)
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFlatpakVersionPair(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
installedVer, installedCommit, remoteVer, remoteCommit string
|
|
||||||
wantFrom, wantTo string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "remote has version - prefer versions",
|
|
||||||
installedVer: "1.0.0", remoteVer: "1.1.0",
|
|
||||||
wantFrom: "1.0.0", wantTo: "1.1.0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "remote has no version - both sides fall to short commit",
|
|
||||||
installedCommit: "8b16fa1a9b2aa189302c2428c8a7bb33dd050faf7e535dd1d975044cb0986855",
|
|
||||||
remoteCommit: "43a1e5d2d3a446919356fd86d9f984ad7c6a0e20f109250d9d868223f26ca586",
|
|
||||||
wantFrom: "8b16fa1a", wantTo: "43a1e5d2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "short commits left as-is",
|
|
||||||
installedCommit: "abc123", remoteCommit: "def456",
|
|
||||||
wantFrom: "abc123", wantTo: "def456",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
from, to := flatpakVersionPair(tt.installedVer, tt.installedCommit, tt.remoteVer, tt.remoteCommit)
|
|
||||||
if from != tt.wantFrom || to != tt.wantTo {
|
|
||||||
t.Errorf("flatpakVersionPair() = (%q, %q), want (%q, %q)", from, to, tt.wantFrom, tt.wantTo)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ func Run(ctx context.Context, argv []string, opts RunOptions) error {
|
|||||||
}
|
}
|
||||||
return cmd.Process.Kill()
|
return cmd.Process.Kill()
|
||||||
}
|
}
|
||||||
cmd.Stdin = os.Stdin
|
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
|
|||||||
Reference in New Issue
Block a user