Compare commits

...

7 Commits

  1. 2
      .gitignore
  2. 2
      .opencode/commands/commit.md
  3. 24
      .opencode/skills/make-commit/SKILL.md
  4. 188
      README.md
  5. 13
      cmd/declpac/main.go
  6. 2
      openspec/changes/add-operation-logging/.openspec.yaml
  7. 66
      openspec/changes/add-operation-logging/design.md
  8. 26
      openspec/changes/add-operation-logging/proposal.md
  9. 45
      openspec/changes/add-operation-logging/tasks.md
  10. 57
      pkg/fetch/fetch.go
  11. 50
      pkg/pacman/pacman.go
  12. 43
      pkg/state/state.go

2
.gitignore vendored

@ -32,4 +32,4 @@ go.work.sum
# .vscode/ # .vscode/
# Binary output # Binary output
declpac /declpac

2
.opencode/commands/commit.md

@ -2,4 +2,4 @@
description: Make a git commit, asking if it was by the user or AI description: Make a git commit, asking if it was by the user or AI
--- ---
skill [name=make-commit] $1 skill [name=make-commit] message: $1

24
.opencode/skills/make-commit/SKILL.md

@ -16,7 +16,7 @@ Make a git commit, distinguishing between user and AI contributions.
**Steps** **Steps**
1. **Ask user if this commit is by them or by AI** 1. **[REQUIRED] Ask user if this commit is by them or by AI**
Use the **question tool** to ask: Use the **question tool** to ask:
> "Was this commit made by you or by AI?" > "Was this commit made by you or by AI?"
@ -27,22 +27,23 @@ Make a git commit, distinguishing between user and AI contributions.
2. **Check for commit message** 2. **Check for commit message**
**Capitalization rule**: Commit messages should start with a capital letter,
unless it refers to a tool or project that explicitly uses lowercase as its
name (e.g., "go", "npm", "rustc").
If the user did NOT provide a commit message, generate one from staged changes: If the user did NOT provide a commit message, generate one from staged changes:
```bash ```bash
git diff --staged --stat git diff --staged --stat
``` ```
Create a reasonable commit message based on the changes. Create a reasonable commit message based on the changes.
**Capitalization rule**: Commit message should start with a capital letter, If thed user DID provide a message, format it into a proper commit message.
unless it refers to a tool or project that explicitly uses lowercase as its
name (e.g., "go", "npm", "rustc").
3. **Show commit message and confirm** 3. **Show commit message and confirm**
Display the commit message to the user.
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>
Options: Options:
- "Looks good" - Proceed with this message - "Looks good" - Proceed with this message
@ -50,14 +51,7 @@ Make a git commit, distinguishing between user and AI contributions.
**If user wants tweaks**: Ask them for the new commit message. **If user wants tweaks**: Ask them for the new commit message.
4. **Get git user config** 4. **Make the commit**
```bash
git config user.name
git config user.email
```
5. **Make the commit**
Use the commit message provided by the user. Use the commit message provided by the user.
@ -69,7 +63,7 @@ Make a git commit, distinguishing between user and AI contributions.
**If by AI:** **If by AI:**
```bash ```bash
git -c user.name="<git-config-name>" -c user.email="<git-config-email>" commit -m "<message>" --author="AI Bot <ai@local>" git commit -m "<message>" --author="AI Bot <ai@local>"
``` ```
(Uses git config for committer, but sets author to AI Bot) (Uses git config for committer, but sets author to AI Bot)

188
README.md

