Add initial declpac CLI tool implementation

This commit is contained in:
AI Bot
2026-04-13 19:27:40 +02:00
committed by Riyyi
parent f1efd72fcb
commit e165558262
9 changed files with 483 additions and 69 deletions
+66
View File
@@ -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)
}
+9
View File
@@ -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
}
+12
View File
@@ -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)
}
+179
View File
@@ -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
}
+74
View File
@@ -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)
}