Compare commits

..
11 Commits
Author SHA1 Message Date
AI BotandRiyyi a934dfa1f0 Update make-commit skill with message format rules 2026-04-18 18:50:25 +02:00
AI BotandRiyyi e64382c1bd Fix issues with dry-run package reporting
- Filter orphans to only those not in requested packages
- Fix Removed count to reflect only actual removals
2026-04-18 18:30:45 +02:00
AI BotandRiyyi 83de6acc74 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.
2026-04-18 18:18:23 +02:00
Riyyi 88ff093365 Extract PacmanState constant 2026-04-18 16:16:13 +02:00
Riyyi c2a7b9ab9c Improve make-commit skill commit message formatting
- Fix typo 'thed' to 'the'
- Add 72-character title convention
- Change --stat flag formatting to be more standard
2026-04-18 15:39:43 +02:00
AI BotandRiyyi 86c6d078d6 Output actual commands run instead of generic progress messages 2026-04-18 15:35:22 +02:00
AI BotandRiyyi bd1108a93f Dont use CombinedOutput() to capture error separately 2026-04-18 15:07:56 +02:00
AI BotandRiyyi 82aeceacac Fixup: correct log path in add-operation-logging, fix pacman -Rns flag in declpac-cli-tool, update pkg path in fix-sync-dbs-notloaded 2026-04-18 14:27:47 +02:00
AI BotandRiyyi 86c1abaad4 Add opsx-fixup command for diverging change specs 2026-04-18 14:07:47 +02:00
AI BotandRiyyi 2b0cb39746 Update operation logging spec to use single timestamp per tool call 2026-04-18 13:44:48 +02:00
Riyyi fc18e27c25 Log timestamp once per tool invocation 2026-04-18 13:37:50 +02:00
17 changed files with 727 additions and 615 deletions
+28
View File
@@ -0,0 +1,28 @@
---
description: Fixup diverging archived openspec change artifacts
---
Fixup archived openspec change artifacts in @openspec/changes/archive/ that have diverged from the codebase.
For each archived change directory:
1. Read the .md artifact
2. Review related code to verify consistency
3. Fix any mismatches (status, fields, missing links, etc.)
Only run make-commit if changes were made, with a specific message like "Fixup: add missing X field to Y change".
---
**Steps**
1. **For each archived openspec change**
Read the change artifact.
Go through the related code.
Correct the change artifact where needed.
2. **Run make-commit if changes were made**
skill [name=make-commit] message: "Fixup: <specific fix>"
+51 -24
View File
@@ -7,13 +7,26 @@ description: >
license: GPL-3.0 license: GPL-3.0
metadata: metadata:
author: riyyi author: riyyi
version: "1.0" version: "1.1"
--- ---
Make a git commit, distinguishing between user and AI contributions. Make a git commit, distinguishing between user and AI contributions.
--- ---
**Commit message format**
A valid commit message consists of a required **title** and an optional **body**:
Rules:
- **Title** (required): max 72 characters, starts with a capital letter —
unless referring to a tool/project that explicitly uses lowercase (e.g.,
"go", "npm", "rustc"). No trailing period.
- **Body** (optional): any further elaboration. Each line max 72 characters.
Wrap manually — do not rely on the terminal to wrap.
---
**Steps** **Steps**
1. **[REQUIRED] Ask user if this commit is by them or by AI** 1. **[REQUIRED] Ask user if this commit is by them or by AI**
@@ -25,49 +38,63 @@ Make a git commit, distinguishing between user and AI contributions.
- "By me" - User made the commit - "By me" - User made the commit
- "By AI" - AI made the commit - "By AI" - AI made the commit
2. **Check for commit message** 2. **Compose the commit message**
**Capitalization rule**: Commit messages should start with a capital letter, If the user did NOT provide a commit message, generate one from staged
unless it refers to a tool or project that explicitly uses lowercase as its changes:
name (e.g., "go", "npm", "rustc").
If the user did NOT provide a commit message, generate one from staged changes:
```bash ```bash
git diff --staged --stat git --no-pager diff --staged
``` ```
Create a reasonable commit message based on the changes. Write a commit message following the format above.
If thed user DID provide a message, format it into a proper commit message. If the user **DID** provide a message, treat it as raw input and apply the
format rules to it.
3. **Show commit message and confirm** 3. **Validate the commit message**
Before presenting the message to the user, check it against every rule:
- [ ] Title is present and non-empty
- [ ] Title is at most 72 characters
- [ ] Title starts with a capital letter (or an intentionally lowercase name)
- [ ] Title has no trailing period
- [ ] Every line in the body is at most 72 characters
Fix any violations silently before showing the message to the user.
4. **Show commit message and confirm**
Use the **question tool** to ask: Use the **question tool** to ask:
> "Is this commit message okay, or would you like to make tweaks?" > "Is this commit message okay, or would you like to make tweaks?"
> ```
> <message> > <message>
> ```
Options: Options:
- "Looks good" - Proceed with this message - "Looks good" - Proceed with this message
- "Make tweaks" - User will provide a new message - "Make tweaks" - User will provide a new message or describe changes
**If user wants tweaks**: Ask them for the new commit message. **If user wants tweaks**: apply the same validation (step 3) to the revised
message before committing.
4. **Make the commit** 5. **Make the commit**
Use the commit message provided by the user. For a title-only message:
**If by user:**
```bash ```bash
git commit -m "<message>" git commit -m "<title>"
``` ```
(Uses git config user as both committer and author)
**If by AI:** For a message with a body, pass `-m` twice (git inserts the blank line):
```bash ```bash
git commit -m "<message>" --author="AI Bot <ai@local>" git commit -m "<title>" -m "<body>"
```
Append `--author="AI Bot <ai@local>"` when the commit is by AI:
```bash
git commit -m "<title>" [-m "<body>"] --author="AI Bot <ai@local>"
``` ```
(Uses git config for committer, but sets author to AI Bot)
**Output** **Output**
- Tell user the commit was made - Tell user the commit was made.
- If AI commit, mention that the author was set to "AI Bot <ai@local>" - If AI commit, mention that the author was set to "AI Bot <ai@local>".
+8 -5
View File
@@ -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
+5 -10
View File
@@ -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 {
@@ -19,47 +19,27 @@
| Pattern | Functions | How | | Pattern | Functions | How |
|---------|------------|-----| |---------|------------|-----|
| Streaming | MarkAllAsDeps, MarkAsExplicit, InstallAUR | `io.MultiWriter` to tee to both terminal and log | | Captured | All state-modifying functions | capture output, write to log with single timestamp at start, write to terminal |
| Captured | SyncPackages, CleanupOrphans | capture with `CombinedOutput()`, write to log, write to terminal |
### One Timestamp Per Tool Call
Instead of streaming with MultiWriter (multiple timestamps), each state-modifying function:
1. Writes timestamp + operation name to log
2. Runs command, captures output
3. Writes captured output to log
4. Writes output to terminal
This ensures exactly 1 timestamp print per tool call.
### Error Handling ### Error Handling
- Write error to log BEFORE returning from function - Write error to log BEFORE returning from function
- Print error to stderr so user sees it - Print error to stderr so user sees it
### Dependencies ### Dependencies
- Add to imports: `io`, `os`, `path/filepath` - Add to imports: `os`, `path/filepath`
### Structure
```go
// pkg/state/state.go
var logFile *os.File
func OpenLog() error {
logPath := filepath.Join("/var/log", "declpac.log")
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
logFile = f
return nil
}
func GetLogWriter() io.Writer {
return logFile
}
func Close() error {
if logFile == nil {
return nil
}
return logFile.Close()
}
```
### Flow in Sync() or main entrypoint ### Flow in Sync() or main entrypoint
1. Call `OpenLog()` at program start, defer close 1. Call `OpenLog()` at program start, defer close
2. Each state-modifying function uses `state.GetLogWriter()` via MultiWriter 2. Each state-modifying function calls `state.Write()` with timestamp prefix
### Wire into main.go ### Wire into main.go
- Open log at start of `run()` - Open log at start of `run()`
@@ -7,7 +7,7 @@ debugging and audit.
## What Changes ## What Changes
- Add state directory initialization creating `~/.local/state/declpac` if not exists - Add state directory initialization creating `~/.local/state/declpac` if not exists
- Open/manage a single log file at `$XDG_STATE_HOME/declpac` (e.g., `~/.local/state/declpac/declpac`) - Open/manage a single log file at `/var/log/declpac.log`
- Instrument all state-modifying exec calls in `pkg/pacman/pacman.go` to tee or append output to this file - Instrument all state-modifying exec calls in `pkg/pacman/pacman.go` to tee or append output to this file
- Skip debug messages (internal timing logs) - Skip debug messages (internal timing logs)
- Capture and write errors before returning - Capture and write errors before returning
@@ -18,15 +18,11 @@ In `cmd/declpac/main.go` `run()`:
Modify `pkg/pacman/pacman.go`: Modify `pkg/pacman/pacman.go`:
All state-modifying functions use `state.Write()` instead of `state.GetLogWriter().Write()`: Each state-modifying function writes timestamp ONCE at start, then captures output:
- Write `timestamp - operation name` to log
``` - Run command, capture output
// OLD - Write captured output to log
state.GetLogWriter().Write(output) - Write output to terminal
// NEW
state.Write(output) // auto-prepends timestamp
```
**Functions updated:** **Functions updated:**
- `SyncPackages()` - write output with timestamp - `SyncPackages()` - write output with timestamp
@@ -47,7 +47,7 @@
- [x] 6.1 Get list of currently installed packages before sync - [x] 6.1 Get list of currently installed packages before sync
- [x] 6.2 Mark declared state packages as explicitly installed via pacman -D --explicit - [x] 6.2 Mark declared state packages as explicitly installed via pacman -D --explicit
- [x] 6.3 Run pacman sync operation (5.x series) - [x] 6.3 Run pacman sync operation (5.x series)
- [x] 6.4 Run pacman -Rsu to remove orphaned packages - [x] 6.4 Run pacman -Rns to remove orphaned packages
- [x] 6.5 Capture and report number of packages removed - [x] 6.5 Capture and report number of packages removed
- [x] 6.6 Handle case where no orphans exist (no packages removed) - [x] 6.6 Handle case where no orphans exist (no packages removed)
@@ -18,4 +18,4 @@ The program fails to find packages that exist in official repositories (like `cm
## Impact ## Impact
- `pkg/pacman/pacman.go`: Modify `New()` function to register sync DBs after getting them from the handle - `pkg/fetch/fetch.go`: Modify `New()` function to register sync DBs after getting them from the handle
+137
View File
@@ -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
View File
@@ -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
}
+28 -237
View File
@@ -1,104 +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"
) )
var (
Root = "/"
LockFile = "/var/lib/pacman/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 { type Fetcher struct {
Results []AURPackage `json:"results"` alpmHandle *alpm.Handle
} aurClient *aur.Client
type AURPackage struct {
Name string `json:"Name"`
PackageBase string `json:"PackageBase"`
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, "/var/lib/pacman") alpmHandle, err := alpm.New()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to initialize alpm: %w", err) return nil, err
} }
localDB, err := handle.LocalDB() aurClient := aur.New()
if err != nil {
handle.Release()
return nil, fmt.Errorf("failed to get local database: %w", err)
}
syncDBs, err := handle.SyncDBs() fmt.Fprintf(os.Stderr, "[debug] fetch.Fetcher New: done (%.2fs)\n", time.Since(start).Seconds())
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] 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
} }
@@ -109,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 {
@@ -224,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]
@@ -232,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
@@ -249,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 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
}
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
}
+11 -5
View File
@@ -1,4 +1,4 @@
package state package log
import ( import (
"fmt" "fmt"
@@ -8,8 +8,12 @@ import (
"time" "time"
) )
// -----------------------------------------
var logFile *os.File var logFile *os.File
// -----------------------------------------
func OpenLog() error { func OpenLog() error {
logPath := filepath.Join("/var/log", "declpac.log") logPath := filepath.Join("/var/log", "declpac.log")
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
@@ -17,6 +21,7 @@ func OpenLog() error {
return err return err
} }
logFile = f logFile = f
writeTimestamp()
return nil return nil
} }
@@ -25,7 +30,7 @@ func GetLogWriter() io.Writer {
} }
func Write(msg []byte) { func Write(msg []byte) {
PrependWithTimestamp(logFile, msg) logFile.Write(msg)
} }
func Close() error { func Close() error {
@@ -35,9 +40,10 @@ func Close() error {
return logFile.Close() return logFile.Close()
} }
func PrependWithTimestamp(w io.Writer, msg []byte) { // -----------------------------------------
func writeTimestamp() {
ts := time.Now().Format("2006-01-02 15:04:05") ts := time.Now().Format("2006-01-02 15:04:05")
header := fmt.Sprintf("\n--- %s ---\n", ts) header := fmt.Sprintf("\n--- %s ---\n", ts)
w.Write([]byte(header)) logFile.Write([]byte(header))
w.Write(msg)
} }
+45 -257
View File
@@ -3,81 +3,34 @@ package pacman
import ( import (
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath"
"regexp"
"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("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) { 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()
@@ -96,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())
@@ -105,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())
@@ -164,211 +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() start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] InstallAUR: starting...\n") fmt.Fprintf(os.Stderr, "[debug] markAllAsDeps: starting...\n")
aurInfo, ok := f.GetAURPackage(pkgName) packages, err := read.List()
if !ok { if err != nil || len(packages) == 0 {
return fmt.Errorf("AUR package not found in cache: %s", pkgName) return fmt.Errorf("failed to list packages: %w", err)
} }
sudoUser := os.Getenv("SUDO_USER") if err := sync.MarkAs(packages, "deps", log.GetLogWriter()); err != nil {
if sudoUser == "" { log.Write([]byte(fmt.Sprintf("error: %v\n", err)))
sudoUser = os.Getenv("USER") return err
if sudoUser == "" {
sudoUser = "root"
}
} }
tmpDir := "/tmp/declpac-aur-" + pkgName fmt.Fprintf(os.Stderr, "[debug] markAllAsDeps: done (%.2fs)\n", time.Since(start).Seconds())
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 return nil
} }
func findPKGFile(dir string) (string, error) { func cleanupOrphans() (int, 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] cleanupOrphans: starting...\n")
cmd := exec.Command("pacman", "-Qq") orphans, err := read.ListOrphans()
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 { 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() removed, err := sync.RemoveOrphans(orphans, log.GetLogWriter())
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 { if err != nil {
errMsg := fmt.Sprintf("%s: %s", err, output) log.Write([]byte(fmt.Sprintf("error: %v\n", err)))
state.Write([]byte(errMsg)) return 0, err
return 0, fmt.Errorf("%s: %s", err, output)
} }
if len(output) > 0 { fmt.Fprintf(os.Stderr, "[debug] cleanupOrphans: done (%.2fs)\n", time.Since(start).Seconds())
state.Write(output) return removed, nil
}
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
} }
+120
View File
@@ -0,0 +1,120 @@
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 && !info.Installed {
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())
pkgSet := make(map[string]bool)
for _, p := range packages {
pkgSet[p] = true
}
var toRemove []string
for _, o := range orphans {
if !pkgSet[o] {
toRemove = append(toRemove, o)
}
}
fmt.Fprintf(os.Stderr, "[debug] DryRun: done (%.2fs)\n", time.Since(start).Seconds())
return &output.Result{
Installed: len(toInstall) + len(aurPkgs),
Removed: len(toRemove),
ToInstall: append(toInstall, aurPkgs...),
ToRemove: toRemove,
}, nil
}
+180
View File
@@ -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
View File
@@ -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
}