@ -1,18 +1,14 @@
# declpac # declpac
`declpac` is a declarative package manager for Arch Linux that syncs your system Declarative package manager for Arch Linux that syncs your system with a declared package list using pacman.
with a declared package list using `pacman`. It ensures your system matches your
desired state, handling package installation, upgrades, and orphan cleanup
automatically.
## Features ## Features
- **Declarative state management** — Define your desired package list in files or stdin - Declarative state management — define your desired package list in files or stdin
- **Automatic dependency resolution** — Pacman handles transitive dependencies - Smart orphan cleanup — removes packages no longer needed
- **Smart orphan cleanup** — Removes packages no longer needed - Explicit package tracking — marks declared packages as explicit
- **Explicit package tracking** — Marks your declared packages as explicit - AUR support — builds and installs AUR packages automatically
- **AUR support** — Falls back to AUR for packages not in official repos - Machine-readable output — perfect for scripting
- **Machine-readable output** — Perfect for scripting and automation
## Installation ## Installation
@ -28,14 +24,13 @@ sudo mv declpac /usr/local/bin/
### Dependencies ### Dependencies
- Go 1.21+ - Go 1.21+
- pacman (system package manager) - pacman
- aur (AUR helper, optional for AUR support) - makepkg (for AUR support)
- Root privileges (required for pacman operations) - git (for AUR support)
- Root privileges
## Usage ## Usage
### Basic Usage
```bash ```bash
# Single state file # Single state file
sudo declpac --state packages.txt sudo declpac --state packages.txt
@ -45,119 +40,63 @@ sudo declpac --state base.txt --state apps.txt
# From stdin # From stdin
cat packages.txt | sudo declpac cat packages.txt | sudo declpac
# Preview changes without applying
sudo declpac --dry-run --state packages.txt
``` ```
### State File Format ### State File Format
State files contain one package name per line: One package name per line, lines beginning with `#` are comments:
``` ```
bash bash
vim vim
git git
docker docker
# this is a comment
``` ```
Lines are treated as package names with whitespace trimmed: ### Options
```
bash # bash
vim # vim
# comment # ignored
```
### Command Line Options
| Flag | Alias | Description | | Flag | Alias | Description |
|------|-------|-------------| |------|-------|-------------|
| `--state` | `-s` | State file(s) to read package list from (can be used multiple times) | | `--state` | `-s` | State file to read package list from (can be used multiple times) |
| `--yes` | `-y` | Skip confirmation prompts (for scripting) | | `--dry-run` | | Preview changes without applying them |
| `--dry-run` | | Simulate sync without making changes |
| `--help` | `-h` | Show help message | | `--help` | `-h` | Show help message |
### Examples
#### Minimal System
```bash
# Create a minimal system package list
echo -e "base\nbase-devel\nlinux-headers\nvim\ngit\ncurl\nwget" > ~/.config/declpac/minimal.txt
# Apply the state
sudo declpac --state ~/.config/declpac/minimal.txt
```
#### Development Environment
```bash
# development.txt
go
nodejs
python
rust
docker
docker-compose
kubectl
helm
terraform
# Apply
sudo declpac --state development.txt
```
#### Full System Sync
```bash
# Combine multiple files
sudo declpac --state ~/.config/declpac/base.txt --state ~/.config/declpac/desktop.txt
# Or use stdin
cat ~/.config/declpac/full-system.txt | sudo declpac
```
#### Dry-Run Preview
```bash
# Preview what would happen without making changes
sudo declpac --dry-run --state packages.txt
# Example output:
# Installed 3 packages, removed 2 packages
# Would install: vim, git, docker
# Would remove: python2, perl-xml-parser
```
## How It Works ## How It Works
1. **Collect packages** — Reads from all `--state` files and stdin 1. **Read** — Collect packages from all state files and stdin
2. **Merge** — Combines all packages additively (duplicates allowed) 2. **Merge** — Combine into single package list
3. **Validate** — Checks packages exist in repos or AUR 3. **Categorize** — Check if packages are in official repos or AUR
4. **Mark explicit** — Marks declared packages as explicit dependencies 4. **Sync** — Install/update packages via pacman
5. **Sync** — Runs `pacman -Syu` to install/upgrade packages 5. **Build** — Build and install AUR packages via makepkg
6. **Cleanup** — Removes orphaned packages with `pacman -Rns` 6. **Mark** — Mark declared packages as explicit, all others as dependencies
7. **Report** — Outputs summary: `Installed X packages, removed Y packages` 7. **Cleanup** — Remove orphaned packages
### Database Freshness ### Database Freshness
If the pacman database is older than 24 hours, `declpac` automatically refreshes it with `pacman -Syy` before validation. If the pacman database is older than 24 hours, it is automatically refreshed.
### Orphan Cleanup
After syncing, `declpac` identifies and removes packages that are: ### Logging
- Not explicitly installed
- Not required by any other package
This keeps your system clean from dependency artifacts. Operations are logged to `/var/log/declpac.log`.
## Output Format ## Output
``` ```
# Success (packages installed/removed) # Packages installed/removed
Installed 5 packages, removed 2 packages Installed 5 packages, removed 2 packages
# Success (no changes) # No changes needed
Installed 0 packages, removed 0 packages Installed 0 packages, removed 0 packages
# Dry-run preview
Installed 3 packages, removed 1 packages
Would install: vim, git, docker
Would remove: python2
# Error # Error
error: package not found: <package-name> error: package not found: <package-name>
``` ```
@ -167,36 +106,54 @@ error: package not found: <package-name>
| Code | Meaning | | Code | Meaning |
|------|---------| |------|---------|
| 0 | Success | | 0 | Success |
| 1 | Error (no packages, validation failure, pacman error) | | 1 | Error |
## Security Considerations ## Examples
- **Run as root**`declpac` requires root privileges for pacman operations ### Minimal System
- **Review state files** — Only install packages from trusted sources
- **Backup** — Consider backing up your system before major changes
## Troubleshooting ```bash
echo -e "base\nbase-devel\nlinux-headers\nvim\ngit\ncurl" > ~/.config/declpac/minimal.txt
sudo declpac --state ~/.config/declpac/minimal.txt
```
### "Permission denied" ### Development Environment
`declpac` requires root privileges. Use `sudo`: ```bash
# development.txt
go
nodejs
python
rust
docker
sudo declpac --state development.txt
```
### Dry-Run
```bash ```bash
sudo declpac --state packages.txt sudo declpac --dry-run --state packages.txt
``` ```
### "Package not found" ## Troubleshooting
The package doesn't exist in pacman repos or AUR. Check the package name: ### Permission denied
Use sudo:
```bash
sudo declpac --state packages.txt
```
### Package not found
Check if the package exists:
```bash ```bash
pacman -Ss <package> pacman -Ss <package>
``` ```
### Database sync fails ### Database sync fails
Refresh manually:
```bash ```bash
sudo pacman -Syy sudo pacman -Syy
``` ```
@ -210,11 +167,12 @@ declpac/
├── pkg/ ├── pkg/
│ ├── input/ # State file/stdin reading │ ├── input/ # State file/stdin reading
│ ├── merge/ # Package merging │ ├── merge/ # Package merging
│ ├── validation/ # Package validation │ ├── fetch/ # Package resolution (pacman/AUR)
│ ├── pacman/ # Pacman integration │ ├── pacman/ # Pacman operations
│ └── output/ # Output formatting │ ├── validation/ # Database freshness check
├── go.mod # Go module │ ├── output/ # Output formatting
└── README.md # This file │ └── state/ # Logging
└── README.md
``` ```
## License ## License

13
cmd/declpac/main.go

@ -12,6 +12,7 @@ import (
"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/validation" "github.com/Riyyi/declpac/pkg/validation"
) )
@ -34,12 +35,6 @@ func main() {
Usage: "State file(s) to read package list from", Usage: "State file(s) to read package list from",
Destination: &cfg.StateFiles, Destination: &cfg.StateFiles,
}, },
&cli.BoolFlag{
Name: "yes",
Aliases: []string{"y"},
Usage: "Skip confirmation prompts",
Destination: &cfg.NoConfirm,
},
&cli.BoolFlag{ &cli.BoolFlag{
Name: "dry-run", Name: "dry-run",
Usage: "Simulate the sync without making changes", Usage: "Simulate the sync without making changes",
@ -57,6 +52,12 @@ func main() {
} }
func run(cfg *Config) error { func run(cfg *Config) error {
if err := state.OpenLog(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return err
}
defer state.Close()
start := time.Now() start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] run: starting...\n") fmt.Fprintf(os.Stderr, "[debug] run: starting...\n")

2
openspec/changes/add-operation-logging/.openspec.yaml

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-15

66
openspec/changes/add-operation-logging/design.md

@ -0,0 +1,66 @@
## Implementation
### Log File Location
- Path: `/var/log/declpac.log`
- Single merged log file (stdout + stderr intermingled in order of arrival)
### State-Modifying Functions (need logging)
1. `SyncPackages()` - `pacman -S --needed <packages>`
2. `InstallAUR()` - `git clone` + `makepkg -si --noconfirm`
3. `MarkAllAsDeps()` - `pacman -D --asdeps`
4. `MarkAsExplicit()` - `pacman -D --asexplicit <packages>`
5. `CleanupOrphans()` - `pacman -Rns`
### Functions to Skip (read-only)
- `DryRun()` - queries only
- `getInstalledCount()` - pacman -Qq
### Execution Patterns
| Pattern | Functions | How |
|---------|------------|-----|
| Streaming | MarkAllAsDeps, MarkAsExplicit, InstallAUR | `io.MultiWriter` to tee to both terminal and log |
| Captured | SyncPackages, CleanupOrphans | capture with `CombinedOutput()`, write to log, write to terminal |
### Error Handling
- Write error to log BEFORE returning from function
- Print error to stderr so user sees it
### Dependencies
- Add to imports: `io`, `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
1. Call `OpenLog()` at program start, defer close
2. Each state-modifying function uses `state.GetLogWriter()` via MultiWriter
### Wire into main.go
- Open log at start of `run()`
- Pass log writer to pacman package (via exported function or global)

