Browse Source

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.
master
AI Bot 4 hours ago committed by Riyyi
parent
commit
83de6acc74
  1. 13
      README.md
  2. 15
      cmd/declpac/main.go
  3. 137
      pkg/fetch/alpm/alpm.go
  4. 94
      pkg/fetch/aur/aur.go
  5. 266
      pkg/fetch/fetch.go
  6. 2
      pkg/log/log.go
  7. 294
      pkg/pacman/pacman.go
  8. 109
      pkg/pacman/read/read.go
  9. 180
      pkg/pacman/sync/sync.go
  10. 33
      pkg/validation/validation.go

13
README.md

@ -167,14 +167,17 @@ declpac/
├── pkg/ ├── pkg/
│ ├── input/ # State file/stdin reading │ ├── input/ # State file/stdin reading
│ ├── merge/ # Package merging │ ├── merge/ # Package merging
│ ├── fetch/ # Package resolution (pacman/AUR) │ ├── fetch/ # Package resolution
│ │ ├── aur/ # AUR support
│ │ └── alpm/ # ALPM support
│ ├── pacman/ # Pacman operations │ ├── pacman/ # Pacman operations
│ ├── validation/ # Database freshness check │ │ ├── read/ # Read packages
│ ├── output/ # Output formatting │ │ └── sync/ # Sync packages
│ └── state/ # Logging │ ├── log/ # Logging
│ └── output/ # Output formatting
└── README.md └── README.md
``` ```
## License ## License
GPL-3.0 GPL-3.0

15
cmd/declpac/main.go

