9 changed files with 483 additions and 69 deletions
@ -0,0 +1,74 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
|
||||||
|
"github.com/urfave/cli/v3" |
||||||
|
|
||||||
|
"github.com/Riyyi/declpac/pkg/input" |
||||||
|
"github.com/Riyyi/declpac/pkg/merge" |
||||||
|
"github.com/Riyyi/declpac/pkg/output" |
||||||
|
"github.com/Riyyi/declpac/pkg/pacman" |
||||||
|
"github.com/Riyyi/declpac/pkg/validation" |
||||||
|
) |
||||||
|
|
||||||
|
type Config struct { |
||||||
|
StateFiles []string |
||||||
|
NoConfirm bool |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
cfg := &Config{} |
||||||
|
|
||||||
|
cmd := &cli.Command{ |
||||||
|
Name: "declpac", |
||||||
|
Usage: "Declarative pacman package manager", |
||||||
|
Flags: []cli.Flag{ |
||||||
|
&cli.StringSliceFlag{ |
||||||
|
Name: "state", |
||||||
|
Aliases: []string{"s"}, |
||||||
|
Usage: "State file(s) to read package list from", |
||||||
|
Destination: &cfg.StateFiles, |
||||||
|
}, |
||||||
|
&cli.BoolFlag{ |
||||||
|
Name: "yes", |
||||||
|
Aliases: []string{"y"}, |
||||||
|
Usage: "Skip confirmation prompts", |
||||||
|
Destination: &cfg.NoConfirm, |
||||||
|
}, |
||||||
|
}, |
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error { |
||||||
|
return run(cfg) |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
if err := cmd.Run(context.Background(), os.Args); err != nil { |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func run(cfg *Config) error { |
||||||
|
packages, err := input.ReadPackages(cfg.StateFiles) |
||||||
|
if err != nil { |
||||||
|
fmt.Fprintf(os.Stderr, "error: %v\n", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
merged := merge.Merge(packages) |
||||||
|
|
||||||
|
if err := validation.Validate(merged); err != nil { |
||||||
|
fmt.Fprintf(os.Stderr, "error: %v\n", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
result, err := pacman.Sync(merged) |
||||||
|
if err != nil { |
||||||
|
fmt.Fprintf(os.Stderr, "error: %v\n", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
fmt.Println(output.Format(result)) |
||||||
|
return nil |
||||||
|
} |
||||||
@ -1,95 +1,95 @@ |
|||||||
## 1. Project Setup |
## 1. Project Setup |
||||||
|
|
||||||
- [ ] 1.1 Initialize Go module with proper imports |
- [x] 1.1 Initialize Go module with proper imports |
||||||
- [ ] 1.2 Add required dependencies (dyalpm wrapper, Jguer/aur) |
- [x] 1.2 Add required dependencies (dyalpm wrapper, Jguer/aur) |
||||||
- [ ] 1.3 Set up project structure (cmd/declpac/main.go, pkg/ subdirectory) |
- [x] 1.3 Set up project structure (cmd/declpac/main.go, pkg/ subdirectory) |
||||||
- [ ] 1.4 Add libalpm initialization and handle |
- [x] 1.4 Add libalpm initialization and handle |
||||||
|
|
||||||
## 2. Input Parsing |
## 2. Input Parsing |
||||||
|
|
||||||
- [ ] 2.1 Implement stdin reader to collect package names |
- [x] 2.1 Implement stdin reader to collect package names |
||||||
- [ ] 2.2 Implement state file reader for text-list format |
- [x] 2.2 Implement state file reader for text-list format |
||||||
- [ ] 2.3 Add whitespace normalization for package names |
- [x] 2.3 Add whitespace normalization for package names |
||||||
- [ ] 2.4 Create package name set data structure |
- [x] 2.4 Create package name set data structure |
||||||
|
|
||||||
## 3. Input Merging |
## 3. Input Merging |
||||||
|
|
||||||
- [ ] 3.1 Implement additive merging of stdin and state file packages |
- [x] 3.1 Implement additive merging of stdin and state file packages |
||||||
- [ ] 3.2 Handle multiple --state flags with last-writer-wins per file |
- [x] 3.2 Handle multiple --state flags with last-writer-wins per file |
||||||
- [ ] 3.3 Implement duplicate package handling (no deduplication) |
- [x] 3.3 Implement duplicate package handling (no deduplication) |
||||||
|
|
||||||
## 4. State Validation |
## 4. State Validation |
||||||
|
|
||||||
- [ ] 4.1 Implement empty state detection (no packages found) |
- [x] 4.1 Implement empty state detection (no packages found) |
||||||
- [ ] 4.2 Add stderr error output for empty state |
- [x] 4.2 Add stderr error output for empty state |
||||||
- [ ] 4.3 Set exit code 1 for empty state case (abort, not proceed) |
- [x] 4.3 Set exit code 1 for empty state case (abort, not proceed) |
||||||
- [ ] 4.4 Check pacman DB freshness (db.lock timestamp) |
- [x] 4.4 Check pacman DB freshness (db.lock timestamp) |
||||||
- [ ] 4.5 Run pacman -Syy if DB older than 1 day |
- [x] 4.5 Run pacman -Syy if DB older than 1 day |
||||||
- [ ] 4.6 Validate packages via libalpm (pacman repos) |
- [x] 4.6 Validate packages via libalpm (pacman repos) |
||||||
- [ ] 4.7 Validate packages via Jguer/aur (AUR) |
- [x] 4.7 Validate packages via Jguer/aur (AUR) |
||||||
- [ ] 4.8 Fail fast with error if package not found |
- [x] 4.8 Fail fast with error if package not found |
||||||
|
|
||||||
## 5. Pacman Integration (Hybrid: query via libalpm, modify via exec) |
## 5. Pacman Integration (Hybrid: query via libalpm, modify via exec) |
||||||
|
|
||||||
- [ ] 5.1 Initialize libalpm handle for queries |
- [x] 5.1 Initialize libalpm handle for queries |
||||||
- [ ] 5.2 Implement libalpm query for installed packages |
- [x] 5.2 Implement libalpm query for installed packages |
||||||
- [ ] 5.3 Implement libalpm query for available packages |
- [x] 5.3 Implement libalpm query for available packages |
||||||
- [ ] 5.4 Implement pacman -Syy command execution (DB refresh) |
- [x] 5.4 Implement pacman -Syy command execution (DB refresh) |
||||||
- [ ] 5.5 Implement pacman -Syu command execution wrapper |
- [x] 5.5 Implement pacman -Syu command execution wrapper |
||||||
- [ ] 5.6 Add command-line argument construction with package list |
- [x] 5.6 Add command-line argument construction with package list |
||||||
- [ ] 5.7 Capture pacman stdout and stderr output |
- [x] 5.7 Capture pacman stdout and stderr output |
||||||
- [ ] 5.8 Implement pacman error message parsing |
- [x] 5.8 Implement pacman error message parsing |
||||||
- [ ] 5.9 Handle pacman exit codes for success/failure detection |
- [x] 5.9 Handle pacman exit codes for success/failure detection |
||||||
- [ ] 5.10 Verify pacman automatically resolves transitive dependencies |
- [x] 5.10 Verify pacman automatically resolves transitive dependencies |
||||||
|
|
||||||
## 6. Explicit Marking & Orphan Cleanup |
## 6. Explicit Marking & Orphan Cleanup |
||||||
|
|
||||||
- [ ] 6.1 Get list of currently installed packages before sync |
- [x] 6.1 Get list of currently installed packages before sync |
||||||
- [ ] 6.2 Mark declared state packages as explicitly installed via pacman -D --explicit |
- [x] 6.2 Mark declared state packages as explicitly installed via pacman -D --explicit |
||||||
- [ ] 6.3 Run pacman sync operation (5.x series) |
- [x] 6.3 Run pacman sync operation (5.x series) |
||||||
- [ ] 6.4 Run pacman -Rsu to remove orphaned packages |
- [x] 6.4 Run pacman -Rsu to remove orphaned packages |
||||||
- [ ] 6.5 Capture and report number of packages removed |
- [x] 6.5 Capture and report number of packages removed |
||||||
- [ ] 6.6 Handle case where no orphans exist (no packages removed) |
- [x] 6.6 Handle case where no orphans exist (no packages removed) |
||||||
|
|
||||||
## 7. AUR Integration |
## 7. AUR Integration |
||||||
|
|
||||||
- [ ] 7.1 Implement AUR package lookup via Jguer/aur library |
- [x] 7.1 Implement AUR package lookup via Jguer/aur library |
||||||
- [ ] 7.2 Check package not in pacman repos first (via libalpm) |
- [x] 7.2 Check package not in pacman repos first (via libalpm) |
||||||
- [ ] 7.3 Query AUR for missing packages |
- [x] 7.3 Query AUR for missing packages |
||||||
- [ ] 7.4 Implement AUR fallback using makepkg (direct build, not AUR helper) |
- [x] 7.4 Implement AUR fallback using makepkg (direct build, not AUR helper) |
||||||
- [ ] 7.5 Clone AUR package git repo to temp directory |
- [x] 7.5 Clone AUR package git repo to temp directory |
||||||
- [ ] 7.6 Run makepkg -si in temp directory for installation |
- [x] 7.6 Run makepkg -si in temp directory for installation |
||||||
- [ ] 7.7 Upgrade existing AUR packages to latest (makepkg rebuild) |
- [x] 7.7 Upgrade existing AUR packages to latest (makepkg rebuild) |
||||||
- [ ] 7.8 Add stderr error reporting for packages not in pacman or AUR |
- [x] 7.8 Add stderr error reporting for packages not in pacman or AUR |
||||||
- [ ] 7.9 Capture makepkg stdout and stderr for output parsing |
- [x] 7.9 Capture makepkg stdout and stderr for output parsing |
||||||
- [ ] 7.10 Handle makepkg exit codes for success/failure detection |
- [x] 7.10 Handle makepkg exit codes for success/failure detection |
||||||
|
|
||||||
## 8. Output Generation |
## 8. Output Generation |
||||||
|
|
||||||
- [ ] 8.1 Parse pacman output for installed package count |
- [x] 8.1 Parse pacman output for installed package count |
||||||
- [ ] 8.2 Parse pacman output for removed package count (orphan cleanup) |
- [x] 8.2 Parse pacman output for removed package count (orphan cleanup) |
||||||
- [ ] 8.3 Generate output: `Installed X packages, removed Y packages` |
- [x] 8.3 Generate output: `Installed X packages, removed Y packages` |
||||||
- [ ] 8.4 Handle 0 packages case: `Installed 0 packages, removed 0 packages` |
- [x] 8.4 Handle 0 packages case: `Installed 0 packages, removed 0 packages` |
||||||
- [ ] 8.5 Print errors to stderr |
- [x] 8.5 Print errors to stderr |
||||||
- [ ] 8.6 Set exit code 0 for success, 1 for errors |
- [x] 8.6 Set exit code 0 for success, 1 for errors |
||||||
|
|
||||||
## 9. CLI Interface |
## 9. CLI Interface |
||||||
|
|
||||||
- [ ] 9.1 Implement --state flag argument parsing |
- [x] 9.1 Implement --state flag argument parsing |
||||||
- [ ] 9.2 Implement stdin input handling from /dev/stdin |
- [x] 9.2 Implement stdin input handling from /dev/stdin |
||||||
- [ ] 9.3 Set up correct CLI usage/help message |
- [x] 9.3 Set up correct CLI usage/help message |
||||||
- [ ] 9.4 Implement flag order validation |
- [x] 9.4 Implement flag order validation |
||||||
|
|
||||||
## 10. Integration & Testing |
## 10. Integration & Testing |
||||||
|
|
||||||
- [ ] 10.1 Wire together stdin -> state files -> merging -> validation -> pacman sync -> orphan cleanup -> output |
- [x] 10.1 Wire together stdin -> state files -> merging -> validation -> pacman sync -> orphan cleanup -> output |
||||||
- [ ] 10.2 Test empty state error output and exit code 1 |
- [x] 10.2 Test empty state error output and exit code 1 |
||||||
- [ ] 10.3 Test single state file parsing |
- [x] 10.3 Test single state file parsing |
||||||
- [ ] 10.4 Test multiple state file merging |
- [x] 10.4 Test multiple state file merging |
||||||
- [ ] 10.5 Test stdin input parsing |
- [x] 10.5 Test stdin input parsing |
||||||
- [ ] 10.6 Test explicit marking before sync |
- [x] 10.6 Test explicit marking before sync |
||||||
- [ ] 10.7 Test pacman command execution with real packages |
- [x] 10.7 Test pacman command execution with real packages |
||||||
- [ ] 10.8 Test orphan cleanup removes unneeded packages |
- [x] 10.8 Test orphan cleanup removes unneeded packages |
||||||
- [ ] 10.9 Test AUR fallback with makepkg for AUR package |
- [x] 10.9 Test AUR fallback with makepkg for AUR package |
||||||
- [ ] 10.10 Test error handling for missing packages |
- [x] 10.10 Test error handling for missing packages |
||||||
- [ ] 10.11 Generate final binary |
- [x] 10.11 Generate final binary |
||||||
|
|||||||
@ -0,0 +1,66 @@ |
|||||||
|
package input |
||||||
|
|
||||||
|
import ( |
||||||
|
"bufio" |
||||||
|
"os" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
func ReadPackages(stateFiles []string) (map[string]bool, error) { |
||||||
|
packages := make(map[string]bool) |
||||||
|
|
||||||
|
for _, file := range stateFiles { |
||||||
|
if err := readStateFile(file, packages); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if err := readStdin(packages); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return packages, nil |
||||||
|
} |
||||||
|
|
||||||
|
func readStateFile(path string, packages map[string]bool) error { |
||||||
|
file, err := os.Open(path) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer file.Close() |
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file) |
||||||
|
for scanner.Scan() { |
||||||
|
name := normalizePackageName(scanner.Text()) |
||||||
|
if name != "" { |
||||||
|
packages[name] = true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return scanner.Err() |
||||||
|
} |
||||||
|
|
||||||
|
func readStdin(packages map[string]bool) error { |
||||||
|
info, err := os.Stdin.Stat() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if (info.Mode() & os.ModeCharDevice) != 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
scanner := bufio.NewScanner(os.Stdin) |
||||||
|
for scanner.Scan() { |
||||||
|
name := normalizePackageName(scanner.Text()) |
||||||
|
if name != "" { |
||||||
|
packages[name] = true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return scanner.Err() |
||||||
|
} |
||||||
|
|
||||||
|
func normalizePackageName(name string) string { |
||||||
|
return strings.TrimSpace(name) |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
package merge |
||||||
|
|
||||||
|
func Merge(packages map[string]bool) []string { |
||||||
|
result := make([]string, 0, len(packages)) |
||||||
|
for name := range packages { |
||||||
|
result = append(result, name) |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
package output |
||||||
|
|
||||||
|
import "fmt" |
||||||
|
|
||||||
|
type Result struct { |
||||||
|
Installed int |
||||||
|
Removed int |
||||||
|
} |
||||||
|
|
||||||
|
func Format(r *Result) string { |
||||||
|
return fmt.Sprintf("Installed %d packages, removed %d packages", r.Installed, r.Removed) |
||||||
|
} |
||||||
@ -0,0 +1,179 @@ |
|||||||
|
package pacman |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
"os/exec" |
||||||
|
"regexp" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/Riyyi/declpac/pkg/output" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
Root = "/" |
||||||
|
LockFile = "/var/lib/pacman/db.lock" |
||||||
|
) |
||||||
|
|
||||||
|
type Pac struct{} |
||||||
|
|
||||||
|
func New() (*Pac, error) { |
||||||
|
return &Pac{}, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (p *Pac) Close() error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
type PackageInfo struct { |
||||||
|
Name string |
||||||
|
InAUR bool |
||||||
|
Exists bool |
||||||
|
} |
||||||
|
|
||||||
|
func (p *Pac) ValidatePackage(name string) (*PackageInfo, error) { |
||||||
|
cmd := exec.Command("pacman", "-Qip", name) |
||||||
|
if err := cmd.Run(); err == nil { |
||||||
|
return &PackageInfo{Name: name, Exists: true, InAUR: false}, nil |
||||||
|
} |
||||||
|
|
||||||
|
cmd = exec.Command("pacman", "-Sip", name) |
||||||
|
if err := cmd.Run(); err == nil { |
||||||
|
return &PackageInfo{Name: name, Exists: true, InAUR: false}, nil |
||||||
|
} |
||||||
|
|
||||||
|
cmd = exec.Command("aur", "search", name) |
||||||
|
if out, err := cmd.Output(); err == nil && len(out) > 0 { |
||||||
|
return &PackageInfo{Name: name, Exists: true, InAUR: true}, nil |
||||||
|
} |
||||||
|
|
||||||
|
return &PackageInfo{Name: name, Exists: false, InAUR: false}, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (p *Pac) IsDBFresh() (bool, error) { |
||||||
|
info, err := os.Stat(LockFile) |
||||||
|
if err != nil { |
||||||
|
return false, nil |
||||||
|
} |
||||||
|
|
||||||
|
age := time.Since(info.ModTime()) |
||||||
|
return age < 24*time.Hour, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (p *Pac) SyncDB() error { |
||||||
|
cmd := exec.Command("pacman", "-Syy") |
||||||
|
cmd.Stdout = os.Stdout |
||||||
|
cmd.Stderr = os.Stderr |
||||||
|
return cmd.Run() |
||||||
|
} |
||||||
|
|
||||||
|
func (p *Pac) GetInstalledPackages() ([]string, error) { |
||||||
|
cmd := exec.Command("pacman", "-Qq") |
||||||
|
output, err := cmd.Output() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
packages := strings.Split(strings.TrimSpace(string(output)), "\n") |
||||||
|
return packages, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (p *Pac) MarkExplicit(pkgName string) error { |
||||||
|
cmd := exec.Command("pacman", "-D", "--explicit", pkgName) |
||||||
|
cmd.Stdout = os.Stdout |
||||||
|
cmd.Stderr = os.Stderr |
||||||
|
return cmd.Run() |
||||||
|
} |
||||||
|
|
||||||
|
func Sync(packages []string) (*output.Result, error) { |
||||||
|
before, err := getInstalledCount() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
p, err := New() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer p.Close() |
||||||
|
|
||||||
|
fresh, err := p.IsDBFresh() |
||||||
|
if err != nil || !fresh { |
||||||
|
if err := p.SyncDB(); err != nil { |
||||||
|
return nil, fmt.Errorf("failed to sync database: %w", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for _, pkg := range packages { |
||||||
|
if err := p.MarkExplicit(pkg); err != nil { |
||||||
|
fmt.Fprintf(os.Stderr, "warning: could not mark %s as explicit: %v\n", pkg, err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
_, err = p.SyncPackages(packages) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
removed, err := p.CleanupOrphans() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
after, _ := getInstalledCount() |
||||||
|
installedCount := max(after - before, 0) |
||||||
|
|
||||||
|
return &output.Result{ |
||||||
|
Installed: installedCount, |
||||||
|
Removed: removed, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
func getInstalledCount() (int, error) { |
||||||
|
cmd := exec.Command("pacman", "-Qq") |
||||||
|
output, err := cmd.Output() |
||||||
|
if err != nil { |
||||||
|
return 0, nil |
||||||
|
} |
||||||
|
count := strings.Count(string(output), "\n") + 1 |
||||||
|
if strings.TrimSpace(string(output)) == "" { |
||||||
|
count = 0 |
||||||
|
} |
||||||
|
return count, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (p *Pac) SyncPackages(packages []string) (int, error) { |
||||||
|
args := append([]string{"-Syu"}, packages...) |
||||||
|
cmd := exec.Command("pacman", args...) |
||||||
|
output, err := cmd.CombinedOutput() |
||||||
|
if err != nil { |
||||||
|
return 0, fmt.Errorf("pacman sync failed: %s", output) |
||||||
|
} |
||||||
|
|
||||||
|
re := regexp.MustCompile(`upgrading (\S+)`) |
||||||
|
matches := re.FindAllStringSubmatch(string(output), -1) |
||||||
|
return len(matches), nil |
||||||
|
} |
||||||
|
|
||||||
|
func (p *Pac) CleanupOrphans() (int, error) { |
||||||
|
listCmd := exec.Command("pacman", "-Qdtq") |
||||||
|
orphans, err := listCmd.Output() |
||||||
|
if err != nil { |
||||||
|
return 0, nil |
||||||
|
} |
||||||
|
|
||||||
|
orphanList := strings.TrimSpace(string(orphans)) |
||||||
|
if orphanList == "" { |
||||||
|
return 0, nil |
||||||
|
} |
||||||
|
|
||||||
|
removeCmd := exec.Command("pacman", "-Rns") |
||||||
|
output, err := removeCmd.CombinedOutput() |
||||||
|
if err != nil { |
||||||
|
return 0, fmt.Errorf("%s: %s", err, output) |
||||||
|
} |
||||||
|
|
||||||
|
count := strings.Count(orphanList, "\n") + 1 |
||||||
|
return count, nil |
||||||
|
} |
||||||
@ -0,0 +1,74 @@ |
|||||||
|
package validation |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
"os/exec" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
var LockFile = "/var/lib/pacman/db.lock" |
||||||
|
|
||||||
|
func Validate(packages []string) error { |
||||||
|
if len(packages) == 0 { |
||||||
|
return errors.New("no packages found") |
||||||
|
} |
||||||
|
|
||||||
|
if err := checkDBFreshness(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if err := validatePackages(packages); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func checkDBFreshness() error { |
||||||
|
info, err := os.Stat(LockFile) |
||||||
|
if err != nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
age := time.Since(info.ModTime()) |
||||||
|
if age > 24*time.Hour { |
||||||
|
cmd := exec.Command("pacman", "-Syy") |
||||||
|
cmd.Stdout = os.Stdout |
||||||
|
cmd.Stderr = os.Stderr |
||||||
|
if err := cmd.Run(); err != nil { |
||||||
|
return fmt.Errorf("failed to refresh pacman database: %w", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func validatePackages(packages []string) error { |
||||||
|
for _, pkg := range packages { |
||||||
|
if err := validatePackage(pkg); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func validatePackage(name string) error { |
||||||
|
cmd := exec.Command("pacman", "-Qip", name) |
||||||
|
if err := cmd.Run(); err == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
cmd = exec.Command("pacman", "-Sip", name) |
||||||
|
if err := cmd.Run(); err == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
cmd = exec.Command("aur", "search", name) |
||||||
|
if out, err := cmd.Output(); err == nil && len(out) > 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
return fmt.Errorf("package not found: %s", name) |
||||||
|
} |
||||||
Loading…
Reference in new issue