Compare commits
7
Commits
2bead6af27
...
192b41e18b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
192b41e18b | ||
|
|
1c74373d6b | ||
|
|
5e9b2521bd | ||
|
|
eb6bb8fb76 | ||
|
|
0b834214cd | ||
|
|
5031fdc1f8 | ||
|
|
0fa0988666 |
+1
-1
@@ -32,4 +32,4 @@ go.work.sum
|
||||
# .vscode/
|
||||
|
||||
# Binary output
|
||||
declpac
|
||||
/declpac
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
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
|
||||
|
||||
@@ -16,7 +16,7 @@ Make a git commit, distinguishing between user and AI contributions.
|
||||
|
||||
**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:
|
||||
> "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**
|
||||
|
||||
**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:
|
||||
```bash
|
||||
git diff --staged --stat
|
||||
```
|
||||
Create a reasonable commit message based on the changes.
|
||||
|
||||
**Capitalization rule**: Commit message 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 thed user DID provide a message, format it into a proper commit message.
|
||||
|
||||
3. **Show commit message and confirm**
|
||||
|
||||
Display the commit message to the user.
|
||||
|
||||
Use the **question tool** to ask:
|
||||
> "Is this commit message okay, or would you like to make tweaks?"
|
||||
> <message>
|
||||
|
||||
Options:
|
||||
- "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.
|
||||
|
||||
4. **Get git user config**
|
||||
|
||||
```bash
|
||||
git config user.name
|
||||
git config user.email
|
||||
```
|
||||
|
||||
5. **Make the commit**
|
||||
4. **Make the commit**
|
||||
|
||||
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:**
|
||||
```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)
|
||||
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
# declpac
|
||||
|
||||
`declpac` is a declarative package manager for Arch Linux that syncs your system
|
||||
with a declared package list using `pacman`. It ensures your system matches your
|
||||
desired state, handling package installation, upgrades, and orphan cleanup
|
||||
automatically.
|
||||
Declarative package manager for Arch Linux that syncs your system with a declared package list using pacman.
|
||||
|
||||
## Features
|
||||
|
||||
- **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
|
||||
- **Explicit package tracking** — Marks your declared packages as explicit
|
||||
- **AUR support** — Falls back to AUR for packages not in official repos
|
||||
- **Machine-readable output** — Perfect for scripting and automation
|
||||
- Declarative state management — define your desired package list in files or stdin
|
||||
- Smart orphan cleanup — removes packages no longer needed
|
||||
- Explicit package tracking — marks declared packages as explicit
|
||||
- AUR support — builds and installs AUR packages automatically
|
||||
- Machine-readable output — perfect for scripting
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -28,14 +24,13 @@ sudo mv declpac /usr/local/bin/
|
||||
### Dependencies
|
||||
|
||||
- Go 1.21+
|
||||
- pacman (system package manager)
|
||||
- aur (AUR helper, optional for AUR support)
|
||||
- Root privileges (required for pacman operations)
|
||||
- pacman
|
||||
- makepkg (for AUR support)
|
||||
- git (for AUR support)
|
||||
- Root privileges
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
# Single state file
|
||||
sudo declpac --state packages.txt
|
||||
@@ -45,119 +40,63 @@ sudo declpac --state base.txt --state apps.txt
|
||||
|
||||
# From stdin
|
||||
cat packages.txt | sudo declpac
|
||||
|
||||
# Preview changes without applying
|
||||
sudo declpac --dry-run --state packages.txt
|
||||
```
|
||||
|
||||
### State File Format
|
||||
|
||||
State files contain one package name per line:
|
||||
One package name per line, lines beginning with `#` are comments:
|
||||
|
||||
```
|
||||
bash
|
||||
vim
|
||||
git
|
||||
docker
|
||||
# this is a comment
|
||||
```
|
||||
|
||||
Lines are treated as package names with whitespace trimmed:
|
||||
|
||||
```
|
||||
bash # bash
|
||||
vim # vim
|
||||
# comment # ignored
|
||||
```
|
||||
|
||||
### Command Line Options
|
||||
### Options
|
||||
|
||||
| Flag | Alias | Description |
|
||||
|------|-------|-------------|
|
||||
| `--state` | `-s` | State file(s) to read package list from (can be used multiple times) |
|
||||
| `--yes` | `-y` | Skip confirmation prompts (for scripting) |
|
||||
| `--dry-run` | | Simulate sync without making changes |
|
||||
| `--state` | `-s` | State file to read package list from (can be used multiple times) |
|
||||
| `--dry-run` | | Preview changes without applying them |
|
||||
| `--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
|
||||
|
||||
1. **Collect packages** — Reads from all `--state` files and stdin
|
||||
2. **Merge** — Combines all packages additively (duplicates allowed)
|
||||
3. **Validate** — Checks packages exist in repos or AUR
|
||||
4. **Mark explicit** — Marks declared packages as explicit dependencies
|
||||
5. **Sync** — Runs `pacman -Syu` to install/upgrade packages
|
||||
6. **Cleanup** — Removes orphaned packages with `pacman -Rns`
|
||||
7. **Report** — Outputs summary: `Installed X packages, removed Y packages`
|
||||
1. **Read** — Collect packages from all state files and stdin
|
||||
2. **Merge** — Combine into single package list
|
||||
3. **Categorize** — Check if packages are in official repos or AUR
|
||||
4. **Sync** — Install/update packages via pacman
|
||||
5. **Build** — Build and install AUR packages via makepkg
|
||||
6. **Mark** — Mark declared packages as explicit, all others as dependencies
|
||||
7. **Cleanup** — Remove orphaned packages
|
||||
|
||||
### 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
|
||||
### Logging
|
||||
|
||||
After syncing, `declpac` identifies and removes packages that are:
|
||||
- Not explicitly installed
|
||||
- Not required by any other package
|
||||
Operations are logged to `/var/log/declpac.log`.
|
||||
|
||||
This keeps your system clean from dependency artifacts.
|
||||
|
||||
## Output Format
|
||||
## Output
|
||||
|
||||
```
|
||||
# Success (packages installed/removed)
|
||||
# Packages installed/removed
|
||||
Installed 5 packages, removed 2 packages
|
||||
|
||||
# Success (no changes)
|
||||
# No changes needed
|
||||
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: package not found: <package-name>
|
||||
```
|
||||
@@ -167,36 +106,54 @@ error: package not found: <package-name>
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 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
|
||||
- **Review state files** — Only install packages from trusted sources
|
||||
- **Backup** — Consider backing up your system before major changes
|
||||
### Minimal System
|
||||
|
||||
```bash
|
||||
echo -e "base\nbase-devel\nlinux-headers\nvim\ngit\ncurl" > ~/.config/declpac/minimal.txt
|
||||
sudo declpac --state ~/.config/declpac/minimal.txt
|
||||
```
|
||||
|
||||
### Development Environment
|
||||
|
||||
```bash
|
||||
# development.txt
|
||||
go
|
||||
nodejs
|
||||
python
|
||||
rust
|
||||
docker
|
||||
|
||||
sudo declpac --state development.txt
|
||||
```
|
||||
|
||||
### Dry-Run
|
||||
|
||||
```bash
|
||||
sudo declpac --dry-run --state packages.txt
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Permission denied"
|
||||
|
||||
`declpac` requires root privileges. Use `sudo`:
|
||||
### Permission denied
|
||||
|
||||
Use sudo:
|
||||
```bash
|
||||
sudo declpac --state packages.txt
|
||||
```
|
||||
|
||||
### "Package not found"
|
||||
|
||||
The package doesn't exist in pacman repos or AUR. Check the package name:
|
||||
### Package not found
|
||||
|
||||
Check if the package exists:
|
||||
```bash
|
||||
pacman -Ss <package>
|
||||
```
|
||||
|
||||
### Database sync fails
|
||||
|
||||
Refresh manually:
|
||||
|
||||
```bash
|
||||
sudo pacman -Syy
|
||||
```
|
||||
@@ -210,13 +167,14 @@ declpac/
|
||||
├── pkg/
|
||||
│ ├── input/ # State file/stdin reading
|
||||
│ ├── merge/ # Package merging
|
||||
│ ├── validation/ # Package validation
|
||||
│ ├── pacman/ # Pacman integration
|
||||
│ └── output/ # Output formatting
|
||||
├── go.mod # Go module
|
||||
└── README.md # This file
|
||||
│ ├── fetch/ # Package resolution (pacman/AUR)
|
||||
│ ├── pacman/ # Pacman operations
|
||||
│ ├── validation/ # Database freshness check
|
||||
│ ├── output/ # Output formatting
|
||||
│ └── state/ # Logging
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
GPL-3.0
|
||||
GPL-3.0
|
||||
+7
-6
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/Riyyi/declpac/pkg/merge"
|
||||
"github.com/Riyyi/declpac/pkg/output"
|
||||
"github.com/Riyyi/declpac/pkg/pacman"
|
||||
"github.com/Riyyi/declpac/pkg/state"
|
||||
"github.com/Riyyi/declpac/pkg/validation"
|
||||
)
|
||||
|
||||
@@ -34,12 +35,6 @@ func main() {
|
||||
Usage: "State file(s) to read package list from",
|
||||
Destination: &cfg.StateFiles,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "yes",
|
||||
Aliases: []string{"y"},
|
||||
Usage: "Skip confirmation prompts",
|
||||
Destination: &cfg.NoConfirm,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "dry-run",
|
||||
Usage: "Simulate the sync without making changes",
|
||||
@@ -57,6 +52,12 @@ func main() {
|
||||
}
|
||||
|
||||
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()
|
||||
fmt.Fprintf(os.Stderr, "[debug] run: starting...\n")
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-15
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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`
|
||||
+28
-31
@@ -188,6 +188,9 @@ func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error) {
|
||||
fmt.Fprintf(os.Stderr, "[debug] Resolve: starting...\n")
|
||||
|
||||
result := make(map[string]*PackageInfo)
|
||||
for _, pkg := range packages {
|
||||
result[pkg] = &PackageInfo{Name: pkg, Exists: true}
|
||||
}
|
||||
|
||||
localPkgs, err := f.buildLocalPkgMap()
|
||||
if err != nil {
|
||||
@@ -215,44 +218,38 @@ func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "[debug] Resolve: 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)
|
||||
f.ensureAURCache(packages)
|
||||
|
||||
for _, pkg := range packages {
|
||||
info := result[pkg]
|
||||
if info == nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(notInSync) > 0 {
|
||||
f.ensureAURCache(notInSync)
|
||||
fmt.Fprintf(os.Stderr, "[debug] Resolve: AUR cache ensured (%.2fs)\n", time.Since(start).Seconds())
|
||||
|
||||
var unfound []string
|
||||
for _, pkg := range notInSync {
|
||||
if info.Installed {
|
||||
if aurInfo, ok := f.aurCache[pkg]; ok {
|
||||
result[pkg] = &PackageInfo{
|
||||
Name: pkg,
|
||||
Exists: true,
|
||||
InAUR: true,
|
||||
Installed: false,
|
||||
AURInfo: &aurInfo,
|
||||
}
|
||||
} else {
|
||||
unfound = append(unfound, pkg)
|
||||
info.InAUR = true
|
||||
info.AURInfo = &aurInfo
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len(unfound) > 0 {
|
||||
return nil, fmt.Errorf("package(s) not found: %s", strings.Join(unfound, ", "))
|
||||
|
||||
if syncPkg, ok := syncPkgs[pkg]; ok {
|
||||
info.InAUR = false
|
||||
info.Installed = false
|
||||
info.syncPkg = syncPkg
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+41
-15
@@ -2,6 +2,7 @@ package pacman
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
|
||||
"github.com/Riyyi/declpac/pkg/fetch"
|
||||
"github.com/Riyyi/declpac/pkg/output"
|
||||
"github.com/Riyyi/declpac/pkg/state"
|
||||
"github.com/Riyyi/declpac/pkg/validation"
|
||||
)
|
||||
|
||||
@@ -18,9 +20,13 @@ func MarkAllAsDeps() error {
|
||||
fmt.Fprintf(os.Stderr, "[debug] MarkAllAsDeps: starting...\n")
|
||||
|
||||
cmd := exec.Command("pacman", "-D", "--asdeps")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
state.Write([]byte("MarkAllAsDeps...\n"))
|
||||
cmd.Stdout = io.MultiWriter(os.Stdout, state.GetLogWriter())
|
||||
cmd.Stderr = io.MultiWriter(os.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
|
||||
@@ -35,9 +41,13 @@ func MarkAsExplicit(packages []string) error {
|
||||
|
||||
args := append([]string{"-D", "--asexplicit"}, packages...)
|
||||
cmd := exec.Command("pacman", args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
state.Write([]byte("MarkAsExplicit...\n"))
|
||||
cmd.Stdout = io.MultiWriter(os.Stdout, state.GetLogWriter())
|
||||
cmd.Stderr = io.MultiWriter(os.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
|
||||
@@ -157,19 +167,25 @@ func InstallAUR(f *fetch.Fetcher, pkgName string) error {
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
cloneURL := "https://aur.archlinux.org/" + aurInfo.PackageBase + ".git"
|
||||
state.Write([]byte("Cloning " + cloneURL + "\n"))
|
||||
cloneCmd := exec.Command("git", "clone", cloneURL, tmpDir)
|
||||
cloneCmd.Stdout = os.Stdout
|
||||
cloneCmd.Stderr = os.Stderr
|
||||
cloneCmd.Stdout = io.MultiWriter(os.Stdout, state.GetLogWriter())
|
||||
cloneCmd.Stderr = io.MultiWriter(os.Stderr, state.GetLogWriter())
|
||||
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)
|
||||
}
|
||||
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.Stdout = os.Stdout
|
||||
makepkgCmd.Stderr = os.Stderr
|
||||
makepkgCmd.Stdout = io.MultiWriter(os.Stdout, state.GetLogWriter())
|
||||
makepkgCmd.Stderr = io.MultiWriter(os.Stderr, state.GetLogWriter())
|
||||
makepkgCmd.Dir = tmpDir
|
||||
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)
|
||||
}
|
||||
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()
|
||||
fmt.Fprintf(os.Stderr, "[debug] SyncPackages: starting...\n")
|
||||
|
||||
args := append([]string{"-Syu"}, packages...)
|
||||
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)
|
||||
|
||||
@@ -233,9 +255,15 @@ func CleanupOrphans() (int, error) {
|
||||
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())
|
||||
@@ -271,12 +299,10 @@ func DryRun(packages []string) (*output.Result, error) {
|
||||
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)
|
||||
}
|
||||
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())
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user