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 -}