@ -9,11 +9,11 @@ import (
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"github.com/Riyyi/declpac/pkg/input" "github.com/Riyyi/declpac/pkg/input"
"github.com/Riyyi/declpac/pkg/log"
"github.com/Riyyi/declpac/pkg/merge" "github.com/Riyyi/declpac/pkg/merge"
"github.com/Riyyi/declpac/pkg/output" "github.com/Riyyi/declpac/pkg/output"
"github.com/Riyyi/declpac/pkg/pacman" "github.com/Riyyi/declpac/pkg/pacman"
"github.com/Riyyi/declpac/pkg/state" "github.com/Riyyi/declpac/pkg/pacman/read"
"github.com/Riyyi/declpac/pkg/validation"
) )
type Config struct { type Config struct {
@ -65,7 +65,7 @@ func run(cfg *Config) error {
merged := merge.Merge(packages) merged := merge.Merge(packages)
if cfg.DryRun { if cfg.DryRun {
result, err := pacman.DryRun(merged) result, err := read.DryRun(merged)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err) fmt.Fprintf(os.Stderr, "error: %v\n", err)
return err return err
@ -75,16 +75,11 @@ func run(cfg *Config) error {
return nil return nil
} }
if err := state.OpenLog(); err != nil { if err := log.OpenLog(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return err
}
defer state.Close()
if err := validation.CheckDBFreshness(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err) fmt.Fprintf(os.Stderr, "error: %v\n", err)
return err return err
} }
defer log.Close()
result, err := pacman.Sync(merged) result, err := pacman.Sync(merged)
if err != nil { if err != nil {

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

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

266
pkg/fetch/fetch.go

@ -1,105 +1,55 @@
package fetch package fetch
import ( import (
"encoding/json"
"fmt" "fmt"
"io"
"net/http"
"net/url"
"os" "os"
"os/exec"
"strings"
"time" "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 { type PackageInfo struct {
Name string Name string
InAUR bool InAUR bool
Exists bool Exists bool
Installed bool Installed bool
AURInfo *AURPackage AURInfo *aur.Package
syncPkg dyalpm.Package
}
type AURResponse struct {
Results []AURPackage `json:"results"`
} }
type AURPackage struct { type Fetcher struct {
Name string `json:"Name"` alpmHandle *alpm.Handle
PackageBase string `json:"PackageBase"` aurClient *aur.Client
Version string `json:"Version"`
URL string `json:"URL"`
} }
func New() (*Fetcher, error) { func New() (*Fetcher, error) {
start := time.Now() start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] Fetcher New: starting...\n") fmt.Fprintf(os.Stderr, "[debug] fetch.Fetcher 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() alpmHandle, err := alpm.New()
if err != nil { if err != nil {
handle.Release() return nil, err
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 { aurClient := aur.New()
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] 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{ return &Fetcher{
aurCache: make(map[string]AURPackage), alpmHandle: alpmHandle,
handle: handle, aurClient: aurClient,
localDB: localDB,
syncDBs: syncDBs,
}, nil }, nil
} }
func (f *Fetcher) Close() error { func (f *Fetcher) Close() error {
if f.handle != nil { return f.alpmHandle.Release()
f.handle.Release()
}
return nil
} }
func (f *Fetcher) GetAURPackage(name string) (AURPackage, bool) { func (f *Fetcher) GetAURPackage(name string) (aur.Package, bool) {
pkg, ok := f.aurCache[name] return f.aurClient.Get(name)
return pkg, ok
} }
func (f *Fetcher) BuildLocalPkgMap() (map[string]interface{}, error) { func (f *Fetcher) BuildLocalPkgMap() (map[string]interface{}, error) {
localPkgs, err := f.buildLocalPkgMap() localPkgs, err := f.alpmHandle.LocalPackages()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -110,106 +60,31 @@ func (f *Fetcher) BuildLocalPkgMap() (map[string]interface{}, error) {
return result, nil 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) { func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error) {
start := time.Now() 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) result := make(map[string]*PackageInfo)
for _, pkg := range packages { for _, pkg := range packages {
result[pkg] = &PackageInfo{Name: pkg, Exists: false} result[pkg] = &PackageInfo{Name: pkg, Exists: false}
} }
syncPkgs, err := f.checkSyncDBs(packages) syncPkgs, err := f.alpmHandle.SyncPackages(packages)
if err != nil { if err != nil {
return nil, err 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].Exists = true
result[pkg].InAUR = false result[pkg].InAUR = false
result[pkg].syncPkg = syncPkg
} }
localPkgs, err := f.buildLocalPkgMap() localPkgs, err := f.alpmHandle.LocalPackages()
if err != nil { if err != nil {
return nil, err 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 { for pkg := range localPkgs {
if info, ok := result[pkg]; ok { if info, ok := result[pkg]; ok {
@ -225,7 +100,9 @@ func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error) {
} }
if len(notInSync) > 0 { 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 { for _, pkg := range packages {
info := result[pkg] info := result[pkg]
@ -233,7 +110,7 @@ func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error) {
continue continue
} }
if aurInfo, ok := f.aurCache[pkg]; ok { if aurInfo, ok := f.aurClient.Get(pkg); ok {
info.InAUR = true info.InAUR = true
info.AURInfo = &aurInfo info.AURInfo = &aurInfo
continue 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()) fmt.Fprintf(os.Stderr, "[debug] fetch.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())
return result, nil 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
}

2
pkg/state/state.go → pkg/log/log.go

@ -1,4 +1,4 @@
package state package log
import ( import (
"fmt" "fmt"

294
pkg/pacman/pacman.go

@ -3,80 +3,34 @@ package pacman
import ( import (
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath"
"strings"
"time" "time"
"github.com/Riyyi/declpac/pkg/fetch" "github.com/Riyyi/declpac/pkg/fetch"
"github.com/Riyyi/declpac/pkg/log"
"github.com/Riyyi/declpac/pkg/output" "github.com/Riyyi/declpac/pkg/output"
"github.com/Riyyi/declpac/pkg/state" "github.com/Riyyi/declpac/pkg/pacman/read"
"github.com/Riyyi/declpac/pkg/validation" "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) { func Sync(packages []string) (*output.Result, error) {
start := time.Now() start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] Sync: starting...\n") fmt.Fprintf(os.Stderr, "[debug] Sync: starting...\n")
before, err := getInstalledCount() list, err := read.List()
if err != nil { if err != nil {
return nil, err return nil, err
} }
before := len(list)
if err := validation.CheckDBFreshness(); err != nil { fresh, err := read.DBFreshness()
if err != nil {
return nil, err 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()) fmt.Fprintf(os.Stderr, "[debug] Sync: database fresh (%.2fs)\n", time.Since(start).Seconds())
f, err := fetch.New() f, err := fetch.New()
@ -95,8 +49,7 @@ func Sync(packages []string) (*output.Result, error) {
if len(pacmanPkgs) > 0 { if len(pacmanPkgs) > 0 {
fmt.Fprintf(os.Stderr, "[debug] Sync: syncing %d pacman packages...\n", len(pacmanPkgs)) fmt.Fprintf(os.Stderr, "[debug] Sync: syncing %d pacman packages...\n", len(pacmanPkgs))
err = SyncPackages(pacmanPkgs) if err := sync.SyncPackages(pacmanPkgs, log.GetLogWriter()); err != nil {
if err != nil {
return nil, err return nil, err
} }
fmt.Fprintf(os.Stderr, "[debug] Sync: pacman packages synced (%.2fs)\n", time.Since(start).Seconds()) 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 { for _, pkg := range aurPkgs {
fmt.Fprintf(os.Stderr, "[debug] Sync: installing AUR package %s...\n", pkg) 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 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: AUR package %s installed (%.2fs)\n", pkg, time.Since(start).Seconds())
} }
fmt.Fprintf(os.Stderr, "[debug] Sync: marking all as deps...\n") fmt.Fprintf(os.Stderr, "[debug] Sync: marking all as deps...\n")
if err := MarkAllAsDeps(); err != nil { markAllAsDeps()
fmt.Fprintf(os.Stderr, "warning: could not mark all as deps: %v\n", err)
}
fmt.Fprintf(os.Stderr, "[debug] Sync: all marked as deps (%.2fs)\n", time.Since(start).Seconds()) 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") 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, "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()) 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 { if err != nil {
return nil, err return nil, err
} }
after, _ := getInstalledCount() list, _ = read.List()
if err != nil {
return nil, err
}
after := len(list)
installedCount := max(after-before, 0) installedCount := max(after-before, 0)
fmt.Fprintf(os.Stderr, "[debug] Sync: done (%.2fs)\n", time.Since(start).Seconds()) 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 return pacmanPkgs, aurPkgs, nil
} }
func InstallAUR(f *fetch.Fetcher, pkgName string) error { func markAllAsDeps() 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) {
start := time.Now() start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] getInstalledCount: starting...\n") fmt.Fprintf(os.Stderr, "[debug] markAllAsDeps: starting...\n")
cmd := exec.Command("pacman", "-Qq") packages, err := read.List()
output, err := cmd.Output() if err != nil || len(packages) == 0 {
if err != nil { return fmt.Errorf("failed to list packages: %w", err)
return 0, nil
}
count := strings.Count(string(output), "\n") + 1
if strings.TrimSpace(string(output)) == "" {
count = 0
} }
fmt.Fprintf(os.Stderr, "[debug] getInstalledCount: done (%.2fs)\n", time.Since(start).Seconds()) if err := sync.MarkAs(packages, "deps", log.GetLogWriter()); err != nil {
return count, nil log.Write([]byte(fmt.Sprintf("error: %v\n", err)))
} return err
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)
} }
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 return nil
} }
func CleanupOrphans() (int, error) { func cleanupOrphans() (int, error) {
start := time.Now() 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 { if err != nil {
log.Write([]byte(fmt.Sprintf("error: %v\n", err)))
return 0, 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") removed, err := sync.RemoveOrphans(orphans, log.GetLogWriter())
state.Write([]byte("pacman -Rns\n"))
removeCmd.Stdout = state.GetLogWriter()
removeCmd.Stderr = state.GetLogWriter()
err = removeCmd.Run()
if err != nil { if err != nil {
state.Write([]byte(fmt.Sprintf("cleanup orphans failed: %v\n", err))) log.Write([]byte(fmt.Sprintf("error: %v\n", err)))
return 0, fmt.Errorf("cleanup orphans failed: %v", err) return 0, 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
} }
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()) fmt.Fprintf(os.Stderr, "[debug] cleanupOrphans: done (%.2fs)\n", time.Since(start).Seconds())
return &output.Result{ return removed, nil
Installed: len(toInstall) + len(aurPkgs),
Removed: len(orphans),
ToInstall: append(toInstall, aurPkgs...),
ToRemove: orphans,
}, nil
} }

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

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

33
pkg/validation/validation.go

@ -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
}
Loading…
Cancel
Save