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

@@ -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) {