Sync a declarative package list with the pacman package manager
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

609 lines
16 KiB

package pacman
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"regexp"
"strings"
"time"
"github.com/Jguer/dyalpm"
"github.com/Riyyi/declpac/pkg/output"
)
var (
Root = "/"
LockFile = "/var/lib/pacman/db.lock"
AURInfoURL = "https://aur.archlinux.org/rpc?v=5&type=info"
)
type Pac struct {
aurCache map[string]AURPackage
handle dyalpm.Handle
localDB dyalpm.Database
syncDBs []dyalpm.Database
}
func New() (*Pac, error) {
start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] New: starting...\n")
handle, err := dyalpm.Initialize(Root, "/var/lib/pacman")
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)
}
fmt.Fprintf(os.Stderr, "[debug] New: done (%.2fs)\n", time.Since(start).Seconds())
return &Pac{
aurCache: make(map[string]AURPackage),
handle: handle,
localDB: localDB,
syncDBs: syncDBs,
}, nil
}
func (p *Pac) Close() error {
if p.handle != nil {
p.handle.Release()
}
return nil
}
type PackageInfo struct {
Name string
InAUR bool
Exists bool
Installed bool
AURInfo *AURPackage
syncPkg dyalpm.Package
}
func (p *Pac) buildLocalPkgMap() (map[string]dyalpm.Package, error) {
start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] buildLocalPkgMap: starting...\n")
localPkgs := make(map[string]dyalpm.Package)
err := p.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 (p *Pac) 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 p.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 (p *Pac) resolvePackages(packages []string) (map[string]*PackageInfo, error) {
start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] resolvePackages: starting...\n")
result := make(map[string]*PackageInfo)
localPkgs, err := p.buildLocalPkgMap()
if err != nil {
return nil, err
}
fmt.Fprintf(os.Stderr, "[debug] resolvePackages: local pkgs built (%.2fs)\n", time.Since(start).Seconds())
var notInLocal []string
for _, pkg := range packages {
if localPkg, ok := localPkgs[pkg]; ok {
result[pkg] = &PackageInfo{
Name: pkg,
Exists: true,
InAUR: false,
Installed: true,
syncPkg: localPkg,
}
} else {
notInLocal = append(notInLocal, pkg)
}
}
if len(notInLocal) > 0 {
syncPkgs, err := p.checkSyncDBs(notInLocal)
if err != nil {
return nil, err
}
fmt.Fprintf(os.Stderr, "[debug] resolvePackages: sync db checked (%.2fs)\n", time.Since(start).Seconds())
var notInSync []string
for _, pkg := range notInLocal {
if syncPkg, ok := syncPkgs[pkg]; ok {
result[pkg] = &PackageInfo{
Name: pkg,
Exists: true,
InAUR: false,
Installed: false,
syncPkg: syncPkg,
}
} else {
notInSync = append(notInSync, pkg)
}
}
if len(notInSync) > 0 {
p.ensureAURCache(notInSync)
fmt.Fprintf(os.Stderr, "[debug] resolvePackages: AUR cache ensured (%.2fs)\n", time.Since(start).Seconds())
var unfound []string
for _, pkg := range notInSync {
if aurInfo, ok := p.aurCache[pkg]; ok {
result[pkg] = &PackageInfo{
Name: pkg,
Exists: true,
InAUR: true,
Installed: false,
AURInfo: &aurInfo,
}
} else {
unfound = append(unfound, pkg)
}
}
if len(unfound) > 0 {
return nil, fmt.Errorf("package(s) not found: %s", strings.Join(unfound, ", "))
}
}
}
fmt.Fprintf(os.Stderr, "[debug] resolvePackages: done (%.2fs)\n", time.Since(start).Seconds())
return result, nil
}
type AURResponse struct {
Results []AURPackage `json:"results"`
}
type AURPackage struct {
Name string `json:"Name"`
PackageBase string `json:"PackageBase"`
Version string `json:"Version"`
URL string `json:"URL"`
}
func (p *Pac) IsDBFresh() (bool, error) {
start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] IsDBFresh: starting...\n")
info, err := os.Stat(LockFile)
if err != nil {
return false, nil
}
age := time.Since(info.ModTime())
fmt.Fprintf(os.Stderr, "[debug] IsDBFresh: done (%.2fs)\n", time.Since(start).Seconds())
return age < 24*time.Hour, nil
}
func (p *Pac) SyncDB() error {
start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] SyncDB: starting...\n")
cmd := exec.Command("pacman", "-Syy")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
fmt.Fprintf(os.Stderr, "[debug] SyncDB: done (%.2fs)\n", time.Since(start).Seconds())
return err
}
func (p *Pac) MarkAllAsDeps() error {
start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] MarkAllAsDeps: starting...\n")
cmd := exec.Command("pacman", "-D", "--asdeps")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
fmt.Fprintf(os.Stderr, "[debug] MarkAllAsDeps: done (%.2fs)\n", time.Since(start).Seconds())
return err
}
func (p *Pac) 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...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
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()
if err != nil {
return nil, err
}
p, err := New()
if err != nil {
return nil, err
}
defer p.Close()
fmt.Fprintf(os.Stderr, "[debug] Sync: initialized pacman (%.2fs)\n", time.Since(start).Seconds())
fresh, err := p.IsDBFresh()
if err != nil || !fresh {
fmt.Fprintf(os.Stderr, "[debug] Sync: syncing database...\n")
if err := p.SyncDB(); err != nil {
return nil, fmt.Errorf("failed to sync database: %w", err)
}
fmt.Fprintf(os.Stderr, "[debug] Sync: database synced (%.2fs)\n", time.Since(start).Seconds())
}
fmt.Fprintf(os.Stderr, "[debug] Sync: categorizing packages...\n")
pacmanPkgs, aurPkgs, err := p.categorizePackages(packages)
if err != nil {
return nil, err
}
fmt.Fprintf(os.Stderr, "[debug] Sync: packages categorized (%.2fs)\n", time.Since(start).Seconds())
if len(pacmanPkgs) > 0 {
fmt.Fprintf(os.Stderr, "[debug] Sync: syncing %d pacman packages...\n", len(pacmanPkgs))
_, err = p.SyncPackages(pacmanPkgs)
if err != nil {
return nil, err
}
fmt.Fprintf(os.Stderr, "[debug] Sync: pacman packages synced (%.2fs)\n", time.Since(start).Seconds())
}
for _, pkg := range aurPkgs {
fmt.Fprintf(os.Stderr, "[debug] Sync: installing AUR package %s...\n", pkg)
if err := p.InstallAUR(pkg); 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 := p.MarkAllAsDeps(); err != nil {
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: marking state packages as explicit...\n")
if err := p.MarkAsExplicit(packages); 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 := p.CleanupOrphans()
if err != nil {
return nil, err
}
after, _ := getInstalledCount()
installedCount := max(after-before, 0)
fmt.Fprintf(os.Stderr, "[debug] Sync: done (%.2fs)\n", time.Since(start).Seconds())
return &output.Result{
Installed: installedCount,
Removed: removed,
}, nil
}
func (p *Pac) categorizePackages(packages []string) (pacmanPkgs, aurPkgs []string, err error) {
start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] categorizePackages: starting...\n")
resolved, err := p.resolvePackages(packages)
if err != nil {
return nil, nil, err
}
for _, pkg := range packages {
info := resolved[pkg]
if info == nil || !info.Exists {
fmt.Fprintf(os.Stderr, "error: package not found: %s\n", pkg)
continue
}
if info.InAUR {
aurPkgs = append(aurPkgs, pkg)
} else {
pacmanPkgs = append(pacmanPkgs, pkg)
}
}
fmt.Fprintf(os.Stderr, "[debug] categorizePackages: done (%.2fs)\n", time.Since(start).Seconds())
return pacmanPkgs, aurPkgs, nil
}
func (p *Pac) 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 := p.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
}
p.fetchAURInfo(uncached)
fmt.Fprintf(os.Stderr, "[debug] ensureAURCache: done (%.2fs)\n", time.Since(start).Seconds())
}
func (p *Pac) fetchAURInfo(packages []string) map[string]AURPackage {
start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] fetchAURInfo: starting...\n")
result := make(map[string]AURPackage)
if len(packages) == 0 {
return result
}
v := url.Values{}
for _, pkg := range packages {
v.Add("arg[]", pkg)
}
resp, err := http.Get(AURInfoURL + "&" + v.Encode())
if err != nil {
return result
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return result
}
var aurResp AURResponse
if err := json.Unmarshal(body, &aurResp); err != nil {
return result
}
for _, r := range aurResp.Results {
p.aurCache[r.Name] = r
result[r.Name] = r
}
fmt.Fprintf(os.Stderr, "[debug] fetchAURInfo: done (%.2fs)\n", time.Since(start).Seconds())
return result
}
func (p *Pac) InstallAUR(pkgName string) error {
start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] InstallAUR: starting...\n")
aurInfo, ok := p.aurCache[pkgName]
if !ok {
return fmt.Errorf("AUR package not found in cache: %s", pkgName)
}
tmpDir, err := os.MkdirTemp("", "declpac-aur-")
if 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("git", "clone", cloneURL, tmpDir)
cloneCmd.Stdout = os.Stdout
cloneCmd.Stderr = os.Stderr
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("makepkg", "-si", "--noconfirm")
makepkgCmd.Stdout = os.Stdout
makepkgCmd.Stderr = os.Stderr
makepkgCmd.Dir = tmpDir
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())
fmt.Fprintf(os.Stderr, "[debug] InstallAUR: done (%.2fs)\n", time.Since(start).Seconds())
return nil
}
func getInstalledCount() (int, error) {
start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] getInstalledCount: 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
}
fmt.Fprintf(os.Stderr, "[debug] getInstalledCount: done (%.2fs)\n", time.Since(start).Seconds())
return count, nil
}
func (p *Pac) SyncPackages(packages []string) (int, error) {
start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] SyncPackages: starting...\n")
args := append([]string{"-Syu"}, packages...)
cmd := exec.Command("pacman", args...)
output, err := cmd.CombinedOutput()
if err != nil {
return 0, fmt.Errorf("pacman sync failed: %s", output)
}
re := regexp.MustCompile(`upgrading (\S+)`)
matches := re.FindAllStringSubmatch(string(output), -1)
fmt.Fprintf(os.Stderr, "[debug] SyncPackages: done (%.2fs)\n", time.Since(start).Seconds())
return len(matches), nil
}
func (p *Pac) CleanupOrphans() (int, error) {
start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] CleanupOrphans: starting...\n")
listCmd := exec.Command("pacman", "-Qdtq")
orphans, err := listCmd.Output()
if err != nil {
return 0, nil
}
orphanList := strings.TrimSpace(string(orphans))
if orphanList == "" {
fmt.Fprintf(os.Stderr, "[debug] CleanupOrphans: done (%.2fs)\n", time.Since(start).Seconds())
return 0, nil
}
removeCmd := exec.Command("pacman", "-Rns")
output, err := removeCmd.CombinedOutput()
if err != nil {
return 0, fmt.Errorf("%s: %s", err, output)
}
count := strings.Count(orphanList, "\n") + 1
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")
p, err := New()
if err != nil {
return nil, err
}
defer p.Close()
fmt.Fprintf(os.Stderr, "[debug] DryRun: initialized pacman (%.2fs)\n", time.Since(start).Seconds())
resolved, err := p.resolvePackages(packages)
if err != nil {
return nil, err
}
fmt.Fprintf(os.Stderr, "[debug] DryRun: packages resolved (%.2fs)\n", time.Since(start).Seconds())
localPkgs, err := p.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 {
return nil, fmt.Errorf("package not found: %s", pkg)
}
if _, installed := localPkgs[pkg]; !installed {
if info.InAUR {
aurPkgs = append(aurPkgs, pkg)
} else {
toInstall = append(toInstall, pkg)
}
}
}
fmt.Fprintf(os.Stderr, "[debug] DryRun: packages categorized (%.2fs)\n", time.Since(start).Seconds())
orphans, err := p.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
}
func (p *Pac) 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
}