mirror of
https://github.com/gogrlx/snack.git
synced 2026-04-01 20:58:42 -07:00
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:
@@ -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...)
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
2
types.go
2
types.go
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user