From 83de6acc74ad41a44a079ead65c6f088824533ce Mon Sep 17 00:00:00 2001 From: AI Bot Date: Sat, 18 Apr 2026 18:18:23 +0200 Subject: [PATCH] Refactor fetch and pacman packages into subpackages Split fetch into alpm and aur subpackages for better organization. Rename state to log. Split pacman into read and sync subpackages. Remove validation in favor of read.DBFreshness. --- README.md | 13 +- cmd/declpac/main.go | 15 +- pkg/fetch/alpm/alpm.go | 137 ++++++++++++++ pkg/fetch/aur/aur.go | 94 +++++++++ pkg/fetch/fetch.go | 266 +++----------------------- pkg/{state/state.go => log/log.go} | 2 +- pkg/pacman/pacman.go | 294 +++++------------------------ pkg/pacman/read/read.go | 109 +++++++++++ pkg/pacman/sync/sync.go | 180 ++++++++++++++++++ pkg/validation/validation.go | 33 ---- 10 files changed, 607 insertions(+), 536 deletions(-) create mode 100644 pkg/fetch/alpm/alpm.go create mode 100644 pkg/fetch/aur/aur.go rename pkg/{state/state.go => log/log.go} (98%) create mode 100644 pkg/pacman/read/read.go create mode 100644 pkg/pacman/sync/sync.go delete mode 100644 pkg/validation/validation.go diff --git a/README.md b/README.md index da78b4f..8a0698c 100644 --- a/README.md +++ b/README.md @@ -167,14 +167,17 @@ declpac/ ├── pkg/ │ ├── input/ # State file/stdin reading │ ├── merge/ # Package merging -│ ├── fetch/ # Package resolution (pacman/AUR) +│ ├── fetch/ # Package resolution +│ │ ├── aur/ # AUR support +│ │ └── alpm/ # ALPM support │ ├── pacman/ # Pacman operations -│ ├── validation/ # Database freshness check -│ ├── output/ # Output formatting -│ └── state/ # Logging +│ │ ├── read/ # Read packages +│ │ └── sync/ # Sync packages +│ ├── log/ # Logging +│ └── output/ # Output formatting └── README.md ``` ## License -GPL-3.0 \ No newline at end of file +GPL-3.0 diff --git a/cmd/declpac/main.go b/cmd/declpac/main.go index 537c929..55e062b 100644 --- a/cmd/declpac/main.go +++ b/cmd/declpac/main.go @@ -9,11 +9,11 @@ import ( "github.com/urfave/cli/v3" "github.com/Riyyi/declpac/pkg/input" + "github.com/Riyyi/declpac/pkg/log" "github.com/Riyyi/declpac/pkg/merge" "github.com/Riyyi/declpac/pkg/output" "github.com/Riyyi/declpac/pkg/pacman" - "github.com/Riyyi/declpac/pkg/state" - "github.com/Riyyi/declpac/pkg/validation" + "github.com/Riyyi/declpac/pkg/pacman/read" ) type Config struct { @@ -65,7 +65,7 @@ func run(cfg *Config) error { merged := merge.Merge(packages) if cfg.DryRun { - result, err := pacman.DryRun(merged) + result, err := read.DryRun(merged) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) return err @@ -75,16 +75,11 @@ func run(cfg *Config) error { return nil } - if err := state.OpenLog(); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - return err - } - defer state.Close() - - if err := validation.CheckDBFreshness(); err != nil { + if err := log.OpenLog(); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) return err } + defer log.Close() result, err := pacman.Sync(merged) if err != nil { diff --git a/pkg/fetch/alpm/alpm.go b/pkg/fetch/alpm/alpm.go new file mode 100644 index 0000000..48f5ee8 --- /dev/null +++ b/pkg/fetch/alpm/alpm.go @@ -0,0 +1,137 @@ +package alpm + +import ( + "fmt" + "os" + "time" + + "github.com/Jguer/dyalpm" +) + +var ( + Root = "/" + PacmanState = "/var/lib/pacman" +) + +type Handle struct { + handle dyalpm.Handle + localDB dyalpm.Database + syncDBs []dyalpm.Database +} + +func New() (*Handle, error) { + start := time.Now() + fmt.Fprintf(os.Stderr, "[debug] alpm.New: starting...\n") + + handle, err := dyalpm.Initialize(Root, PacmanState) + if err != nil { + return nil, fmt.Errorf("failed to initialize alpm: %w", err) + } + + localDB, err := handle.LocalDB() + if err != nil { + handle.Release() + return nil, fmt.Errorf("failed to get local database: %w", err) + } + + syncDBs, err := handle.SyncDBs() + if err != nil { + handle.Release() + return nil, fmt.Errorf("failed to get sync databases: %w", err) + } + + if len(syncDBs) == 0 { + syncDBs, err = registerSyncDBs(handle) + if err != nil { + handle.Release() + return nil, fmt.Errorf("failed to register sync databases: %w", err) + } + } + + fmt.Fprintf(os.Stderr, "[debug] alpm.New: done (%.2fs)\n", time.Since(start).Seconds()) + return &Handle{ + handle: handle, + localDB: localDB, + syncDBs: syncDBs, + }, nil +} + +func (h *Handle) Release() error { + if h.handle != nil { + h.handle.Release() + } + return nil +} + +func registerSyncDBs(handle dyalpm.Handle) ([]dyalpm.Database, error) { + fmt.Fprintf(os.Stderr, "[debug] registerSyncDBs: starting...\n") + + repos := []string{"core", "extra", "multilib"} + var dbs []dyalpm.Database + + for _, repo := range repos { + db, err := handle.RegisterSyncDB(repo, 0) + if err != nil { + continue + } + + count := 0 + db.PkgCache().ForEach(func(pkg dyalpm.Package) error { + count++ + return nil + }) + + if count > 0 { + dbs = append(dbs, db) + } + } + + fmt.Fprintf(os.Stderr, "[debug] registerSyncDBs: done (%d dbs)\n", len(dbs)) + return dbs, nil +} + +func (h *Handle) LocalPackages() (map[string]dyalpm.Package, error) { + start := time.Now() + fmt.Fprintf(os.Stderr, "[debug] LocalPackages: starting...\n") + + localPkgs := make(map[string]dyalpm.Package) + + err := h.localDB.PkgCache().ForEach(func(pkg dyalpm.Package) error { + localPkgs[pkg.Name()] = pkg + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to iterate local package cache: %w", err) + } + + fmt.Fprintf(os.Stderr, "[debug] LocalPackages: done (%.2fs)\n", time.Since(start).Seconds()) + return localPkgs, nil +} + +func (h *Handle) SyncPackages(pkgNames []string) (map[string]dyalpm.Package, error) { + start := time.Now() + fmt.Fprintf(os.Stderr, "[debug] SyncPackages: starting...\n") + + syncPkgs := make(map[string]dyalpm.Package) + pkgSet := make(map[string]bool) + for _, name := range pkgNames { + pkgSet[name] = true + } + + for _, db := range h.syncDBs { + err := db.PkgCache().ForEach(func(pkg dyalpm.Package) error { + if pkgSet[pkg.Name()] { + if _, exists := syncPkgs[pkg.Name()]; !exists { + syncPkgs[pkg.Name()] = pkg + } + } + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to iterate sync database %s: %w", db.Name(), err) + } + } + + fmt.Fprintf(os.Stderr, "[debug] SyncPackages: done (%.2fs)\n", time.Since(start).Seconds()) + return syncPkgs, nil +} diff --git a/pkg/fetch/aur/aur.go b/pkg/fetch/aur/aur.go new file mode 100644 index 0000000..60bea0a --- /dev/null +++ b/pkg/fetch/aur/aur.go @@ -0,0 +1,94 @@ +package aur + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "time" +) + +var AURInfoURL = "https://aur.archlinux.org/rpc?v=5&type=info" + +type Package struct { + Name string `json:"Name"` + PackageBase string `json:"PackageBase"` + Version string `json:"Version"` + URL string `json:"URL"` +} + +type Response struct { + Results []Package `json:"results"` +} + +type Client struct { + cache map[string]Package +} + +func New() *Client { + return &Client{ + cache: make(map[string]Package), + } +} + +func (c *Client) Fetch(packages []string) (map[string]Package, error) { + start := time.Now() + fmt.Fprintf(os.Stderr, "[debug] aur.Fetch: starting...\n") + + result := make(map[string]Package) + + if len(packages) == 0 { + return result, nil + } + + var uncached []string + for _, pkg := range packages { + if _, ok := c.cache[pkg]; !ok { + uncached = append(uncached, pkg) + } + } + + if len(uncached) == 0 { + fmt.Fprintf(os.Stderr, "[debug] aur.Fetch: done (cached) (%.2fs)\n", time.Since(start).Seconds()) + for _, pkg := range packages { + result[pkg] = c.cache[pkg] + } + return result, nil + } + + v := url.Values{} + for _, pkg := range packages { + v.Add("arg[]", pkg) + } + + resp, err := http.Get(AURInfoURL + "&" + v.Encode()) + if err != nil { + return result, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return result, err + } + + var aurResp Response + if err := json.Unmarshal(body, &aurResp); err != nil { + return result, err + } + + for _, r := range aurResp.Results { + c.cache[r.Name] = r + result[r.Name] = r + } + + fmt.Fprintf(os.Stderr, "[debug] aur.Fetch: done (%.2fs)\n", time.Since(start).Seconds()) + return result, nil +} + +func (c *Client) Get(name string) (Package, bool) { + pkg, ok := c.cache[name] + return pkg, ok +} diff --git a/pkg/fetch/fetch.go b/pkg/fetch/fetch.go index 8168ca4..41306ba 100644 --- a/pkg/fetch/fetch.go +++ b/pkg/fetch/fetch.go @@ -1,105 +1,55 @@ package fetch import ( - "encoding/json" "fmt" - "io" - "net/http" - "net/url" "os" - "os/exec" - "strings" "time" - "github.com/Jguer/dyalpm" + "github.com/Riyyi/declpac/pkg/fetch/alpm" + "github.com/Riyyi/declpac/pkg/fetch/aur" ) -const ( - Root = "/" - PacmanState = "/var/lib/pacman" - LockFile = PacmanState + "/db.lock" - AURInfoURL = "https://aur.archlinux.org/rpc?v=5&type=info" -) - -type Fetcher struct { - aurCache map[string]AURPackage - handle dyalpm.Handle - localDB dyalpm.Database - syncDBs []dyalpm.Database -} - type PackageInfo struct { Name string InAUR bool Exists bool Installed bool - AURInfo *AURPackage - syncPkg dyalpm.Package -} - -type AURResponse struct { - Results []AURPackage `json:"results"` + AURInfo *aur.Package } -type AURPackage struct { - Name string `json:"Name"` - PackageBase string `json:"PackageBase"` - Version string `json:"Version"` - URL string `json:"URL"` +type Fetcher struct { + alpmHandle *alpm.Handle + aurClient *aur.Client } func New() (*Fetcher, error) { start := time.Now() - fmt.Fprintf(os.Stderr, "[debug] Fetcher New: starting...\n") - - handle, err := dyalpm.Initialize(Root, PacmanState) - if err != nil { - return nil, fmt.Errorf("failed to initialize alpm: %w", err) - } + fmt.Fprintf(os.Stderr, "[debug] fetch.Fetcher New: starting...\n") - localDB, err := handle.LocalDB() + alpmHandle, err := alpm.New() if err != nil { - handle.Release() - return nil, fmt.Errorf("failed to get local database: %w", err) - } - - syncDBs, err := handle.SyncDBs() - if err != nil { - handle.Release() - return nil, fmt.Errorf("failed to get sync databases: %w", err) + return nil, err } - if len(syncDBs) == 0 { - syncDBs, err = registerSyncDBs(handle) - if err != nil { - handle.Release() - return nil, fmt.Errorf("failed to register sync databases: %w", err) - } - } + aurClient := aur.New() - fmt.Fprintf(os.Stderr, "[debug] Fetcher New: done (%.2fs)\n", time.Since(start).Seconds()) + fmt.Fprintf(os.Stderr, "[debug] fetch.Fetcher New: done (%.2fs)\n", time.Since(start).Seconds()) return &Fetcher{ - aurCache: make(map[string]AURPackage), - handle: handle, - localDB: localDB, - syncDBs: syncDBs, + alpmHandle: alpmHandle, + aurClient: aurClient, }, nil } func (f *Fetcher) Close() error { - if f.handle != nil { - f.handle.Release() - } - return nil + return f.alpmHandle.Release() } -func (f *Fetcher) GetAURPackage(name string) (AURPackage, bool) { - pkg, ok := f.aurCache[name] - return pkg, ok +func (f *Fetcher) GetAURPackage(name string) (aur.Package, bool) { + return f.aurClient.Get(name) } func (f *Fetcher) BuildLocalPkgMap() (map[string]interface{}, error) { - localPkgs, err := f.buildLocalPkgMap() + localPkgs, err := f.alpmHandle.LocalPackages() if err != nil { return nil, err } @@ -110,106 +60,31 @@ func (f *Fetcher) BuildLocalPkgMap() (map[string]interface{}, error) { return result, nil } -func registerSyncDBs(handle dyalpm.Handle) ([]dyalpm.Database, error) { - fmt.Fprintf(os.Stderr, "[debug] registerSyncDBs: starting...\n") - - repos := []string{"core", "extra", "multilib"} - var dbs []dyalpm.Database - - for _, repo := range repos { - db, err := handle.RegisterSyncDB(repo, 0) - if err != nil { - continue - } - - count := 0 - db.PkgCache().ForEach(func(pkg dyalpm.Package) error { - count++ - return nil - }) - - if count > 0 { - dbs = append(dbs, db) - } - } - - fmt.Fprintf(os.Stderr, "[debug] registerSyncDBs: done (%d dbs)\n", len(dbs)) - return dbs, nil -} - -func (f *Fetcher) buildLocalPkgMap() (map[string]dyalpm.Package, error) { - start := time.Now() - fmt.Fprintf(os.Stderr, "[debug] buildLocalPkgMap: starting...\n") - - localPkgs := make(map[string]dyalpm.Package) - - err := f.localDB.PkgCache().ForEach(func(pkg dyalpm.Package) error { - localPkgs[pkg.Name()] = pkg - return nil - }) - - if err != nil { - return nil, fmt.Errorf("failed to iterate local package cache: %w", err) - } - - fmt.Fprintf(os.Stderr, "[debug] buildLocalPkgMap: done (%.2fs)\n", time.Since(start).Seconds()) - return localPkgs, nil -} - -func (f *Fetcher) checkSyncDBs(pkgNames []string) (map[string]dyalpm.Package, error) { - start := time.Now() - fmt.Fprintf(os.Stderr, "[debug] checkSyncDBs: starting...\n") - - syncPkgs := make(map[string]dyalpm.Package) - pkgSet := make(map[string]bool) - for _, name := range pkgNames { - pkgSet[name] = true - } - - for _, db := range f.syncDBs { - err := db.PkgCache().ForEach(func(pkg dyalpm.Package) error { - if pkgSet[pkg.Name()] { - if _, exists := syncPkgs[pkg.Name()]; !exists { - syncPkgs[pkg.Name()] = pkg - } - } - return nil - }) - if err != nil { - return nil, fmt.Errorf("failed to iterate sync database %s: %w", db.Name(), err) - } - } - - fmt.Fprintf(os.Stderr, "[debug] checkSyncDBs: done (%.2fs)\n", time.Since(start).Seconds()) - return syncPkgs, nil -} - func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error) { start := time.Now() - fmt.Fprintf(os.Stderr, "[debug] Resolve: starting...\n") + fmt.Fprintf(os.Stderr, "[debug] fetch.Resolve: starting...\n") result := make(map[string]*PackageInfo) for _, pkg := range packages { result[pkg] = &PackageInfo{Name: pkg, Exists: false} } - syncPkgs, err := f.checkSyncDBs(packages) + syncPkgs, err := f.alpmHandle.SyncPackages(packages) if err != nil { return nil, err } - fmt.Fprintf(os.Stderr, "[debug] Resolve: sync db check done (%.2fs)\n", time.Since(start).Seconds()) + fmt.Fprintf(os.Stderr, "[debug] fetch.Resolve: sync db check done (%.2fs)\n", time.Since(start).Seconds()) - for pkg, syncPkg := range syncPkgs { + for pkg := range syncPkgs { result[pkg].Exists = true result[pkg].InAUR = false - result[pkg].syncPkg = syncPkg } - localPkgs, err := f.buildLocalPkgMap() + localPkgs, err := f.alpmHandle.LocalPackages() if err != nil { return nil, err } - fmt.Fprintf(os.Stderr, "[debug] Resolve: local pkgs built (%.2fs)\n", time.Since(start).Seconds()) + fmt.Fprintf(os.Stderr, "[debug] fetch.Resolve: local pkgs built (%.2fs)\n", time.Since(start).Seconds()) for pkg := range localPkgs { if info, ok := result[pkg]; ok { @@ -225,7 +100,9 @@ func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error) { } if len(notInSync) > 0 { - f.ensureAURCache(notInSync) + if _, err := f.aurClient.Fetch(notInSync); err != nil { + fmt.Fprintf(os.Stderr, "[debug] fetch.Resolve: aur fetch error: %v\n", err) + } for _, pkg := range packages { info := result[pkg] @@ -233,7 +110,7 @@ func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error) { continue } - if aurInfo, ok := f.aurCache[pkg]; ok { + if aurInfo, ok := f.aurClient.Get(pkg); ok { info.InAUR = true info.AURInfo = &aurInfo continue @@ -250,93 +127,6 @@ func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error) { } } - fmt.Fprintf(os.Stderr, "[debug] Resolve: done (%.2fs)\n", time.Since(start).Seconds()) - return result, nil -} - -func (f *Fetcher) ensureAURCache(packages []string) { - start := time.Now() - fmt.Fprintf(os.Stderr, "[debug] ensureAURCache: starting...\n") - - if len(packages) == 0 { - return - } - - var uncached []string - for _, pkg := range packages { - if _, ok := f.aurCache[pkg]; !ok { - uncached = append(uncached, pkg) - } - } - - if len(uncached) == 0 { - fmt.Fprintf(os.Stderr, "[debug] ensureAURCache: done (%.2fs)\n", time.Since(start).Seconds()) - return - } - - _, err := f.fetchAURInfo(uncached) - if err != nil { - fmt.Fprintf(os.Stderr, "[debug] ensureAURCache: fetch error: %v\n", err) - } - fmt.Fprintf(os.Stderr, "[debug] ensureAURCache: done (%.2fs)\n", time.Since(start).Seconds()) -} - -func (f *Fetcher) fetchAURInfo(packages []string) (map[string]AURPackage, error) { - start := time.Now() - fmt.Fprintf(os.Stderr, "[debug] fetchAURInfo: starting...\n") - - result := make(map[string]AURPackage) - - if len(packages) == 0 { - return result, nil - } - - v := url.Values{} - for _, pkg := range packages { - v.Add("arg[]", pkg) - } - - resp, err := http.Get(AURInfoURL + "&" + v.Encode()) - if err != nil { - return result, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return result, err - } - - var aurResp AURResponse - if err := json.Unmarshal(body, &aurResp); err != nil { - return result, err - } - - for _, r := range aurResp.Results { - f.aurCache[r.Name] = r - result[r.Name] = r - } - - fmt.Fprintf(os.Stderr, "[debug] fetchAURInfo: done (%.2fs)\n", time.Since(start).Seconds()) + fmt.Fprintf(os.Stderr, "[debug] fetch.Resolve: done (%.2fs)\n", time.Since(start).Seconds()) return result, nil } - -func (f *Fetcher) ListOrphans() ([]string, error) { - start := time.Now() - fmt.Fprintf(os.Stderr, "[debug] ListOrphans: starting...\n") - - cmd := exec.Command("pacman", "-Qdtq") - orphans, err := cmd.Output() - if err != nil { - return nil, nil - } - - list := strings.TrimSpace(string(orphans)) - if list == "" { - fmt.Fprintf(os.Stderr, "[debug] ListOrphans: done (%.2fs)\n", time.Since(start).Seconds()) - return nil, nil - } - - fmt.Fprintf(os.Stderr, "[debug] ListOrphans: done (%.2fs)\n", time.Since(start).Seconds()) - return strings.Split(list, "\n"), nil -} diff --git a/pkg/state/state.go b/pkg/log/log.go similarity index 98% rename from pkg/state/state.go rename to pkg/log/log.go index ea0e4ac..d0e9127 100644 --- a/pkg/state/state.go +++ b/pkg/log/log.go @@ -1,4 +1,4 @@ -package state +package log import ( "fmt" diff --git a/pkg/pacman/pacman.go b/pkg/pacman/pacman.go index e4588de..506a193 100644 --- a/pkg/pacman/pacman.go +++ b/pkg/pacman/pacman.go @@ -3,80 +3,34 @@ package pacman import ( "fmt" "os" - "os/exec" - "path/filepath" - "strings" "time" "github.com/Riyyi/declpac/pkg/fetch" + "github.com/Riyyi/declpac/pkg/log" "github.com/Riyyi/declpac/pkg/output" - "github.com/Riyyi/declpac/pkg/state" - "github.com/Riyyi/declpac/pkg/validation" + "github.com/Riyyi/declpac/pkg/pacman/read" + "github.com/Riyyi/declpac/pkg/pacman/sync" ) -func MarkAllAsDeps() error { - start := time.Now() - fmt.Fprintf(os.Stderr, "[debug] MarkAllAsDeps: starting...\n") - - listCmd := exec.Command("pacman", "-Qq") - output, err := listCmd.Output() - if err != nil { - return fmt.Errorf("failed to list packages: %w", err) - } - - packages := strings.Split(strings.TrimSpace(string(output)), "\n") - if len(packages) == 0 || packages[0] == "" { - fmt.Fprintf(os.Stderr, "[debug] MarkAllAsDeps: no packages to mark (%.2fs)\n", time.Since(start).Seconds()) - return nil - } - - args := append([]string{"-D", "--asdeps"}, packages...) - cmd := exec.Command("pacman", args...) - state.Write([]byte("pacman " + strings.Join(args, " ") + "\n")) - cmd.Stdout = state.GetLogWriter() - cmd.Stderr = state.GetLogWriter() - err = cmd.Run() - if err != nil { - state.Write([]byte(fmt.Sprintf("error: %v\n", err))) - } - - fmt.Fprintf(os.Stderr, "[debug] MarkAllAsDeps: done (%.2fs)\n", time.Since(start).Seconds()) - return err -} - -func MarkAsExplicit(packages []string) error { - if len(packages) == 0 { - return nil - } - start := time.Now() - fmt.Fprintf(os.Stderr, "[debug] MarkAsExplicit: starting...\n") - - args := append([]string{"-D", "--asexplicit"}, packages...) - cmd := exec.Command("pacman", args...) - state.Write([]byte("pacman " + strings.Join(args, " ") + "\n")) - cmd.Stdout = state.GetLogWriter() - cmd.Stderr = state.GetLogWriter() - err := cmd.Run() - if err != nil { - state.Write([]byte(fmt.Sprintf("error: %v\n", err))) - } - - fmt.Fprintf(os.Stderr, "[debug] MarkAsExplicit: done (%.2fs)\n", time.Since(start).Seconds()) - return err -} - func Sync(packages []string) (*output.Result, error) { start := time.Now() fmt.Fprintf(os.Stderr, "[debug] Sync: starting...\n") - before, err := getInstalledCount() + list, err := read.List() if err != nil { return nil, err } + before := len(list) - if err := validation.CheckDBFreshness(); err != nil { + fresh, err := read.DBFreshness() + if err != nil { return nil, err } + if !fresh { + if err := sync.RefreshDB(log.GetLogWriter()); err != nil { + return nil, err + } + } fmt.Fprintf(os.Stderr, "[debug] Sync: database fresh (%.2fs)\n", time.Since(start).Seconds()) f, err := fetch.New() @@ -95,8 +49,7 @@ func Sync(packages []string) (*output.Result, error) { if len(pacmanPkgs) > 0 { fmt.Fprintf(os.Stderr, "[debug] Sync: syncing %d pacman packages...\n", len(pacmanPkgs)) - err = SyncPackages(pacmanPkgs) - if err != nil { + if err := sync.SyncPackages(pacmanPkgs, log.GetLogWriter()); err != nil { return nil, err } fmt.Fprintf(os.Stderr, "[debug] Sync: pacman packages synced (%.2fs)\n", time.Since(start).Seconds()) @@ -104,30 +57,37 @@ func Sync(packages []string) (*output.Result, error) { for _, pkg := range aurPkgs { fmt.Fprintf(os.Stderr, "[debug] Sync: installing AUR package %s...\n", pkg) - if err := InstallAUR(f, pkg); err != nil { + aurInfo, ok := f.GetAURPackage(pkg) + if !ok { + return nil, fmt.Errorf("AUR package not found in cache: %s", pkg) + } + if err := sync.InstallAUR(pkg, aurInfo.PackageBase, log.GetLogWriter()); err != nil { return nil, err } fmt.Fprintf(os.Stderr, "[debug] Sync: AUR package %s installed (%.2fs)\n", pkg, time.Since(start).Seconds()) } fmt.Fprintf(os.Stderr, "[debug] Sync: marking all as deps...\n") - if err := MarkAllAsDeps(); err != nil { - fmt.Fprintf(os.Stderr, "warning: could not mark all as deps: %v\n", err) - } + markAllAsDeps() fmt.Fprintf(os.Stderr, "[debug] Sync: all marked as deps (%.2fs)\n", time.Since(start).Seconds()) fmt.Fprintf(os.Stderr, "[debug] Sync: marking state packages as explicit...\n") - if err := MarkAsExplicit(packages); err != nil { + if err := sync.MarkAs(packages, "explicit", log.GetLogWriter()); err != nil { fmt.Fprintf(os.Stderr, "warning: could not mark state packages as explicit: %v\n", err) } fmt.Fprintf(os.Stderr, "[debug] Sync: state packages marked as explicit (%.2fs)\n", time.Since(start).Seconds()) - removed, err := CleanupOrphans() + removed, err := cleanupOrphans() if err != nil { return nil, err } - after, _ := getInstalledCount() + list, _ = read.List() + if err != nil { + return nil, err + } + after := len(list) + installedCount := max(after-before, 0) fmt.Fprintf(os.Stderr, "[debug] Sync: done (%.2fs)\n", time.Since(start).Seconds()) @@ -163,204 +123,40 @@ func categorizePackages(f *fetch.Fetcher, packages []string) (pacmanPkgs, aurPkg return pacmanPkgs, aurPkgs, nil } -func InstallAUR(f *fetch.Fetcher, pkgName string) error { - start := time.Now() - fmt.Fprintf(os.Stderr, "[debug] InstallAUR: starting...\n") - - aurInfo, ok := f.GetAURPackage(pkgName) - if !ok { - return fmt.Errorf("AUR package not found in cache: %s", pkgName) - } - - sudoUser := os.Getenv("SUDO_USER") - if sudoUser == "" { - sudoUser = os.Getenv("USER") - if sudoUser == "" { - sudoUser = "root" - } - } - - tmpDir := "/tmp/declpac-aur-" + pkgName - mkdirCmd := exec.Command("su", "-", sudoUser, "-c", "rm -rf "+tmpDir+" && mkdir -p "+tmpDir) - if err := mkdirCmd.Run(); err != nil { - return fmt.Errorf("failed to create temp directory: %w", err) - } - defer os.RemoveAll(tmpDir) - - cloneURL := "https://aur.archlinux.org/" + aurInfo.PackageBase + ".git" - cloneCmd := exec.Command("su", "-", sudoUser, "-c", "git clone "+cloneURL+" "+tmpDir) - state.Write([]byte("git clone " + cloneURL + " " + tmpDir + "\n")) - cloneCmd.Stdout = state.GetLogWriter() - cloneCmd.Stderr = state.GetLogWriter() - if err := cloneCmd.Run(); err != nil { - errMsg := fmt.Sprintf("failed to clone AUR repo: %v\n", err) - state.Write([]byte("error: " + errMsg)) - return fmt.Errorf("failed to clone AUR repo: %w", err) - } - fmt.Fprintf(os.Stderr, "[debug] InstallAUR: cloned (%.2fs)\n", time.Since(start).Seconds()) - - state.Write([]byte("makepkg -s --noconfirm\n")) - makepkgCmd := exec.Command("su", "-", sudoUser, "-c", "cd "+tmpDir+" && makepkg -s --noconfirm") - makepkgCmd.Stdout = state.GetLogWriter() - makepkgCmd.Stderr = state.GetLogWriter() - if err := makepkgCmd.Run(); err != nil { - errMsg := fmt.Sprintf("makepkg failed to build AUR package: %v\n", err) - state.Write([]byte("error: " + errMsg)) - return fmt.Errorf("makepkg failed to build AUR package: %w", err) - } - fmt.Fprintf(os.Stderr, "[debug] InstallAUR: built (%.2fs)\n", time.Since(start).Seconds()) - - pkgFile, err := findPKGFile(tmpDir) - if err != nil { - return fmt.Errorf("failed to find built package: %w", err) - } - - state.Write([]byte("pacman -U --noconfirm " + pkgFile + "\n")) - installCmd := exec.Command("pacman", "-U", "--noconfirm", pkgFile) - installCmd.Stdout = state.GetLogWriter() - installCmd.Stderr = state.GetLogWriter() - if err := installCmd.Run(); err != nil { - errMsg := fmt.Sprintf("failed to install package: %v\n", err) - state.Write([]byte("error: " + errMsg)) - return fmt.Errorf("failed to install package: %w", err) - } - fmt.Fprintf(os.Stderr, "[debug] InstallAUR: built (%.2fs)\n", time.Since(start).Seconds()) - - fmt.Fprintf(os.Stderr, "[debug] InstallAUR: done (%.2fs)\n", time.Since(start).Seconds()) - return nil -} - -func findPKGFile(dir string) (string, error) { - entries, err := os.ReadDir(dir) - if err != nil { - return "", err - } - for _, entry := range entries { - name := entry.Name() - if strings.HasSuffix(name, ".pkg.tar.zst") || strings.HasSuffix(name, ".pkg.tar.gz") { - return filepath.Join(dir, name), nil - } - } - return "", fmt.Errorf("no package file found in %s", dir) -} - -func getInstalledCount() (int, error) { +func markAllAsDeps() error { start := time.Now() - fmt.Fprintf(os.Stderr, "[debug] getInstalledCount: starting...\n") + fmt.Fprintf(os.Stderr, "[debug] markAllAsDeps: starting...\n") - 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 + packages, err := read.List() + if err != nil || len(packages) == 0 { + return fmt.Errorf("failed to list packages: %w", err) } - fmt.Fprintf(os.Stderr, "[debug] getInstalledCount: done (%.2fs)\n", time.Since(start).Seconds()) - return count, nil -} - -func SyncPackages(packages []string) error { - start := time.Now() - fmt.Fprintf(os.Stderr, "[debug] SyncPackages: starting...\n") - - args := append([]string{"-S", "--needed"}, packages...) - cmd := exec.Command("pacman", args...) - state.Write([]byte("pacman " + strings.Join(args, " ") + "\n")) - cmd.Stdout = state.GetLogWriter() - cmd.Stderr = state.GetLogWriter() - err := cmd.Run() - if err != nil { - state.Write([]byte(fmt.Sprintf("pacman sync failed: %v\n", err))) - return fmt.Errorf("pacman sync failed: %v", err) + if err := sync.MarkAs(packages, "deps", log.GetLogWriter()); err != nil { + log.Write([]byte(fmt.Sprintf("error: %v\n", err))) + return err } - fmt.Fprintf(os.Stderr, "[debug] SyncPackages: done (%.2fs)\n", time.Since(start).Seconds()) + fmt.Fprintf(os.Stderr, "[debug] markAllAsDeps: done (%.2fs)\n", time.Since(start).Seconds()) return nil } -func CleanupOrphans() (int, error) { +func cleanupOrphans() (int, error) { start := time.Now() - fmt.Fprintf(os.Stderr, "[debug] CleanupOrphans: starting...\n") + fmt.Fprintf(os.Stderr, "[debug] cleanupOrphans: starting...\n") - f, err := fetch.New() + orphans, err := read.ListOrphans() if err != nil { + log.Write([]byte(fmt.Sprintf("error: %v\n", err))) return 0, err } - defer f.Close() - - orphans, err := f.ListOrphans() - if err != nil || len(orphans) == 0 { - fmt.Fprintf(os.Stderr, "[debug] CleanupOrphans: done (%.2fs)\n", time.Since(start).Seconds()) - return 0, nil - } - removeCmd := exec.Command("pacman", "-Rns") - state.Write([]byte("pacman -Rns\n")) - removeCmd.Stdout = state.GetLogWriter() - removeCmd.Stderr = state.GetLogWriter() - err = removeCmd.Run() + removed, err := sync.RemoveOrphans(orphans, log.GetLogWriter()) if err != nil { - state.Write([]byte(fmt.Sprintf("cleanup orphans failed: %v\n", err))) - return 0, fmt.Errorf("cleanup orphans failed: %v", err) - } - - count := len(orphans) - - fmt.Fprintf(os.Stderr, "[debug] CleanupOrphans: done (%.2fs)\n", time.Since(start).Seconds()) - return count, nil -} - -func DryRun(packages []string) (*output.Result, error) { - start := time.Now() - fmt.Fprintf(os.Stderr, "[debug] DryRun: starting...\n") - - f, err := fetch.New() - if err != nil { - return nil, err - } - defer f.Close() - fmt.Fprintf(os.Stderr, "[debug] DryRun: initialized fetcher (%.2fs)\n", time.Since(start).Seconds()) - - resolved, err := f.Resolve(packages) - if err != nil { - return nil, err - } - fmt.Fprintf(os.Stderr, "[debug] DryRun: packages resolved (%.2fs)\n", time.Since(start).Seconds()) - - localPkgs, err := f.BuildLocalPkgMap() - if err != nil { - return nil, err - } - - var toInstall []string - var aurPkgs []string - for _, pkg := range packages { - info := resolved[pkg] - if info == nil || (!info.Exists && !info.InAUR) { - return nil, fmt.Errorf("package not found: %s", pkg) - } - if info.InAUR { - aurPkgs = append(aurPkgs, pkg) - } else if _, installed := localPkgs[pkg]; !installed { - toInstall = append(toInstall, pkg) - } - } - fmt.Fprintf(os.Stderr, "[debug] DryRun: packages categorized (%.2fs)\n", time.Since(start).Seconds()) - - orphans, err := f.ListOrphans() - if err != nil { - return nil, err + log.Write([]byte(fmt.Sprintf("error: %v\n", err))) + return 0, err } - fmt.Fprintf(os.Stderr, "[debug] DryRun: orphans listed (%.2fs)\n", time.Since(start).Seconds()) - fmt.Fprintf(os.Stderr, "[debug] DryRun: done (%.2fs)\n", time.Since(start).Seconds()) - return &output.Result{ - Installed: len(toInstall) + len(aurPkgs), - Removed: len(orphans), - ToInstall: append(toInstall, aurPkgs...), - ToRemove: orphans, - }, nil + fmt.Fprintf(os.Stderr, "[debug] cleanupOrphans: done (%.2fs)\n", time.Since(start).Seconds()) + return removed, nil } diff --git a/pkg/pacman/read/read.go b/pkg/pacman/read/read.go new file mode 100644 index 0000000..c9975e8 --- /dev/null +++ b/pkg/pacman/read/read.go @@ -0,0 +1,109 @@ +package read + +import ( + "fmt" + "os" + "os/exec" + "strings" + "time" + + "github.com/Riyyi/declpac/pkg/fetch" + "github.com/Riyyi/declpac/pkg/output" +) + +var LockFile = "/var/lib/pacman/db.lock" + +func List() ([]string, error) { + start := time.Now() + fmt.Fprintf(os.Stderr, "[debug] List: starting...\n") + + cmd := exec.Command("pacman", "-Qq") + output, err := cmd.Output() + if err != nil { + return nil, err + } + + list := strings.Split(strings.TrimSpace(string(output)), "\n") + if list[0] == "" { + list = nil + } + + fmt.Fprintf(os.Stderr, "[debug] List: done (%.2fs)\n", time.Since(start).Seconds()) + return list, nil +} + +func ListOrphans() ([]string, error) { + start := time.Now() + fmt.Fprintf(os.Stderr, "[debug] ListOrphans: starting...\n") + + cmd := exec.Command("pacman", "-Qdtq") + output, err := cmd.Output() + if err != nil { + return nil, err + } + + orphans := strings.Split(strings.TrimSpace(string(output)), "\n") + if len(orphans) > 0 && orphans[0] == "" { + orphans = orphans[1:] + } + + fmt.Fprintf(os.Stderr, "[debug] ListOrphans: done (%.2fs)\n", time.Since(start).Seconds()) + return orphans, nil +} + +func DBFreshness() (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 DryRun(packages []string) (*output.Result, error) { + start := time.Now() + fmt.Fprintf(os.Stderr, "[debug] DryRun: starting...\n") + + f, err := fetch.New() + if err != nil { + return nil, err + } + defer f.Close() + fmt.Fprintf(os.Stderr, "[debug] DryRun: initialized fetcher (%.2fs)\n", time.Since(start).Seconds()) + + resolved, err := f.Resolve(packages) + if err != nil { + return nil, err + } + fmt.Fprintf(os.Stderr, "[debug] DryRun: packages resolved (%.2fs)\n", time.Since(start).Seconds()) + + var toInstall []string + var aurPkgs []string + for _, pkg := range packages { + info := resolved[pkg] + if info == nil || (!info.Exists && !info.InAUR) { + return nil, fmt.Errorf("package not found: %s", pkg) + } + if info.InAUR { + aurPkgs = append(aurPkgs, pkg) + } else if !info.Installed { + toInstall = append(toInstall, pkg) + } + } + fmt.Fprintf(os.Stderr, "[debug] DryRun: packages categorized (%.2fs)\n", time.Since(start).Seconds()) + + orphans, err := ListOrphans() + if err != nil { + return nil, err + } + fmt.Fprintf(os.Stderr, "[debug] DryRun: orphans listed (%.2fs)\n", time.Since(start).Seconds()) + + fmt.Fprintf(os.Stderr, "[debug] DryRun: done (%.2fs)\n", time.Since(start).Seconds()) + return &output.Result{ + Installed: len(toInstall) + len(aurPkgs), + Removed: len(orphans), + ToInstall: append(toInstall, aurPkgs...), + ToRemove: orphans, + }, nil +} diff --git a/pkg/pacman/sync/sync.go b/pkg/pacman/sync/sync.go new file mode 100644 index 0000000..95b9292 --- /dev/null +++ b/pkg/pacman/sync/sync.go @@ -0,0 +1,180 @@ +package sync + +import ( + "fmt" + "io" + "os" + "os/exec" + "strings" + "time" +) + +type Result struct { + Installed int + Removed int +} + +func SyncPackages(packages []string, logWriter io.Writer) error { + start := time.Now() + fmt.Fprintf(os.Stderr, "[debug] SyncPackages: starting...\n") + + if logWriter == nil { + logWriter = os.Stderr + } + + args := append([]string{"-S", "--needed"}, packages...) + cmd := exec.Command("pacman", args...) + cmd.Stdout = logWriter + cmd.Stderr = logWriter + err := cmd.Run() + if err != nil { + return fmt.Errorf("pacman sync failed: %w", err) + } + + fmt.Fprintf(os.Stderr, "[debug] SyncPackages: done (%.2fs)\n", time.Since(start).Seconds()) + return nil +} + +func RefreshDB(logWriter io.Writer) error { + start := time.Now() + fmt.Fprintf(os.Stderr, "[debug] RefreshDB: starting...\n") + + if logWriter == nil { + logWriter = os.Stderr + } + + cmd := exec.Command("pacman", "-Syy") + cmd.Stdout = logWriter + cmd.Stderr = logWriter + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to refresh pacman database: %w", err) + } + + fmt.Fprintf(os.Stderr, "[debug] RefreshDB: done (%.2fs)\n", time.Since(start).Seconds()) + return nil +} + +func MarkAs(packages []string, flag string, logWriter io.Writer) error { + if len(packages) == 0 { + return nil + } + start := time.Now() + flagName := map[string]string{"deps": "asdeps", "explicit": "asexplicit"}[flag] + fmt.Fprintf(os.Stderr, "[debug] MarkAs(%s): starting...\n", flag) + + if logWriter == nil { + logWriter = os.Stderr + } + + args := append([]string{"-D", "--" + flagName}, packages...) + cmd := exec.Command("pacman", args...) + cmd.Stdout = logWriter + cmd.Stderr = logWriter + err := cmd.Run() + if err != nil { + return fmt.Errorf("mark as %s failed: %w", flag, err) + } + + fmt.Fprintf(os.Stderr, "[debug] MarkAs(%s): done (%.2fs)\n", flag, time.Since(start).Seconds()) + return nil +} + +func RemoveOrphans(orphans []string, logWriter io.Writer) (int, error) { + start := time.Now() + fmt.Fprintf(os.Stderr, "[debug] RemoveOrphans: starting...\n") + + if logWriter == nil { + logWriter = os.Stderr + } + + if len(orphans) == 0 { + fmt.Fprintf(os.Stderr, "[debug] RemoveOrphans: done (no orphans) (%.2fs)\n", time.Since(start).Seconds()) + return 0, nil + } + + args := make([]string, 0, 2+len(orphans)) + args = append(args, "pacman", "-Rns") + args = append(args, orphans...) + removeCmd := exec.Command(args[0], args[1:]...) + removeCmd.Stdout = logWriter + removeCmd.Stderr = logWriter + err := removeCmd.Run() + if err != nil { + return 0, fmt.Errorf("remove orphans failed: %w", err) + } + + count := len(orphans) + + fmt.Fprintf(os.Stderr, "[debug] RemoveOrphans: done (%d) (%.2fs)\n", count, time.Since(start).Seconds()) + return count, nil +} + +func InstallAUR(pkgName string, packageBase string, logWriter io.Writer) error { + start := time.Now() + fmt.Fprintf(os.Stderr, "[debug] InstallAUR: starting...\n") + + if logWriter == nil { + logWriter = os.Stderr + } + + sudoUser := os.Getenv("SUDO_USER") + if sudoUser == "" { + sudoUser = os.Getenv("USER") + if sudoUser == "" { + sudoUser = "root" + } + } + + tmpDir := "/tmp/declpac-aur-" + pkgName + mkdirCmd := exec.Command("su", "-", sudoUser, "-c", "rm -rf "+tmpDir+" && mkdir -p "+tmpDir) + if err := mkdirCmd.Run(); err != nil { + return fmt.Errorf("failed to create temp directory: %w", err) + } + defer os.RemoveAll(tmpDir) + + cloneURL := "https://aur.archlinux.org/" + packageBase + ".git" + cloneCmd := exec.Command("su", "-", sudoUser, "-c", "git clone "+cloneURL+" "+tmpDir) + cloneCmd.Stdout = logWriter + cloneCmd.Stderr = logWriter + if err := cloneCmd.Run(); err != nil { + return fmt.Errorf("failed to clone AUR repo: %w", err) + } + fmt.Fprintf(os.Stderr, "[debug] InstallAUR: cloned (%.2fs)\n", time.Since(start).Seconds()) + + makepkgCmd := exec.Command("su", "-", sudoUser, "-c", "cd "+tmpDir+" && makepkg -s --noconfirm") + makepkgCmd.Stdout = logWriter + makepkgCmd.Stderr = logWriter + if err := makepkgCmd.Run(); err != nil { + return fmt.Errorf("makepkg failed to build AUR package: %w", err) + } + fmt.Fprintf(os.Stderr, "[debug] InstallAUR: built (%.2fs)\n", time.Since(start).Seconds()) + + pkgFile, err := findPKGFile(tmpDir) + if err != nil { + return fmt.Errorf("failed to find built package: %w", err) + } + + installCmd := exec.Command("pacman", "-U", "--noconfirm", pkgFile) + installCmd.Stdout = logWriter + installCmd.Stderr = logWriter + if err := installCmd.Run(); err != nil { + return fmt.Errorf("failed to install package: %w", err) + } + fmt.Fprintf(os.Stderr, "[debug] InstallAUR: done (%.2fs)\n", time.Since(start).Seconds()) + + return nil +} + +func findPKGFile(dir string) (string, error) { + entries, err := os.ReadDir(dir) + if err != nil { + return "", err + } + for _, entry := range entries { + name := entry.Name() + if strings.HasSuffix(name, ".pkg.tar.zst") || strings.HasSuffix(name, ".pkg.tar.gz") { + return strings.Join([]string{dir, name}, "/"), nil + } + } + return "", fmt.Errorf("no package file found in %s", dir) +} diff --git a/pkg/validation/validation.go b/pkg/validation/validation.go deleted file mode 100644 index 8d0c67e..0000000 --- a/pkg/validation/validation.go +++ /dev/null @@ -1,33 +0,0 @@ -package validation - -import ( - "fmt" - "os" - "os/exec" - "time" -) - -var LockFile = "/var/lib/pacman/db.lock" - -func CheckDBFreshness() error { - start := time.Now() - fmt.Fprintf(os.Stderr, "[debug] CheckDBFreshness: starting...\n") - - 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) - } - } - - fmt.Fprintf(os.Stderr, "[debug] CheckDBFreshness: done (%.2fs)\n", time.Since(start).Seconds()) - return nil -}