package platform import ( "context" "os" "path/filepath" "strings" ) // AURHelper abstracts an AUR front-end (currently only yay). type AURHelper interface { EnsureInstalled(ctx context.Context) error Install(ctx context.Context, pkgs []string) error } type Yay struct { Runner Runner pm *Pacman } func NewYay(r Runner) *Yay { return &Yay{Runner: r, pm: NewPacman(r)} } func (y *Yay) EnsureInstalled(ctx context.Context) error { ok, _ := y.pm.IsInstalled(ctx, "yay") if ok { return nil } // Bootstrap: install build deps, clone AUR yay, makepkg -si. if err := y.pm.Install(ctx, []string{"base-devel", "git"}); err != nil { return err } tmp, err := os.MkdirTemp("", "archduino-yay-") if err != nil { return err } defer os.RemoveAll(tmp) clone := filepath.Join(tmp, "yay") if err := y.Runner.Run(ctx, "git", "clone", "https://aur.archlinux.org/yay.git", clone); err != nil { return err } // `makepkg -si --noconfirm` needs to cd into the cloned dir. os/exec does // not have a per-command cwd without exec.Cmd; our Runner interface does // not expose that, so we invoke via sh -c with a pushd-style prefix. return y.Runner.Run(ctx, "sh", "-c", "cd "+shellQuote(clone)+" && makepkg -si --noconfirm") } func (y *Yay) Install(ctx context.Context, pkgs []string) error { if len(pkgs) == 0 { return nil } args := append([]string{"-S", "--needed", "--noconfirm"}, pkgs...) return y.Runner.Run(ctx, "yay", args...) } // Reinstall runs `yay -S --noconfirm ` WITHOUT `--needed`, forcing // yay/pacman to re-fetch and reinstall the packages even if already present. // Used by the `--force` flag to bypass idempotency. func (y *Yay) Reinstall(ctx context.Context, pkgs []string) error { if len(pkgs) == 0 { return nil } args := append([]string{"-S", "--noconfirm"}, pkgs...) return y.Runner.Run(ctx, "yay", args...) } func shellQuote(s string) string { return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" }