Files
snack/pacman/capabilities_linux.go

269 lines
7.2 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
}
// parseGroupPkgSet parses "group pkg" lines and returns the set of package names.
func parseGroupPkgSet(output string) map[string]struct{} {
set := make(map[string]struct{})
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
parts := strings.Fields(line)
if len(parts) >= 2 {
set[parts[1]] = struct{}{}
}
}
return set
}
func groupIsInstalled(ctx context.Context, group string) (bool, error) {
// Get all packages in the group from the sync database.
availOut, err := run(ctx, []string{"-Sg", group}, snack.Options{})
if err != nil {
if strings.Contains(err.Error(), "exit status 1") {
return false, fmt.Errorf("pacman groupIsInstalled %s: %w", group, snack.ErrNotFound)
}
return false, fmt.Errorf("pacman groupIsInstalled: %w", err)
}
avail := parseGroupPkgSet(availOut)
if len(avail) == 0 {
return false, fmt.Errorf("pacman groupIsInstalled %s: %w", group, snack.ErrNotFound)
}
// Get packages from the group that are installed (local database).
instOut, err := run(ctx, []string{"-Qg", group}, snack.Options{})
if err != nil {
// exit status 1 means nothing from this group is installed.
if strings.Contains(err.Error(), "exit status 1") {
return false, nil
}
return false, fmt.Errorf("pacman groupIsInstalled: %w", err)
}
inst := parseGroupPkgSet(instOut)
for pkg := range avail {
if _, ok := inst[pkg]; !ok {
return false, nil
}
}
return true, nil
}