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.
 
 

374 lines
11 KiB

package pacman
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/Riyyi/declpac/pkg/fetch"
"github.com/Riyyi/declpac/pkg/output"
"github.com/Riyyi/declpac/pkg/state"
"github.com/Riyyi/declpac/pkg/validation"
)
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("MarkAllAsDeps...\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("MarkAsExplicit...\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()
if err != nil {
return nil, err
}
if err := validation.CheckDBFreshness(); err != nil {
return nil, err
}
fmt.Fprintf(os.Stderr, "[debug] Sync: database fresh (%.2fs)\n", time.Since(start).Seconds())
f, err := fetch.New()
if err != nil {
return nil, err
}
defer f.Close()
fmt.Fprintf(os.Stderr, "[debug] Sync: initialized fetcher (%.2fs)\n", time.Since(start).Seconds())
fmt.Fprintf(os.Stderr, "[debug] Sync: categorizing packages...\n")
pacmanPkgs, aurPkgs, err := categorizePackages(f, 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 = 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 := InstallAUR(f, 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 := 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 := 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 := 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 categorizePackages(f *fetch.Fetcher, packages []string) (pacmanPkgs, aurPkgs []string, err error) {
start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] categorizePackages: starting...\n")
resolved, err := f.Resolve(packages)
if err != nil {
return nil, nil, err
}
for _, pkg := range packages {
info := resolved[pkg]
if info == nil || (!info.Exists && !info.InAUR) {
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 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("Cloning " + cloneURL + "\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("Building package...\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("Installing package...\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()
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 SyncPackages(packages []string) (int, error) {
start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] SyncPackages: starting...\n")
args := append([]string{"-S", "--needed"}, packages...)
cmd := exec.Command("pacman", args...)
output, err := cmd.CombinedOutput()
if err != nil {
errMsg := fmt.Sprintf("pacman sync failed: %s", output)
state.Write([]byte(errMsg))
return 0, fmt.Errorf("pacman sync failed: %s", output)
}
if len(output) > 0 {
state.Write(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 CleanupOrphans() (int, error) {
start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] CleanupOrphans: starting...\n")
f, err := fetch.New()
if err != nil {
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")
output, err := removeCmd.CombinedOutput()
if err != nil {
errMsg := fmt.Sprintf("%s: %s", err, output)
state.Write([]byte(errMsg))
return 0, fmt.Errorf("%s: %s", err, output)
}
if len(output) > 0 {
state.Write(output)
}
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())
return &output.Result{
Installed: len(toInstall) + len(aurPkgs),
Removed: len(orphans),
ToInstall: append(toInstall, aurPkgs...),
ToRemove: orphans,
}, nil
}