26
openspec/changes/add-operation-logging/proposal.md

@ -0,0 +1,26 @@
## Why
The tool exits without saving the full pacman output, making debugging difficult
when operations fail. Users need a persistent log of all pacman operations for
debugging and audit.
## What Changes
- 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`)
- 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)
- Capture and write errors before returning
## Capabilities
### New Capabilities
- Operation logging: Persist stdout/stderr from all pacman operations
### Modified Capabilities
- None
## Impact
- `pkg/pacman/pacman.go`: Instrument all state-modifying functions to write to log file
- New module: May create `pkg/state/state.go` or similar for log file management

45
openspec/changes/add-operation-logging/tasks.md

@ -0,0 +1,45 @@
## Tasks
- [x] 1. Create state module
Create `pkg/state/state.go`:
- `OpenLog()` - opens `/var/log/declpac.log` in append mode
- `GetLogWriter()` - returns the raw log file writer (for MultiWriter)
- `Write(msg []byte)` - writes message with timestamp + dashes separator
- `Close()` - closes the file
- [x] 2. Wire into main.go
In `cmd/declpac/main.go` `run()`:
- Call `OpenLog()` at start
- `defer Close()` log
- [x] 3. Instrument pkg/pacman
Modify `pkg/pacman/pacman.go`:
All state-modifying functions use `state.Write()` instead of `state.GetLogWriter().Write()`:
```
// OLD
state.GetLogWriter().Write(output)
// NEW
state.Write(output) // auto-prepends timestamp
```
**Functions updated:**
- `SyncPackages()` - write output with timestamp
- `CleanupOrphans()` - write output with timestamp
- `MarkAllAsDeps()` - write operation name with timestamp before running
- `MarkAsExplicit()` - write operation name with timestamp before running
- `InstallAUR()` - write "Cloning ..." and "Building package..." with timestamps
- Error handling - `state.Write([]byte("error: ..."))` for all error paths
- [x] 4. Add io import
Add `io` to imports in pacman.go
- [x] 5. Test
Run a sync operation and verify log file created at `/var/log/declpac.log`

57
pkg/fetch/fetch.go

@ -188,6 +188,9 @@ func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error) {
fmt.Fprintf(os.Stderr, "[debug] Resolve: starting...\n") fmt.Fprintf(os.Stderr, "[debug] Resolve: starting...\n")
result := make(map[string]*PackageInfo) result := make(map[string]*PackageInfo)
for _, pkg := range packages {
result[pkg] = &PackageInfo{Name: pkg, Exists: true}
}
localPkgs, err := f.buildLocalPkgMap() localPkgs, err := f.buildLocalPkgMap()
if err != nil { if err != nil {
@ -215,44 +218,38 @@ func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
fmt.Fprintf(os.Stderr, "[debug] Resolve: sync db checked (%.2fs)\n", time.Since(start).Seconds())
var notInSync []string f.ensureAURCache(packages)
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 { for _, pkg := range packages {
f.ensureAURCache(notInSync) info := result[pkg]
fmt.Fprintf(os.Stderr, "[debug] Resolve: AUR cache ensured (%.2fs)\n", time.Since(start).Seconds()) if info == nil {
continue
}
var unfound []string if info.Installed {
for _, pkg := range notInSync {
if aurInfo, ok := f.aurCache[pkg]; ok { if aurInfo, ok := f.aurCache[pkg]; ok {
result[pkg] = &PackageInfo{ info.InAUR = true
Name: pkg, info.AURInfo = &aurInfo
Exists: true,
InAUR: true,
Installed: false,
AURInfo: &aurInfo,
} }
} else { continue
unfound = append(unfound, pkg)
} }
if syncPkg, ok := syncPkgs[pkg]; ok {
info.InAUR = false
info.Installed = false
info.syncPkg = syncPkg
continue
} }
if len(unfound) > 0 {
return nil, fmt.Errorf("package(s) not found: %s", strings.Join(unfound, ", ")) if aurInfo, ok := f.aurCache[pkg]; ok {
info.InAUR = true
info.Installed = false
info.AURInfo = &aurInfo
continue
} }
return nil, fmt.Errorf("package not found: %s", pkg)
} }
} }

50
pkg/pacman/pacman.go

@ -2,6 +2,7 @@ package pacman
import ( import (
"fmt" "fmt"
"io"
"os" "os"
"os/exec" "os/exec"
"regexp" "regexp"
@ -10,6 +11,7 @@ import (
"github.com/Riyyi/declpac/pkg/fetch" "github.com/Riyyi/declpac/pkg/fetch"
"github.com/Riyyi/declpac/pkg/output" "github.com/Riyyi/declpac/pkg/output"
"github.com/Riyyi/declpac/pkg/state"
"github.com/Riyyi/declpac/pkg/validation" "github.com/Riyyi/declpac/pkg/validation"
) )
@ -18,9 +20,13 @@ func MarkAllAsDeps() error {
fmt.Fprintf(os.Stderr, "[debug] MarkAllAsDeps: starting...\n") fmt.Fprintf(os.Stderr, "[debug] MarkAllAsDeps: starting...\n")
cmd := exec.Command("pacman", "-D", "--asdeps") cmd := exec.Command("pacman", "-D", "--asdeps")
cmd.Stdout = os.Stdout state.Write([]byte("MarkAllAsDeps...\n"))
cmd.Stderr = os.Stderr cmd.Stdout = io.MultiWriter(os.Stdout, state.GetLogWriter())
cmd.Stderr = io.MultiWriter(os.Stderr, state.GetLogWriter())
err := cmd.Run() 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()) fmt.Fprintf(os.Stderr, "[debug] MarkAllAsDeps: done (%.2fs)\n", time.Since(start).Seconds())
return err return err
@ -35,9 +41,13 @@ func MarkAsExplicit(packages []string) error {
args := append([]string{"-D", "--asexplicit"}, packages...) args := append([]string{"-D", "--asexplicit"}, packages...)
cmd := exec.Command("pacman", args...) cmd := exec.Command("pacman", args...)
cmd.Stdout = os.Stdout state.Write([]byte("MarkAsExplicit...\n"))
cmd.Stderr = os.Stderr cmd.Stdout = io.MultiWriter(os.Stdout, state.GetLogWriter())
cmd.Stderr = io.MultiWriter(os.Stderr, state.GetLogWriter())
err := cmd.Run() 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()) fmt.Fprintf(os.Stderr, "[debug] MarkAsExplicit: done (%.2fs)\n", time.Since(start).Seconds())
return err return err
@ -157,19 +167,25 @@ func InstallAUR(f *fetch.Fetcher, pkgName string) error {
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
cloneURL := "https://aur.archlinux.org/" + aurInfo.PackageBase + ".git" cloneURL := "https://aur.archlinux.org/" + aurInfo.PackageBase + ".git"
state.Write([]byte("Cloning " + cloneURL + "\n"))
cloneCmd := exec.Command("git", "clone", cloneURL, tmpDir) cloneCmd := exec.Command("git", "clone", cloneURL, tmpDir)
cloneCmd.Stdout = os.Stdout cloneCmd.Stdout = io.MultiWriter(os.Stdout, state.GetLogWriter())
cloneCmd.Stderr = os.Stderr cloneCmd.Stderr = io.MultiWriter(os.Stderr, state.GetLogWriter())
if err := cloneCmd.Run(); err != nil { if err := cloneCmd.Run(); err != nil {
errMsg := fmt.Sprintf("failed to clone AUR repo: %w\n", err)
state.Write([]byte("error: " + errMsg))
return fmt.Errorf("failed to clone AUR repo: %w", err) return fmt.Errorf("failed to clone AUR repo: %w", err)
} }
fmt.Fprintf(os.Stderr, "[debug] InstallAUR: cloned (%.2fs)\n", time.Since(start).Seconds()) fmt.Fprintf(os.Stderr, "[debug] InstallAUR: cloned (%.2fs)\n", time.Since(start).Seconds())
state.Write([]byte("Building package...\n"))
makepkgCmd := exec.Command("makepkg", "-si", "--noconfirm") makepkgCmd := exec.Command("makepkg", "-si", "--noconfirm")
makepkgCmd.Stdout = os.Stdout makepkgCmd.Stdout = io.MultiWriter(os.Stdout, state.GetLogWriter())
makepkgCmd.Stderr = os.Stderr makepkgCmd.Stderr = io.MultiWriter(os.Stderr, state.GetLogWriter())
makepkgCmd.Dir = tmpDir makepkgCmd.Dir = tmpDir
if err := makepkgCmd.Run(); err != nil { if err := makepkgCmd.Run(); err != nil {
errMsg := fmt.Sprintf("makepkg failed to build AUR package: %w\n", err)
state.Write([]byte("error: " + errMsg))
return fmt.Errorf("makepkg failed to build AUR package: %w", err) 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: built (%.2fs)\n", time.Since(start).Seconds())
@ -200,13 +216,19 @@ func SyncPackages(packages []string) (int, error) {
start := time.Now() start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] SyncPackages: starting...\n") fmt.Fprintf(os.Stderr, "[debug] SyncPackages: starting...\n")
args := append([]string{"-Syu"}, packages...) args := append([]string{"-S", "--needed"}, packages...)
cmd := exec.Command("pacman", args...) cmd := exec.Command("pacman", args...)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
errMsg := fmt.Sprintf("pacman sync failed: %s", output)
state.Write([]byte(errMsg))
return 0, fmt.Errorf("pacman sync failed: %s", output) return 0, fmt.Errorf("pacman sync failed: %s", output)
} }
if len(output) > 0 {
state.Write(output)
}
re := regexp.MustCompile(`upgrading (\S+)`) re := regexp.MustCompile(`upgrading (\S+)`)
matches := re.FindAllStringSubmatch(string(output), -1) matches := re.FindAllStringSubmatch(string(output), -1)
@ -233,9 +255,15 @@ func CleanupOrphans() (int, error) {
removeCmd := exec.Command("pacman", "-Rns") removeCmd := exec.Command("pacman", "-Rns")
output, err := removeCmd.CombinedOutput() output, err := removeCmd.CombinedOutput()
if err != nil { if err != nil {
errMsg := fmt.Sprintf("%s: %s", err, output)
state.Write([]byte(errMsg))
return 0, fmt.Errorf("%s: %s", err, output) return 0, fmt.Errorf("%s: %s", err, output)
} }
if len(output) > 0 {
state.Write(output)
}
count := len(orphans) count := len(orphans)
fmt.Fprintf(os.Stderr, "[debug] CleanupOrphans: done (%.2fs)\n", time.Since(start).Seconds()) fmt.Fprintf(os.Stderr, "[debug] CleanupOrphans: done (%.2fs)\n", time.Since(start).Seconds())
@ -271,14 +299,12 @@ func DryRun(packages []string) (*output.Result, error) {
if info == nil || !info.Exists { if info == nil || !info.Exists {
return nil, fmt.Errorf("package not found: %s", pkg) return nil, fmt.Errorf("package not found: %s", pkg)
} }
if _, installed := localPkgs[pkg]; !installed {
if info.InAUR { if info.InAUR {
aurPkgs = append(aurPkgs, pkg) aurPkgs = append(aurPkgs, pkg)
} else { } else if _, installed := localPkgs[pkg]; !installed {
toInstall = append(toInstall, pkg) toInstall = append(toInstall, pkg)
} }
} }
}
fmt.Fprintf(os.Stderr, "[debug] DryRun: packages categorized (%.2fs)\n", time.Since(start).Seconds()) fmt.Fprintf(os.Stderr, "[debug] DryRun: packages categorized (%.2fs)\n", time.Since(start).Seconds())
orphans, err := f.ListOrphans() orphans, err := f.ListOrphans()

43
pkg/state/state.go

@ -0,0 +1,43 @@
package state
import (
"fmt"
"io"
"os"
"path/filepath"
"time"
)
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 Write(msg []byte) {
PrependWithTimestamp(logFile, msg)
}
func Close() error {
if logFile == nil {
return nil
}
return logFile.Close()
}
func PrependWithTimestamp(w io.Writer, msg []byte) {
ts := time.Now().Format("2006-01-02 15:04:05")
header := fmt.Sprintf("\n--- %s ---\n", ts)
w.Write([]byte(header))
w.Write(msg)
}
Loading…
Cancel
Save