From 34578efcad31a389480a0941beb8c568a0bf630a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 06:31:01 +0000 Subject: [PATCH] Update all package managers to return InstallResult/RemoveResult Change Install and Remove method signatures across all package manager implementations (apt, apk, dnf, pacman, rpm, dpkg, snap, flatpak, ports, pkg) to match the updated Manager interface. - Wrapper files: update Install/Remove to return (snack.InstallResult, error) and (snack.RemoveResult, error) respectively - Platform files (_linux.go, _openbsd.go, _freebsd.go): implement pre-check logic using isInstalled() to classify packages as unchanged or to-process, run command on actionable packages only, then collect results with version() - Stub files (_other.go): return (snack.InstallResult{}, ErrUnsupportedPlatform) and (snack.RemoveResult{}, ErrUnsupportedPlatform) - DNF special case: add v5 bool parameter to internal install/remove functions and thread d.v5 from the wrapper; update Purge to discard the result - cmd/snack/main.go: update install/remove commands to discard InstallResult/ RemoveResult and return only the error to cobra Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- apk/apk.go | 4 +- apk/apk_linux.go | 53 ++++++++++++++++--- apk/apk_other.go | 8 +-- apt/apt.go | 4 +- apt/apt_linux.go | 47 +++++++++++++++-- apt/apt_other.go | 8 +-- cmd/snack/main.go | 6 ++- dnf/dnf.go | 11 ++-- dnf/dnf_linux.go | 81 +++++++++++++++++++++-------- dnf/dnf_other.go | 8 +-- dpkg/dpkg.go | 4 +- dpkg/dpkg_linux.go | 108 +++++++++++++++++++++++++-------------- dpkg/dpkg_other.go | 8 +-- flatpak/flatpak.go | 4 +- flatpak/flatpak_linux.go | 47 ++++++++++++++--- flatpak/flatpak_other.go | 8 +-- pacman/pacman.go | 4 +- pacman/pacman_linux.go | 63 +++++++++++++++++------ pacman/pacman_other.go | 8 +-- pkg/pkg.go | 4 +- pkg/pkg_freebsd.go | 53 ++++++++++++++++--- pkg/pkg_other.go | 8 +-- ports/ports.go | 4 +- ports/ports_openbsd.go | 51 +++++++++++++++--- ports/ports_other.go | 8 +-- rpm/rpm.go | 7 +-- rpm/rpm_linux.go | 53 ++++++++++++++++--- rpm/rpm_other.go | 8 +-- snack.go | 8 +-- snap/snap.go | 4 +- snap/snap_linux.go | 48 ++++++++++++++--- snap/snap_other.go | 8 +-- types.go | 20 ++++++++ 33 files changed, 571 insertions(+), 197 deletions(-) diff --git a/apk/apk.go b/apk/apk.go index e841855..2e47ddb 100644 --- a/apk/apk.go +++ b/apk/apk.go @@ -27,14 +27,14 @@ func (a *Apk) Name() string { return "apk" } func (a *Apk) Available() bool { return available() } // Install one or more packages. -func (a *Apk) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (a *Apk) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { a.Lock() defer a.Unlock() return install(ctx, pkgs, opts...) } // Remove one or more packages. -func (a *Apk) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (a *Apk) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) { a.Lock() defer a.Unlock() return remove(ctx, pkgs, opts...) diff --git a/apk/apk_linux.go b/apk/apk_linux.go index 0d99774..e98e9b6 100644 --- a/apk/apk_linux.go +++ b/apk/apk_linux.go @@ -66,16 +66,53 @@ func formatTargets(targets []snack.Target) []string { return args } -func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { - args := append([]string{"add"}, formatTargets(pkgs)...) - _, err := run(ctx, args, opts...) - return err +func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { + var toInstall []snack.Target + var unchanged []string + for _, t := range pkgs { + ok, _ := isInstalled(ctx, t.Name) + if ok { + unchanged = append(unchanged, t.Name) + } else { + toInstall = append(toInstall, t) + } + } + if len(toInstall) > 0 { + args := append([]string{"add"}, formatTargets(toInstall)...) + if _, err := run(ctx, args, opts...); err != nil { + return snack.InstallResult{}, err + } + } + var installed []snack.Package + for _, t := range toInstall { + v, _ := version(ctx, t.Name) + installed = append(installed, snack.Package{Name: t.Name, Version: v, Installed: true}) + } + return snack.InstallResult{Installed: installed, Unchanged: unchanged}, nil } -func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { - args := append([]string{"del"}, snack.TargetNames(pkgs)...) - _, err := run(ctx, args, opts...) - return err +func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) { + var toRemove []snack.Target + var unchanged []string + for _, t := range pkgs { + ok, _ := isInstalled(ctx, t.Name) + if !ok { + unchanged = append(unchanged, t.Name) + } else { + toRemove = append(toRemove, t) + } + } + if len(toRemove) > 0 { + args := append([]string{"del"}, snack.TargetNames(toRemove)...) + if _, err := run(ctx, args, opts...); err != nil { + return snack.RemoveResult{}, err + } + } + var removed []snack.Package + for _, t := range toRemove { + removed = append(removed, snack.Package{Name: t.Name}) + } + return snack.RemoveResult{Removed: removed, Unchanged: unchanged}, nil } func purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { diff --git a/apk/apk_other.go b/apk/apk_other.go index ab5155a..310c588 100644 --- a/apk/apk_other.go +++ b/apk/apk_other.go @@ -10,12 +10,12 @@ import ( func available() bool { return false } -func install(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func install(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.InstallResult, error) { + return snack.InstallResult{}, snack.ErrUnsupportedPlatform } -func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.RemoveResult, error) { + return snack.RemoveResult{}, snack.ErrUnsupportedPlatform } func purge(_ context.Context, _ []snack.Target, _ ...snack.Option) error { diff --git a/apt/apt.go b/apt/apt.go index 18fe7ad..a0aeae7 100644 --- a/apt/apt.go +++ b/apt/apt.go @@ -21,14 +21,14 @@ func New() *Apt { func (a *Apt) Name() string { return "apt" } // Install one or more packages. -func (a *Apt) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (a *Apt) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { a.Lock() defer a.Unlock() return install(ctx, pkgs, opts...) } // Remove one or more packages. -func (a *Apt) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (a *Apt) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) { a.Lock() defer a.Unlock() return remove(ctx, pkgs, opts...) diff --git a/apt/apt_linux.go b/apt/apt_linux.go index 8f8a6e3..5a41016 100644 --- a/apt/apt_linux.go +++ b/apt/apt_linux.go @@ -77,12 +77,51 @@ func runAptGet(ctx context.Context, command string, pkgs []snack.Target, opts .. return nil } -func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { - return runAptGet(ctx, "install", pkgs, opts...) +func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { + var toInstall []snack.Target + var unchanged []string + for _, t := range pkgs { + ok, _ := isInstalled(ctx, t.Name) + if ok { + unchanged = append(unchanged, t.Name) + } else { + toInstall = append(toInstall, t) + } + } + if len(toInstall) > 0 { + if err := runAptGet(ctx, "install", toInstall, opts...); err != nil { + return snack.InstallResult{}, err + } + } + var installed []snack.Package + for _, t := range toInstall { + v, _ := version(ctx, t.Name) + installed = append(installed, snack.Package{Name: t.Name, Version: v, Installed: true}) + } + return snack.InstallResult{Installed: installed, Unchanged: unchanged}, nil } -func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { - return runAptGet(ctx, "remove", pkgs, opts...) +func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) { + var toRemove []snack.Target + var unchanged []string + for _, t := range pkgs { + ok, _ := isInstalled(ctx, t.Name) + if !ok { + unchanged = append(unchanged, t.Name) + } else { + toRemove = append(toRemove, t) + } + } + if len(toRemove) > 0 { + if err := runAptGet(ctx, "remove", toRemove, opts...); err != nil { + return snack.RemoveResult{}, err + } + } + var removed []snack.Package + for _, t := range toRemove { + removed = append(removed, snack.Package{Name: t.Name}) + } + return snack.RemoveResult{Removed: removed, Unchanged: unchanged}, nil } func purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { diff --git a/apt/apt_other.go b/apt/apt_other.go index da908dd..1f5f38e 100644 --- a/apt/apt_other.go +++ b/apt/apt_other.go @@ -10,12 +10,12 @@ import ( func available() bool { return false } -func install(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func install(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.InstallResult, error) { + return snack.InstallResult{}, snack.ErrUnsupportedPlatform } -func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.RemoveResult, error) { + return snack.RemoveResult{}, snack.ErrUnsupportedPlatform } func purge(_ context.Context, _ []snack.Target, _ ...snack.Option) error { diff --git a/cmd/snack/main.go b/cmd/snack/main.go index 72a6a61..cad838d 100644 --- a/cmd/snack/main.go +++ b/cmd/snack/main.go @@ -102,7 +102,8 @@ func installCmd() *cobra.Command { if err != nil { return err } - return m.Install(cmd.Context(), targets(args, ver), opts()...) + _, err = m.Install(cmd.Context(), targets(args, ver), opts()...) + return err }, } cmd.Flags().StringVar(&ver, "version", "", "pin version for all targets") @@ -119,7 +120,8 @@ func removeCmd() *cobra.Command { if err != nil { return err } - return m.Remove(cmd.Context(), snack.Targets(args...), opts()...) + _, err = m.Remove(cmd.Context(), snack.Targets(args...), opts()...) + return err }, } } diff --git a/dnf/dnf.go b/dnf/dnf.go index 20f2aeb..bfe8fde 100644 --- a/dnf/dnf.go +++ b/dnf/dnf.go @@ -31,24 +31,25 @@ func (d *DNF) Name() string { return "dnf" } func (d *DNF) Available() bool { return available() } // Install one or more packages. -func (d *DNF) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (d *DNF) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { d.Lock() defer d.Unlock() - return install(ctx, pkgs, opts...) + return install(ctx, d.v5, pkgs, opts...) } // Remove one or more packages. -func (d *DNF) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (d *DNF) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) { d.Lock() defer d.Unlock() - return remove(ctx, pkgs, opts...) + return remove(ctx, d.v5, pkgs, opts...) } // Purge removes packages including configuration files (same as Remove for dnf). func (d *DNF) Purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { d.Lock() defer d.Unlock() - return remove(ctx, pkgs, opts...) + _, err := remove(ctx, d.v5, pkgs, opts...) + return err } // Upgrade all installed packages to their latest versions. diff --git a/dnf/dnf_linux.go b/dnf/dnf_linux.go index 3dab77c..85a2e79 100644 --- a/dnf/dnf_linux.go +++ b/dnf/dnf_linux.go @@ -89,34 +89,71 @@ func formatTargets(targets []snack.Target) []string { return args } -func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { - o := snack.ApplyOptions(opts...) - base := []string{"install", "-y"} - if o.Refresh { - base = append(base, "--refresh") - } - if o.FromRepo != "" { - base = append(base, "--repo="+o.FromRepo) - } - if o.Reinstall { - base[0] = "reinstall" - } +func install(ctx context.Context, v5 bool, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { + var toInstall []snack.Target + var unchanged []string for _, t := range pkgs { - if t.FromRepo != "" { - base = append(base, "--repo="+t.FromRepo) - break + ok, _ := isInstalled(ctx, t.Name, v5) + if ok { + unchanged = append(unchanged, t.Name) + } else { + toInstall = append(toInstall, t) } } - args := append(base, formatTargets(pkgs)...) - _, err := run(ctx, args, o) - return err + o := snack.ApplyOptions(opts...) + if len(toInstall) > 0 { + base := []string{"install", "-y"} + if o.Refresh { + base = append(base, "--refresh") + } + if o.FromRepo != "" { + base = append(base, "--repo="+o.FromRepo) + } + if o.Reinstall { + base[0] = "reinstall" + } + for _, t := range toInstall { + if t.FromRepo != "" { + base = append(base, "--repo="+t.FromRepo) + break + } + } + args := append(base, formatTargets(toInstall)...) + if _, err := run(ctx, args, o); err != nil { + return snack.InstallResult{}, err + } + } + var installed []snack.Package + for _, t := range toInstall { + v, _ := version(ctx, t.Name, v5) + installed = append(installed, snack.Package{Name: t.Name, Version: v, Installed: true}) + } + return snack.InstallResult{Installed: installed, Unchanged: unchanged}, nil } -func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func remove(ctx context.Context, v5 bool, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) { + var toRemove []snack.Target + var unchanged []string + for _, t := range pkgs { + ok, _ := isInstalled(ctx, t.Name, v5) + if !ok { + unchanged = append(unchanged, t.Name) + } else { + toRemove = append(toRemove, t) + } + } o := snack.ApplyOptions(opts...) - args := append([]string{"remove", "-y"}, snack.TargetNames(pkgs)...) - _, err := run(ctx, args, o) - return err + if len(toRemove) > 0 { + args := append([]string{"remove", "-y"}, snack.TargetNames(toRemove)...) + if _, err := run(ctx, args, o); err != nil { + return snack.RemoveResult{}, err + } + } + var removed []snack.Package + for _, t := range toRemove { + removed = append(removed, snack.Package{Name: t.Name}) + } + return snack.RemoveResult{Removed: removed, Unchanged: unchanged}, nil } func upgrade(ctx context.Context, opts ...snack.Option) error { diff --git a/dnf/dnf_other.go b/dnf/dnf_other.go index 509aaf9..49a648f 100644 --- a/dnf/dnf_other.go +++ b/dnf/dnf_other.go @@ -12,12 +12,12 @@ func available() bool { return false } func (d *DNF) detectVersion() {} -func install(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func install(_ context.Context, _ bool, _ []snack.Target, _ ...snack.Option) (snack.InstallResult, error) { + return snack.InstallResult{}, snack.ErrUnsupportedPlatform } -func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func remove(_ context.Context, _ bool, _ []snack.Target, _ ...snack.Option) (snack.RemoveResult, error) { + return snack.RemoveResult{}, snack.ErrUnsupportedPlatform } func upgrade(_ context.Context, _ ...snack.Option) error { diff --git a/dpkg/dpkg.go b/dpkg/dpkg.go index ed922ed..76fa30e 100644 --- a/dpkg/dpkg.go +++ b/dpkg/dpkg.go @@ -21,14 +21,14 @@ func New() *Dpkg { func (d *Dpkg) Name() string { return "dpkg" } // Install one or more .deb files. -func (d *Dpkg) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (d *Dpkg) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { d.Lock() defer d.Unlock() return install(ctx, pkgs, opts...) } // Remove one or more packages. -func (d *Dpkg) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (d *Dpkg) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) { d.Lock() defer d.Unlock() return remove(ctx, pkgs, opts...) diff --git a/dpkg/dpkg_linux.go b/dpkg/dpkg_linux.go index 82ae927..00fbc92 100644 --- a/dpkg/dpkg_linux.go +++ b/dpkg/dpkg_linux.go @@ -17,55 +17,87 @@ func available() bool { return err == nil } -func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { - o := snack.ApplyOptions(opts...) - var args []string - if o.Sudo { - args = append(args, "sudo") - } - args = append(args, "dpkg", "-i") - if o.DryRun { - args = append(args, "--simulate") - } - // dpkg -i takes file paths; use Source if set, otherwise Name +func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { + var toInstall []snack.Target + var unchanged []string for _, t := range pkgs { - if t.Source != "" { - args = append(args, t.Source) + ok, _ := isInstalled(ctx, t.Name) + if ok { + unchanged = append(unchanged, t.Name) } else { - args = append(args, t.Name) + toInstall = append(toInstall, t) } } - cmd := exec.CommandContext(ctx, args[0], args[1:]...) - var stderr bytes.Buffer - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - errMsg := stderr.String() - if strings.Contains(errMsg, "Permission denied") || strings.Contains(errMsg, "are you root") { - return fmt.Errorf("dpkg -i: %w", snack.ErrPermissionDenied) + o := snack.ApplyOptions(opts...) + if len(toInstall) > 0 { + var args []string + if o.Sudo { + args = append(args, "sudo") + } + args = append(args, "dpkg", "-i") + if o.DryRun { + args = append(args, "--simulate") + } + for _, t := range toInstall { + if t.Source != "" { + args = append(args, t.Source) + } else { + args = append(args, t.Name) + } + } + cmd := exec.CommandContext(ctx, args[0], args[1:]...) + var stderr bytes.Buffer + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + errMsg := stderr.String() + if strings.Contains(errMsg, "Permission denied") || strings.Contains(errMsg, "are you root") { + return snack.InstallResult{}, fmt.Errorf("dpkg -i: %w", snack.ErrPermissionDenied) + } + return snack.InstallResult{}, fmt.Errorf("dpkg -i: %w: %s", err, errMsg) } - return fmt.Errorf("dpkg -i: %w: %s", err, errMsg) } - return nil + var installed []snack.Package + for _, t := range toInstall { + v, _ := version(ctx, t.Name) + installed = append(installed, snack.Package{Name: t.Name, Version: v, Installed: true}) + } + return snack.InstallResult{Installed: installed, Unchanged: unchanged}, nil } -func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) { + var toRemove []snack.Target + var unchanged []string + for _, t := range pkgs { + ok, _ := isInstalled(ctx, t.Name) + if !ok { + unchanged = append(unchanged, t.Name) + } else { + toRemove = append(toRemove, t) + } + } o := snack.ApplyOptions(opts...) - var args []string - if o.Sudo { - args = append(args, "sudo") + if len(toRemove) > 0 { + var args []string + if o.Sudo { + args = append(args, "sudo") + } + args = append(args, "dpkg", "-r") + if o.DryRun { + args = append(args, "--simulate") + } + args = append(args, snack.TargetNames(toRemove)...) + cmd := exec.CommandContext(ctx, args[0], args[1:]...) + var stderr bytes.Buffer + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return snack.RemoveResult{}, fmt.Errorf("dpkg -r: %w: %s", err, stderr.String()) + } } - args = append(args, "dpkg", "-r") - if o.DryRun { - args = append(args, "--simulate") + var removed []snack.Package + for _, t := range toRemove { + removed = append(removed, snack.Package{Name: t.Name}) } - args = append(args, snack.TargetNames(pkgs)...) - cmd := exec.CommandContext(ctx, args[0], args[1:]...) - var stderr bytes.Buffer - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - return fmt.Errorf("dpkg -r: %w: %s", err, stderr.String()) - } - return nil + return snack.RemoveResult{Removed: removed, Unchanged: unchanged}, nil } func purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { diff --git a/dpkg/dpkg_other.go b/dpkg/dpkg_other.go index 0109456..ccf58c6 100644 --- a/dpkg/dpkg_other.go +++ b/dpkg/dpkg_other.go @@ -10,12 +10,12 @@ import ( func available() bool { return false } -func install(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func install(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.InstallResult, error) { + return snack.InstallResult{}, snack.ErrUnsupportedPlatform } -func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.RemoveResult, error) { + return snack.RemoveResult{}, snack.ErrUnsupportedPlatform } func purge(_ context.Context, _ []snack.Target, _ ...snack.Option) error { diff --git a/flatpak/flatpak.go b/flatpak/flatpak.go index e9e1834..20f5c76 100644 --- a/flatpak/flatpak.go +++ b/flatpak/flatpak.go @@ -24,14 +24,14 @@ func (f *Flatpak) Name() string { return "flatpak" } func (f *Flatpak) Available() bool { return available() } // Install one or more packages. -func (f *Flatpak) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (f *Flatpak) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { f.Lock() defer f.Unlock() return install(ctx, pkgs, opts...) } // Remove one or more packages. -func (f *Flatpak) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (f *Flatpak) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) { f.Lock() defer f.Unlock() return remove(ctx, pkgs, opts...) diff --git a/flatpak/flatpak_linux.go b/flatpak/flatpak_linux.go index 845ea61..f1bb3e6 100644 --- a/flatpak/flatpak_linux.go +++ b/flatpak/flatpak_linux.go @@ -33,25 +33,58 @@ func run(ctx context.Context, args []string) (string, error) { return stdout.String(), nil } -func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { _ = snack.ApplyOptions(opts...) + var toInstall []snack.Target + var unchanged []string for _, t := range pkgs { + ok, _ := isInstalled(ctx, t.Name) + if ok { + unchanged = append(unchanged, t.Name) + } else { + toInstall = append(toInstall, t) + } + } + for _, t := range toInstall { remote := t.FromRepo if remote == "" { remote = "flathub" } args := []string{"install", "-y", remote, t.Name} if _, err := run(ctx, args); err != nil { - return err + return snack.InstallResult{}, err } } - return nil + var installed []snack.Package + for _, t := range toInstall { + v, _ := version(ctx, t.Name) + installed = append(installed, snack.Package{Name: t.Name, Version: v, Installed: true}) + } + return snack.InstallResult{Installed: installed, Unchanged: unchanged}, nil } -func remove(ctx context.Context, pkgs []snack.Target, _ ...snack.Option) error { - args := append([]string{"uninstall", "-y"}, snack.TargetNames(pkgs)...) - _, err := run(ctx, args) - return err +func remove(ctx context.Context, pkgs []snack.Target, _ ...snack.Option) (snack.RemoveResult, error) { + var toRemove []snack.Target + var unchanged []string + for _, t := range pkgs { + ok, _ := isInstalled(ctx, t.Name) + if !ok { + unchanged = append(unchanged, t.Name) + } else { + toRemove = append(toRemove, t) + } + } + if len(toRemove) > 0 { + args := append([]string{"uninstall", "-y"}, snack.TargetNames(toRemove)...) + if _, err := run(ctx, args); err != nil { + return snack.RemoveResult{}, err + } + } + var removed []snack.Package + for _, t := range toRemove { + removed = append(removed, snack.Package{Name: t.Name}) + } + return snack.RemoveResult{Removed: removed, Unchanged: unchanged}, nil } func purge(ctx context.Context, pkgs []snack.Target, _ ...snack.Option) error { diff --git a/flatpak/flatpak_other.go b/flatpak/flatpak_other.go index 62395ef..81b6e11 100644 --- a/flatpak/flatpak_other.go +++ b/flatpak/flatpak_other.go @@ -10,12 +10,12 @@ import ( func available() bool { return false } -func install(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func install(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.InstallResult, error) { + return snack.InstallResult{}, snack.ErrUnsupportedPlatform } -func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.RemoveResult, error) { + return snack.RemoveResult{}, snack.ErrUnsupportedPlatform } func purge(_ context.Context, _ []snack.Target, _ ...snack.Option) error { diff --git a/pacman/pacman.go b/pacman/pacman.go index 5d7e7ea..2e7a79e 100644 --- a/pacman/pacman.go +++ b/pacman/pacman.go @@ -24,14 +24,14 @@ func (p *Pacman) Name() string { return "pacman" } func (p *Pacman) Available() bool { return available() } // Install one or more packages. -func (p *Pacman) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (p *Pacman) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { p.Lock() defer p.Unlock() return install(ctx, pkgs, opts...) } // Remove one or more packages. -func (p *Pacman) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (p *Pacman) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) { p.Lock() defer p.Unlock() return remove(ctx, pkgs, opts...) diff --git a/pacman/pacman_linux.go b/pacman/pacman_linux.go index 2c2b0b7..5ceffc4 100644 --- a/pacman/pacman_linux.go +++ b/pacman/pacman_linux.go @@ -72,28 +72,59 @@ func formatTargets(targets []snack.Target) []string { return args } -func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { - o := snack.ApplyOptions(opts...) - base := []string{"-S", "--noconfirm"} - if o.Refresh { - base = []string{"-Sy", "--noconfirm"} - } +func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { + var toInstall []snack.Target + var unchanged []string for _, t := range pkgs { - if t.FromRepo != "" || o.FromRepo != "" { - // Not directly supported by pacman CLI; user should configure repos - break + ok, _ := isInstalled(ctx, t.Name) + if ok { + unchanged = append(unchanged, t.Name) + } else { + toInstall = append(toInstall, t) } } - args := append(base, formatTargets(pkgs)...) - _, err := run(ctx, args, o) - return err + o := snack.ApplyOptions(opts...) + if len(toInstall) > 0 { + base := []string{"-S", "--noconfirm"} + if o.Refresh { + base = []string{"-Sy", "--noconfirm"} + } + args := append(base, formatTargets(toInstall)...) + if _, err := run(ctx, args, o); err != nil { + return snack.InstallResult{}, err + } + } + var installed []snack.Package + for _, t := range toInstall { + v, _ := version(ctx, t.Name) + installed = append(installed, snack.Package{Name: t.Name, Version: v, Installed: true}) + } + return snack.InstallResult{Installed: installed, Unchanged: unchanged}, nil } -func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) { + var toRemove []snack.Target + var unchanged []string + for _, t := range pkgs { + ok, _ := isInstalled(ctx, t.Name) + if !ok { + unchanged = append(unchanged, t.Name) + } else { + toRemove = append(toRemove, t) + } + } o := snack.ApplyOptions(opts...) - args := append([]string{"-R", "--noconfirm"}, snack.TargetNames(pkgs)...) - _, err := run(ctx, args, o) - return err + if len(toRemove) > 0 { + args := append([]string{"-R", "--noconfirm"}, snack.TargetNames(toRemove)...) + if _, err := run(ctx, args, o); err != nil { + return snack.RemoveResult{}, err + } + } + var removed []snack.Package + for _, t := range toRemove { + removed = append(removed, snack.Package{Name: t.Name}) + } + return snack.RemoveResult{Removed: removed, Unchanged: unchanged}, nil } func purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { diff --git a/pacman/pacman_other.go b/pacman/pacman_other.go index 08cc776..69459a3 100644 --- a/pacman/pacman_other.go +++ b/pacman/pacman_other.go @@ -10,12 +10,12 @@ import ( func available() bool { return false } -func install(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func install(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.InstallResult, error) { + return snack.InstallResult{}, snack.ErrUnsupportedPlatform } -func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.RemoveResult, error) { + return snack.RemoveResult{}, snack.ErrUnsupportedPlatform } func purge(_ context.Context, _ []snack.Target, _ ...snack.Option) error { diff --git a/pkg/pkg.go b/pkg/pkg.go index d25c8e3..5ee528a 100644 --- a/pkg/pkg.go +++ b/pkg/pkg.go @@ -24,14 +24,14 @@ func (p *Pkg) Name() string { return "pkg" } func (p *Pkg) Available() bool { return available() } // Install one or more packages. -func (p *Pkg) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (p *Pkg) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { p.Lock() defer p.Unlock() return install(ctx, pkgs, opts...) } // Remove one or more packages. -func (p *Pkg) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (p *Pkg) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) { p.Lock() defer p.Unlock() return remove(ctx, pkgs, opts...) diff --git a/pkg/pkg_freebsd.go b/pkg/pkg_freebsd.go index 8233c60..f4e6829 100644 --- a/pkg/pkg_freebsd.go +++ b/pkg/pkg_freebsd.go @@ -54,18 +54,55 @@ func formatTargets(targets []snack.Target) []string { return args } -func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { + var toInstall []snack.Target + var unchanged []string + for _, t := range pkgs { + ok, _ := isInstalled(ctx, t.Name) + if ok { + unchanged = append(unchanged, t.Name) + } else { + toInstall = append(toInstall, t) + } + } o := snack.ApplyOptions(opts...) - args := append([]string{"install", "-y"}, formatTargets(pkgs)...) - _, err := run(ctx, args, o) - return err + if len(toInstall) > 0 { + args := append([]string{"install", "-y"}, formatTargets(toInstall)...) + if _, err := run(ctx, args, o); err != nil { + return snack.InstallResult{}, err + } + } + var installed []snack.Package + for _, t := range toInstall { + v, _ := version(ctx, t.Name) + installed = append(installed, snack.Package{Name: t.Name, Version: v, Installed: true}) + } + return snack.InstallResult{Installed: installed, Unchanged: unchanged}, nil } -func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) { + var toRemove []snack.Target + var unchanged []string + for _, t := range pkgs { + ok, _ := isInstalled(ctx, t.Name) + if !ok { + unchanged = append(unchanged, t.Name) + } else { + toRemove = append(toRemove, t) + } + } o := snack.ApplyOptions(opts...) - args := append([]string{"delete", "-y"}, snack.TargetNames(pkgs)...) - _, err := run(ctx, args, o) - return err + if len(toRemove) > 0 { + args := append([]string{"delete", "-y"}, snack.TargetNames(toRemove)...) + if _, err := run(ctx, args, o); err != nil { + return snack.RemoveResult{}, err + } + } + var removed []snack.Package + for _, t := range toRemove { + removed = append(removed, snack.Package{Name: t.Name}) + } + return snack.RemoveResult{Removed: removed, Unchanged: unchanged}, nil } func purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { diff --git a/pkg/pkg_other.go b/pkg/pkg_other.go index 986e6d1..fa6e513 100644 --- a/pkg/pkg_other.go +++ b/pkg/pkg_other.go @@ -10,12 +10,12 @@ import ( func available() bool { return false } -func install(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func install(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.InstallResult, error) { + return snack.InstallResult{}, snack.ErrUnsupportedPlatform } -func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.RemoveResult, error) { + return snack.RemoveResult{}, snack.ErrUnsupportedPlatform } func purge(_ context.Context, _ []snack.Target, _ ...snack.Option) error { diff --git a/ports/ports.go b/ports/ports.go index 099e096..67446a6 100644 --- a/ports/ports.go +++ b/ports/ports.go @@ -24,14 +24,14 @@ func (p *Ports) Name() string { return "ports" } func (p *Ports) Available() bool { return available() } // Install one or more packages. -func (p *Ports) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (p *Ports) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { p.Lock() defer p.Unlock() return install(ctx, pkgs, opts...) } // Remove one or more packages. -func (p *Ports) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (p *Ports) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) { p.Lock() defer p.Unlock() return remove(ctx, pkgs, opts...) diff --git a/ports/ports_openbsd.go b/ports/ports_openbsd.go index e473c29..1afe867 100644 --- a/ports/ports_openbsd.go +++ b/ports/ports_openbsd.go @@ -42,18 +42,53 @@ func runCmd(ctx context.Context, name string, args []string, opts snack.Options) return stdout.String(), nil } -func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { + var toInstall []snack.Target + var unchanged []string + for _, t := range pkgs { + ok, _ := isInstalled(ctx, t.Name) + if ok { + unchanged = append(unchanged, t.Name) + } else { + toInstall = append(toInstall, t) + } + } o := snack.ApplyOptions(opts...) - args := snack.TargetNames(pkgs) - _, err := runCmd(ctx, "pkg_add", args, o) - return err + if len(toInstall) > 0 { + if _, err := runCmd(ctx, "pkg_add", snack.TargetNames(toInstall), o); err != nil { + return snack.InstallResult{}, err + } + } + var installed []snack.Package + for _, t := range toInstall { + v, _ := version(ctx, t.Name) + installed = append(installed, snack.Package{Name: t.Name, Version: v, Installed: true}) + } + return snack.InstallResult{Installed: installed, Unchanged: unchanged}, nil } -func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) { + var toRemove []snack.Target + var unchanged []string + for _, t := range pkgs { + ok, _ := isInstalled(ctx, t.Name) + if !ok { + unchanged = append(unchanged, t.Name) + } else { + toRemove = append(toRemove, t) + } + } o := snack.ApplyOptions(opts...) - args := snack.TargetNames(pkgs) - _, err := runCmd(ctx, "pkg_delete", args, o) - return err + if len(toRemove) > 0 { + if _, err := runCmd(ctx, "pkg_delete", snack.TargetNames(toRemove), o); err != nil { + return snack.RemoveResult{}, err + } + } + var removed []snack.Package + for _, t := range toRemove { + removed = append(removed, snack.Package{Name: t.Name}) + } + return snack.RemoveResult{Removed: removed, Unchanged: unchanged}, nil } func purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { diff --git a/ports/ports_other.go b/ports/ports_other.go index 25e2238..5242806 100644 --- a/ports/ports_other.go +++ b/ports/ports_other.go @@ -10,12 +10,12 @@ import ( func available() bool { return false } -func install(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func install(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.InstallResult, error) { + return snack.InstallResult{}, snack.ErrUnsupportedPlatform } -func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.RemoveResult, error) { + return snack.RemoveResult{}, snack.ErrUnsupportedPlatform } func purge(_ context.Context, _ []snack.Target, _ ...snack.Option) error { diff --git a/rpm/rpm.go b/rpm/rpm.go index babb36b..55213ff 100644 --- a/rpm/rpm.go +++ b/rpm/rpm.go @@ -24,14 +24,14 @@ func (r *RPM) Name() string { return "rpm" } func (r *RPM) Available() bool { return available() } // Install one or more packages. -func (r *RPM) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (r *RPM) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { r.Lock() defer r.Unlock() return install(ctx, pkgs, opts...) } // Remove one or more packages. -func (r *RPM) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (r *RPM) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) { r.Lock() defer r.Unlock() return remove(ctx, pkgs, opts...) @@ -41,7 +41,8 @@ func (r *RPM) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Opt func (r *RPM) Purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { r.Lock() defer r.Unlock() - return remove(ctx, pkgs, opts...) + _, err := remove(ctx, pkgs, opts...) + return err } // Upgrade upgrades packages from files. diff --git a/rpm/rpm_linux.go b/rpm/rpm_linux.go index 20604a3..fc5fce6 100644 --- a/rpm/rpm_linux.go +++ b/rpm/rpm_linux.go @@ -67,18 +67,55 @@ func formatSources(targets []snack.Target) []string { return args } -func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { + var toInstall []snack.Target + var unchanged []string + for _, t := range pkgs { + ok, _ := isInstalled(ctx, t.Name) + if ok { + unchanged = append(unchanged, t.Name) + } else { + toInstall = append(toInstall, t) + } + } o := snack.ApplyOptions(opts...) - args := append([]string{"-i"}, formatSources(pkgs)...) - _, err := runWithSudo(ctx, args, o.Sudo) - return err + if len(toInstall) > 0 { + args := append([]string{"-i"}, formatSources(toInstall)...) + if _, err := runWithSudo(ctx, args, o.Sudo); err != nil { + return snack.InstallResult{}, err + } + } + var installed []snack.Package + for _, t := range toInstall { + v, _ := version(ctx, t.Name) + installed = append(installed, snack.Package{Name: t.Name, Version: v, Installed: true}) + } + return snack.InstallResult{Installed: installed, Unchanged: unchanged}, nil } -func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) { + var toRemove []snack.Target + var unchanged []string + for _, t := range pkgs { + ok, _ := isInstalled(ctx, t.Name) + if !ok { + unchanged = append(unchanged, t.Name) + } else { + toRemove = append(toRemove, t) + } + } o := snack.ApplyOptions(opts...) - args := append([]string{"-e"}, snack.TargetNames(pkgs)...) - _, err := runWithSudo(ctx, args, o.Sudo) - return err + if len(toRemove) > 0 { + args := append([]string{"-e"}, snack.TargetNames(toRemove)...) + if _, err := runWithSudo(ctx, args, o.Sudo); err != nil { + return snack.RemoveResult{}, err + } + } + var removed []snack.Package + for _, t := range toRemove { + removed = append(removed, snack.Package{Name: t.Name}) + } + return snack.RemoveResult{Removed: removed, Unchanged: unchanged}, nil } func upgradeAll(ctx context.Context, opts ...snack.Option) error { diff --git a/rpm/rpm_other.go b/rpm/rpm_other.go index 63723d2..b5e68d2 100644 --- a/rpm/rpm_other.go +++ b/rpm/rpm_other.go @@ -10,12 +10,12 @@ import ( func available() bool { return false } -func install(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func install(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.InstallResult, error) { + return snack.InstallResult{}, snack.ErrUnsupportedPlatform } -func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.RemoveResult, error) { + return snack.RemoveResult{}, snack.ErrUnsupportedPlatform } func upgradeAll(_ context.Context, _ ...snack.Option) error { diff --git a/snack.go b/snack.go index 9dae405..bf047c8 100644 --- a/snack.go +++ b/snack.go @@ -58,11 +58,11 @@ func Targets(names ...string) []Target { // log.Warn("hold not supported by", mgr.Name()) // } type Manager interface { - // Install one or more packages. - Install(ctx context.Context, pkgs []Target, opts ...Option) error + // Install one or more packages. Returns a result describing what changed. + Install(ctx context.Context, pkgs []Target, opts ...Option) (InstallResult, error) - // Remove one or more packages. - Remove(ctx context.Context, pkgs []Target, opts ...Option) error + // Remove one or more packages. Returns a result describing what changed. + Remove(ctx context.Context, pkgs []Target, opts ...Option) (RemoveResult, error) // Purge one or more packages (remove including config files). Purge(ctx context.Context, pkgs []Target, opts ...Option) error diff --git a/snap/snap.go b/snap/snap.go index 1bb4c0c..36a8f97 100644 --- a/snap/snap.go +++ b/snap/snap.go @@ -24,14 +24,14 @@ func (s *Snap) Name() string { return "snap" } func (s *Snap) Available() bool { return available() } // Install one or more packages. -func (s *Snap) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (s *Snap) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { s.Lock() defer s.Unlock() return install(ctx, pkgs, opts...) } // Remove one or more packages. -func (s *Snap) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func (s *Snap) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.RemoveResult, error) { s.Lock() defer s.Unlock() return remove(ctx, pkgs, opts...) diff --git a/snap/snap_linux.go b/snap/snap_linux.go index cad8c6c..b23905c 100644 --- a/snap/snap_linux.go +++ b/snap/snap_linux.go @@ -37,11 +37,20 @@ func run(ctx context.Context, args []string) (string, error) { return stdout.String(), nil } -func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error { +func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) (snack.InstallResult, error) { _ = snack.ApplyOptions(opts...) + var toInstall []snack.Target + var unchanged []string for _, t := range pkgs { + ok, _ := isInstalled(ctx, t.Name) + if ok { + unchanged = append(unchanged, t.Name) + } else { + toInstall = append(toInstall, t) + } + } + for _, t := range toInstall { args := []string{"install"} - // Handle --classic or --channel via FromRepo if t.FromRepo != "" { if t.FromRepo == "classic" { args = append(args, "--classic") @@ -51,16 +60,39 @@ func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) err } args = append(args, t.Name) if _, err := run(ctx, args); err != nil { - return err + return snack.InstallResult{}, err } } - return nil + var installed []snack.Package + for _, t := range toInstall { + v, _ := version(ctx, t.Name) + installed = append(installed, snack.Package{Name: t.Name, Version: v, Installed: true}) + } + return snack.InstallResult{Installed: installed, Unchanged: unchanged}, nil } -func remove(ctx context.Context, pkgs []snack.Target, _ ...snack.Option) error { - args := append([]string{"remove"}, snack.TargetNames(pkgs)...) - _, err := run(ctx, args) - return err +func remove(ctx context.Context, pkgs []snack.Target, _ ...snack.Option) (snack.RemoveResult, error) { + var toRemove []snack.Target + var unchanged []string + for _, t := range pkgs { + ok, _ := isInstalled(ctx, t.Name) + if !ok { + unchanged = append(unchanged, t.Name) + } else { + toRemove = append(toRemove, t) + } + } + if len(toRemove) > 0 { + args := append([]string{"remove"}, snack.TargetNames(toRemove)...) + if _, err := run(ctx, args); err != nil { + return snack.RemoveResult{}, err + } + } + var removed []snack.Package + for _, t := range toRemove { + removed = append(removed, snack.Package{Name: t.Name}) + } + return snack.RemoveResult{Removed: removed, Unchanged: unchanged}, nil } func purge(ctx context.Context, pkgs []snack.Target, _ ...snack.Option) error { diff --git a/snap/snap_other.go b/snap/snap_other.go index 861c052..a843336 100644 --- a/snap/snap_other.go +++ b/snap/snap_other.go @@ -10,12 +10,12 @@ import ( func available() bool { return false } -func install(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func install(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.InstallResult, error) { + return snack.InstallResult{}, snack.ErrUnsupportedPlatform } -func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) error { - return snack.ErrUnsupportedPlatform +func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) (snack.RemoveResult, error) { + return snack.RemoveResult{}, snack.ErrUnsupportedPlatform } func purge(_ context.Context, _ []snack.Target, _ ...snack.Option) error { diff --git a/types.go b/types.go index 7cccfaa..041d935 100644 --- a/types.go +++ b/types.go @@ -1,5 +1,25 @@ package snack +// InstallResult holds the outcome of an Install operation. +type InstallResult struct { + // Installed contains packages that were newly installed by this operation. + Installed []Package + // Updated contains packages that were upgraded by this operation. + Updated []Package + // Unchanged contains the names of packages that were already at the + // desired state and required no action. + Unchanged []string +} + +// RemoveResult holds the outcome of a Remove operation. +type RemoveResult struct { + // Removed contains packages that were removed by this operation. + Removed []Package + // Unchanged contains the names of packages that were not installed + // and required no action. + Unchanged []string +} + // Package represents a system package. type Package struct { Name string `json:"name"`