mirror of
https://github.com/gogrlx/snack.git
synced 2026-04-02 05:08:42 -07:00
Merge pull request #5 from gogrlx/cd/target-mutex-update
feat: per-provider mutex and Target-based package operations
This commit is contained in:
30
README.md
30
README.md
@@ -77,10 +77,40 @@ if err != nil {
|
||||
fmt.Println("Detected:", mgr.Name())
|
||||
```
|
||||
|
||||
## Interfaces
|
||||
|
||||
snack uses a layered interface design. Every provider implements `Manager` (the base). Extended capabilities are optional — use type assertions to check support:
|
||||
|
||||
```go
|
||||
// Base — every provider
|
||||
snack.Manager // Install, Remove, Purge, Upgrade, Update, List, Search, Info, IsInstalled, Version
|
||||
|
||||
// Optional capabilities — type-assert to check
|
||||
snack.VersionQuerier // LatestVersion, ListUpgrades, UpgradeAvailable, VersionCmp
|
||||
snack.Holder // Hold, Unhold, ListHeld (version pinning)
|
||||
snack.Cleaner // Autoremove, Clean (orphan/cache cleanup)
|
||||
snack.FileOwner // FileList, Owner (file-to-package queries)
|
||||
snack.RepoManager // ListRepos, AddRepo, RemoveRepo
|
||||
snack.KeyManager // AddKey, RemoveKey, ListKeys (GPG keys)
|
||||
snack.Grouper // GroupList, GroupInfo, GroupInstall
|
||||
snack.NameNormalizer // NormalizeName, ParseArch
|
||||
```
|
||||
|
||||
Check capabilities at runtime:
|
||||
|
||||
```go
|
||||
caps := snack.GetCapabilities(mgr)
|
||||
if caps.Hold {
|
||||
mgr.(snack.Holder).Hold(ctx, []string{"nginx"})
|
||||
}
|
||||
```
|
||||
|
||||
## Design
|
||||
|
||||
- **Thin CLI wrappers** — each sub-package wraps a package manager's CLI tools. No FFI, no library bindings.
|
||||
- **Common interface** — all managers implement `snack.Manager`, making them interchangeable.
|
||||
- **Capability interfaces** — extended features via type assertion, so providers aren't forced to stub unsupported operations.
|
||||
- **Per-provider mutex** — each provider serializes mutating operations independently; apt + snap can run in parallel.
|
||||
- **Context-aware** — all operations accept `context.Context` for cancellation and timeouts.
|
||||
- **Platform-safe** — build tags ensure packages compile everywhere but only run where appropriate.
|
||||
- **No root assumption** — use `snack.WithSudo()` when elevated privileges are needed.
|
||||
|
||||
20
apk/apk.go
20
apk/apk.go
@@ -8,7 +8,9 @@ import (
|
||||
)
|
||||
|
||||
// Apk wraps apk-tools operations.
|
||||
type Apk struct{}
|
||||
type Apk struct {
|
||||
snack.Locker
|
||||
}
|
||||
|
||||
// New returns a new Apk manager.
|
||||
func New() *Apk {
|
||||
@@ -25,27 +27,37 @@ 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 []string, opts ...snack.Option) error {
|
||||
func (a *Apk) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return install(ctx, pkgs, opts...)
|
||||
}
|
||||
|
||||
// Remove one or more packages.
|
||||
func (a *Apk) Remove(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
func (a *Apk) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return remove(ctx, pkgs, opts...)
|
||||
}
|
||||
|
||||
// Purge removes packages including config files.
|
||||
func (a *Apk) Purge(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
func (a *Apk) Purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return purge(ctx, pkgs, opts...)
|
||||
}
|
||||
|
||||
// Upgrade all installed packages.
|
||||
func (a *Apk) Upgrade(ctx context.Context, opts ...snack.Option) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return upgrade(ctx, opts...)
|
||||
}
|
||||
|
||||
// Update refreshes the package index.
|
||||
func (a *Apk) Update(ctx context.Context) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return update(ctx)
|
||||
}
|
||||
|
||||
|
||||
@@ -52,20 +52,34 @@ func run(ctx context.Context, base []string, opts ...snack.Option) (string, erro
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
|
||||
func install(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
args := append([]string{"add"}, pkgs...)
|
||||
// formatTargets converts targets to apk CLI arguments.
|
||||
// apk uses "pkg=version" for version pinning.
|
||||
func formatTargets(targets []snack.Target) []string {
|
||||
args := make([]string, 0, len(targets))
|
||||
for _, t := range targets {
|
||||
if t.Version != "" {
|
||||
args = append(args, t.Name+"="+t.Version)
|
||||
} else {
|
||||
args = append(args, t.Name)
|
||||
}
|
||||
}
|
||||
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 remove(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
args := append([]string{"del"}, pkgs...)
|
||||
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 purge(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
args := append([]string{"del", "--purge"}, pkgs...)
|
||||
func purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
args := append([]string{"del", "--purge"}, snack.TargetNames(pkgs)...)
|
||||
_, err := run(ctx, args, opts...)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -10,15 +10,15 @@ import (
|
||||
|
||||
func available() bool { return false }
|
||||
|
||||
func install(_ context.Context, _ []string, _ ...snack.Option) error {
|
||||
func install(_ context.Context, _ []snack.Target, _ ...snack.Option) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func remove(_ context.Context, _ []string, _ ...snack.Option) error {
|
||||
func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func purge(_ context.Context, _ []string, _ ...snack.Option) error {
|
||||
func purge(_ context.Context, _ []snack.Target, _ ...snack.Option) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
|
||||
20
apt/apt.go
20
apt/apt.go
@@ -8,7 +8,9 @@ import (
|
||||
)
|
||||
|
||||
// Apt implements the snack.Manager interface using apt-get and apt-cache.
|
||||
type Apt struct{}
|
||||
type Apt struct {
|
||||
snack.Locker
|
||||
}
|
||||
|
||||
// New returns a new Apt manager.
|
||||
func New() *Apt {
|
||||
@@ -19,27 +21,37 @@ func New() *Apt {
|
||||
func (a *Apt) Name() string { return "apt" }
|
||||
|
||||
// Install one or more packages.
|
||||
func (a *Apt) Install(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
func (a *Apt) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return install(ctx, pkgs, opts...)
|
||||
}
|
||||
|
||||
// Remove one or more packages.
|
||||
func (a *Apt) Remove(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
func (a *Apt) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return remove(ctx, pkgs, opts...)
|
||||
}
|
||||
|
||||
// Purge one or more packages including config files.
|
||||
func (a *Apt) Purge(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
func (a *Apt) Purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return purge(ctx, pkgs, opts...)
|
||||
}
|
||||
|
||||
// Upgrade all installed packages.
|
||||
func (a *Apt) Upgrade(ctx context.Context, opts ...snack.Option) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return upgrade(ctx, opts...)
|
||||
}
|
||||
|
||||
// Update refreshes the package index.
|
||||
func (a *Apt) Update(ctx context.Context) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return update(ctx)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,21 @@ func available() bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func buildArgs(command string, pkgs []string, opts ...snack.Option) []string {
|
||||
// formatTargets converts targets to apt CLI arguments.
|
||||
// apt uses "pkg=version" for version pinning.
|
||||
func formatTargets(targets []snack.Target) []string {
|
||||
args := make([]string, 0, len(targets))
|
||||
for _, t := range targets {
|
||||
if t.Version != "" {
|
||||
args = append(args, t.Name+"="+t.Version)
|
||||
} else {
|
||||
args = append(args, t.Name)
|
||||
}
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func buildArgs(command string, pkgs []snack.Target, opts ...snack.Option) []string {
|
||||
o := snack.ApplyOptions(opts...)
|
||||
var args []string
|
||||
if o.Sudo {
|
||||
@@ -30,11 +44,17 @@ func buildArgs(command string, pkgs []string, opts ...snack.Option) []string {
|
||||
if o.DryRun {
|
||||
args = append(args, "--dry-run")
|
||||
}
|
||||
args = append(args, pkgs...)
|
||||
if o.FromRepo != "" {
|
||||
args = append(args, "-t", o.FromRepo)
|
||||
}
|
||||
if o.Reinstall && command == "install" {
|
||||
args = append(args, "--reinstall")
|
||||
}
|
||||
args = append(args, formatTargets(pkgs)...)
|
||||
return args
|
||||
}
|
||||
|
||||
func runAptGet(ctx context.Context, command string, pkgs []string, opts ...snack.Option) error {
|
||||
func runAptGet(ctx context.Context, command string, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
args := buildArgs(command, pkgs, opts...)
|
||||
var cmd *exec.Cmd
|
||||
if args[0] == "sudo" {
|
||||
@@ -57,15 +77,15 @@ func runAptGet(ctx context.Context, command string, pkgs []string, opts ...snack
|
||||
return nil
|
||||
}
|
||||
|
||||
func install(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
return runAptGet(ctx, "install", pkgs, opts...)
|
||||
}
|
||||
|
||||
func remove(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
return runAptGet(ctx, "remove", pkgs, opts...)
|
||||
}
|
||||
|
||||
func purge(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
func purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
return runAptGet(ctx, "purge", pkgs, opts...)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,15 +10,15 @@ import (
|
||||
|
||||
func available() bool { return false }
|
||||
|
||||
func install(_ context.Context, _ []string, _ ...snack.Option) error {
|
||||
func install(_ context.Context, _ []snack.Target, _ ...snack.Option) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func remove(_ context.Context, _ []string, _ ...snack.Option) error {
|
||||
func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func purge(_ context.Context, _ []string, _ ...snack.Option) error {
|
||||
func purge(_ context.Context, _ []snack.Target, _ ...snack.Option) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
|
||||
37
capabilities.go
Normal file
37
capabilities.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package snack
|
||||
|
||||
// Capabilities reports which optional interfaces a Manager implements.
|
||||
// Useful for grlx to determine what operations are available before
|
||||
// attempting them.
|
||||
type Capabilities struct {
|
||||
VersionQuery bool
|
||||
Hold bool
|
||||
Clean bool
|
||||
FileOwnership bool
|
||||
RepoManagement bool
|
||||
KeyManagement bool
|
||||
Groups bool
|
||||
NameNormalize bool
|
||||
}
|
||||
|
||||
// GetCapabilities probes a Manager for all optional interface support.
|
||||
func GetCapabilities(m Manager) Capabilities {
|
||||
_, vq := m.(VersionQuerier)
|
||||
_, h := m.(Holder)
|
||||
_, c := m.(Cleaner)
|
||||
_, fo := m.(FileOwner)
|
||||
_, rm := m.(RepoManager)
|
||||
_, km := m.(KeyManager)
|
||||
_, g := m.(Grouper)
|
||||
_, nn := m.(NameNormalizer)
|
||||
return Capabilities{
|
||||
VersionQuery: vq,
|
||||
Hold: h,
|
||||
Clean: c,
|
||||
FileOwnership: fo,
|
||||
RepoManagement: rm,
|
||||
KeyManagement: km,
|
||||
Groups: g,
|
||||
NameNormalize: nn,
|
||||
}
|
||||
}
|
||||
16
dpkg/dpkg.go
16
dpkg/dpkg.go
@@ -8,7 +8,9 @@ import (
|
||||
)
|
||||
|
||||
// Dpkg implements the snack.Manager interface using dpkg and dpkg-query.
|
||||
type Dpkg struct{}
|
||||
type Dpkg struct {
|
||||
snack.Locker
|
||||
}
|
||||
|
||||
// New returns a new Dpkg manager.
|
||||
func New() *Dpkg {
|
||||
@@ -19,17 +21,23 @@ func New() *Dpkg {
|
||||
func (d *Dpkg) Name() string { return "dpkg" }
|
||||
|
||||
// Install one or more .deb files.
|
||||
func (d *Dpkg) Install(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
func (d *Dpkg) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
return install(ctx, pkgs, opts...)
|
||||
}
|
||||
|
||||
// Remove one or more packages.
|
||||
func (d *Dpkg) Remove(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
func (d *Dpkg) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
return remove(ctx, pkgs, opts...)
|
||||
}
|
||||
|
||||
// Purge one or more packages including config files.
|
||||
func (d *Dpkg) Purge(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
func (d *Dpkg) Purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
return purge(ctx, pkgs, opts...)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ func available() bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func install(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
o := snack.ApplyOptions(opts...)
|
||||
var args []string
|
||||
if o.Sudo {
|
||||
@@ -27,7 +27,14 @@ func install(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
if o.DryRun {
|
||||
args = append(args, "--simulate")
|
||||
}
|
||||
args = append(args, pkgs...)
|
||||
// dpkg -i takes file paths; use Source if set, otherwise Name
|
||||
for _, t := range pkgs {
|
||||
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
|
||||
@@ -41,7 +48,7 @@ func install(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func remove(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
o := snack.ApplyOptions(opts...)
|
||||
var args []string
|
||||
if o.Sudo {
|
||||
@@ -51,7 +58,7 @@ func remove(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
if o.DryRun {
|
||||
args = append(args, "--simulate")
|
||||
}
|
||||
args = append(args, pkgs...)
|
||||
args = append(args, snack.TargetNames(pkgs)...)
|
||||
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
@@ -61,7 +68,7 @@ func remove(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func purge(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
func purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
o := snack.ApplyOptions(opts...)
|
||||
var args []string
|
||||
if o.Sudo {
|
||||
@@ -71,7 +78,7 @@ func purge(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
if o.DryRun {
|
||||
args = append(args, "--simulate")
|
||||
}
|
||||
args = append(args, pkgs...)
|
||||
args = append(args, snack.TargetNames(pkgs)...)
|
||||
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
@@ -10,15 +10,15 @@ import (
|
||||
|
||||
func available() bool { return false }
|
||||
|
||||
func install(_ context.Context, _ []string, _ ...snack.Option) error {
|
||||
func install(_ context.Context, _ []snack.Target, _ ...snack.Option) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func remove(_ context.Context, _ []string, _ ...snack.Option) error {
|
||||
func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func purge(_ context.Context, _ []string, _ ...snack.Option) error {
|
||||
func purge(_ context.Context, _ []snack.Target, _ ...snack.Option) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
|
||||
27
mutex.go
Normal file
27
mutex.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package snack
|
||||
|
||||
import "sync"
|
||||
|
||||
// Locker is an embeddable type that provides per-provider mutex locking.
|
||||
// Each package manager backend should embed this in its struct to serialize
|
||||
// mutating operations (Install, Remove, Purge, Upgrade, Update).
|
||||
//
|
||||
// Read-only operations (List, Search, Info, IsInstalled, Version) generally
|
||||
// don't need the lock, but backends may choose to lock them if the underlying
|
||||
// tool doesn't support concurrent reads.
|
||||
//
|
||||
// Different providers use independent locks, so an apt Install and a snap
|
||||
// Install can run concurrently, but two apt Installs will serialize.
|
||||
type Locker struct {
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Lock acquires the provider lock. Call before mutating operations.
|
||||
func (l *Locker) Lock() {
|
||||
l.mu.Lock()
|
||||
}
|
||||
|
||||
// Unlock releases the provider lock. Defer after Lock.
|
||||
func (l *Locker) Unlock() {
|
||||
l.mu.Unlock()
|
||||
}
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
)
|
||||
|
||||
// Pacman wraps the pacman package manager CLI.
|
||||
type Pacman struct{}
|
||||
type Pacman struct {
|
||||
snack.Locker
|
||||
}
|
||||
|
||||
// New returns a new Pacman manager.
|
||||
func New() *Pacman {
|
||||
@@ -22,27 +24,37 @@ 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 []string, opts ...snack.Option) error {
|
||||
func (p *Pacman) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
return install(ctx, pkgs, opts...)
|
||||
}
|
||||
|
||||
// Remove one or more packages.
|
||||
func (p *Pacman) Remove(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
func (p *Pacman) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
return remove(ctx, pkgs, opts...)
|
||||
}
|
||||
|
||||
// Purge removes packages including configuration files.
|
||||
func (p *Pacman) Purge(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
func (p *Pacman) Purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
return purge(ctx, pkgs, opts...)
|
||||
}
|
||||
|
||||
// Upgrade all installed packages to their latest versions.
|
||||
func (p *Pacman) Upgrade(ctx context.Context, opts ...snack.Option) error {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
return upgrade(ctx, opts...)
|
||||
}
|
||||
|
||||
// Update refreshes the package database.
|
||||
func (p *Pacman) Update(ctx context.Context) error {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
return update(ctx)
|
||||
}
|
||||
|
||||
|
||||
@@ -58,23 +58,47 @@ func run(ctx context.Context, baseArgs []string, opts snack.Options) (string, er
|
||||
return stdout.String(), nil
|
||||
}
|
||||
|
||||
func install(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
// formatTargets converts targets to pacman CLI arguments.
|
||||
// Pacman uses "pkg=version" for version pinning.
|
||||
func formatTargets(targets []snack.Target) []string {
|
||||
args := make([]string, 0, len(targets))
|
||||
for _, t := range targets {
|
||||
if t.Version != "" {
|
||||
args = append(args, t.Name+"="+t.Version)
|
||||
} else {
|
||||
args = append(args, t.Name)
|
||||
}
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
o := snack.ApplyOptions(opts...)
|
||||
args := append([]string{"-S", "--noconfirm"}, pkgs...)
|
||||
base := []string{"-S", "--noconfirm"}
|
||||
if o.Refresh {
|
||||
base = []string{"-Sy", "--noconfirm"}
|
||||
}
|
||||
for _, t := range pkgs {
|
||||
if t.FromRepo != "" || o.FromRepo != "" {
|
||||
// Not directly supported by pacman CLI; user should configure repos
|
||||
break
|
||||
}
|
||||
}
|
||||
args := append(base, formatTargets(pkgs)...)
|
||||
_, err := run(ctx, args, o)
|
||||
return err
|
||||
}
|
||||
|
||||
func remove(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
o := snack.ApplyOptions(opts...)
|
||||
args := append([]string{"-R", "--noconfirm"}, pkgs...)
|
||||
args := append([]string{"-R", "--noconfirm"}, snack.TargetNames(pkgs)...)
|
||||
_, err := run(ctx, args, o)
|
||||
return err
|
||||
}
|
||||
|
||||
func purge(ctx context.Context, pkgs []string, opts ...snack.Option) error {
|
||||
func purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||
o := snack.ApplyOptions(opts...)
|
||||
args := append([]string{"-Rns", "--noconfirm"}, pkgs...)
|
||||
args := append([]string{"-Rns", "--noconfirm"}, snack.TargetNames(pkgs)...)
|
||||
_, err := run(ctx, args, o)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -10,15 +10,15 @@ import (
|
||||
|
||||
func available() bool { return false }
|
||||
|
||||
func install(_ context.Context, _ []string, _ ...snack.Option) error {
|
||||
func install(_ context.Context, _ []snack.Target, _ ...snack.Option) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func remove(_ context.Context, _ []string, _ ...snack.Option) error {
|
||||
func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func purge(_ context.Context, _ []string, _ ...snack.Option) error {
|
||||
func purge(_ context.Context, _ []snack.Target, _ ...snack.Option) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
|
||||
117
snack.go
117
snack.go
@@ -46,7 +46,17 @@ func Targets(names ...string) []Target {
|
||||
return targets
|
||||
}
|
||||
|
||||
// Manager is the common interface implemented by all package manager wrappers.
|
||||
// Manager is the base interface implemented by all package manager wrappers.
|
||||
// It covers the core operations every package manager supports.
|
||||
//
|
||||
// For extended capabilities, use type assertions against the optional
|
||||
// interfaces below. Example:
|
||||
//
|
||||
// if holder, ok := mgr.(snack.Holder); ok {
|
||||
// holder.Hold(ctx, "nginx")
|
||||
// } else {
|
||||
// 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
|
||||
@@ -84,3 +94,108 @@ type Manager interface {
|
||||
// Name returns the package manager's identifier (e.g. "apt", "pacman").
|
||||
Name() string
|
||||
}
|
||||
|
||||
// VersionQuerier provides version comparison and upgrade availability checks.
|
||||
// Supported by: apt, pacman, apk, dnf.
|
||||
type VersionQuerier interface {
|
||||
// LatestVersion returns the latest available version of a package
|
||||
// from configured repositories.
|
||||
LatestVersion(ctx context.Context, pkg string) (string, error)
|
||||
|
||||
// ListUpgrades returns packages that have newer versions available.
|
||||
ListUpgrades(ctx context.Context) ([]Package, error)
|
||||
|
||||
// UpgradeAvailable reports whether a newer version of a package is
|
||||
// available in the repositories.
|
||||
UpgradeAvailable(ctx context.Context, pkg string) (bool, error)
|
||||
|
||||
// VersionCmp compares two version strings using the package manager's
|
||||
// native version comparison logic. Returns -1, 0, or 1.
|
||||
VersionCmp(ctx context.Context, ver1, ver2 string) (int, error)
|
||||
}
|
||||
|
||||
// Holder provides package version pinning (hold/unhold).
|
||||
// Supported by: apt, pacman (with pacman-contrib), dnf.
|
||||
type Holder interface {
|
||||
// Hold pins packages at their current version, preventing upgrades.
|
||||
Hold(ctx context.Context, pkgs []string) error
|
||||
|
||||
// Unhold removes version pins, allowing packages to be upgraded.
|
||||
Unhold(ctx context.Context, pkgs []string) error
|
||||
|
||||
// ListHeld returns all currently held/pinned packages.
|
||||
ListHeld(ctx context.Context) ([]Package, error)
|
||||
}
|
||||
|
||||
// Cleaner provides orphan/cache cleanup operations.
|
||||
// Supported by: apt, pacman, apk, dnf.
|
||||
type Cleaner interface {
|
||||
// Autoremove removes packages that were installed as dependencies
|
||||
// but are no longer required by any installed package.
|
||||
Autoremove(ctx context.Context, opts ...Option) error
|
||||
|
||||
// Clean removes cached package files from the local cache.
|
||||
Clean(ctx context.Context) error
|
||||
}
|
||||
|
||||
// FileOwner provides file-to-package ownership queries.
|
||||
// Supported by: apt/dpkg, pacman, rpm/dnf, apk.
|
||||
type FileOwner interface {
|
||||
// FileList returns all files installed by a package.
|
||||
FileList(ctx context.Context, pkg string) ([]string, error)
|
||||
|
||||
// Owner returns the package that owns a given file path.
|
||||
Owner(ctx context.Context, path string) (string, error)
|
||||
}
|
||||
|
||||
// RepoManager provides repository configuration operations.
|
||||
// Supported by: apt, dnf, pacman (partially).
|
||||
type RepoManager interface {
|
||||
// ListRepos returns all configured package repositories.
|
||||
ListRepos(ctx context.Context) ([]Repository, error)
|
||||
|
||||
// AddRepo adds a new package repository.
|
||||
AddRepo(ctx context.Context, repo Repository) error
|
||||
|
||||
// RemoveRepo removes a configured repository.
|
||||
RemoveRepo(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
// KeyManager provides GPG/signing key management for repositories.
|
||||
// Supported by: apt, rpm/dnf.
|
||||
type KeyManager interface {
|
||||
// AddKey imports a GPG key for package verification.
|
||||
// The key can be a URL, file path, or key ID.
|
||||
AddKey(ctx context.Context, key string) error
|
||||
|
||||
// RemoveKey removes a GPG key.
|
||||
RemoveKey(ctx context.Context, keyID string) error
|
||||
|
||||
// ListKeys returns all trusted package signing keys.
|
||||
ListKeys(ctx context.Context) ([]string, error)
|
||||
}
|
||||
|
||||
// Grouper provides package group operations.
|
||||
// Supported by: pacman, dnf/yum.
|
||||
type Grouper interface {
|
||||
// GroupList returns all available package groups.
|
||||
GroupList(ctx context.Context) ([]string, error)
|
||||
|
||||
// GroupInfo returns the packages in a group.
|
||||
GroupInfo(ctx context.Context, group string) ([]Package, error)
|
||||
|
||||
// GroupInstall installs all packages in a group.
|
||||
GroupInstall(ctx context.Context, group string, opts ...Option) error
|
||||
}
|
||||
|
||||
// NormalizeName provides package name normalization.
|
||||
// Supported by: apt (strips :arch suffixes), rpm.
|
||||
type NameNormalizer interface {
|
||||
// NormalizeName returns the canonical form of a package name,
|
||||
// stripping architecture suffixes, epoch prefixes, etc.
|
||||
NormalizeName(name string) string
|
||||
|
||||
// ParseArch extracts the architecture from a package name if present.
|
||||
// Returns the name without arch and the arch string.
|
||||
ParseArch(name string) (string, string)
|
||||
}
|
||||
|
||||
10
target.go
Normal file
10
target.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package snack
|
||||
|
||||
// TargetNames extracts just the package names from a slice of targets.
|
||||
func TargetNames(targets []Target) []string {
|
||||
names := make([]string, len(targets))
|
||||
for i, t := range targets {
|
||||
names[i] = t.Name
|
||||
}
|
||||
return names
|
||||
}
|
||||
12
types.go
12
types.go
@@ -67,6 +67,18 @@ func WithReinstall() Option {
|
||||
return func(o *Options) { o.Reinstall = true }
|
||||
}
|
||||
|
||||
// Repository represents a configured package repository.
|
||||
type Repository struct {
|
||||
ID string `json:"id"` // unique identifier
|
||||
Name string `json:"name,omitempty"` // human-readable name
|
||||
URL string `json:"url"` // repository URL
|
||||
Enabled bool `json:"enabled"` // whether the repo is active
|
||||
GPGCheck bool `json:"gpg_check,omitempty"` // whether GPG verification is enabled
|
||||
GPGKey string `json:"gpg_key,omitempty"` // GPG key URL or ID
|
||||
Type string `json:"type,omitempty"` // e.g. "deb", "rpm-md", "pkg"
|
||||
Arch string `json:"arch,omitempty"` // architecture filter
|
||||
}
|
||||
|
||||
// ApplyOptions processes functional options into an Options struct.
|
||||
func ApplyOptions(opts ...Option) Options {
|
||||
var o Options
|
||||
|
||||
Reference in New Issue
Block a user