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 ` 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 `. 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 -Sy`. func (p Pacman) Update(ctx context.Context) error { return p.Runner.Run(ctx, "sudo", "pacman", "-Sy") }