fix: improve feature completeness and correctness

Pass 1 (Feature & Completeness):
- Replace apt CLI with apt-get for listUpgrades (apt CLI is unstable for scripting)
- Verify snapd daemon is running in snap Available() check
- Add ErrDaemonNotRunning sentinel error for daemon-dependent managers
- Fix staticcheck S1011: replace loop with append(keys, matches...)
- Fix staticcheck SA1012: use context.TODO() instead of nil in dpkg tests
This commit is contained in:
2026-02-26 01:26:51 +00:00
parent d4c7a058fb
commit 6edb79df3f
6 changed files with 45 additions and 32 deletions

View File

@@ -37,37 +37,43 @@ func latestVersion(ctx context.Context, pkg string) (string, error) {
} }
func listUpgrades(ctx context.Context) ([]snack.Package, error) { func listUpgrades(ctx context.Context) ([]snack.Package, error) {
cmd := exec.CommandContext(ctx, "apt", "list", "--upgradable") // Use apt-get --just-print upgrade instead of `apt list --upgradable`
cmd.Env = append(os.Environ(), "LANG=C") // because `apt` has unstable CLI output not intended for scripting.
cmd := exec.CommandContext(ctx, "apt-get", "--just-print", "upgrade")
cmd.Env = append(os.Environ(), "LANG=C", "DEBIAN_FRONTEND=noninteractive")
out, err := cmd.Output() out, err := cmd.Output()
if err != nil { if err != nil {
return nil, fmt.Errorf("apt list --upgradable: %w", err) return nil, fmt.Errorf("apt-get --just-print upgrade: %w", err)
} }
var pkgs []snack.Package var pkgs []snack.Package
for _, line := range strings.Split(string(out), "\n") { for _, line := range strings.Split(string(out), "\n") {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "Listing...") { // Lines starting with "Inst " indicate upgradable packages.
// Format: "Inst pkg [old-ver] (new-ver repo [arch])"
if !strings.HasPrefix(line, "Inst ") {
continue continue
} }
// Format: "pkg/source version arch [upgradable from: old-version]" line = strings.TrimPrefix(line, "Inst ")
slashIdx := strings.Index(line, "/") fields := strings.Fields(line)
if slashIdx < 0 {
continue
}
name := line[:slashIdx]
rest := line[slashIdx+1:]
fields := strings.Fields(rest)
if len(fields) < 2 { if len(fields) < 2 {
continue continue
} }
name := fields[0]
// Find the new version in parentheses
parenStart := strings.Index(line, "(")
parenEnd := strings.Index(line, ")")
if parenStart < 0 || parenEnd < 0 {
continue
}
verFields := strings.Fields(line[parenStart+1 : parenEnd])
if len(verFields) < 1 {
continue
}
p := snack.Package{ p := snack.Package{
Name: name, Name: name,
Version: fields[1], Version: verFields[0],
Installed: true, Installed: true,
} }
if len(fields) > 2 {
p.Arch = fields[2]
}
pkgs = append(pkgs, p) pkgs = append(pkgs, p)
} }
return pkgs, nil return pkgs, nil
@@ -361,9 +367,7 @@ func listKeys(ctx context.Context) ([]string, error) {
// List keyring files // List keyring files
matches, _ := filepath.Glob("/etc/apt/keyrings/*.gpg") matches, _ := filepath.Glob("/etc/apt/keyrings/*.gpg")
for _, m := range matches { keys = append(keys, matches...)
keys = append(keys, m)
}
ascMatches, _ := filepath.Glob("/etc/apt/keyrings/*.asc") ascMatches, _ := filepath.Glob("/etc/apt/keyrings/*.asc")
keys = append(keys, ascMatches...) keys = append(keys, ascMatches...)

View File

@@ -4,14 +4,14 @@ package snack
// Useful for grlx to determine what operations are available before // Useful for grlx to determine what operations are available before
// attempting them. // attempting them.
type Capabilities struct { type Capabilities struct {
VersionQuery bool VersionQuery bool
Hold bool Hold bool
Clean bool Clean bool
FileOwnership bool FileOwnership bool
RepoManagement bool RepoManagement bool
KeyManagement bool KeyManagement bool
Groups bool Groups bool
NameNormalize bool NameNormalize bool
} }
// GetCapabilities probes a Manager for all optional interface support. // GetCapabilities probes a Manager for all optional interface support.

View File

@@ -1,6 +1,7 @@
package dpkg package dpkg
import ( import (
"context"
"testing" "testing"
"github.com/gogrlx/snack" "github.com/gogrlx/snack"
@@ -73,14 +74,14 @@ func TestNew(t *testing.T) {
func TestUpgradeUnsupported(t *testing.T) { func TestUpgradeUnsupported(t *testing.T) {
d := New() d := New()
if err := d.Upgrade(nil); err != snack.ErrUnsupportedPlatform { if err := d.Upgrade(context.TODO()); err != snack.ErrUnsupportedPlatform {
t.Errorf("expected ErrUnsupportedPlatform, got %v", err) t.Errorf("expected ErrUnsupportedPlatform, got %v", err)
} }
} }
func TestUpdateUnsupported(t *testing.T) { func TestUpdateUnsupported(t *testing.T) {
d := New() d := New()
if err := d.Update(nil); err != snack.ErrUnsupportedPlatform { if err := d.Update(context.TODO()); err != snack.ErrUnsupportedPlatform {
t.Errorf("expected ErrUnsupportedPlatform, got %v", err) t.Errorf("expected ErrUnsupportedPlatform, got %v", err)
} }
} }

View File

@@ -28,4 +28,8 @@ var (
// ErrManagerNotFound is returned by detect when no supported package // ErrManagerNotFound is returned by detect when no supported package
// manager can be found on the system. // manager can be found on the system.
ErrManagerNotFound = errors.New("no supported package manager found") ErrManagerNotFound = errors.New("no supported package manager found")
// ErrDaemonNotRunning is returned when a package manager's required
// daemon (e.g. snapd) is not running.
ErrDaemonNotRunning = errors.New("package manager daemon is not running")
) )

View File

@@ -13,8 +13,12 @@ import (
) )
func available() bool { func available() bool {
_, err := exec.LookPath("snap") if _, err := exec.LookPath("snap"); err != nil {
return err == nil return false
}
// Verify snapd is running by checking snap version (requires daemon).
cmd := exec.Command("snap", "version")
return cmd.Run() == nil
} }
func run(ctx context.Context, args []string) (string, error) { func run(ctx context.Context, args []string) (string, error) {

View File

@@ -69,7 +69,7 @@ func WithReinstall() Option {
// Repository represents a configured package repository. // Repository represents a configured package repository.
type Repository struct { type Repository struct {
ID string `json:"id"` // unique identifier ID string `json:"id"` // unique identifier
Name string `json:"name,omitempty"` // human-readable name Name string `json:"name,omitempty"` // human-readable name
URL string `json:"url"` // repository URL URL string `json:"url"` // repository URL
Enabled bool `json:"enabled"` // whether the repo is active Enabled bool `json:"enabled"` // whether the repo is active