mirror of
https://github.com/gogrlx/snack.git
synced 2026-04-02 05:08:42 -07:00
feat(apt,dpkg): implement capability interfaces
apt: VersionQuerier, Holder, Cleaner, FileOwner, RepoManager, KeyManager, NameNormalizer dpkg: FileOwner, NameNormalizer All new interfaces follow the existing pattern of exported methods on struct delegating to unexported platform-specific functions. Mutating operations use Lock/Unlock; read-only operations do not. Platform stubs in _other.go files return ErrUnsupportedPlatform. Compile-time interface checks added for all implemented interfaces.
This commit is contained in:
123
apt/apt.go
123
apt/apt.go
@@ -84,3 +84,126 @@ func (a *Apt) Version(ctx context.Context, pkg string) (string, error) {
|
||||
func (a *Apt) Available() bool {
|
||||
return available()
|
||||
}
|
||||
|
||||
// LatestVersion returns the latest available version of a package.
|
||||
func (a *Apt) LatestVersion(ctx context.Context, pkg string) (string, error) {
|
||||
return latestVersion(ctx, pkg)
|
||||
}
|
||||
|
||||
// ListUpgrades returns packages that have newer versions available.
|
||||
func (a *Apt) ListUpgrades(ctx context.Context) ([]snack.Package, error) {
|
||||
return listUpgrades(ctx)
|
||||
}
|
||||
|
||||
// UpgradeAvailable reports whether a newer version of a package is available.
|
||||
func (a *Apt) UpgradeAvailable(ctx context.Context, pkg string) (bool, error) {
|
||||
return upgradeAvailable(ctx, pkg)
|
||||
}
|
||||
|
||||
// VersionCmp compares two version strings using dpkg's native comparison.
|
||||
func (a *Apt) VersionCmp(ctx context.Context, ver1, ver2 string) (int, error) {
|
||||
return versionCmp(ctx, ver1, ver2)
|
||||
}
|
||||
|
||||
// Hold pins packages at their current version.
|
||||
func (a *Apt) Hold(ctx context.Context, pkgs []string) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return hold(ctx, pkgs)
|
||||
}
|
||||
|
||||
// Unhold removes version pins from packages.
|
||||
func (a *Apt) Unhold(ctx context.Context, pkgs []string) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return unhold(ctx, pkgs)
|
||||
}
|
||||
|
||||
// ListHeld returns all currently held packages.
|
||||
func (a *Apt) ListHeld(ctx context.Context) ([]snack.Package, error) {
|
||||
return listHeld(ctx)
|
||||
}
|
||||
|
||||
// Autoremove removes packages no longer required as dependencies.
|
||||
func (a *Apt) Autoremove(ctx context.Context, opts ...snack.Option) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return autoremove(ctx, opts...)
|
||||
}
|
||||
|
||||
// Clean removes cached package files.
|
||||
func (a *Apt) Clean(ctx context.Context) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return clean(ctx)
|
||||
}
|
||||
|
||||
// FileList returns all files installed by a package.
|
||||
func (a *Apt) FileList(ctx context.Context, pkg string) ([]string, error) {
|
||||
return fileList(ctx, pkg)
|
||||
}
|
||||
|
||||
// Owner returns the package that owns a given file path.
|
||||
func (a *Apt) Owner(ctx context.Context, path string) (string, error) {
|
||||
return owner(ctx, path)
|
||||
}
|
||||
|
||||
// ListRepos returns all configured package repositories.
|
||||
func (a *Apt) ListRepos(ctx context.Context) ([]snack.Repository, error) {
|
||||
return listRepos(ctx)
|
||||
}
|
||||
|
||||
// AddRepo adds a new package repository.
|
||||
func (a *Apt) AddRepo(ctx context.Context, repo snack.Repository) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return addRepo(ctx, repo)
|
||||
}
|
||||
|
||||
// RemoveRepo removes a configured repository.
|
||||
func (a *Apt) RemoveRepo(ctx context.Context, id string) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return removeRepo(ctx, id)
|
||||
}
|
||||
|
||||
// AddKey imports a GPG key for package verification.
|
||||
func (a *Apt) AddKey(ctx context.Context, key string) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return addKey(ctx, key)
|
||||
}
|
||||
|
||||
// RemoveKey removes a GPG key.
|
||||
func (a *Apt) RemoveKey(ctx context.Context, keyID string) error {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
return removeKey(ctx, keyID)
|
||||
}
|
||||
|
||||
// ListKeys returns all trusted package signing keys.
|
||||
func (a *Apt) ListKeys(ctx context.Context) ([]string, error) {
|
||||
return listKeys(ctx)
|
||||
}
|
||||
|
||||
// NormalizeName returns the canonical form of a package name.
|
||||
func (a *Apt) NormalizeName(name string) string {
|
||||
return normalizeName(name)
|
||||
}
|
||||
|
||||
// ParseArch extracts the architecture from a package name if present.
|
||||
func (a *Apt) ParseArch(name string) (string, string) {
|
||||
return parseArch(name)
|
||||
}
|
||||
|
||||
// Compile-time interface checks.
|
||||
var (
|
||||
_ snack.Manager = (*Apt)(nil)
|
||||
_ snack.VersionQuerier = (*Apt)(nil)
|
||||
_ snack.Holder = (*Apt)(nil)
|
||||
_ snack.Cleaner = (*Apt)(nil)
|
||||
_ snack.FileOwner = (*Apt)(nil)
|
||||
_ snack.RepoManager = (*Apt)(nil)
|
||||
_ snack.KeyManager = (*Apt)(nil)
|
||||
_ snack.NameNormalizer = (*Apt)(nil)
|
||||
)
|
||||
|
||||
383
apt/capabilities_linux.go
Normal file
383
apt/capabilities_linux.go
Normal file
@@ -0,0 +1,383 @@
|
||||
//go:build linux
|
||||
|
||||
package apt
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gogrlx/snack"
|
||||
)
|
||||
|
||||
// --- VersionQuerier ---
|
||||
|
||||
func latestVersion(ctx context.Context, pkg string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, "apt-cache", "policy", pkg)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("apt-cache policy %s: %w", pkg, snack.ErrNotFound)
|
||||
}
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "Candidate:") {
|
||||
candidate := strings.TrimSpace(strings.TrimPrefix(line, "Candidate:"))
|
||||
if candidate == "(none)" {
|
||||
return "", fmt.Errorf("apt-cache policy %s: %w", pkg, snack.ErrNotFound)
|
||||
}
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("apt-cache policy %s: %w", pkg, snack.ErrNotFound)
|
||||
}
|
||||
|
||||
func listUpgrades(ctx context.Context) ([]snack.Package, error) {
|
||||
cmd := exec.CommandContext(ctx, "apt", "list", "--upgradable")
|
||||
cmd.Env = append(os.Environ(), "LANG=C")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("apt list --upgradable: %w", err)
|
||||
}
|
||||
var pkgs []snack.Package
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "Listing...") {
|
||||
continue
|
||||
}
|
||||
// Format: "pkg/source version arch [upgradable from: old-version]"
|
||||
slashIdx := strings.Index(line, "/")
|
||||
if slashIdx < 0 {
|
||||
continue
|
||||
}
|
||||
name := line[:slashIdx]
|
||||
rest := line[slashIdx+1:]
|
||||
fields := strings.Fields(rest)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
p := snack.Package{
|
||||
Name: name,
|
||||
Version: fields[1],
|
||||
Installed: true,
|
||||
}
|
||||
if len(fields) > 2 {
|
||||
p.Arch = fields[2]
|
||||
}
|
||||
pkgs = append(pkgs, p)
|
||||
}
|
||||
return pkgs, nil
|
||||
}
|
||||
|
||||
func upgradeAvailable(ctx context.Context, pkg string) (bool, error) {
|
||||
cmd := exec.CommandContext(ctx, "apt-cache", "policy", pkg)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("apt-cache policy %s: %w", pkg, snack.ErrNotFound)
|
||||
}
|
||||
var installed, candidate string
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "Installed:") {
|
||||
installed = strings.TrimSpace(strings.TrimPrefix(line, "Installed:"))
|
||||
} else if strings.HasPrefix(line, "Candidate:") {
|
||||
candidate = strings.TrimSpace(strings.TrimPrefix(line, "Candidate:"))
|
||||
}
|
||||
}
|
||||
if installed == "(none)" || installed == "" {
|
||||
return false, fmt.Errorf("apt-cache policy %s: %w", pkg, snack.ErrNotInstalled)
|
||||
}
|
||||
if candidate == "(none)" || candidate == "" || candidate == installed {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func versionCmp(ctx context.Context, ver1, ver2 string) (int, error) {
|
||||
// Check if ver1 < ver2
|
||||
cmd := exec.CommandContext(ctx, "dpkg", "--compare-versions", ver1, "lt", ver2)
|
||||
if err := cmd.Run(); err == nil {
|
||||
return -1, nil
|
||||
}
|
||||
// Check if ver1 = ver2
|
||||
cmd = exec.CommandContext(ctx, "dpkg", "--compare-versions", ver1, "eq", ver2)
|
||||
if err := cmd.Run(); err == nil {
|
||||
return 0, nil
|
||||
}
|
||||
// Must be ver1 > ver2
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
// --- Holder ---
|
||||
|
||||
func hold(ctx context.Context, pkgs []string) error {
|
||||
args := append([]string{"hold"}, pkgs...)
|
||||
cmd := exec.CommandContext(ctx, "apt-mark", args...)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("apt-mark hold: %w: %s", err, stderr.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unhold(ctx context.Context, pkgs []string) error {
|
||||
args := append([]string{"unhold"}, pkgs...)
|
||||
cmd := exec.CommandContext(ctx, "apt-mark", args...)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("apt-mark unhold: %w: %s", err, stderr.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func listHeld(ctx context.Context) ([]snack.Package, error) {
|
||||
cmd := exec.CommandContext(ctx, "apt-mark", "showhold")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("apt-mark showhold: %w", err)
|
||||
}
|
||||
var pkgs []snack.Package
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
pkgs = append(pkgs, snack.Package{Name: line, Installed: true})
|
||||
}
|
||||
return pkgs, nil
|
||||
}
|
||||
|
||||
// --- Cleaner ---
|
||||
|
||||
func autoremove(ctx context.Context, opts ...snack.Option) error {
|
||||
return runAptGet(ctx, "autoremove", nil, append(opts, snack.WithAssumeYes())...)
|
||||
}
|
||||
|
||||
func clean(ctx context.Context) error {
|
||||
cmd := exec.CommandContext(ctx, "apt-get", "clean")
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("apt-get clean: %w: %s", err, stderr.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- FileOwner ---
|
||||
|
||||
func fileList(ctx context.Context, pkg string) ([]string, error) {
|
||||
cmd := exec.CommandContext(ctx, "dpkg-query", "-L", pkg)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
errMsg := stderr.String()
|
||||
if strings.Contains(errMsg, "is not installed") || strings.Contains(errMsg, "not found") {
|
||||
return nil, fmt.Errorf("dpkg-query -L %s: %w", pkg, snack.ErrNotInstalled)
|
||||
}
|
||||
return nil, fmt.Errorf("dpkg-query -L %s: %w: %s", pkg, err, errMsg)
|
||||
}
|
||||
var files []string
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
files = append(files, line)
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func owner(ctx context.Context, path string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, "dpkg", "-S", path)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("dpkg -S %s: %w", path, snack.ErrNotFound)
|
||||
}
|
||||
// Output format: "package: /path/to/file" or "package1, package2: /path"
|
||||
line := strings.TrimSpace(strings.Split(string(out), "\n")[0])
|
||||
colonIdx := strings.Index(line, ":")
|
||||
if colonIdx < 0 {
|
||||
return "", fmt.Errorf("dpkg -S %s: unexpected output", path)
|
||||
}
|
||||
// Return first package if multiple
|
||||
pkgPart := line[:colonIdx]
|
||||
if commaIdx := strings.Index(pkgPart, ","); commaIdx >= 0 {
|
||||
pkgPart = strings.TrimSpace(pkgPart[:commaIdx])
|
||||
}
|
||||
return strings.TrimSpace(pkgPart), nil
|
||||
}
|
||||
|
||||
// --- RepoManager ---
|
||||
|
||||
func listRepos(_ context.Context) ([]snack.Repository, error) {
|
||||
var repos []snack.Repository
|
||||
|
||||
files := []string{"/etc/apt/sources.list"}
|
||||
// Glob sources.list.d
|
||||
matches, _ := filepath.Glob("/etc/apt/sources.list.d/*.list")
|
||||
files = append(files, matches...)
|
||||
sourcesMatches, _ := filepath.Glob("/etc/apt/sources.list.d/*.sources")
|
||||
files = append(files, sourcesMatches...)
|
||||
|
||||
for _, f := range files {
|
||||
data, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
scanner := bufio.NewScanner(bytes.NewReader(data))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
enabled := true
|
||||
// deb822 format (.sources files) not fully parsed; treat as single entry
|
||||
if strings.HasPrefix(line, "deb ") || strings.HasPrefix(line, "deb-src ") {
|
||||
repos = append(repos, snack.Repository{
|
||||
ID: line,
|
||||
URL: extractURL(line),
|
||||
Enabled: enabled,
|
||||
Type: strings.Fields(line)[0],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return repos, nil
|
||||
}
|
||||
|
||||
// extractURL pulls the URL from a deb/deb-src line.
|
||||
func extractURL(line string) string {
|
||||
fields := strings.Fields(line)
|
||||
for i, f := range fields {
|
||||
if i == 0 {
|
||||
continue // skip deb/deb-src
|
||||
}
|
||||
if strings.HasPrefix(f, "[") {
|
||||
// skip options block
|
||||
for ; i < len(fields); i++ {
|
||||
if strings.HasSuffix(fields[i], "]") {
|
||||
break
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
return f
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func addRepo(ctx context.Context, repo snack.Repository) error {
|
||||
repoLine := repo.URL
|
||||
if repo.Type != "" {
|
||||
repoLine = repo.Type + " " + repo.URL
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, "add-apt-repository", "-y", repoLine)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("add-apt-repository: %w: %s", err, stderr.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeRepo(ctx context.Context, id string) error {
|
||||
cmd := exec.CommandContext(ctx, "add-apt-repository", "--remove", "-y", id)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("add-apt-repository --remove: %w: %s", err, stderr.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- KeyManager ---
|
||||
|
||||
func addKey(ctx context.Context, key string) error {
|
||||
if strings.HasPrefix(key, "http://") || strings.HasPrefix(key, "https://") {
|
||||
// Modern approach: download and dearmor to /etc/apt/keyrings/
|
||||
name := filepath.Base(key)
|
||||
name = strings.TrimSuffix(name, filepath.Ext(name)) + ".gpg"
|
||||
dest := filepath.Join("/etc/apt/keyrings", name)
|
||||
|
||||
// Ensure keyrings dir exists
|
||||
_ = os.MkdirAll("/etc/apt/keyrings", 0o755)
|
||||
|
||||
// Download and dearmor
|
||||
shell := fmt.Sprintf("curl -fsSL %q | gpg --dearmor -o %q", key, dest)
|
||||
cmd := exec.CommandContext(ctx, "sh", "-c", shell)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
// Fallback to apt-key
|
||||
cmd2 := exec.CommandContext(ctx, "apt-key", "adv", "--fetch-keys", key)
|
||||
var stderr2 bytes.Buffer
|
||||
cmd2.Stderr = &stderr2
|
||||
if err2 := cmd2.Run(); err2 != nil {
|
||||
return fmt.Errorf("add key %s: %w: %s", key, err2, stderr2.String())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Treat as key ID or file path
|
||||
cmd := exec.CommandContext(ctx, "apt-key", "add", key)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("apt-key add: %w: %s", err, stderr.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeKey(ctx context.Context, keyID string) error {
|
||||
// Try removing from keyrings dir first
|
||||
matches, _ := filepath.Glob("/etc/apt/keyrings/*")
|
||||
for _, m := range matches {
|
||||
if strings.Contains(filepath.Base(m), keyID) {
|
||||
if err := os.Remove(m); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback to apt-key del
|
||||
cmd := exec.CommandContext(ctx, "apt-key", "del", keyID)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("apt-key del %s: %w: %s", keyID, err, stderr.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func listKeys(ctx context.Context) ([]string, error) {
|
||||
var keys []string
|
||||
|
||||
// List keyring files
|
||||
matches, _ := filepath.Glob("/etc/apt/keyrings/*.gpg")
|
||||
for _, m := range matches {
|
||||
keys = append(keys, m)
|
||||
}
|
||||
ascMatches, _ := filepath.Glob("/etc/apt/keyrings/*.asc")
|
||||
keys = append(keys, ascMatches...)
|
||||
|
||||
// Also list apt-key entries
|
||||
cmd := exec.CommandContext(ctx, "apt-key", "list")
|
||||
out, _ := cmd.Output()
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
// Key fingerprint lines are hex strings
|
||||
if len(line) > 0 && !strings.Contains(line, "/") && !strings.Contains(line, "pub") && !strings.Contains(line, "uid") && !strings.Contains(line, "sub") && !strings.HasPrefix(line, "-") {
|
||||
keys = append(keys, line)
|
||||
}
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// NameNormalizer functions are in normalize.go (no build constraints needed).
|
||||
79
apt/capabilities_other.go
Normal file
79
apt/capabilities_other.go
Normal file
@@ -0,0 +1,79 @@
|
||||
//go:build !linux
|
||||
|
||||
package apt
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogrlx/snack"
|
||||
)
|
||||
|
||||
func latestVersion(_ context.Context, _ string) (string, error) {
|
||||
return "", snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func listUpgrades(_ context.Context) ([]snack.Package, error) {
|
||||
return nil, snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func upgradeAvailable(_ context.Context, _ string) (bool, error) {
|
||||
return false, snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func versionCmp(_ context.Context, _, _ string) (int, error) {
|
||||
return 0, snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func hold(_ context.Context, _ []string) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func unhold(_ context.Context, _ []string) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func listHeld(_ context.Context) ([]snack.Package, error) {
|
||||
return nil, snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func autoremove(_ context.Context, _ ...snack.Option) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func clean(_ context.Context) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func fileList(_ context.Context, _ string) ([]string, error) {
|
||||
return nil, snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func owner(_ context.Context, _ string) (string, error) {
|
||||
return "", snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func listRepos(_ context.Context) ([]snack.Repository, error) {
|
||||
return nil, snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func addRepo(_ context.Context, _ snack.Repository) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func removeRepo(_ context.Context, _ string) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func addKey(_ context.Context, _ string) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func removeKey(_ context.Context, _ string) error {
|
||||
return snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func listKeys(_ context.Context) ([]string, error) {
|
||||
return nil, snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
// normalizeName and parseArch are in normalize.go (no build constraints).
|
||||
21
apt/normalize.go
Normal file
21
apt/normalize.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package apt
|
||||
|
||||
import "strings"
|
||||
|
||||
func normalizeName(name string) string {
|
||||
n, _ := parseArch(name)
|
||||
return n
|
||||
}
|
||||
|
||||
func parseArch(name string) (string, string) {
|
||||
if idx := strings.LastIndex(name, ":"); idx >= 0 {
|
||||
pkg := name[:idx]
|
||||
arch := name[idx+1:]
|
||||
switch arch {
|
||||
case "amd64", "i386", "arm64", "armhf", "armel", "mips", "mipsel",
|
||||
"mips64el", "ppc64el", "s390x", "all", "any":
|
||||
return pkg, arch
|
||||
}
|
||||
}
|
||||
return name, ""
|
||||
}
|
||||
55
dpkg/capabilities_linux.go
Normal file
55
dpkg/capabilities_linux.go
Normal file
@@ -0,0 +1,55 @@
|
||||
//go:build linux
|
||||
|
||||
package dpkg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/gogrlx/snack"
|
||||
)
|
||||
|
||||
func fileList(ctx context.Context, pkg string) ([]string, error) {
|
||||
cmd := exec.CommandContext(ctx, "dpkg", "-L", pkg)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
errMsg := stderr.String()
|
||||
if strings.Contains(errMsg, "is not installed") || strings.Contains(errMsg, "not found") {
|
||||
return nil, fmt.Errorf("dpkg -L %s: %w", pkg, snack.ErrNotInstalled)
|
||||
}
|
||||
return nil, fmt.Errorf("dpkg -L %s: %w: %s", pkg, err, errMsg)
|
||||
}
|
||||
var files []string
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
files = append(files, line)
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func owner(ctx context.Context, path string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, "dpkg", "-S", path)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("dpkg -S %s: %w", path, snack.ErrNotFound)
|
||||
}
|
||||
line := strings.TrimSpace(strings.Split(string(out), "\n")[0])
|
||||
colonIdx := strings.Index(line, ":")
|
||||
if colonIdx < 0 {
|
||||
return "", fmt.Errorf("dpkg -S %s: unexpected output", path)
|
||||
}
|
||||
pkgPart := line[:colonIdx]
|
||||
if commaIdx := strings.Index(pkgPart, ","); commaIdx >= 0 {
|
||||
pkgPart = strings.TrimSpace(pkgPart[:commaIdx])
|
||||
}
|
||||
return strings.TrimSpace(pkgPart), nil
|
||||
}
|
||||
19
dpkg/capabilities_other.go
Normal file
19
dpkg/capabilities_other.go
Normal file
@@ -0,0 +1,19 @@
|
||||
//go:build !linux
|
||||
|
||||
package dpkg
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogrlx/snack"
|
||||
)
|
||||
|
||||
func fileList(_ context.Context, _ string) ([]string, error) {
|
||||
return nil, snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
func owner(_ context.Context, _ string) (string, error) {
|
||||
return "", snack.ErrUnsupportedPlatform
|
||||
}
|
||||
|
||||
// normalizeName and parseArch are in normalize.go (no build constraints).
|
||||
27
dpkg/dpkg.go
27
dpkg/dpkg.go
@@ -80,3 +80,30 @@ func (d *Dpkg) Version(ctx context.Context, pkg string) (string, error) {
|
||||
func (d *Dpkg) Available() bool {
|
||||
return available()
|
||||
}
|
||||
|
||||
// FileList returns all files installed by a package.
|
||||
func (d *Dpkg) FileList(ctx context.Context, pkg string) ([]string, error) {
|
||||
return fileList(ctx, pkg)
|
||||
}
|
||||
|
||||
// Owner returns the package that owns a given file path.
|
||||
func (d *Dpkg) Owner(ctx context.Context, path string) (string, error) {
|
||||
return owner(ctx, path)
|
||||
}
|
||||
|
||||
// NormalizeName returns the canonical form of a package name.
|
||||
func (d *Dpkg) NormalizeName(name string) string {
|
||||
return normalizeName(name)
|
||||
}
|
||||
|
||||
// ParseArch extracts the architecture from a package name if present.
|
||||
func (d *Dpkg) ParseArch(name string) (string, string) {
|
||||
return parseArch(name)
|
||||
}
|
||||
|
||||
// Compile-time interface checks.
|
||||
var (
|
||||
_ snack.Manager = (*Dpkg)(nil)
|
||||
_ snack.FileOwner = (*Dpkg)(nil)
|
||||
_ snack.NameNormalizer = (*Dpkg)(nil)
|
||||
)
|
||||
|
||||
21
dpkg/normalize.go
Normal file
21
dpkg/normalize.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package dpkg
|
||||
|
||||
import "strings"
|
||||
|
||||
func normalizeName(name string) string {
|
||||
n, _ := parseArch(name)
|
||||
return n
|
||||
}
|
||||
|
||||
func parseArch(name string) (string, string) {
|
||||
if idx := strings.LastIndex(name, ":"); idx >= 0 {
|
||||
pkg := name[:idx]
|
||||
arch := name[idx+1:]
|
||||
switch arch {
|
||||
case "amd64", "i386", "arm64", "armhf", "armel", "mips", "mipsel",
|
||||
"mips64el", "ppc64el", "s390x", "all", "any":
|
||||
return pkg, arch
|
||||
}
|
||||
}
|
||||
return name, ""
|
||||
}
|
||||
Reference in New Issue
Block a user