Compare commits

..
29 Commits
Author SHA1 Message Date
AI BotandRiyyi 1159be59d0 Add safety check before package pruning
Verifies all state packages are installed locally before running
markAllAsDeps and orphan cleanup.
2026-05-04 23:28:58 +02:00
Riyyi 0fc0684801 Improve tarball selection when installing AUR packages 2026-05-04 22:47:07 +02:00
AI BotandRiyyi bcddfd13e7 Also install repo dependencies when installing AUR packages 2026-05-04 21:13:25 +02:00
Riyyi 4472bbc2e9 Add grill-me skill 2026-05-04 20:25:43 +02:00
Riyyi b371c74e9a Make tool request elevated privileges on-demand 2026-05-03 23:04:34 +02:00
Riyyi 76b83437ac Add safety check before rm -rf call 2026-05-03 22:17:51 +02:00
AI BotandRiyyi b04869f0e1 Move log to user owned directory 2026-05-03 21:29:49 +02:00
AI BotandRiyyi 7c4b0eda8b Apply coding conventions 2026-05-03 18:01:00 +02:00
AI BotandRiyyi 317d5f4be9 Cache sudo user to avoid repeated env lookups 2026-05-03 17:49:53 +02:00
AI BotandRiyyi b13c9da7a1 Improve AUR with AUR dependency installations 2026-05-03 17:40:31 +02:00
AI BotandRiyyi c8cdace4bf Update Go file structure conventions in AGENTS.md 2026-05-03 15:48:00 +02:00
AI BotandRiyyi 31fad42ab3 Reorder functions to follow public-first convention 2026-05-03 15:40:07 +02:00
AI BotandRiyyi db4e22a580 Add --prune flag for explicit marking and orphan cleanup 2026-05-03 15:10:54 +02:00
AI BotandRiyyi d59d5c084d Add implicit state file support
Read implicit package list from /home/rick/.config/declpac or ~/.config/declpac
2026-05-03 14:30:16 +02:00
AI BotandRiyyi c4746697b5 Remove merge package, move Merge to input 2026-05-03 14:30:16 +02:00
AI BotandRiyyi 5ad29767c9 Add safety check for package list validation 2026-05-03 14:30:16 +02:00
AI BotandRiyyi 38d26fd2d8 Add Go line length guidelines
Adds 120/100/80 character guidelines to AGENTS.md
2026-05-03 12:17:12 +02:00
AI BotandRiyyi 399a7157e2 Add tilde path expansion for state files 2026-05-03 11:48:05 +02:00
AI BotandRiyyi 578eb16ab2 Add Go file structure conventions to AGENTS.md 2026-05-03 11:27:30 +02:00
AI BotandRiyyi 584f4fbdd9 Skip debug packages when finding built AUR package
The findPKGFile function now takes pkgName as parameter to skip
debug packages (*-debug.pkg.tar.zst) when locating built packages.
2026-05-03 11:13:06 +02:00
Riyyi 39b8d9ac4b Add fmt command to makefile 2026-05-03 10:22:30 +02:00
AI BotandRiyyi d36bba6925 Add centralized command logging function
Refactors command execution to use a new log.Command helper that handles both logging and cmd creation, reducing duplication in sync.go
2026-05-03 10:05:10 +02:00
AI BotandRiyyi b1a376dbbe Add build, install and uninstall targets to Makefile 2026-04-24 21:14:03 +02:00
Riyyi 8a074e02b0 Update README.md 2026-04-19 11:20:08 +02:00
AI BotandRiyyi fdd9ecb3a5 Handle pacman -Qdtq exit code 1 as non-error
pacman -Qdtq exits with code 1 when there are no orphans, which is not an error condition. Handle this gracefully by returning nil instead of an error.
2026-04-19 11:08:32 +02:00
AI BotandRiyyi c90d2e8916 Add verbose flag for debug output control
Introduces log.Debug() function that respects a Verbose config flag, replacing direct [debug] fmt.Fprintf calls throughout the codebase. This allows debug logging to be toggled via -v/--verbose CLI flag.
2026-04-19 11:06:52 +02:00
Riyyi 1dffd70db2 Skip interactive prompt when removing orphans 2026-04-19 10:21:57 +02:00
AI BotandRiyyi 5ebb958bf1 Add command logging and skip already installed AUR packages 2026-04-19 10:15:00 +02:00
Riyyi c7bed95107 Skip confirmation when installing packages 2026-04-18 20:35:18 +02:00
19 changed files with 852 additions and 368 deletions
+1 -1
View File
@@ -32,4 +32,4 @@ go.work.sum
# .vscode/ # .vscode/
# Binary output # Binary output
/declpac bin/
+10
View File
@@ -0,0 +1,10 @@
---
name: grill-me
description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me".
---
Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
Ask the questions one at a time.
If a question can be answered by exploring the codebase, explore the codebase instead.
+25 -1
View File
@@ -2,6 +2,30 @@
## Skills: Always Active ## Skills: Always Active
At the start of every conversation, load the following skills using the `skill` tool before responding to the user: At the start of every conversation, load the following skills using the `skill`
tool before responding to the user:
1. **caveman** — Always use caveman mode (full intensity) for all responses 1. **caveman** — Always use caveman mode (full intensity) for all responses
## Code Organization
### Go: File Structure
- Variables must appear at the top of each file.
- Types must appear after variables.
- Constructors must appear after types.
- Public (exported) functions must appear after constructors.
- Private (unexported) functions must appear at the bottom of each file.
- Within each section, definitions must be sorted alphabetically by name.
- The sections must be separated by exactly these dividers, filling in the section:
// -----------------------------------------
// <section>
### Go: Line Length
Keep Go lines as short as reasonably possible.
- **Hard limit:** 120 characters — never exceed this.
- **Preferred:** 100 characters or fewer in normal cases.
- **Target:** 80 characters if breaking the line produces cleaner, more readable
code (e.g. chained calls, long argument lists, multi-condition `if` statements).
+23 -2
View File
@@ -1,9 +1,23 @@
.PHONY: fmt vet check githook .PHONY: build fmt fmtcheck vet check githook install uninstall
PKG := github.com/Riyyi/declpac
BIN := declpac
BUILD_DIR := bin
BUILD := $(BUILD_DIR)/$(BIN)
PREFIX ?= /usr/local
DESTDIR ?=
STAGED_GO := $(shell git diff --cached --name-only --diff-filter=ACM | grep '\.go$$') STAGED_GO := $(shell git diff --cached --name-only --diff-filter=ACM | grep '\.go$$')
STAGED_PKGS := $(shell echo "$(STAGED_GO)" | tr ' ' '\n' | xargs -I{} dirname {} | sort -u | sed 's|^|./|') STAGED_PKGS := $(shell echo "$(STAGED_GO)" | tr ' ' '\n' | xargs -I{} dirname {} | sort -u | sed 's|^|./|')
build:
@mkdir -p $(BUILD_DIR)
go build -o $(BUILD) ./cmd/declpac
fmt: fmt:
@gofmt -w cmd pkg
fmtcheck:
@if [ -z "$(STAGED_GO)" ]; then exit 0; fi; \ @if [ -z "$(STAGED_GO)" ]; then exit 0; fi; \
unformatted=$$(gofmt -l $(STAGED_GO)); \ unformatted=$$(gofmt -l $(STAGED_GO)); \
if [ -n "$$unformatted" ]; then \ if [ -n "$$unformatted" ]; then \
@@ -15,7 +29,7 @@ vet:
@if [ -z "$(STAGED_GO)" ]; then exit 0; fi; \ @if [ -z "$(STAGED_GO)" ]; then exit 0; fi; \
go vet $(STAGED_PKGS) go vet $(STAGED_PKGS)
check: fmt vet check: fmtcheck vet
githook: githook:
@mkdir -p .git/hooks @mkdir -p .git/hooks
@@ -23,3 +37,10 @@ githook:
@echo 'make check' >> .git/hooks/pre-commit @echo 'make check' >> .git/hooks/pre-commit
@chmod +x .git/hooks/pre-commit @chmod +x .git/hooks/pre-commit
@echo "Installed pre-commit hook: .git/hooks/pre-commit" @echo "Installed pre-commit hook: .git/hooks/pre-commit"
install: build
@mkdir -p $(DESTDIR)$(PREFIX)/bin
@install -m 0755 $(BUILD) $(DESTDIR)$(PREFIX)/bin/$(BIN)
uninstall:
@rm -f $(DESTDIR)$(PREFIX)/bin/$(BIN)
+20 -72
View File
@@ -5,8 +5,8 @@ Declarative package manager for Arch Linux that syncs your system with a declare
## 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
- Smart orphan cleanup — removes packages no longer needed - Explicit package tracking — marks declared packages as explicit (with `--prune`)
- Explicit package tracking — marks declared packages as explicit - Smart orphan cleanup — removes packages no longer needed (with `--prune`)
- AUR support — builds and installs AUR packages automatically - AUR support — builds and installs AUR packages automatically
- Machine-readable output — perfect for scripting - Machine-readable output — perfect for scripting
@@ -27,22 +27,22 @@ sudo mv declpac /usr/local/bin/
- pacman - pacman
- makepkg (for AUR support) - makepkg (for AUR support)
- git (for AUR support) - git (for AUR support)
- Root privileges - sudo/doas (root privileges)
## Usage ## Usage
```bash ```bash
# Single state file # Single state file
sudo declpac --state packages.txt declpac --state packages.txt
# Multiple state files # Multiple state files
sudo declpac --state base.txt --state apps.txt declpac --state base.txt --state apps.txt
# From stdin # From stdin
cat packages.txt | sudo declpac cat packages.txt | declpac
# Preview changes without applying # Preview changes without applying
sudo declpac --dry-run --state packages.txt declpac --dry-run --state packages.txt
``` ```
### State File Format ### State File Format
@@ -57,12 +57,20 @@ docker
# this is a comment # this is a comment
``` ```
### Implicit State File
If `$XDG_CONFIG_HOME/declpac` (or `~/.config/declpac` on fallback) exists, its
contents are automatically included in the package list.
### Options ### Options
| Flag | Alias | Description | | Flag | Alias | Description |
|------|-------|-------------| |------|-------|-------------|
| `--state` | `-s` | State file to read package list from (can be used multiple times) | | `--state` | `-s` | State file(s) to read package list from (can be used multiple times) |
| `--nocheck` | | Skip safety check (allow significant package count reductions) |
| `--dry-run` | | Preview changes without applying them | | `--dry-run` | | Preview changes without applying them |
| `--prune` | | Mark packages as explicit and cleanup orphans |
| `--verbose` | `-v` | Enable verbose output |
| `--help` | `-h` | Show help message | | `--help` | `-h` | Show help message |
## How It Works ## How It Works
@@ -72,8 +80,8 @@ docker
3. **Categorize** — Check if packages are in official repos or AUR 3. **Categorize** — Check if packages are in official repos or AUR
4. **Sync** — Install/update packages via pacman 4. **Sync** — Install/update packages via pacman
5. **Build** — Build and install AUR packages via makepkg 5. **Build** — Build and install AUR packages via makepkg
6. **Mark** — Mark declared packages as explicit, all others as dependencies 6. **Mark** (with `--prune`) — Mark declared packages as explicit, all others as dependencies
7. **Cleanup** — Remove orphaned packages 7. **Cleanup** (with `--prune`) — Remove orphaned packages
### Database Freshness ### Database Freshness
@@ -81,70 +89,11 @@ If the pacman database is older than 24 hours, it is automatically refreshed.
### Logging ### Logging
Operations are logged to `/var/log/declpac.log`. Operation are logged to `$XDG_STATE_HOME/declpac.log`
(or `~/.local/state/declpac.log` on fallback)
## Output
```
# Packages installed/removed
Installed 5 packages, removed 2 packages
# 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>
```
### Exit Codes
| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | Error |
## Examples
### 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 ## Troubleshooting
### Permission denied
Use sudo:
```bash
sudo declpac --state packages.txt
```
### Package not found ### Package not found
Check if the package exists: Check if the package exists:
@@ -166,7 +115,6 @@ declpac/
│ └── main.go # Entry point │ └── main.go # Entry point
├── pkg/ ├── pkg/
│ ├── input/ # State file/stdin reading │ ├── input/ # State file/stdin reading
│ ├── merge/ # Package merging
│ ├── fetch/ # Package resolution │ ├── fetch/ # Package resolution
│ │ ├── aur/ # AUR support │ │ ├── aur/ # AUR support
│ │ └── alpm/ # ALPM support │ │ └── alpm/ # ALPM support
+33 -8
View File
@@ -8,9 +8,9 @@ import (
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"github.com/Riyyi/declpac/pkg/auth"
"github.com/Riyyi/declpac/pkg/input" "github.com/Riyyi/declpac/pkg/input"
"github.com/Riyyi/declpac/pkg/log" "github.com/Riyyi/declpac/pkg/log"
"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/pacman/read" "github.com/Riyyi/declpac/pkg/pacman/read"
@@ -18,8 +18,10 @@ import (
type Config struct { type Config struct {
StateFiles []string StateFiles []string
NoConfirm bool NoCheck bool
DryRun bool DryRun bool
Prune bool
Verbose bool
} }
func main() { func main() {
@@ -35,13 +37,30 @@ 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: "nocheck",
Usage: "Skip safety check",
Destination: &cfg.NoCheck,
},
&cli.BoolFlag{ &cli.BoolFlag{
Name: "dry-run", Name: "dry-run",
Usage: "Simulate the sync without making changes", Usage: "Simulate the sync without making changes",
Destination: &cfg.DryRun, Destination: &cfg.DryRun,
}, },
&cli.BoolFlag{
Name: "prune",
Usage: "Mark packages and cleanup orphans",
Destination: &cfg.Prune,
},
&cli.BoolFlag{
Name: "verbose",
Aliases: []string{"v"},
Usage: "Enable verbose output",
Destination: &cfg.Verbose,
},
}, },
Action: func(ctx context.Context, cmd *cli.Command) error { Action: func(ctx context.Context, cmd *cli.Command) error {
log.Verbose = cfg.Verbose
return run(cfg) return run(cfg)
}, },
} }
@@ -53,16 +72,20 @@ func main() {
func run(cfg *Config) error { func run(cfg *Config) error {
start := time.Now() start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] run: starting...\n") log.Debug("run: starting...")
packages, err := input.ReadPackages(cfg.StateFiles) packages, err := input.ReadPackages(cfg.StateFiles)
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
} }
fmt.Fprintf(os.Stderr, "[debug] run: packages read (%.2fs)\n", time.Since(start).Seconds()) log.Debug("run: packages read (%.2fs)", time.Since(start).Seconds())
merged := merge.Merge(packages) merged, err := input.Merge(packages)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return err
}
if cfg.DryRun { if cfg.DryRun {
result, err := read.DryRun(merged) result, err := read.DryRun(merged)
@@ -71,23 +94,25 @@ func run(cfg *Config) error {
return err return err
} }
fmt.Println(output.Format(result)) fmt.Println(output.Format(result))
fmt.Fprintf(os.Stderr, "[debug] run: dry-run done (%.2fs)\n", time.Since(start).Seconds()) log.Debug("run: dry-run done (%.2fs)", time.Since(start).Seconds())
return nil return nil
} }
auth.Start()
if err := log.OpenLog(); err != nil { if err := log.OpenLog(); 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() defer log.Close()
result, err := pacman.Sync(merged) result, err := pacman.Sync(merged, cfg.NoCheck, cfg.Prune)
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
} }
fmt.Println(output.Format(result)) fmt.Println(output.Format(result))
fmt.Fprintf(os.Stderr, "[debug] run: sync done (%.2fs)\n", time.Since(start).Seconds()) log.Debug("run: sync done (%.2fs)", time.Since(start).Seconds())
return nil return nil
} }
+104
View File
@@ -0,0 +1,104 @@
package auth
import (
"fmt"
"os/exec"
"regexp"
"strconv"
"time"
"github.com/Riyyi/declpac/pkg/log"
)
var tool string
var timeout time.Duration = 5 * time.Minute
var refreshCommand []string = []string{"-n", "true"}
// -----------------------------------------
// public
func Command(name string, args ...string) *exec.Cmd {
if tool == "" {
return log.Command(name, args...)
}
args = append([]string{name}, args...)
return log.Command(tool, args...)
}
func Run() {
exec.Command(tool, refreshCommand...).Run()
}
func Start() error {
err := detect()
if err != nil {
return err
}
// Automatically refresh privilege elevation to prevent user prompts
go func() {
for {
Run()
time.Sleep(timeout)
}
}()
return nil
}
// -----------------------------------------
// private
func detect() error {
tool = getTool()
if tool == "" {
return fmt.Errorf("no privilege elevation tool detected in PATH")
}
parseTimeout()
// We have to be a little faster than the actual timeout
timeout -= 30 * time.Second
return nil
}
func execLookPath(name string) string {
path, err := exec.LookPath(name)
if err != nil {
return ""
}
return path
}
func getTool() string {
sudo := execLookPath("sudo")
doas := execLookPath("doas")
if sudo != "" {
return "sudo"
}
if doas != "" {
return "doas"
}
return ""
}
func parseTimeout() {
switch tool {
case "sudo":
out, err := exec.Command("sudo", "sudo", "-V").CombinedOutput()
if err != nil {
return
}
re := regexp.MustCompile(`Authentication timestamp timeout: (\d+)\..*`)
matches := re.FindStringSubmatch(string(out))
if len(matches) == 2 {
if minutes, err := strconv.Atoi(matches[1]); err == nil {
timeout = time.Duration(minutes) * time.Minute
}
}
case "doas":
exec.Command("doas", "true").Run()
}
}
+68 -57
View File
@@ -2,10 +2,10 @@ package alpm
import ( import (
"fmt" "fmt"
"os"
"time" "time"
"github.com/Jguer/dyalpm" "github.com/Jguer/dyalpm"
"github.com/Riyyi/declpac/pkg/log"
) )
var ( var (
@@ -19,9 +19,70 @@ type Handle struct {
syncDBs []dyalpm.Database syncDBs []dyalpm.Database
} }
func (h *Handle) LocalPackages() (map[string]dyalpm.Package, error) {
start := time.Now()
log.Debug("LocalPackages: starting...")
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)
}
log.Debug("LocalPackages: done (%.2fs)", time.Since(start).Seconds())
return localPkgs, nil
}
func (h *Handle) FindProvidingPackage(depName string) (string, bool) {
pkg := h.handle.FindDBSatisfier(h.syncDBs, depName)
if pkg != nil {
return pkg.Name(), true
}
return "", false
}
func (h *Handle) Release() error {
if h.handle != nil {
h.handle.Release()
}
return nil
}
func (h *Handle) SyncPackages(pkgNames []string) (map[string]dyalpm.Package, error) {
start := time.Now()
log.Debug("SyncPackages: starting...")
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)
}
}
log.Debug("SyncPackages: done (%.2fs)", time.Since(start).Seconds())
return syncPkgs, nil
}
func New() (*Handle, error) { func New() (*Handle, error) {
start := time.Now() start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] alpm.New: starting...\n") log.Debug("alpm.New: starting...")
handle, err := dyalpm.Initialize(Root, PacmanState) handle, err := dyalpm.Initialize(Root, PacmanState)
if err != nil { if err != nil {
@@ -48,7 +109,7 @@ func New() (*Handle, error) {
} }
} }
fmt.Fprintf(os.Stderr, "[debug] alpm.New: done (%.2fs)\n", time.Since(start).Seconds()) log.Debug("alpm.New: done (%.2fs)", time.Since(start).Seconds())
return &Handle{ return &Handle{
handle: handle, handle: handle,
localDB: localDB, localDB: localDB,
@@ -56,15 +117,11 @@ func New() (*Handle, error) {
}, nil }, nil
} }
func (h *Handle) Release() error { // -----------------------------------------
if h.handle != nil { // private
h.handle.Release()
}
return nil
}
func registerSyncDBs(handle dyalpm.Handle) ([]dyalpm.Database, error) { func registerSyncDBs(handle dyalpm.Handle) ([]dyalpm.Database, error) {
fmt.Fprintf(os.Stderr, "[debug] registerSyncDBs: starting...\n") log.Debug("registerSyncDBs: starting...")
repos := []string{"core", "extra", "multilib"} repos := []string{"core", "extra", "multilib"}
var dbs []dyalpm.Database var dbs []dyalpm.Database
@@ -86,52 +143,6 @@ func registerSyncDBs(handle dyalpm.Handle) ([]dyalpm.Database, error) {
} }
} }
fmt.Fprintf(os.Stderr, "[debug] registerSyncDBs: done (%d dbs)\n", len(dbs)) log.Debug("registerSyncDBs: done (%d dbs)", len(dbs))
return dbs, nil 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
}
+11 -5
View File
@@ -2,12 +2,12 @@ package aur
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"os"
"time" "time"
"github.com/Riyyi/declpac/pkg/log"
) )
var AURInfoURL = "https://aur.archlinux.org/rpc?v=5&type=info" var AURInfoURL = "https://aur.archlinux.org/rpc?v=5&type=info"
@@ -17,6 +17,12 @@ type Package struct {
PackageBase string `json:"PackageBase"` PackageBase string `json:"PackageBase"`
Version string `json:"Version"` Version string `json:"Version"`
URL string `json:"URL"` URL string `json:"URL"`
Depends []string `json:"Depends"`
MakeDepends []string `json:"MakeDepends"`
}
func (p Package) AllDepends() []string {
return append(p.Depends, p.MakeDepends...)
} }
type Response struct { type Response struct {
@@ -35,7 +41,7 @@ func New() *Client {
func (c *Client) Fetch(packages []string) (map[string]Package, error) { func (c *Client) Fetch(packages []string) (map[string]Package, error) {
start := time.Now() start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] aur.Fetch: starting...\n") log.Debug("aur.Fetch: starting...")
result := make(map[string]Package) result := make(map[string]Package)
@@ -51,7 +57,7 @@ func (c *Client) Fetch(packages []string) (map[string]Package, error) {
} }
if len(uncached) == 0 { if len(uncached) == 0 {
fmt.Fprintf(os.Stderr, "[debug] aur.Fetch: done (cached) (%.2fs)\n", time.Since(start).Seconds()) log.Debug("aur.Fetch: done (cached) (%.2fs)", time.Since(start).Seconds())
for _, pkg := range packages { for _, pkg := range packages {
result[pkg] = c.cache[pkg] result[pkg] = c.cache[pkg]
} }
@@ -84,7 +90,7 @@ func (c *Client) Fetch(packages []string) (map[string]Package, error) {
result[r.Name] = r result[r.Name] = r
} }
fmt.Fprintf(os.Stderr, "[debug] aur.Fetch: done (%.2fs)\n", time.Since(start).Seconds()) log.Debug("aur.Fetch: done (%.2fs)", time.Since(start).Seconds())
return result, nil return result, nil
} }
+37 -15
View File
@@ -2,11 +2,11 @@ package fetch
import ( import (
"fmt" "fmt"
"os"
"time" "time"
"github.com/Riyyi/declpac/pkg/fetch/alpm" "github.com/Riyyi/declpac/pkg/fetch/alpm"
"github.com/Riyyi/declpac/pkg/fetch/aur" "github.com/Riyyi/declpac/pkg/fetch/aur"
"github.com/Riyyi/declpac/pkg/log"
) )
type PackageInfo struct { type PackageInfo struct {
@@ -14,6 +14,7 @@ type PackageInfo struct {
InAUR bool InAUR bool
Exists bool Exists bool
Installed bool Installed bool
Provided string
AURInfo *aur.Package AURInfo *aur.Package
} }
@@ -22,9 +23,12 @@ type Fetcher struct {
aurClient *aur.Client aurClient *aur.Client
} }
// -----------------------------------------
// constructor
func New() (*Fetcher, error) { func New() (*Fetcher, error) {
start := time.Now() start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] fetch.Fetcher New: starting...\n") log.Debug("fetch.Fetcher New: starting...")
alpmHandle, err := alpm.New() alpmHandle, err := alpm.New()
if err != nil { if err != nil {
@@ -33,20 +37,15 @@ func New() (*Fetcher, error) {
aurClient := aur.New() aurClient := aur.New()
fmt.Fprintf(os.Stderr, "[debug] fetch.Fetcher New: done (%.2fs)\n", time.Since(start).Seconds()) log.Debug("fetch.Fetcher New: done (%.2fs)", time.Since(start).Seconds())
return &Fetcher{ return &Fetcher{
alpmHandle: alpmHandle, alpmHandle: alpmHandle,
aurClient: aurClient, aurClient: aurClient,
}, nil }, nil
} }
func (f *Fetcher) Close() error { // -----------------------------------------
return f.alpmHandle.Release() // public
}
func (f *Fetcher) GetAURPackage(name string) (aur.Package, bool) {
return f.aurClient.Get(name)
}
func (f *Fetcher) BuildLocalPkgMap() (map[string]interface{}, error) { func (f *Fetcher) BuildLocalPkgMap() (map[string]interface{}, error) {
localPkgs, err := f.alpmHandle.LocalPackages() localPkgs, err := f.alpmHandle.LocalPackages()
@@ -60,9 +59,25 @@ func (f *Fetcher) BuildLocalPkgMap() (map[string]interface{}, error) {
return result, nil return result, nil
} }
func (f *Fetcher) Close() error {
return f.alpmHandle.Release()
}
func (f *Fetcher) FetchAur(packages []string) (map[string]aur.Package, error) {
return f.aurClient.Fetch(packages)
}
func (f *Fetcher) FindProvidingPackage(depName string) (string, bool) {
return f.alpmHandle.FindProvidingPackage(depName)
}
func (f *Fetcher) GetAURPackage(name string) (aur.Package, bool) {
return f.aurClient.Get(name)
}
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] fetch.Resolve: starting...\n") log.Debug("fetch.Resolve: starting...")
result := make(map[string]*PackageInfo) result := make(map[string]*PackageInfo)
for _, pkg := range packages { for _, pkg := range packages {
@@ -73,7 +88,7 @@ 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] fetch.Resolve: sync db check done (%.2fs)\n", time.Since(start).Seconds()) log.Debug("fetch.Resolve: sync db check done (%.2fs)", time.Since(start).Seconds())
for pkg := range syncPkgs { for pkg := range syncPkgs {
result[pkg].Exists = true result[pkg].Exists = true
@@ -84,7 +99,7 @@ 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] fetch.Resolve: local pkgs built (%.2fs)\n", time.Since(start).Seconds()) log.Debug("fetch.Resolve: local pkgs built (%.2fs)", time.Since(start).Seconds())
for pkg := range localPkgs { for pkg := range localPkgs {
if info, ok := result[pkg]; ok { if info, ok := result[pkg]; ok {
@@ -101,7 +116,7 @@ func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error) {
if len(notInSync) > 0 { if len(notInSync) > 0 {
if _, err := f.aurClient.Fetch(notInSync); err != nil { if _, err := f.aurClient.Fetch(notInSync); err != nil {
fmt.Fprintf(os.Stderr, "[debug] fetch.Resolve: aur fetch error: %v\n", err) log.Debug("fetch.Resolve: aur fetch error: %v", err)
} }
for _, pkg := range packages { for _, pkg := range packages {
@@ -116,6 +131,13 @@ func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error) {
continue continue
} }
if providedBy, ok := f.FindProvidingPackage(pkg); ok {
log.Debug("fetch.Resolve: %s provided by %s", pkg, providedBy)
info.Provided = providedBy
info.Exists = true
continue
}
return nil, fmt.Errorf("package not found: %s", pkg) return nil, fmt.Errorf("package not found: %s", pkg)
} }
} }
@@ -127,6 +149,6 @@ func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error) {
} }
} }
fmt.Fprintf(os.Stderr, "[debug] fetch.Resolve: done (%.2fs)\n", time.Since(start).Seconds()) log.Debug("fetch.Resolve: done (%.2fs)", time.Since(start).Seconds())
return result, nil return result, nil
} }
+54 -9
View File
@@ -2,15 +2,43 @@ package input
import ( import (
"bufio" "bufio"
"errors"
"os" "os"
"path/filepath"
"strings" "strings"
"github.com/Riyyi/declpac/pkg/lib"
) )
var ErrEmptyList = errors.New("package list is empty")
// -----------------------------------------
// public
func Merge(packages map[string]bool) ([]string, error) {
result := make([]string, 0, len(packages))
for name := range packages {
result = append(result, name)
}
if len(result) == 0 {
return nil, ErrEmptyList
}
return result, nil
}
func ReadPackages(stateFiles []string) (map[string]bool, error) { func ReadPackages(stateFiles []string) (map[string]bool, error) {
packages := make(map[string]bool) packages := make(map[string]bool)
for _, file := range stateFiles { for _, file := range stateFiles {
if err := readStateFile(file, packages); err != nil { expanded := lib.ExpandPath(file)
if err := readStateFile(expanded, packages); err != nil {
return nil, err
}
}
implicitStateFile := getImplicitStateFile()
if fileExists(implicitStateFile) {
if err := readStateFile(implicitStateFile, packages); err != nil {
return nil, err return nil, err
} }
} }
@@ -22,6 +50,31 @@ func ReadPackages(stateFiles []string) (map[string]bool, error) {
return packages, nil return packages, nil
} }
// -----------------------------------------
// private
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func getImplicitStateFile() string {
cfgDir := os.Getenv("XDG_CONFIG_HOME")
if cfgDir == "" {
cfgDir = "~/.config"
}
cfgDir = lib.ExpandPath(cfgDir)
return filepath.Join(cfgDir, "declpac")
}
func normalizePackageName(name string) string {
name = strings.TrimSpace(name)
if name == "" || strings.HasPrefix(name, "#") {
return ""
}
return name
}
func readStateFile(path string, packages map[string]bool) error { func readStateFile(path string, packages map[string]bool) error {
file, err := os.Open(path) file, err := os.Open(path)
if err != nil { if err != nil {
@@ -60,11 +113,3 @@ func readStdin(packages map[string]bool) error {
return scanner.Err() return scanner.Err()
} }
func normalizePackageName(name string) string {
name = strings.TrimSpace(name)
if name == "" || strings.HasPrefix(name, "#") {
return ""
}
return name
}
+21
View File
@@ -0,0 +1,21 @@
package lib
import (
"os"
"path/filepath"
"strings"
)
// -----------------------------------------
// public
func ExpandPath(path string) string {
if strings.HasPrefix(path, "~/") {
home, err := os.UserHomeDir()
if err != nil {
return path
}
return filepath.Join(home, path[2:])
}
return path
}
+46 -21
View File
@@ -4,34 +4,19 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"github.com/Riyyi/declpac/pkg/lib"
) )
// -----------------------------------------
var logFile *os.File var logFile *os.File
var Verbose bool
// ----------------------------------------- // -----------------------------------------
// public
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
writeTimestamp()
return nil
}
func GetLogWriter() io.Writer {
return logFile
}
func Write(msg []byte) {
logFile.Write(msg)
}
func Close() error { func Close() error {
if logFile == nil { if logFile == nil {
@@ -40,7 +25,47 @@ func Close() error {
return logFile.Close() return logFile.Close()
} }
func Command(name string, args ...string) *exec.Cmd {
cmdStr := name + " " + strings.Join(args, " ")
fmt.Fprintf(logFile, "[cmd] %s\n", cmdStr)
return exec.Command(name, args...)
}
func Debug(format string, args ...any) {
if !Verbose {
return
}
fmt.Fprintf(os.Stderr, "[debug] "+format+"\n", args...)
}
func GetLogWriter() io.Writer {
return logFile
}
func OpenLog() error {
stateDir := os.Getenv("XDG_STATE_HOME")
if stateDir == "" {
stateDir = "~/.local/state"
}
stateDir = lib.ExpandPath(stateDir)
logPath := filepath.Join(stateDir, "declpac.log")
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
logFile = f
writeTimestamp()
return nil
}
func Write(msg []byte) {
logFile.Write(msg)
}
// ----------------------------------------- // -----------------------------------------
// private
func writeTimestamp() { func writeTimestamp() {
ts := time.Now().Format("2006-01-02 15:04:05") ts := time.Now().Format("2006-01-02 15:04:05")
-9
View File
@@ -1,9 +0,0 @@
package merge
func Merge(packages map[string]bool) []string {
result := make([]string, 0, len(packages))
for name := range packages {
result = append(result, name)
}
return result
}
+6 -3
View File
@@ -12,15 +12,18 @@ type Result struct {
ToRemove []string ToRemove []string
} }
// -----------------------------------------
// public
func Format(r *Result) string { func Format(r *Result) string {
var b strings.Builder var b strings.Builder
b.WriteString(fmt.Sprintf("Installed %d packages, removed %d packages", r.Installed, r.Removed)) b.WriteString(fmt.Sprintf("installed %d packages, removed %d packages", r.Installed, r.Removed))
if len(r.ToInstall) > 0 { if len(r.ToInstall) > 0 {
b.WriteString("\nWould install: ") b.WriteString("\nwould install: ")
b.WriteString(strings.Join(r.ToInstall, ", ")) b.WriteString(strings.Join(r.ToInstall, ", "))
} }
if len(r.ToRemove) > 0 { if len(r.ToRemove) > 0 {
b.WriteString("\nWould remove: ") b.WriteString("\nwould remove: ")
b.WriteString(strings.Join(r.ToRemove, ", ")) b.WriteString(strings.Join(r.ToRemove, ", "))
} }
return b.String() return b.String()
+95 -40
View File
@@ -3,6 +3,7 @@ package pacman
import ( import (
"fmt" "fmt"
"os" "os"
"slices"
"time" "time"
"github.com/Riyyi/declpac/pkg/fetch" "github.com/Riyyi/declpac/pkg/fetch"
@@ -12,9 +13,20 @@ import (
"github.com/Riyyi/declpac/pkg/pacman/sync" "github.com/Riyyi/declpac/pkg/pacman/sync"
) )
func Sync(packages []string) (*output.Result, error) { func Sync(packages []string, noCheck bool, prune bool) (*output.Result, error) {
start := time.Now() start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] Sync: starting...\n") log.Debug("Sync: starting...")
explicitList, err := read.ExplicitList()
if err != nil {
return nil, err
}
explicitCount := len(explicitList)
if !noCheck && len(packages) < explicitCount/2 {
errMsg := "safety check: state packages (%d) less than half of explicitly installed (%d), override with --nocheck"
return nil, fmt.Errorf(errMsg, len(packages), explicitCount)
}
list, err := read.List() list, err := read.List()
if err != nil { if err != nil {
@@ -31,56 +43,70 @@ func Sync(packages []string) (*output.Result, error) {
return nil, err return nil, err
} }
} }
fmt.Fprintf(os.Stderr, "[debug] Sync: database fresh (%.2fs)\n", time.Since(start).Seconds()) log.Debug("Sync: database fresh (%.2fs)", time.Since(start).Seconds())
f, err := fetch.New() f, err := fetch.New()
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer f.Close() defer f.Close()
fmt.Fprintf(os.Stderr, "[debug] Sync: initialized fetcher (%.2fs)\n", time.Since(start).Seconds()) log.Debug("Sync: initialized fetcher (%.2fs)", time.Since(start).Seconds())
fmt.Fprintf(os.Stderr, "[debug] Sync: categorizing packages...\n") log.Debug("Sync: categorizing packages...")
pacmanPkgs, aurPkgs, err := categorizePackages(f, packages) pacmanPkgs, aurPkgs, err := categorizePackages(f, packages)
if err != nil { if err != nil {
return nil, err return nil, err
} }
fmt.Fprintf(os.Stderr, "[debug] Sync: packages categorized (%.2fs)\n", time.Since(start).Seconds()) log.Debug("Sync: packages categorized (%.2fs)", time.Since(start).Seconds())
if len(pacmanPkgs) > 0 { if len(pacmanPkgs) > 0 {
fmt.Fprintf(os.Stderr, "[debug] Sync: syncing %d pacman packages...\n", len(pacmanPkgs)) log.Debug("Sync: syncing %d pacman packages...", len(pacmanPkgs))
if err := sync.SyncPackages(pacmanPkgs, log.GetLogWriter()); err != nil { if err := sync.SyncPackages(pacmanPkgs, log.GetLogWriter()); err != nil {
return nil, err return nil, err
} }
fmt.Fprintf(os.Stderr, "[debug] Sync: pacman packages synced (%.2fs)\n", time.Since(start).Seconds()) log.Debug("Sync: pacman packages synced (%.2fs)", time.Since(start).Seconds())
} }
for _, pkg := range aurPkgs { for _, pkg := range aurPkgs {
fmt.Fprintf(os.Stderr, "[debug] Sync: installing AUR package %s...\n", pkg) if slices.Contains(list, pkg) {
continue
}
log.Debug("Sync: installing AUR package %s...", pkg)
aurInfo, ok := f.GetAURPackage(pkg) aurInfo, ok := f.GetAURPackage(pkg)
if !ok { if !ok {
return nil, fmt.Errorf("AUR package not found in cache: %s", pkg) return nil, fmt.Errorf("AUR package not found in cache: %s", pkg)
} }
if err := sync.InstallAUR(pkg, aurInfo.PackageBase, log.GetLogWriter()); err != nil { if err := sync.InstallAUR(f, pkg, aurInfo.PackageBase, false, 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()) log.Debug("Sync: AUR package %s installed (%.2fs)", pkg, time.Since(start).Seconds())
} }
fmt.Fprintf(os.Stderr, "[debug] Sync: marking all as deps...\n") var removed int
markAllAsDeps() if prune {
fmt.Fprintf(os.Stderr, "[debug] Sync: all marked as deps (%.2fs)\n", time.Since(start).Seconds()) log.Debug("Sync: running prune sanity check...")
if err := pruneSanityCheck(packages); err != nil {
return nil, err
}
log.Debug("Sync: prune sanity check passed (%.2fs)", time.Since(start).Seconds())
fmt.Fprintf(os.Stderr, "[debug] Sync: marking state packages as explicit...\n") log.Debug("Sync: marking all as deps...")
if err := markAllAsDeps(); err != nil {
return nil, err
}
log.Debug("Sync: all marked as deps (%.2fs)", time.Since(start).Seconds())
log.Debug("Sync: marking state packages as explicit...")
if err := sync.MarkAs(packages, "explicit", log.GetLogWriter()); 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()) log.Debug("Sync: state packages marked as explicit (%.2fs)", time.Since(start).Seconds())
removed, err := cleanupOrphans() removed, err = cleanupOrphans()
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
list, _ = read.List() list, _ = read.List()
if err != nil { if err != nil {
@@ -90,16 +116,19 @@ func Sync(packages []string) (*output.Result, error) {
installedCount := max(after-before, 0) installedCount := max(after-before, 0)
fmt.Fprintf(os.Stderr, "[debug] Sync: done (%.2fs)\n", time.Since(start).Seconds()) log.Debug("Sync: done (%.2fs)", time.Since(start).Seconds())
return &output.Result{ return &output.Result{
Installed: installedCount, Installed: installedCount,
Removed: removed, Removed: removed,
}, nil }, nil
} }
// -----------------------------------------
// private
func categorizePackages(f *fetch.Fetcher, packages []string) (pacmanPkgs, aurPkgs []string, err error) { func categorizePackages(f *fetch.Fetcher, packages []string) (pacmanPkgs, aurPkgs []string, err error) {
start := time.Now() start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] categorizePackages: starting...\n") log.Debug("categorizePackages: starting...")
resolved, err := f.Resolve(packages) resolved, err := f.Resolve(packages)
if err != nil { if err != nil {
@@ -119,31 +148,13 @@ func categorizePackages(f *fetch.Fetcher, packages []string) (pacmanPkgs, aurPkg
} }
} }
fmt.Fprintf(os.Stderr, "[debug] categorizePackages: done (%.2fs)\n", time.Since(start).Seconds()) log.Debug("categorizePackages: done (%.2fs)", time.Since(start).Seconds())
return pacmanPkgs, aurPkgs, nil return pacmanPkgs, aurPkgs, nil
} }
func markAllAsDeps() error {
start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] markAllAsDeps: starting...\n")
packages, err := read.List()
if err != nil || len(packages) == 0 {
return fmt.Errorf("failed to list packages: %w", err)
}
if err := sync.MarkAs(packages, "deps", log.GetLogWriter()); err != nil {
log.Write([]byte(fmt.Sprintf("error: %v\n", err)))
return err
}
fmt.Fprintf(os.Stderr, "[debug] markAllAsDeps: done (%.2fs)\n", time.Since(start).Seconds())
return nil
}
func cleanupOrphans() (int, error) { func cleanupOrphans() (int, error) {
start := time.Now() start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] cleanupOrphans: starting...\n") log.Debug("cleanupOrphans: starting...")
orphans, err := read.ListOrphans() orphans, err := read.ListOrphans()
if err != nil { if err != nil {
@@ -157,6 +168,50 @@ func cleanupOrphans() (int, error) {
return 0, err return 0, err
} }
fmt.Fprintf(os.Stderr, "[debug] cleanupOrphans: done (%.2fs)\n", time.Since(start).Seconds()) log.Debug("cleanupOrphans: done (%.2fs)", time.Since(start).Seconds())
return removed, nil return removed, nil
} }
func markAllAsDeps() error {
start := time.Now()
log.Debug("markAllAsDeps: starting...")
packages, err := read.List()
if err != nil || len(packages) == 0 {
return fmt.Errorf("failed to list packages: %w", err)
}
if err := sync.MarkAs(packages, "deps", log.GetLogWriter()); err != nil {
log.Write([]byte(fmt.Sprintf("error: %v\n", err)))
return err
}
log.Debug("markAllAsDeps: done (%.2fs)", time.Since(start).Seconds())
return nil
}
// pruneSanityCheck checks if the installation of all state packages succeeded,
// before attempting to do package marking and orphan cleanup.
func pruneSanityCheck(statePackages []string) error {
start := time.Now()
log.Debug("pruneSanityCheck: starting...")
localPackages, err := read.List()
if err != nil {
return fmt.Errorf("failed to list local packages: %w", err)
}
var missing []string
for _, pkg := range statePackages {
if !slices.Contains(localPackages, pkg) {
missing = append(missing, pkg)
}
}
if len(missing) > 0 {
return fmt.Errorf("safety check: missing state packages: %v", missing)
}
log.Debug("pruneSanityCheck: done (%.2fs)", time.Since(start).Seconds())
return nil
}
+73 -44
View File
@@ -1,6 +1,8 @@
package read package read
import ( import (
"bytes"
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@@ -8,49 +10,12 @@ import (
"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"
) )
var LockFile = "/var/lib/pacman/db.lock" 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) { func DBFreshness() (bool, error) {
info, err := os.Stat(LockFile) info, err := os.Stat(LockFile)
if err != nil { if err != nil {
@@ -63,20 +28,20 @@ func DBFreshness() (bool, error) {
func DryRun(packages []string) (*output.Result, error) { func DryRun(packages []string) (*output.Result, error) {
start := time.Now() start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] DryRun: starting...\n") log.Debug("DryRun: starting...")
f, err := fetch.New() f, err := fetch.New()
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer f.Close() defer f.Close()
fmt.Fprintf(os.Stderr, "[debug] DryRun: initialized fetcher (%.2fs)\n", time.Since(start).Seconds()) log.Debug("DryRun: initialized fetcher (%.2fs)", time.Since(start).Seconds())
resolved, err := f.Resolve(packages) resolved, err := f.Resolve(packages)
if err != nil { if err != nil {
return nil, err return nil, err
} }
fmt.Fprintf(os.Stderr, "[debug] DryRun: packages resolved (%.2fs)\n", time.Since(start).Seconds()) log.Debug("DryRun: packages resolved (%.2fs)", time.Since(start).Seconds())
var toInstall []string var toInstall []string
var aurPkgs []string var aurPkgs []string
@@ -91,13 +56,13 @@ func DryRun(packages []string) (*output.Result, error) {
toInstall = append(toInstall, pkg) toInstall = append(toInstall, pkg)
} }
} }
fmt.Fprintf(os.Stderr, "[debug] DryRun: packages categorized (%.2fs)\n", time.Since(start).Seconds()) log.Debug("DryRun: packages categorized (%.2fs)", time.Since(start).Seconds())
orphans, err := ListOrphans() orphans, err := ListOrphans()
if err != nil { if err != nil {
return nil, err return nil, err
} }
fmt.Fprintf(os.Stderr, "[debug] DryRun: orphans listed (%.2fs)\n", time.Since(start).Seconds()) log.Debug("DryRun: orphans listed (%.2fs)", time.Since(start).Seconds())
pkgSet := make(map[string]bool) pkgSet := make(map[string]bool)
for _, p := range packages { for _, p := range packages {
@@ -110,7 +75,7 @@ func DryRun(packages []string) (*output.Result, error) {
} }
} }
fmt.Fprintf(os.Stderr, "[debug] DryRun: done (%.2fs)\n", time.Since(start).Seconds()) log.Debug("DryRun: done (%.2fs)", time.Since(start).Seconds())
return &output.Result{ return &output.Result{
Installed: len(toInstall) + len(aurPkgs), Installed: len(toInstall) + len(aurPkgs),
Removed: len(toRemove), Removed: len(toRemove),
@@ -118,3 +83,67 @@ func DryRun(packages []string) (*output.Result, error) {
ToRemove: toRemove, ToRemove: toRemove,
}, nil }, nil
} }
func ExplicitList() ([]string, error) {
start := time.Now()
log.Debug("ExplicitList: starting...")
cmd := exec.Command("pacman", "-Qqe")
output, err := cmd.Output()
if err != nil {
return nil, err
}
list := strings.Split(strings.TrimSpace(string(output)), "\n")
if len(list) > 0 && list[0] == "" {
list = nil
}
log.Debug("ExplicitList: done (%.2fs)", time.Since(start).Seconds())
return list, nil
}
func List() ([]string, error) {
start := time.Now()
log.Debug("List: starting...")
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
}
log.Debug("List: done (%.2fs)", time.Since(start).Seconds())
return list, nil
}
func ListOrphans() ([]string, error) {
start := time.Now()
log.Debug("ListOrphans: starting...")
cmd := exec.Command("pacman", "-Qdtq")
var stderr bytes.Buffer
cmd.Stderr = &stderr
output, err := cmd.Output()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 && stderr.Len() == 0 {
return nil, nil
}
return nil, err
}
orphans := strings.Split(strings.TrimSpace(string(output)), "\n")
if len(orphans) > 0 && orphans[0] == "" {
orphans = orphans[1:]
}
log.Debug("ListOrphans: done (%.2fs)", time.Since(start).Seconds())
return orphans, nil
}
+210 -72
View File
@@ -4,9 +4,14 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec" "os/user"
"strings" "strings"
"time" "time"
"github.com/Riyyi/declpac/pkg/auth"
"github.com/Riyyi/declpac/pkg/fetch"
"github.com/Riyyi/declpac/pkg/fetch/aur"
"github.com/Riyyi/declpac/pkg/log"
) )
type Result struct { type Result struct {
@@ -14,43 +19,48 @@ type Result struct {
Removed int Removed int
} }
func SyncPackages(packages []string, logWriter io.Writer) error { // -----------------------------------------
// public
func InstallAUR(f *fetch.Fetcher, pkgName string, packageBase string, asDeps bool, logWriter io.Writer) error {
start := time.Now() start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] SyncPackages: starting...\n") log.Debug("InstallAUR: starting...")
if logWriter == nil { if logWriter == nil {
logWriter = os.Stderr logWriter = os.Stderr
} }
args := append([]string{"-S", "--needed"}, packages...) aurInfo := getAURInfo(f, pkgName, packageBase)
cmd := exec.Command("pacman", args...) if err := resolveAndInstallDeps(f, aurInfo, logWriter); err != nil {
cmd.Stdout = logWriter return err
cmd.Stderr = logWriter }
err := cmd.Run()
tmpDir := getTempDirName() + "/" + pkgName
if err := createTempDir(tmpDir); err != nil {
return err
}
defer os.RemoveAll(tmpDir)
if err := cloneRepo(packageBase, tmpDir, logWriter); err != nil {
return err
}
log.Debug("InstallAUR: cloned (%.2fs)", time.Since(start).Seconds())
if err := buildPackage(tmpDir, asDeps, logWriter); err != nil {
return err
}
log.Debug("InstallAUR: built (%.2fs)", time.Since(start).Seconds())
pkgFile, err := findPKGFile(pkgName, tmpDir)
if err != nil { if err != nil {
return fmt.Errorf("pacman sync failed: %w", err) return fmt.Errorf("failed to find built package: %w", err)
} }
fmt.Fprintf(os.Stderr, "[debug] SyncPackages: done (%.2fs)\n", time.Since(start).Seconds()) if err := installBuiltPackage(pkgFile, logWriter); err != nil {
return nil return err
}
func RefreshDB(logWriter io.Writer) error {
start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] RefreshDB: starting...\n")
if logWriter == nil {
logWriter = os.Stderr
} }
log.Debug("InstallAUR: done (%.2fs)", time.Since(start).Seconds())
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 return nil
} }
@@ -60,14 +70,14 @@ func MarkAs(packages []string, flag string, logWriter io.Writer) error {
} }
start := time.Now() start := time.Now()
flagName := map[string]string{"deps": "asdeps", "explicit": "asexplicit"}[flag] flagName := map[string]string{"deps": "asdeps", "explicit": "asexplicit"}[flag]
fmt.Fprintf(os.Stderr, "[debug] MarkAs(%s): starting...\n", flag) log.Debug("MarkAs(%s): starting...", flag)
if logWriter == nil { if logWriter == nil {
logWriter = os.Stderr logWriter = os.Stderr
} }
args := append([]string{"-D", "--" + flagName}, packages...) args := append([]string{"-D", "--" + flagName}, packages...)
cmd := exec.Command("pacman", args...) cmd := auth.Command("pacman", args...)
cmd.Stdout = logWriter cmd.Stdout = logWriter
cmd.Stderr = logWriter cmd.Stderr = logWriter
err := cmd.Run() err := cmd.Run()
@@ -75,27 +85,46 @@ func MarkAs(packages []string, flag string, logWriter io.Writer) error {
return fmt.Errorf("mark as %s failed: %w", flag, err) 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()) log.Debug("MarkAs(%s): done (%.2fs)", flag, time.Since(start).Seconds())
return nil
}
func RefreshDB(logWriter io.Writer) error {
start := time.Now()
log.Debug("RefreshDB: starting...")
if logWriter == nil {
logWriter = os.Stderr
}
cmd := auth.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)
}
log.Debug("RefreshDB: done (%.2fs)", time.Since(start).Seconds())
return nil return nil
} }
func RemoveOrphans(orphans []string, logWriter io.Writer) (int, error) { func RemoveOrphans(orphans []string, logWriter io.Writer) (int, error) {
start := time.Now() start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] RemoveOrphans: starting...\n") log.Debug("RemoveOrphans: starting...")
if logWriter == nil { if logWriter == nil {
logWriter = os.Stderr logWriter = os.Stderr
} }
if len(orphans) == 0 { if len(orphans) == 0 {
fmt.Fprintf(os.Stderr, "[debug] RemoveOrphans: done (no orphans) (%.2fs)\n", time.Since(start).Seconds()) log.Debug("RemoveOrphans: done (no orphans) (%.2fs)", time.Since(start).Seconds())
return 0, nil return 0, nil
} }
args := make([]string, 0, 2+len(orphans)) args := make([]string, 0, 3+len(orphans))
args = append(args, "pacman", "-Rns") args = append(args, "pacman", "-Rns", "--noconfirm")
args = append(args, orphans...) args = append(args, orphans...)
removeCmd := exec.Command(args[0], args[1:]...) removeCmd := auth.Command(args[0], args[1:]...)
removeCmd.Stdout = logWriter removeCmd.Stdout = logWriter
removeCmd.Stderr = logWriter removeCmd.Stderr = logWriter
err := removeCmd.Run() err := removeCmd.Run()
@@ -105,76 +134,185 @@ func RemoveOrphans(orphans []string, logWriter io.Writer) (int, error) {
count := len(orphans) count := len(orphans)
fmt.Fprintf(os.Stderr, "[debug] RemoveOrphans: done (%d) (%.2fs)\n", count, time.Since(start).Seconds()) log.Debug("RemoveOrphans: done (%d) (%.2fs)", count, time.Since(start).Seconds())
return count, nil return count, nil
} }
func InstallAUR(pkgName string, packageBase string, logWriter io.Writer) error { func SyncPackages(packages []string, logWriter io.Writer) error {
start := time.Now() start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] InstallAUR: starting...\n") log.Debug("SyncPackages: starting...")
if logWriter == nil { if logWriter == nil {
logWriter = os.Stderr logWriter = os.Stderr
} }
sudoUser := os.Getenv("SUDO_USER") args := append([]string{"-S", "--needed", "--noconfirm"}, packages...)
if sudoUser == "" { cmd := auth.Command("pacman", args...)
sudoUser = os.Getenv("USER") cmd.Stdout = logWriter
if sudoUser == "" { cmd.Stderr = logWriter
sudoUser = "root" err := cmd.Run()
} if err != nil {
return fmt.Errorf("pacman sync failed: %w", err)
} }
tmpDir := "/tmp/declpac-aur-" + pkgName log.Debug("SyncPackages: done (%.2fs)", time.Since(start).Seconds())
mkdirCmd := exec.Command("su", "-", sudoUser, "-c", "rm -rf "+tmpDir+" && mkdir -p "+tmpDir) return nil
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) // private
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") func buildPackage(tmpDir string, asDeps bool, logWriter io.Writer) error {
makepkgArgs := []string{"-D", tmpDir, "-s", "--noconfirm"}
if asDeps {
makepkgArgs = append(makepkgArgs, "--asdeps")
}
makepkgCmd := log.Command("makepkg", makepkgArgs...)
makepkgCmd.Stdout = logWriter makepkgCmd.Stdout = logWriter
makepkgCmd.Stderr = logWriter makepkgCmd.Stderr = logWriter
if err := makepkgCmd.Run(); err != nil { if err := makepkgCmd.Run(); err != nil {
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()) return nil
}
pkgFile, err := findPKGFile(tmpDir) func cloneRepo(packageBase string, tmpDir string, logWriter io.Writer) error {
if err != nil { cloneURL := "https://aur.archlinux.org/" + packageBase + ".git"
return fmt.Errorf("failed to find built package: %w", err) cloneCmd := log.Command("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)
}
return nil
}
func createTempDir(tmpDir string) error {
if tmpDir == "" || tmpDir == "/" || !strings.HasPrefix(tmpDir, "/tmp") {
return fmt.Errorf("safety check: prevented malformed rm -rf call")
} }
installCmd := exec.Command("pacman", "-U", "--noconfirm", pkgFile) rmdirCmd := log.Command("rm", "-rf", tmpDir)
installCmd.Stdout = logWriter if err := rmdirCmd.Run(); err != nil {
installCmd.Stderr = logWriter return fmt.Errorf("failed to remove temp directory: %w", err)
if err := installCmd.Run(); err != nil { }
return fmt.Errorf("failed to install package: %w", err)
mkdirCmd := log.Command("mkdir", "-p", tmpDir)
if err := mkdirCmd.Run(); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
} }
fmt.Fprintf(os.Stderr, "[debug] InstallAUR: done (%.2fs)\n", time.Since(start).Seconds())
return nil return nil
} }
func findPKGFile(dir string) (string, error) { func findPKGFile(pkgName string, dir string) (string, error) {
entries, err := os.ReadDir(dir) entries, err := os.ReadDir(dir)
if err != nil { if err != nil {
return "", err return "", err
} }
for _, entry := range entries { for _, entry := range entries {
name := entry.Name() name := entry.Name()
if strings.HasSuffix(name, ".pkg.tar.zst") || strings.HasSuffix(name, ".pkg.tar.gz") { if !strings.HasSuffix(name, ".pkg.tar.zst") && !strings.HasSuffix(name, ".pkg.tar.gz") {
return strings.Join([]string{dir, name}, "/"), nil continue
} }
// Skip packages that do not start with the exact package name, ex: sunshine-bin -> sunshine
if !strings.HasPrefix(name, pkgName) {
continue
}
// Skip packages that provide a debug package, ex: sunshine-bin -> sunshine-debug
if strings.HasPrefix(name, pkgName+"-debug") {
continue
}
return strings.Join([]string{dir, name}, "/"), nil
} }
return "", fmt.Errorf("no package file found in %s", dir) return "", fmt.Errorf("no package file found in %s", dir)
} }
func getAURInfo(f *fetch.Fetcher, pkgName string, packageBase string) *aur.Package {
if packageBase == "" {
return nil
}
info, ok := f.GetAURPackage(pkgName)
if !ok {
return nil
}
return &info
}
func getTempDirName() string {
user, err := user.Current()
if err != nil {
return "/tmp/declpac"
}
return "/tmp/declpac-" + user.Username
}
func installBuiltPackage(pkgFile string, logWriter io.Writer) error {
installCmd := auth.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)
}
return nil
}
func resolveAndInstallDeps(f *fetch.Fetcher, aurInfo *aur.Package, logWriter io.Writer) error {
if aurInfo == nil {
return nil
}
depends := aurInfo.AllDepends()
if len(depends) == 0 {
return nil
}
resolved, err := f.Resolve(depends)
if err != nil {
return fmt.Errorf("failed to resolve dependencies: %w", err)
}
var repoDeps, aurDeps []string
for _, dep := range depends {
info := resolved[dep]
if info.Installed {
continue
}
pkg := dep
if info.Provided != "" {
pkg = info.Provided
}
if info.Exists {
repoDeps = append(repoDeps, pkg)
} else if info.InAUR {
aurDeps = append(aurDeps, pkg)
}
}
if len(repoDeps) > 0 {
if err := SyncPackages(repoDeps, logWriter); err != nil {
return fmt.Errorf("failed to install repo dependencies: %w", err)
}
}
if len(aurDeps) == 0 {
return nil
}
fetched, err := f.FetchAur(aurDeps)
if err != nil {
log.Debug("sync.resolveAndInstallDeps: aur fetch error: %v", err)
}
for _, dep := range aurDeps {
depInfo, ok := fetched[dep]
if !ok {
continue
}
if err := InstallAUR(f, dep, depInfo.PackageBase, true, logWriter); err != nil {
return err
}
}
return nil
}
+6
View File
@@ -5,6 +5,12 @@
"source": "JuliusBrussee/caveman", "source": "JuliusBrussee/caveman",
"sourceType": "github", "sourceType": "github",
"computedHash": "a818cdc41dcfaa50dd891c5cb5e5705968338de02e7e37949ca56e8c30ad4176" "computedHash": "a818cdc41dcfaa50dd891c5cb5e5705968338de02e7e37949ca56e8c30ad4176"
},
"grill-me": {
"source": "mattpocock/skills",
"sourceType": "github",
"skillPath": "skills/productivity/grill-me/SKILL.md",
"computedHash": "784f0dbb7403b0f00324bce9a112f715342777a0daee7bbb7385f9c6f0a170ea"
} }
} }
} }