mirror of
https://github.com/gogrlx/snack.git
synced 2026-04-02 05:08:42 -07:00
pacman: - VersionQuerier: latestVersion via pacman -Si, listUpgrades via pacman -Qu, upgradeAvailable via pacman -Qu <pkg>, versionCmp via vercmp - Cleaner: autoremove via pacman -Qdtq | pacman -Rns, clean via pacman -Sc - FileOwner: fileList via pacman -Ql, owner via pacman -Qo - Grouper: groupList/groupInfo via pacman -Sg, groupInstall via pacman -S - Note: Holder skipped (no clean CLI support) apk: - VersionQuerier: latestVersion via apk policy, listUpgrades via apk upgrade --simulate, upgradeAvailable by checking upgrade list, versionCmp via apk version -t - Cleaner: clean via apk cache clean, autoremove is no-op (not supported) - FileOwner: fileList via apk info -L, owner via apk info --who-owns
222 lines
5.7 KiB
Go
222 lines
5.7 KiB
Go
//go:build linux
|
|
|
|
package pacman
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gogrlx/snack"
|
|
)
|
|
|
|
func latestVersion(ctx context.Context, pkg string) (string, error) {
|
|
out, err := run(ctx, []string{"-Si", pkg}, snack.Options{})
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "exit status 1") {
|
|
return "", fmt.Errorf("pacman latestVersion %s: %w", pkg, snack.ErrNotFound)
|
|
}
|
|
return "", fmt.Errorf("pacman latestVersion: %w", err)
|
|
}
|
|
p := parseInfo(out)
|
|
if p == nil || p.Version == "" {
|
|
return "", fmt.Errorf("pacman latestVersion %s: %w", pkg, snack.ErrNotFound)
|
|
}
|
|
return p.Version, nil
|
|
}
|
|
|
|
func listUpgrades(ctx context.Context) ([]snack.Package, error) {
|
|
out, err := run(ctx, []string{"-Qu"}, snack.Options{})
|
|
if err != nil {
|
|
// exit status 1 means no upgrades available
|
|
if strings.Contains(err.Error(), "exit status 1") {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("pacman listUpgrades: %w", err)
|
|
}
|
|
return parseUpgrades(out), nil
|
|
}
|
|
|
|
// parseUpgrades parses `pacman -Qu` output.
|
|
// Format: "pkg oldver -> newver"
|
|
func parseUpgrades(output string) []snack.Package {
|
|
var pkgs []snack.Package
|
|
for _, line := range strings.Split(output, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
// "pkg oldver -> newver"
|
|
parts := strings.Fields(line)
|
|
if len(parts) >= 4 && parts[2] == "->" {
|
|
pkgs = append(pkgs, snack.Package{
|
|
Name: parts[0],
|
|
Version: parts[3],
|
|
Installed: true,
|
|
})
|
|
} else if len(parts) >= 2 {
|
|
// fallback: "pkg newver"
|
|
pkgs = append(pkgs, snack.Package{
|
|
Name: parts[0],
|
|
Version: parts[1],
|
|
Installed: true,
|
|
})
|
|
}
|
|
}
|
|
return pkgs
|
|
}
|
|
|
|
func upgradeAvailable(ctx context.Context, pkg string) (bool, error) {
|
|
_, err := run(ctx, []string{"-Qu", pkg}, snack.Options{})
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "exit status 1") {
|
|
return false, nil
|
|
}
|
|
return false, fmt.Errorf("pacman upgradeAvailable: %w", err)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func versionCmp(ctx context.Context, ver1, ver2 string) (int, error) {
|
|
c := exec.CommandContext(ctx, "vercmp", ver1, ver2)
|
|
out, err := c.Output()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("vercmp: %w", err)
|
|
}
|
|
n, err := strconv.Atoi(strings.TrimSpace(string(out)))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("vercmp: unexpected output %q: %w", string(out), err)
|
|
}
|
|
// Normalize to -1, 0, 1
|
|
switch {
|
|
case n < 0:
|
|
return -1, nil
|
|
case n > 0:
|
|
return 1, nil
|
|
default:
|
|
return 0, nil
|
|
}
|
|
}
|
|
|
|
func autoremove(ctx context.Context, opts ...snack.Option) error {
|
|
o := snack.ApplyOptions(opts...)
|
|
|
|
// First get list of orphans
|
|
orphans, err := run(ctx, []string{"-Qdtq"}, snack.Options{})
|
|
if err != nil {
|
|
// exit status 1 means no orphans
|
|
if strings.Contains(err.Error(), "exit status 1") {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("pacman autoremove: %w", err)
|
|
}
|
|
orphans = strings.TrimSpace(orphans)
|
|
if orphans == "" {
|
|
return nil
|
|
}
|
|
|
|
pkgs := strings.Fields(orphans)
|
|
args := append([]string{"-Rns", "--noconfirm"}, pkgs...)
|
|
_, err = run(ctx, args, o)
|
|
return err
|
|
}
|
|
|
|
func clean(ctx context.Context) error {
|
|
_, err := run(ctx, []string{"-Sc", "--noconfirm"}, snack.Options{})
|
|
return err
|
|
}
|
|
|
|
func fileList(ctx context.Context, pkg string) ([]string, error) {
|
|
out, err := run(ctx, []string{"-Ql", pkg}, snack.Options{})
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "exit status 1") {
|
|
return nil, fmt.Errorf("pacman fileList %s: %w", pkg, snack.ErrNotInstalled)
|
|
}
|
|
return nil, fmt.Errorf("pacman fileList: %w", err)
|
|
}
|
|
var files []string
|
|
for _, line := range strings.Split(out, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
// "pkg /path/to/file"
|
|
parts := strings.SplitN(line, " ", 2)
|
|
if len(parts) == 2 {
|
|
files = append(files, strings.TrimSpace(parts[1]))
|
|
}
|
|
}
|
|
return files, nil
|
|
}
|
|
|
|
func owner(ctx context.Context, path string) (string, error) {
|
|
out, err := run(ctx, []string{"-Qo", path}, snack.Options{})
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "exit status 1") || strings.Contains(err.Error(), "No package owns") {
|
|
return "", fmt.Errorf("pacman owner %s: %w", path, snack.ErrNotFound)
|
|
}
|
|
return "", fmt.Errorf("pacman owner: %w", err)
|
|
}
|
|
// Output: "/path is owned by pkg ver"
|
|
out = strings.TrimSpace(out)
|
|
if idx := strings.Index(out, "is owned by "); idx != -1 {
|
|
remainder := out[idx+len("is owned by "):]
|
|
parts := strings.Fields(remainder)
|
|
if len(parts) >= 1 {
|
|
return parts[0], nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("pacman owner %s: unexpected output %q", path, out)
|
|
}
|
|
|
|
func groupList(ctx context.Context) ([]string, error) {
|
|
out, err := run(ctx, []string{"-Sg"}, snack.Options{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("pacman groupList: %w", err)
|
|
}
|
|
seen := make(map[string]struct{})
|
|
var groups []string
|
|
for _, line := range strings.Split(out, "\n") {
|
|
parts := strings.Fields(line)
|
|
if len(parts) >= 1 {
|
|
g := parts[0]
|
|
if _, ok := seen[g]; !ok {
|
|
seen[g] = struct{}{}
|
|
groups = append(groups, g)
|
|
}
|
|
}
|
|
}
|
|
return groups, nil
|
|
}
|
|
|
|
func groupInfo(ctx context.Context, group string) ([]snack.Package, error) {
|
|
out, err := run(ctx, []string{"-Sg", group}, snack.Options{})
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "exit status 1") {
|
|
return nil, fmt.Errorf("pacman groupInfo %s: %w", group, snack.ErrNotFound)
|
|
}
|
|
return nil, fmt.Errorf("pacman groupInfo: %w", err)
|
|
}
|
|
var pkgs []snack.Package
|
|
for _, line := range strings.Split(out, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
// "group pkg"
|
|
parts := strings.Fields(line)
|
|
if len(parts) >= 2 {
|
|
pkgs = append(pkgs, snack.Package{Name: parts[1]})
|
|
}
|
|
}
|
|
return pkgs, nil
|
|
}
|
|
|
|
func groupInstall(ctx context.Context, group string, opts ...snack.Option) error {
|
|
o := snack.ApplyOptions(opts...)
|
|
_, err := run(ctx, []string{"-S", "--noconfirm", group}, o)
|
|
return err
|
|
}
|