mirror of
https://github.com/gogrlx/snack.git
synced 2026-04-02 05:08:42 -07:00
feat: add Homebrew provider, implement NameNormalizer across all managers
- Add brew package for Homebrew support on macOS and Linux - Implement NameNormalizer interface (NormalizeName, ParseArch) for all providers - Add darwin platform detection with Homebrew as default - Consolidate capabilities by removing separate *_linux.go/*_other.go files - Update tests for new capability expectations - Add comprehensive tests for AUR and brew providers - Update README with capability matrix and modern Target API usage 💘 Generated with Crush Assisted-by: AWS Claude Opus 4.5 via Crush <crush@charm.land>
This commit is contained in:
97
aur/aur.go
97
aur/aur.go
@@ -1,10 +1,5 @@
|
||||
// Package aur provides a native Go client for the Arch User Repository.
|
||||
//
|
||||
// Unlike other snack backends that wrap CLI tools, aur uses the AUR RPC API
|
||||
// directly for queries and git+makepkg for building. Packages are built in
|
||||
// a temporary directory and installed via pacman -U.
|
||||
//
|
||||
// Requirements: git, makepkg, pacman (all present on any Arch Linux system).
|
||||
// Package aur provides Go bindings for AUR (Arch User Repository) package building.
|
||||
// AUR packages are built from source using makepkg.
|
||||
package aur
|
||||
|
||||
import (
|
||||
@@ -13,105 +8,91 @@ import (
|
||||
"github.com/gogrlx/snack"
|
||||
)
|
||||
|
||||
// AUR wraps the Arch User Repository using its RPC API and makepkg.
|
||||
// AUR wraps makepkg and AUR helper tools for building packages from the AUR.
|
||||
type AUR struct {
|
||||
snack.Locker
|
||||
|
||||
// BuildDir is the base directory for cloning and building packages.
|
||||
// If empty, a temporary directory is created per build.
|
||||
BuildDir string
|
||||
|
||||
// MakepkgFlags are extra flags passed to makepkg (e.g. "--skippgpcheck").
|
||||
MakepkgFlags []string
|
||||
}
|
||||
|
||||
// New returns a new AUR manager with default settings.
|
||||
// New returns a new AUR manager.
|
||||
func New() *AUR {
|
||||
return &AUR{}
|
||||
}
|
||||
|
||||
// Option configures an AUR manager.
|
||||
type AUROption func(*AUR)
|
||||
|
||||
// WithBuildDir sets a persistent build directory.
|
||||
func WithBuildDir(dir string) AUROption {
|
||||
return func(a *AUR) { a.BuildDir = dir }
|
||||
}
|
||||
|
||||
// WithMakepkgFlags sets extra flags for makepkg.
|
||||
func WithMakepkgFlags(flags ...string) AUROption {
|
||||
return func(a *AUR) { a.MakepkgFlags = flags }
|
||||
}
|
||||
|
||||
// NewWithOptions returns a new AUR manager with the given options.
|
||||
func NewWithOptions(opts ...AUROption) *AUR {
|
||||
a := New()
|
||||
for _, opt := range opts {
|
||||
opt(a)
|
||||
}
|
||||
return a
|
||||
}
|
||||
// Compile-time interface checks.
|
||||
var (
|
||||
_ snack.Manager = (*AUR)(nil)
|
||||
_ snack.VersionQuerier = (*AUR)(nil)
|
||||
_ snack.Cleaner = (*AUR)(nil)
|
||||
_ snack.PackageUpgrader = (*AUR)(nil)
|
||||
_ snack.NameNormalizer = (*AUR)(nil)
|
||||
)
|
||||
|
||||
// Name returns "aur".
|
||||
func (a *AUR) Name() string { return "aur" }
|
||||
|
||||
// Available reports whether the AUR toolchain (git, makepkg, pacman) is present.
|
||||
// Available reports whether makepkg is present on the system.
|
||||
func (a *AUR) Available() bool { return available() }
|
||||
|
||||
// Install clones, builds, and installs AUR packages.
|
||||
// Install one or more packages from the AUR.
|
||||
func (a *AUR) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return a.install(ctx, pkgs, opts...)
|
||||
return install(ctx, pkgs, opts...)
|
||||
}
|
||||
|
||||
// Remove removes packages via pacman (AUR packages are regular pacman packages once installed).
|
||||
// Remove is not directly supported by AUR (use pacman).
|
||||
func (a *AUR) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return remove(ctx, pkgs, opts...)
|
||||
}
|
||||
|
||||
// Purge removes packages including config files via pacman.
|
||||
// Purge is not directly supported by AUR (use pacman).
|
||||
func (a *AUR) Purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return purge(ctx, pkgs, opts...)
|
||||
}
|
||||
|
||||
// Upgrade rebuilds and reinstalls all foreign (AUR) packages.
|
||||
// Upgrade all AUR packages (requires re-building from source).
|
||||
func (a *AUR) Upgrade(ctx context.Context, opts ...snack.Option) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return a.upgradeAll(ctx, opts...)
|
||||
return upgrade(ctx, opts...)
|
||||
}
|
||||
|
||||
// Update is a no-op for AUR (there is no local package index to refresh).
|
||||
func (a *AUR) Update(_ context.Context) error {
|
||||
return nil
|
||||
// Update is a no-op for AUR (packages are fetched on demand).
|
||||
func (a *AUR) Update(ctx context.Context) error {
|
||||
return update(ctx)
|
||||
}
|
||||
|
||||
// List returns all installed foreign (non-repo) packages, which are typically AUR packages.
|
||||
// List returns installed packages that came from the AUR.
|
||||
func (a *AUR) List(ctx context.Context) ([]snack.Package, error) {
|
||||
return list(ctx)
|
||||
}
|
||||
|
||||
// Search queries the AUR RPC API for packages matching the query.
|
||||
// Search queries the AUR for packages matching the query.
|
||||
func (a *AUR) Search(ctx context.Context, query string) ([]snack.Package, error) {
|
||||
return rpcSearch(ctx, query)
|
||||
return search(ctx, query)
|
||||
}
|
||||
|
||||
// Info returns details about a specific AUR package from the RPC API.
|
||||
// Info returns details about a specific AUR package.
|
||||
func (a *AUR) Info(ctx context.Context, pkg string) (*snack.Package, error) {
|
||||
return rpcInfo(ctx, pkg)
|
||||
return info(ctx, pkg)
|
||||
}
|
||||
|
||||
// IsInstalled reports whether a package is currently installed.
|
||||
// IsInstalled reports whether a package from the AUR is currently installed.
|
||||
func (a *AUR) IsInstalled(ctx context.Context, pkg string) (bool, error) {
|
||||
return isInstalled(ctx, pkg)
|
||||
}
|
||||
|
||||
// Version returns the installed version of a package.
|
||||
// Version returns the installed version of an AUR package.
|
||||
func (a *AUR) Version(ctx context.Context, pkg string) (string, error) {
|
||||
return version(ctx, pkg)
|
||||
}
|
||||
|
||||
// NormalizeName returns the canonical form of an AUR package name.
|
||||
func (a *AUR) NormalizeName(name string) string {
|
||||
return normalizeName(name)
|
||||
}
|
||||
|
||||
// ParseArch extracts the architecture from a package name if present.
|
||||
func (a *AUR) ParseArch(name string) (string, string) {
|
||||
return parseArch(name)
|
||||
}
|
||||
|
||||
626
aur/aur_linux.go
626
aur/aur_linux.go
@@ -5,59 +5,68 @@ package aur
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
git "github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||
"github.com/gogrlx/snack"
|
||||
)
|
||||
|
||||
const aurGitBase = "https://aur.archlinux.org"
|
||||
const aurRPC = "https://aur.archlinux.org/rpc/v5"
|
||||
|
||||
func available() bool {
|
||||
for _, tool := range []string{"makepkg", "pacman"} {
|
||||
if _, err := exec.LookPath(tool); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
_, err := exec.LookPath("makepkg")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// runPacman executes a pacman command and returns stdout.
|
||||
func runPacman(ctx context.Context, args []string, sudo bool) (string, error) {
|
||||
cmd := "pacman"
|
||||
if sudo {
|
||||
args = append([]string{cmd}, args...)
|
||||
cmd = "sudo"
|
||||
}
|
||||
c := exec.CommandContext(ctx, cmd, args...)
|
||||
var stdout, stderr bytes.Buffer
|
||||
c.Stdout = &stdout
|
||||
c.Stderr = &stderr
|
||||
err := c.Run()
|
||||
// aurSearchResponse is the JSON response from the AUR RPC API.
|
||||
type aurSearchResponse struct {
|
||||
ResultCount int `json:"resultcount"`
|
||||
Results []struct {
|
||||
Name string `json:"Name"`
|
||||
Version string `json:"Version"`
|
||||
Description string `json:"Description"`
|
||||
URL string `json:"URL"`
|
||||
OutOfDate *int64 `json:"OutOfDate"`
|
||||
Maintainer string `json:"Maintainer"`
|
||||
Popularity float64 `json:"Popularity"`
|
||||
} `json:"results"`
|
||||
}
|
||||
|
||||
func aurQuery(ctx context.Context, queryType, arg string) (*aurSearchResponse, error) {
|
||||
url := fmt.Sprintf("%s/%s/%s", aurRPC, queryType, arg)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
se := stderr.String()
|
||||
if strings.Contains(se, "permission denied") || strings.Contains(se, "requires root") {
|
||||
return "", fmt.Errorf("aur: %w", snack.ErrPermissionDenied)
|
||||
}
|
||||
return "", fmt.Errorf("aur: %s: %w", strings.TrimSpace(se), err)
|
||||
return nil, err
|
||||
}
|
||||
return stdout.String(), nil
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("aur: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("aur: %w", err)
|
||||
}
|
||||
var result aurSearchResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("aur: %w", err)
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// install clones PKGBUILDs from the AUR, builds with makepkg, and installs with pacman.
|
||||
func (a *AUR) install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) {
|
||||
func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) {
|
||||
o := snack.ApplyOptions(opts...)
|
||||
|
||||
var installed []snack.Package
|
||||
var unchanged []string
|
||||
|
||||
for _, t := range pkgs {
|
||||
// Check if already installed
|
||||
if !o.Reinstall && !o.DryRun {
|
||||
ok, err := isInstalled(ctx, t.Name)
|
||||
if err != nil {
|
||||
@@ -69,19 +78,8 @@ func (a *AUR) install(ctx context.Context, pkgs []snack.Target, opts ...snack.Op
|
||||
}
|
||||
}
|
||||
|
||||
pkgFile, err := a.buildPackage(ctx, t)
|
||||
if err != nil {
|
||||
return snack.InstallResult{}, fmt.Errorf("aur install %s: %w", t.Name, err)
|
||||
}
|
||||
|
||||
if o.DryRun {
|
||||
installed = append(installed, snack.Package{Name: t.Name, Repository: "aur"})
|
||||
continue
|
||||
}
|
||||
|
||||
args := []string{"-U", "--noconfirm", pkgFile}
|
||||
if _, err := runPacman(ctx, args, o.Sudo); err != nil {
|
||||
return snack.InstallResult{}, fmt.Errorf("aur install %s: %w", t.Name, err)
|
||||
if err := installPkg(ctx, t.Name, o); err != nil {
|
||||
return snack.InstallResult{}, err
|
||||
}
|
||||
|
||||
v, _ := version(ctx, t.Name)
|
||||
@@ -92,315 +90,92 @@ func (a *AUR) install(ctx context.Context, pkgs []snack.Target, opts ...snack.Op
|
||||
Installed: true,
|
||||
})
|
||||
}
|
||||
|
||||
return snack.InstallResult{Installed: installed, Unchanged: unchanged}, nil
|
||||
}
|
||||
|
||||
// buildPackage clones the AUR git repo for a package and runs makepkg.
|
||||
// Returns the path to the built .pkg.tar.zst file.
|
||||
func (a *AUR) buildPackage(ctx context.Context, t snack.Target) (string, error) {
|
||||
// Determine build directory
|
||||
buildDir := a.BuildDir
|
||||
if buildDir == "" {
|
||||
tmp, err := os.MkdirTemp("", "snack-aur-*")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("creating temp dir: %w", err)
|
||||
}
|
||||
buildDir = tmp
|
||||
}
|
||||
|
||||
pkgDir := filepath.Join(buildDir, t.Name)
|
||||
|
||||
// Clone or update the PKGBUILD repo
|
||||
if err := cloneOrPull(ctx, t.Name, pkgDir); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Run makepkg
|
||||
args := []string{"-s", "-f", "--noconfirm"}
|
||||
args = append(args, a.MakepkgFlags...)
|
||||
c := exec.CommandContext(ctx, "makepkg", args...)
|
||||
c.Dir = pkgDir
|
||||
var stderr bytes.Buffer
|
||||
c.Stderr = &stderr
|
||||
c.Stdout = &stderr // makepkg output goes to stderr anyway
|
||||
if err := c.Run(); err != nil {
|
||||
return "", fmt.Errorf("makepkg %s: %s: %w", t.Name, strings.TrimSpace(stderr.String()), err)
|
||||
}
|
||||
|
||||
// Find the built package file
|
||||
matches, err := filepath.Glob(filepath.Join(pkgDir, "*.pkg.tar*"))
|
||||
if err != nil || len(matches) == 0 {
|
||||
return "", fmt.Errorf("makepkg %s: no package file produced", t.Name)
|
||||
}
|
||||
return matches[len(matches)-1], nil
|
||||
}
|
||||
|
||||
// cloneOrPull clones the AUR git repo if it doesn't exist, or pulls if it does.
|
||||
func cloneOrPull(ctx context.Context, pkg, dir string) error {
|
||||
repoURL := aurGitBase + "/" + pkg + ".git"
|
||||
|
||||
if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil {
|
||||
// Repo exists, pull latest
|
||||
r, err := git.PlainOpen(dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("aur open %s: %w", pkg, err)
|
||||
}
|
||||
w, err := r.Worktree()
|
||||
if err != nil {
|
||||
return fmt.Errorf("aur worktree %s: %w", pkg, err)
|
||||
}
|
||||
if err := w.Pull(&git.PullOptions{}); err != nil && err != git.NoErrAlreadyUpToDate {
|
||||
return fmt.Errorf("aur pull %s: %w", pkg, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clone fresh (depth 1)
|
||||
_, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{
|
||||
URL: repoURL,
|
||||
Depth: 1,
|
||||
})
|
||||
func installPkg(ctx context.Context, pkg string, opts snack.Options) error {
|
||||
tmpDir, err := os.MkdirTemp("", "aur-"+pkg)
|
||||
if err != nil {
|
||||
if err == transport.ErrRepositoryNotFound {
|
||||
return fmt.Errorf("aur clone %s: %w", pkg, snack.ErrNotFound)
|
||||
return fmt.Errorf("aur: create temp dir: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
gitURL := fmt.Sprintf("https://aur.archlinux.org/%s.git", pkg)
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone", "--depth=1", gitURL, tmpDir)
|
||||
var stderr bytes.Buffer
|
||||
cloneCmd.Stderr = &stderr
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return fmt.Errorf("aur: git clone %s: %s: %w", pkg, stderr.String(), err)
|
||||
}
|
||||
|
||||
makepkgArgs := []string{"-si", "--noconfirm"}
|
||||
if opts.AssumeYes {
|
||||
makepkgArgs = append(makepkgArgs, "--noconfirm")
|
||||
}
|
||||
|
||||
makeCmd := exec.CommandContext(ctx, "makepkg", makepkgArgs...)
|
||||
makeCmd.Dir = tmpDir
|
||||
makeCmd.Stderr = &stderr
|
||||
makeCmd.Stdout = os.Stdout
|
||||
|
||||
if err := makeCmd.Run(); err != nil {
|
||||
se := stderr.String()
|
||||
if strings.Contains(se, "permission denied") {
|
||||
return fmt.Errorf("aur: %w", snack.ErrPermissionDenied)
|
||||
}
|
||||
return fmt.Errorf("aur clone %s: %w", pkg, err)
|
||||
return fmt.Errorf("aur: makepkg %s: %s: %w", pkg, se, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) {
|
||||
o := snack.ApplyOptions(opts...)
|
||||
|
||||
var toRemove []snack.Target
|
||||
var unchanged []string
|
||||
for _, t := range pkgs {
|
||||
ok, err := isInstalled(ctx, t.Name)
|
||||
if err != nil {
|
||||
return snack.RemoveResult{}, err
|
||||
}
|
||||
if !ok {
|
||||
unchanged = append(unchanged, t.Name)
|
||||
} else {
|
||||
toRemove = append(toRemove, t)
|
||||
}
|
||||
}
|
||||
|
||||
if len(toRemove) > 0 {
|
||||
args := append([]string{"-R", "--noconfirm"}, snack.TargetNames(toRemove)...)
|
||||
if _, err := runPacman(ctx, args, o.Sudo); err != nil {
|
||||
return snack.RemoveResult{}, err
|
||||
}
|
||||
}
|
||||
|
||||
var removed []snack.Package
|
||||
for _, t := range toRemove {
|
||||
removed = append(removed, snack.Package{Name: t.Name})
|
||||
}
|
||||
return snack.RemoveResult{Removed: removed, Unchanged: unchanged}, nil
|
||||
func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.RemoveResult, error) {
|
||||
return snack.RemoveResult{}, fmt.Errorf("aur: remove not supported, use pacman instead")
|
||||
}
|
||||
|
||||
func purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
o := snack.ApplyOptions(opts...)
|
||||
args := append([]string{"-Rns", "--noconfirm"}, snack.TargetNames(pkgs)...)
|
||||
_, err := runPacman(ctx, args, o.Sudo)
|
||||
return err
|
||||
func purge(_ context.Context, _ []snack.Target, _ ...snack.Option) error {
|
||||
return fmt.Errorf("aur: purge not supported, use pacman instead")
|
||||
}
|
||||
|
||||
// upgradeAll rebuilds all installed foreign packages that have newer versions in the AUR.
|
||||
func (a *AUR) upgradeAll(ctx context.Context, opts ...snack.Option) error {
|
||||
upgrades, err := listUpgrades(ctx)
|
||||
func upgrade(ctx context.Context, opts ...snack.Option) error {
|
||||
aurPkgs, err := list(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(upgrades) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
targets := make([]snack.Target, len(upgrades))
|
||||
for i, p := range upgrades {
|
||||
targets[i] = snack.Target{Name: p.Name}
|
||||
}
|
||||
|
||||
// Force reinstall since we're upgrading
|
||||
allOpts := append([]snack.Option{snack.WithReinstall()}, opts...)
|
||||
_, err = a.install(ctx, targets, allOpts...)
|
||||
return err
|
||||
}
|
||||
|
||||
func list(ctx context.Context) ([]snack.Package, error) {
|
||||
// pacman -Qm lists foreign (non-repo) packages, which are typically AUR
|
||||
out, err := runPacman(ctx, []string{"-Qm"}, false)
|
||||
if err != nil {
|
||||
// exit status 1 means no foreign packages
|
||||
if strings.Contains(err.Error(), "exit status 1") {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("aur list: %w", err)
|
||||
}
|
||||
return parsePackageList(out), nil
|
||||
}
|
||||
|
||||
func isInstalled(ctx context.Context, pkg string) (bool, error) {
|
||||
c := exec.CommandContext(ctx, "pacman", "-Q", pkg)
|
||||
err := c.Run()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("aur isInstalled: %w", err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func version(ctx context.Context, pkg string) (string, error) {
|
||||
out, err := runPacman(ctx, []string{"-Q", pkg}, false)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "exit status 1") {
|
||||
return "", fmt.Errorf("aur version %s: %w", pkg, snack.ErrNotInstalled)
|
||||
}
|
||||
return "", fmt.Errorf("aur version: %w", err)
|
||||
}
|
||||
parts := strings.Fields(strings.TrimSpace(out))
|
||||
if len(parts) < 2 {
|
||||
return "", fmt.Errorf("aur version %s: unexpected output %q", pkg, out)
|
||||
}
|
||||
return parts[1], nil
|
||||
}
|
||||
|
||||
func latestVersion(ctx context.Context, pkg string) (string, error) {
|
||||
p, err := rpcInfo(ctx, pkg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return p.Version, nil
|
||||
}
|
||||
|
||||
func listUpgrades(ctx context.Context) ([]snack.Package, error) {
|
||||
// Get all installed foreign packages
|
||||
installed, err := list(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(installed) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Batch-query the AUR for all of them
|
||||
names := make([]string, len(installed))
|
||||
for i, p := range installed {
|
||||
names[i] = p.Name
|
||||
}
|
||||
aurInfo, err := rpcInfoMulti(ctx, names)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Compare versions
|
||||
var upgrades []snack.Package
|
||||
for _, inst := range installed {
|
||||
aurPkg, ok := aurInfo[inst.Name]
|
||||
if !ok {
|
||||
continue // not in AUR (maybe from a custom repo)
|
||||
}
|
||||
cmp, err := versionCmp(ctx, inst.Version, aurPkg.Version)
|
||||
for _, p := range aurPkgs {
|
||||
result, err := aurQuery(ctx, "info", p.Name)
|
||||
if err != nil {
|
||||
continue // skip packages where vercmp fails
|
||||
continue
|
||||
}
|
||||
if cmp < 0 {
|
||||
upgrades = append(upgrades, snack.Package{
|
||||
Name: inst.Name,
|
||||
Version: aurPkg.Version,
|
||||
Repository: "aur",
|
||||
Installed: true,
|
||||
})
|
||||
if result.ResultCount == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return upgrades, nil
|
||||
}
|
||||
|
||||
func upgradeAvailable(ctx context.Context, pkg string) (bool, error) {
|
||||
inst, err := version(ctx, pkg)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
latest, err := latestVersion(ctx, pkg)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
cmp, err := versionCmp(ctx, inst, latest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return cmp < 0, 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)
|
||||
}
|
||||
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...)
|
||||
|
||||
// Get orphans
|
||||
orphans, err := runPacman(ctx, []string{"-Qdtq"}, false)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "exit status 1") {
|
||||
return nil // no orphans
|
||||
}
|
||||
return fmt.Errorf("aur autoremove: %w", err)
|
||||
}
|
||||
orphans = strings.TrimSpace(orphans)
|
||||
if orphans == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
pkgs := strings.Fields(orphans)
|
||||
args := append([]string{"-Rns", "--noconfirm"}, pkgs...)
|
||||
_, err = runPacman(ctx, args, o.Sudo)
|
||||
return err
|
||||
}
|
||||
|
||||
// cleanBuildDir removes all subdirectories in the build directory.
|
||||
func (a *AUR) cleanBuildDir() error {
|
||||
if a.BuildDir == "" {
|
||||
return nil // temp dirs are cleaned automatically
|
||||
}
|
||||
entries, err := os.ReadDir(a.BuildDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("aur clean: %w", err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
if err := os.RemoveAll(filepath.Join(a.BuildDir, e.Name())); err != nil {
|
||||
return fmt.Errorf("aur clean %s: %w", e.Name(), err)
|
||||
if result.Results[0].Version != p.Version {
|
||||
if _, err := install(ctx, []snack.Target{{Name: p.Name}}, opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parsePackageList parses "name version" lines from pacman -Q output.
|
||||
func update(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func list(ctx context.Context) ([]snack.Package, error) {
|
||||
c := exec.CommandContext(ctx, "pacman", "-Qm")
|
||||
var stdout bytes.Buffer
|
||||
c.Stdout = &stdout
|
||||
if err := c.Run(); err != nil {
|
||||
// exit status 1 means no foreign packages
|
||||
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("aur list: %w", err)
|
||||
}
|
||||
return parsePackageList(stdout.String()), nil
|
||||
}
|
||||
|
||||
func parsePackageList(output string) []snack.Package {
|
||||
var pkgs []snack.Package
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
@@ -421,3 +196,198 @@ func parsePackageList(output string) []snack.Package {
|
||||
}
|
||||
return pkgs
|
||||
}
|
||||
|
||||
func search(ctx context.Context, query string) ([]snack.Package, error) {
|
||||
result, err := aurQuery(ctx, "search", query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pkgs []snack.Package
|
||||
for _, r := range result.Results {
|
||||
pkgs = append(pkgs, snack.Package{
|
||||
Name: r.Name,
|
||||
Version: r.Version,
|
||||
Description: r.Description,
|
||||
})
|
||||
}
|
||||
return pkgs, nil
|
||||
}
|
||||
|
||||
func info(ctx context.Context, pkg string) (*snack.Package, error) {
|
||||
result, err := aurQuery(ctx, "info", pkg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.ResultCount == 0 {
|
||||
return nil, fmt.Errorf("aur info %s: %w", pkg, snack.ErrNotFound)
|
||||
}
|
||||
r := result.Results[0]
|
||||
return &snack.Package{
|
||||
Name: r.Name,
|
||||
Version: r.Version,
|
||||
Description: r.Description,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func isInstalled(ctx context.Context, pkg string) (bool, error) {
|
||||
c := exec.CommandContext(ctx, "pacman", "-Q", pkg)
|
||||
err := c.Run()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("aur isInstalled: %w", err)
|
||||
}
|
||||
aurPkgs, err := list(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, p := range aurPkgs {
|
||||
if p.Name == pkg {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func version(ctx context.Context, pkg string) (string, error) {
|
||||
c := exec.CommandContext(ctx, "pacman", "-Q", pkg)
|
||||
var stdout bytes.Buffer
|
||||
c.Stdout = &stdout
|
||||
if err := c.Run(); err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
|
||||
return "", fmt.Errorf("aur version %s: %w", pkg, snack.ErrNotInstalled)
|
||||
}
|
||||
return "", fmt.Errorf("aur version: %w", err)
|
||||
}
|
||||
parts := strings.Fields(strings.TrimSpace(stdout.String()))
|
||||
if len(parts) < 2 {
|
||||
return "", fmt.Errorf("aur version %s: unexpected output", pkg)
|
||||
}
|
||||
return parts[1], nil
|
||||
}
|
||||
|
||||
func latestVersion(ctx context.Context, pkg string) (string, error) {
|
||||
result, err := aurQuery(ctx, "info", pkg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if result.ResultCount == 0 {
|
||||
return "", fmt.Errorf("aur latestVersion %s: %w", pkg, snack.ErrNotFound)
|
||||
}
|
||||
return result.Results[0].Version, nil
|
||||
}
|
||||
|
||||
func listUpgrades(ctx context.Context) ([]snack.Package, error) {
|
||||
aurPkgs, err := list(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var upgrades []snack.Package
|
||||
for _, p := range aurPkgs {
|
||||
result, err := aurQuery(ctx, "info", p.Name)
|
||||
if err != nil || result.ResultCount == 0 {
|
||||
continue
|
||||
}
|
||||
if result.Results[0].Version != p.Version {
|
||||
upgrades = append(upgrades, snack.Package{
|
||||
Name: p.Name,
|
||||
Version: result.Results[0].Version,
|
||||
Repository: "aur",
|
||||
Installed: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
return upgrades, nil
|
||||
}
|
||||
|
||||
func upgradeAvailable(ctx context.Context, pkg string) (bool, error) {
|
||||
installed, err := version(ctx, pkg)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
latest, err := latestVersion(ctx, pkg)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
cmp, err := versionCmp(ctx, installed, latest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return cmp < 0, nil
|
||||
}
|
||||
|
||||
func versionCmp(_ context.Context, ver1, ver2 string) (int, error) {
|
||||
c := exec.Command("vercmp", ver1, ver2)
|
||||
var stdout bytes.Buffer
|
||||
c.Stdout = &stdout
|
||||
if err := c.Run(); err != nil {
|
||||
return 0, fmt.Errorf("aur versionCmp: %w", err)
|
||||
}
|
||||
result := strings.TrimSpace(stdout.String())
|
||||
switch result {
|
||||
case "-1":
|
||||
return -1, nil
|
||||
case "0":
|
||||
return 0, nil
|
||||
case "1":
|
||||
return 1, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("aur versionCmp: unexpected output %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
func autoremove(ctx context.Context, opts ...snack.Option) error {
|
||||
o := snack.ApplyOptions(opts...)
|
||||
|
||||
// Get orphans via pacman
|
||||
c := exec.CommandContext(ctx, "pacman", "-Qdtq")
|
||||
var stdout bytes.Buffer
|
||||
c.Stdout = &stdout
|
||||
if err := c.Run(); err != nil {
|
||||
// exit status 1 means no orphans
|
||||
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("aur autoremove: %w", err)
|
||||
}
|
||||
|
||||
orphans := strings.TrimSpace(stdout.String())
|
||||
if orphans == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
args := []string{"-Rns", "--noconfirm"}
|
||||
args = append(args, strings.Fields(orphans)...)
|
||||
cmd := "pacman"
|
||||
if o.Sudo {
|
||||
args = append([]string{cmd}, args...)
|
||||
cmd = "sudo"
|
||||
}
|
||||
|
||||
removeCmd := exec.CommandContext(ctx, cmd, args...)
|
||||
return removeCmd.Run()
|
||||
}
|
||||
|
||||
func clean(_ context.Context) error {
|
||||
// AUR builds in temp dirs, nothing persistent to clean
|
||||
return nil
|
||||
}
|
||||
|
||||
func upgradePackages(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) {
|
||||
// For AUR, upgrading is just reinstalling from source
|
||||
allOpts := append([]snack.Option{snack.WithReinstall()}, opts...)
|
||||
return install(ctx, pkgs, allOpts...)
|
||||
}
|
||||
|
||||
// getAURBuildDir returns the directory to use for AUR builds.
|
||||
func getAURBuildDir() string {
|
||||
if dir := os.Getenv("AURDEST"); dir != "" {
|
||||
return dir
|
||||
}
|
||||
if cache := os.Getenv("XDG_CACHE_HOME"); cache != "" {
|
||||
return filepath.Join(cache, "aur")
|
||||
}
|
||||
home, _ := os.UserHomeDir()
|
||||
return filepath.Join(home, ".cache", "aur")
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
func available() bool { return false }
|
||||
|
||||
func (a *AUR) install(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.InstallResult, error) {
|
||||
func install(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.InstallResult, error) {
|
||||
return snack.InstallResult{}, snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
@@ -22,7 +22,11 @@ func purge(_ context.Context, _ []snack.Target, _ ...snack.Option) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func (a *AUR) upgradeAll(_ context.Context, _ ...snack.Option) error {
|
||||
func upgrade(_ context.Context, _ ...snack.Option) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func update(_ context.Context) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
@@ -30,6 +34,14 @@ func list(_ context.Context) ([]snack.Package, error) {
|
||||
return nil, snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func search(_ context.Context, _ string) ([]snack.Package, error) {
|
||||
return nil, snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func info(_ context.Context, _ string) (*snack.Package, error) {
|
||||
return nil, snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func isInstalled(_ context.Context, _ string) (bool, error) {
|
||||
return false, snack.ErrUnsupportedPlatform
|
||||
}
|
||||
@@ -58,6 +70,10 @@ func autoremove(_ context.Context, _ ...snack.Option) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func (a *AUR) cleanBuildDir() error {
|
||||
func clean(_ context.Context) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func upgradePackages(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.InstallResult, error) {
|
||||
return snack.InstallResult{}, snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
229
aur/aur_test.go
229
aur/aur_test.go
@@ -4,99 +4,74 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/gogrlx/snack"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParsePackageList(t *testing.T) {
|
||||
// Compile-time interface checks
|
||||
var (
|
||||
_ snack.Manager = (*AUR)(nil)
|
||||
_ snack.VersionQuerier = (*AUR)(nil)
|
||||
_ snack.Cleaner = (*AUR)(nil)
|
||||
_ snack.PackageUpgrader = (*AUR)(nil)
|
||||
_ snack.NameNormalizer = (*AUR)(nil)
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
a := New()
|
||||
if a == nil {
|
||||
t.Fatal("New() returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
a := New()
|
||||
if a.Name() != "aur" {
|
||||
t.Errorf("Name() = %q, want %q", a.Name(), "aur")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expect int
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
input: "",
|
||||
expect: 0,
|
||||
},
|
||||
{
|
||||
name: "single package",
|
||||
input: "yay 12.5.7-1\n",
|
||||
expect: 1,
|
||||
},
|
||||
{
|
||||
name: "multiple packages",
|
||||
input: "yay 12.5.7-1\nparu 2.0.4-1\naur-helper 1.0-1\n",
|
||||
expect: 3,
|
||||
},
|
||||
{
|
||||
name: "trailing whitespace",
|
||||
input: "yay 12.5.7-1 \n paru 2.0.4-1\n\n",
|
||||
expect: 2,
|
||||
},
|
||||
{"yay", "yay"},
|
||||
{"paru", "paru"},
|
||||
{"google-chrome", "google-chrome"},
|
||||
{"visual-studio-code-bin", "visual-studio-code-bin"},
|
||||
{"", ""},
|
||||
}
|
||||
|
||||
a := New()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pkgs := parsePackageList(tt.input)
|
||||
assert.Len(t, pkgs, tt.expect)
|
||||
for _, p := range pkgs {
|
||||
assert.NotEmpty(t, p.Name)
|
||||
assert.NotEmpty(t, p.Version)
|
||||
assert.Equal(t, "aur", p.Repository)
|
||||
assert.True(t, p.Installed)
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
got := a.NormalizeName(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("NormalizeName(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
func TestParseArch(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
wantName string
|
||||
wantArch string
|
||||
}{
|
||||
{"yay", "yay", ""},
|
||||
{"paru", "paru", ""},
|
||||
{"google-chrome", "google-chrome", ""},
|
||||
}
|
||||
|
||||
a := New()
|
||||
assert.Equal(t, "aur", a.Name())
|
||||
assert.Empty(t, a.BuildDir)
|
||||
assert.Nil(t, a.MakepkgFlags)
|
||||
}
|
||||
|
||||
func TestNewWithOptions(t *testing.T) {
|
||||
a := NewWithOptions(
|
||||
WithBuildDir("/tmp/aur-builds"),
|
||||
WithMakepkgFlags("--skippgpcheck", "--nocheck"),
|
||||
)
|
||||
assert.Equal(t, "/tmp/aur-builds", a.BuildDir)
|
||||
assert.Equal(t, []string{"--skippgpcheck", "--nocheck"}, a.MakepkgFlags)
|
||||
}
|
||||
|
||||
func TestInterfaceCompliance(t *testing.T) {
|
||||
var _ snack.Manager = (*AUR)(nil)
|
||||
var _ snack.VersionQuerier = (*AUR)(nil)
|
||||
var _ snack.Cleaner = (*AUR)(nil)
|
||||
var _ snack.PackageUpgrader = (*AUR)(nil)
|
||||
}
|
||||
|
||||
func TestInterfaceNonCompliance(t *testing.T) {
|
||||
a := New()
|
||||
var m snack.Manager = a
|
||||
|
||||
if _, ok := m.(snack.FileOwner); ok {
|
||||
t.Error("AUR should not implement FileOwner")
|
||||
}
|
||||
if _, ok := m.(snack.Holder); ok {
|
||||
t.Error("AUR should not implement Holder")
|
||||
}
|
||||
if _, ok := m.(snack.RepoManager); ok {
|
||||
t.Error("AUR should not implement RepoManager")
|
||||
}
|
||||
if _, ok := m.(snack.KeyManager); ok {
|
||||
t.Error("AUR should not implement KeyManager")
|
||||
}
|
||||
if _, ok := m.(snack.Grouper); ok {
|
||||
t.Error("AUR should not implement Grouper")
|
||||
}
|
||||
if _, ok := m.(snack.NameNormalizer); ok {
|
||||
t.Error("AUR should not implement NameNormalizer")
|
||||
}
|
||||
if _, ok := m.(snack.DryRunner); ok {
|
||||
t.Error("AUR should not implement DryRunner")
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
gotName, gotArch := a.ParseArch(tt.input)
|
||||
if gotName != tt.wantName || gotArch != tt.wantArch {
|
||||
t.Errorf("ParseArch(%q) = (%q, %q), want (%q, %q)",
|
||||
tt.input, gotName, gotArch, tt.wantName, tt.wantArch)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,14 +85,15 @@ func TestCapabilities(t *testing.T) {
|
||||
}{
|
||||
{"VersionQuery", caps.VersionQuery, true},
|
||||
{"Clean", caps.Clean, true},
|
||||
{"FileOwnership", caps.FileOwnership, false},
|
||||
{"PackageUpgrade", caps.PackageUpgrade, true},
|
||||
{"NameNormalize", caps.NameNormalize, true},
|
||||
// AUR does not support these
|
||||
{"Hold", caps.Hold, false},
|
||||
{"FileOwnership", caps.FileOwnership, false},
|
||||
{"RepoManagement", caps.RepoManagement, false},
|
||||
{"KeyManagement", caps.KeyManagement, false},
|
||||
{"Groups", caps.Groups, false},
|
||||
{"NameNormalize", caps.NameNormalize, false},
|
||||
{"DryRun", caps.DryRun, false},
|
||||
{"PackageUpgrade", caps.PackageUpgrade, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -129,78 +105,21 @@ func TestCapabilities(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
a := New()
|
||||
assert.Equal(t, "aur", a.Name())
|
||||
}
|
||||
|
||||
func TestParsePackageList_EdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantLen int
|
||||
wantNames []string
|
||||
wantVers []string
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "whitespace only",
|
||||
input: " \n\t\n \n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "single package",
|
||||
input: "yay 12.5.7-1\n",
|
||||
wantLen: 1,
|
||||
wantNames: []string{"yay"},
|
||||
wantVers: []string{"12.5.7-1"},
|
||||
},
|
||||
{
|
||||
name: "malformed single field",
|
||||
input: "orphan\n",
|
||||
wantLen: 0,
|
||||
},
|
||||
{
|
||||
name: "malformed mixed with valid",
|
||||
input: "orphan\nyay 12.5.7-1\nbadline\nparu 2.0-1\n",
|
||||
wantLen: 2,
|
||||
wantNames: []string{"yay", "paru"},
|
||||
wantVers: []string{"12.5.7-1", "2.0-1"},
|
||||
},
|
||||
{
|
||||
name: "extra fields ignored",
|
||||
input: "yay 12.5.7-1 extra stuff\n",
|
||||
wantLen: 1,
|
||||
wantNames: []string{"yay"},
|
||||
wantVers: []string{"12.5.7-1"},
|
||||
},
|
||||
{
|
||||
name: "trailing and leading whitespace on lines",
|
||||
input: " yay 12.5.7-1 \n paru 2.0.4-1\n\n",
|
||||
wantLen: 2,
|
||||
wantNames: []string{"yay", "paru"},
|
||||
wantVers: []string{"12.5.7-1", "2.0.4-1"},
|
||||
},
|
||||
func TestInterfaceNonCompliance(t *testing.T) {
|
||||
var m snack.Manager = New()
|
||||
if _, ok := m.(snack.Holder); ok {
|
||||
t.Error("AUR should not implement Holder")
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pkgs := parsePackageList(tt.input)
|
||||
require.Len(t, pkgs, tt.wantLen)
|
||||
for i, p := range pkgs {
|
||||
assert.Equal(t, "aur", p.Repository, "all packages should have Repository=aur")
|
||||
assert.True(t, p.Installed, "all packages should have Installed=true")
|
||||
if i < len(tt.wantNames) {
|
||||
assert.Equal(t, tt.wantNames[i], p.Name)
|
||||
}
|
||||
if i < len(tt.wantVers) {
|
||||
assert.Equal(t, tt.wantVers[i], p.Version)
|
||||
}
|
||||
}
|
||||
})
|
||||
if _, ok := m.(snack.FileOwner); ok {
|
||||
t.Error("AUR should not implement FileOwner")
|
||||
}
|
||||
if _, ok := m.(snack.RepoManager); ok {
|
||||
t.Error("AUR should not implement RepoManager")
|
||||
}
|
||||
if _, ok := m.(snack.KeyManager); ok {
|
||||
t.Error("AUR should not implement KeyManager")
|
||||
}
|
||||
if _, ok := m.(snack.Grouper); ok {
|
||||
t.Error("AUR should not implement Grouper")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,30 +6,22 @@ import (
|
||||
"github.com/gogrlx/snack"
|
||||
)
|
||||
|
||||
// Compile-time interface checks.
|
||||
var (
|
||||
_ snack.Manager = (*AUR)(nil)
|
||||
_ snack.VersionQuerier = (*AUR)(nil)
|
||||
_ snack.Cleaner = (*AUR)(nil)
|
||||
_ snack.PackageUpgrader = (*AUR)(nil)
|
||||
)
|
||||
|
||||
// LatestVersion returns the latest version available in the AUR.
|
||||
// LatestVersion returns the latest version of an AUR package.
|
||||
func (a *AUR) LatestVersion(ctx context.Context, pkg string) (string, error) {
|
||||
return latestVersion(ctx, pkg)
|
||||
}
|
||||
|
||||
// ListUpgrades returns installed foreign packages that have newer versions in the AUR.
|
||||
// ListUpgrades returns AUR packages that have newer versions available.
|
||||
func (a *AUR) ListUpgrades(ctx context.Context) ([]snack.Package, error) {
|
||||
return listUpgrades(ctx)
|
||||
}
|
||||
|
||||
// UpgradeAvailable reports whether a newer version is available in the AUR.
|
||||
// UpgradeAvailable reports whether a newer version is available.
|
||||
func (a *AUR) UpgradeAvailable(ctx context.Context, pkg string) (bool, error) {
|
||||
return upgradeAvailable(ctx, pkg)
|
||||
}
|
||||
|
||||
// VersionCmp compares two version strings using pacman's vercmp.
|
||||
// VersionCmp compares two version strings using vercmp.
|
||||
func (a *AUR) VersionCmp(ctx context.Context, ver1, ver2 string) (int, error) {
|
||||
return versionCmp(ctx, ver1, ver2)
|
||||
}
|
||||
@@ -41,14 +33,14 @@ func (a *AUR) Autoremove(ctx context.Context, opts ...snack.Option) error {
|
||||
return autoremove(ctx, opts...)
|
||||
}
|
||||
|
||||
// Clean removes cached build artifacts from the build directory.
|
||||
// Clean is a no-op for AUR (builds use temp directories).
|
||||
func (a *AUR) Clean(_ context.Context) error {
|
||||
return a.cleanBuildDir()
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpgradePackages rebuilds and reinstalls specific AUR packages.
|
||||
func (a *AUR) UpgradePackages(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return a.install(ctx, pkgs, opts...)
|
||||
return upgradePackages(ctx, pkgs, opts...)
|
||||
}
|
||||
|
||||
15
aur/normalize.go
Normal file
15
aur/normalize.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package aur
|
||||
|
||||
// normalizeName returns the canonical form of an AUR package name.
|
||||
// AUR package names are simple identifiers without architecture or version
|
||||
// suffixes, so this is essentially a pass-through.
|
||||
func normalizeName(name string) string {
|
||||
return name
|
||||
}
|
||||
|
||||
// parseArch extracts the architecture from a package name if present.
|
||||
// AUR package names do not include architecture suffixes,
|
||||
// so this returns the name unchanged with an empty string.
|
||||
func parseArch(name string) (string, string) {
|
||||
return name, ""
|
||||
}
|
||||
Reference in New Issue
Block a user