Files
Archduino/internal/platform/pacman.go

97 lines
2.8 KiB
Go

package platform
import (
"bytes"
"context"
"errors"
"fmt"
"os/exec"
"strings"
)
// ExecRunner runs commands via os/exec. This is the production Runner.
type ExecRunner struct{}
func (ExecRunner) Run(ctx context.Context, name string, args ...string) error {
cmd := exec.CommandContext(ctx, name, args...)
var stderr bytes.Buffer
cmd.Stdout = nil
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
msg := strings.TrimSpace(stderr.String())
if msg == "" {
return fmt.Errorf("%s %s: %w", name, strings.Join(args, " "), err)
}
return fmt.Errorf("%s %s: %w: %s", name, strings.Join(args, " "), err, msg)
}
return nil
}
func (ExecRunner) Output(ctx context.Context, name string, args ...string) ([]byte, error) {
cmd := exec.CommandContext(ctx, name, args...)
var stderr bytes.Buffer
cmd.Stderr = &stderr
out, err := cmd.Output()
if err != nil {
return out, fmt.Errorf("%s %s: %w: %s", name, strings.Join(args, " "), err, stderr.String())
}
return out, nil
}
// Pacman implements PackageManager via `pacman` on Arch.
type Pacman struct {
Runner Runner
}
func NewPacman(r Runner) *Pacman {
if r == nil {
r = ExecRunner{}
}
return &Pacman{Runner: r}
}
func (Pacman) Name() string { return "pacman" }
// IsInstalled returns true when `pacman -Q <pkg>` succeeds.
func (p Pacman) IsInstalled(ctx context.Context, pkg string) (bool, error) {
if pkg == "" {
return false, errors.New("empty package name")
}
_, err := p.Runner.Output(ctx, "pacman", "-Q", pkg)
if err == nil {
return true, nil
}
// pacman -Q exits 1 when not installed; we treat any error as "not installed"
// because false-positives on transient errors here are harmless (Install
// is idempotent via --needed).
return false, nil
}
// Install runs `sudo pacman -S --needed --noconfirm <pkgs...>`.
func (p Pacman) Install(ctx context.Context, pkgs []string) error {
if len(pkgs) == 0 {
return nil
}
args := append([]string{"pacman", "-S", "--needed", "--noconfirm"}, pkgs...)
return p.Runner.Run(ctx, "sudo", args...)
}
// Reinstall runs `sudo pacman -S --noconfirm <pkgs...>` WITHOUT `--needed`,
// so pacman physically re-fetches and reinstalls the packages even if they
// are already present. Used by the `--force` flag to bypass idempotency.
func (p Pacman) Reinstall(ctx context.Context, pkgs []string) error {
if len(pkgs) == 0 {
return nil
}
args := append([]string{"pacman", "-S", "--noconfirm"}, pkgs...)
return p.Runner.Run(ctx, "sudo", args...)
}
// Update runs `sudo pacman -Syu --noconfirm` (full sync + system upgrade).
// On Arch, a bare `pacman -Sy` followed by an install is the partial-upgrade
// anti-pattern that can break the system when a dep's ABI changes, so we
// always pair the sync with the upgrade.
func (p Pacman) Update(ctx context.Context) error {
return p.Runner.Run(ctx, "sudo", "pacman", "-Syu", "--noconfirm")
}