- neofetch was removed from Arch's official repos; fastfetch is the maintained replacement. - ExecRunner.Run previously discarded stderr, so any 'sudo pacman' failure surfaced only as 'exit status 1'. Now the returned error includes the captured stderr (matching what Output already did).
86 lines
2.4 KiB
Go
86 lines
2.4 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...)
|
|
}
|
|
|
|
// 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")
|
|
}
|