- privilege: wire sudo -v to real stdin/stdout/stderr so it can prompt - cmd: validate --only/--skip IDs against registry (prevents silent run-all on typo); gate install to Arch-only (spec requirement); mark --only and --skip mutually exclusive - platform: Pacman.Update now runs -Syu --noconfirm, not -Sy (avoids Arch partial-upgrade anti-pattern) - test/integration: trap-clean the named docker volume on any exit - go.mod: tidy — yaml.v3 is a direct dep
78 lines
2.1 KiB
Go
78 lines
2.1 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...)
|
|
cmd.Stdout = nil
|
|
cmd.Stderr = nil
|
|
return cmd.Run()
|
|
}
|
|
|
|
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")
|
|
}
|