Add dry-run flag and implement AUR support
This commit is contained in:
@@ -72,6 +72,7 @@ bash # bash
|
|||||||
|------|-------|-------------|
|
|------|-------|-------------|
|
||||||
| `--state` | `-s` | State file(s) 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) |
|
||||||
| `--yes` | `-y` | Skip confirmation prompts (for scripting) |
|
| `--yes` | `-y` | Skip confirmation prompts (for scripting) |
|
||||||
|
| `--dry-run` | | Simulate sync without making changes |
|
||||||
| `--help` | `-h` | Show help message |
|
| `--help` | `-h` | Show help message |
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
@@ -114,6 +115,18 @@ sudo declpac --state ~/.config/declpac/base.txt --state ~/.config/declpac/deskto
|
|||||||
cat ~/.config/declpac/full-system.txt | sudo declpac
|
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. **Collect packages** — Reads from all `--state` files and stdin
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
StateFiles []string
|
StateFiles []string
|
||||||
NoConfirm bool
|
NoConfirm bool
|
||||||
|
DryRun bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -38,6 +39,11 @@ func main() {
|
|||||||
Usage: "Skip confirmation prompts",
|
Usage: "Skip confirmation prompts",
|
||||||
Destination: &cfg.NoConfirm,
|
Destination: &cfg.NoConfirm,
|
||||||
},
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "dry-run",
|
||||||
|
Usage: "Simulate the sync without making changes",
|
||||||
|
Destination: &cfg.DryRun,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
return run(cfg)
|
return run(cfg)
|
||||||
@@ -63,6 +69,16 @@ func run(cfg *Config) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.DryRun {
|
||||||
|
result, err := pacman.DryRun(merged)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(output.Format(result))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
result, err := pacman.Sync(merged)
|
result, err := pacman.Sync(merged)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ package list, with all packages at their latest available versions.
|
|||||||
|
|
||||||
**AUR Integration**
|
**AUR Integration**
|
||||||
- First attempt: Try pacman -Syu for all packages (includes AUR auto-install if enabled)
|
- First attempt: Try pacman -Syu for all packages (includes AUR auto-install if enabled)
|
||||||
- For packages not found in pacman repos: Check AUR via Jguer/aur library
|
- For packages not found in pacman repos: Batch query AUR via info endpoint (single HTTP request for multiple packages)
|
||||||
- If package in AUR: Build and install with makepkg (no AUR helpers)
|
- If package in AUR: Build and install with makepkg (no AUR helpers)
|
||||||
- AUR packages should also upgrade to latest version (no partial updates)
|
- AUR packages should also upgrade to latest version (no partial updates)
|
||||||
- Clone AUR git repo to temp directory
|
- Clone AUR git repo to temp directory
|
||||||
@@ -110,10 +110,12 @@ package list, with all packages at their latest available versions.
|
|||||||
- Multiple --state flags allowed, all additive
|
- Multiple --state flags allowed, all additive
|
||||||
- Stdin input via standard input stream
|
- Stdin input via standard input stream
|
||||||
- No interactive prompts - fully automated
|
- No interactive prompts - fully automated
|
||||||
|
- `--dry-run`: Simulate sync without making changes, print what would be installed/removed
|
||||||
|
|
||||||
**Output Format**
|
**Output Format**
|
||||||
- Success: Print to stdout: `Installed X packages, removed Y packages`
|
- Success: Print to stdout: `Installed X packages, removed Y packages`
|
||||||
- No changes: Print `Installed 0 packages, removed 0 packages`
|
- No changes: Print `Installed 0 packages, removed 0 packages`
|
||||||
|
- Dry-run: Print `Installed X packages, removed Y packages` with `Would install: ...` and `Would remove: ...` lines
|
||||||
- Errors: Print error message to stderr
|
- Errors: Print error message to stderr
|
||||||
- Exit codes: 0 for success, 1 for errors
|
- Exit codes: 0 for success, 1 for errors
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ list, ensuring all packages are at the latest version.
|
|||||||
- Machine-readable output (install/remove counts, exit codes)
|
- Machine-readable output (install/remove counts, exit codes)
|
||||||
- No conflict resolution for missing packages (append-only)
|
- No conflict resolution for missing packages (append-only)
|
||||||
- Print error to stderr for empty state input and exit with code 1
|
- Print error to stderr for empty state input and exit with code 1
|
||||||
|
- Dry-run mode: simulate sync without making changes, show what would be installed/removed
|
||||||
|
|
||||||
## Capabilities
|
## Capabilities
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ latest versions.
|
|||||||
|
|
||||||
#### Scenario: Fall back to AUR
|
#### Scenario: Fall back to AUR
|
||||||
- **WHEN** package is not in pacman repositories but is in AUR
|
- **WHEN** package is not in pacman repositories but is in AUR
|
||||||
- **THEN** query AUR via Jguer/aur library
|
- **THEN** batch query AUR via info endpoint (multiple packages in single request)
|
||||||
- **AND** build and install with makepkg -si
|
- **AND** build and install with makepkg -si
|
||||||
|
|
||||||
#### Scenario: Upgrade AUR packages
|
#### Scenario: Upgrade AUR packages
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Dry-Run Mode
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Add `--dry-run` flag to simulate the sync operation without making any changes
|
||||||
|
to the system. Shows what packages would be installed and what would be removed.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
Users want to preview the effects of a sync operation before committing changes.
|
||||||
|
This is useful for:
|
||||||
|
- Verifying the intended changes are correct
|
||||||
|
- Avoiding unintended package installations
|
||||||
|
- Understanding what orphan cleanup will remove
|
||||||
|
|
||||||
|
## Interface
|
||||||
|
|
||||||
|
```
|
||||||
|
declpac --dry-run --state packages.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
1. Read state files and stdin (same as normal mode)
|
||||||
|
2. Validate packages exist (same as normal mode)
|
||||||
|
3. Query current installed packages via `pacman -Qq`
|
||||||
|
4. Compare declared packages to current state
|
||||||
|
5. Identify packages that would be installed (not currently installed)
|
||||||
|
6. Identify orphans that would be removed via `pacman -Qdtq`
|
||||||
|
7. Output results with "Would install:" and "Would remove:" sections
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
```
|
||||||
|
Installed 3 packages, removed 2 packages
|
||||||
|
Would install: vim, git, docker
|
||||||
|
Would remove: python2, perl-xml-parser
|
||||||
|
```
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Actual package operations (no pacman -Syu, no pacman -Rns execution)
|
||||||
|
- Package version comparison
|
||||||
|
- Detailed dependency analysis
|
||||||
|
|
||||||
|
## Trade-offs
|
||||||
|
|
||||||
|
- Doesn't predict transitive dependencies that pacman might install
|
||||||
|
- Orphan list may change after packages are installed
|
||||||
@@ -93,3 +93,12 @@
|
|||||||
- [x] 10.9 Test AUR fallback with makepkg for AUR package
|
- [x] 10.9 Test AUR fallback with makepkg for AUR package
|
||||||
- [x] 10.10 Test error handling for missing packages
|
- [x] 10.10 Test error handling for missing packages
|
||||||
- [x] 10.11 Generate final binary
|
- [x] 10.11 Generate final binary
|
||||||
|
|
||||||
|
## 11. Dry-Run Mode
|
||||||
|
|
||||||
|
- [x] 11.1 Add --dry-run flag to CLI argument parsing
|
||||||
|
- [x] 11.2 Implement DryRun function to query current state
|
||||||
|
- [x] 11.3 Compare declared packages to current installations
|
||||||
|
- [x] 11.4 Identify packages to install (not currently installed)
|
||||||
|
- [x] 11.5 Identify orphans to remove via pacman -Qdtq
|
||||||
|
- [x] 11.6 Output "Would install:" and "Would remove:" sections
|
||||||
|
|||||||
+17
-2
@@ -1,12 +1,27 @@
|
|||||||
package output
|
package output
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
type Result struct {
|
type Result struct {
|
||||||
Installed int
|
Installed int
|
||||||
Removed int
|
Removed int
|
||||||
|
ToInstall []string
|
||||||
|
ToRemove []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Format(r *Result) string {
|
func Format(r *Result) string {
|
||||||
return fmt.Sprintf("Installed %d packages, removed %d packages", r.Installed, r.Removed)
|
var b strings.Builder
|
||||||
|
b.WriteString(fmt.Sprintf("Installed %d packages, removed %d packages", r.Installed, r.Removed))
|
||||||
|
if len(r.ToInstall) > 0 {
|
||||||
|
b.WriteString("\nWould install: ")
|
||||||
|
b.WriteString(strings.Join(r.ToInstall, ", "))
|
||||||
|
}
|
||||||
|
if len(r.ToRemove) > 0 {
|
||||||
|
b.WriteString("\nWould remove: ")
|
||||||
|
b.WriteString(strings.Join(r.ToRemove, ", "))
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|||||||
+207
-6
@@ -1,7 +1,11 @@
|
|||||||
package pacman
|
package pacman
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -14,12 +18,15 @@ import (
|
|||||||
var (
|
var (
|
||||||
Root = "/"
|
Root = "/"
|
||||||
LockFile = "/var/lib/pacman/db.lock"
|
LockFile = "/var/lib/pacman/db.lock"
|
||||||
|
AURInfoURL = "https://aur.archlinux.org/rpc?v=5&type=info"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Pac struct{}
|
type Pac struct {
|
||||||
|
aurCache map[string]AURPackage
|
||||||
|
}
|
||||||
|
|
||||||
func New() (*Pac, error) {
|
func New() (*Pac, error) {
|
||||||
return &Pac{}, nil
|
return &Pac{aurCache: make(map[string]AURPackage)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Pac) Close() error {
|
func (p *Pac) Close() error {
|
||||||
@@ -30,6 +37,18 @@ type PackageInfo struct {
|
|||||||
Name string
|
Name string
|
||||||
InAUR bool
|
InAUR bool
|
||||||
Exists bool
|
Exists bool
|
||||||
|
AURInfo *AURPackage
|
||||||
|
}
|
||||||
|
|
||||||
|
type AURResponse struct {
|
||||||
|
Results []AURPackage `json:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AURPackage struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
PackageBase string `json:"PackageBase"`
|
||||||
|
Version string `json:"Version"`
|
||||||
|
URL string `json:"URL"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Pac) ValidatePackage(name string) (*PackageInfo, error) {
|
func (p *Pac) ValidatePackage(name string) (*PackageInfo, error) {
|
||||||
@@ -43,9 +62,9 @@ func (p *Pac) ValidatePackage(name string) (*PackageInfo, error) {
|
|||||||
return &PackageInfo{Name: name, Exists: true, InAUR: false}, nil
|
return &PackageInfo{Name: name, Exists: true, InAUR: false}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd = exec.Command("aur", "search", name)
|
p.ensureAURCache([]string{name})
|
||||||
if out, err := cmd.Output(); err == nil && len(out) > 0 {
|
if aurInfo, ok := p.aurCache[name]; ok {
|
||||||
return &PackageInfo{Name: name, Exists: true, InAUR: true}, nil
|
return &PackageInfo{Name: name, Exists: true, InAUR: true, AURInfo: &aurInfo}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &PackageInfo{Name: name, Exists: false, InAUR: false}, nil
|
return &PackageInfo{Name: name, Exists: false, InAUR: false}, nil
|
||||||
@@ -105,16 +124,26 @@ func Sync(packages []string) (*output.Result, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pacmanPkgs, aurPkgs := p.categorizePackages(packages)
|
||||||
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
if err := p.MarkExplicit(pkg); err != nil {
|
if err := p.MarkExplicit(pkg); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "warning: could not mark %s as explicit: %v\n", pkg, err)
|
fmt.Fprintf(os.Stderr, "warning: could not mark %s as explicit: %v\n", pkg, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = p.SyncPackages(packages)
|
if len(pacmanPkgs) > 0 {
|
||||||
|
_, err = p.SyncPackages(pacmanPkgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkg := range aurPkgs {
|
||||||
|
if err := p.InstallAUR(pkg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
removed, err := p.CleanupOrphans()
|
removed, err := p.CleanupOrphans()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -130,6 +159,118 @@ func Sync(packages []string) (*output.Result, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Pac) categorizePackages(packages []string) (pacmanPkgs, aurPkgs []string) {
|
||||||
|
var notInPacman []string
|
||||||
|
|
||||||
|
for _, pkg := range packages {
|
||||||
|
info, err := p.ValidatePackage(pkg)
|
||||||
|
if err != nil || !info.Exists {
|
||||||
|
notInPacman = append(notInPacman, pkg)
|
||||||
|
} else if !info.InAUR {
|
||||||
|
pacmanPkgs = append(pacmanPkgs, pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(notInPacman) > 0 {
|
||||||
|
p.ensureAURCache(notInPacman)
|
||||||
|
for _, pkg := range notInPacman {
|
||||||
|
if _, ok := p.aurCache[pkg]; ok {
|
||||||
|
aurPkgs = append(aurPkgs, pkg)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: package not found: %s\n", pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pacmanPkgs, aurPkgs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pac) ensureAURCache(packages []string) {
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var uncached []string
|
||||||
|
for _, pkg := range packages {
|
||||||
|
if _, ok := p.aurCache[pkg]; !ok {
|
||||||
|
uncached = append(uncached, pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(uncached) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.fetchAURInfo(uncached)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pac) fetchAURInfo(packages []string) map[string]AURPackage {
|
||||||
|
result := make(map[string]AURPackage)
|
||||||
|
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
v := url.Values{}
|
||||||
|
for _, pkg := range packages {
|
||||||
|
v.Add("arg[]", pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(AURInfoURL + "&" + v.Encode())
|
||||||
|
if err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
var aurResp AURResponse
|
||||||
|
if err := json.Unmarshal(body, &aurResp); err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range aurResp.Results {
|
||||||
|
p.aurCache[r.Name] = r
|
||||||
|
result[r.Name] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pac) InstallAUR(pkgName string) error {
|
||||||
|
aurInfo, ok := p.aurCache[pkgName]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("AUR package not found in cache: %s", pkgName)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir, err := os.MkdirTemp("", "declpac-aur-")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
cloneURL := "https://aur.archlinux.org/" + aurInfo.PackageBase + ".git"
|
||||||
|
cloneCmd := exec.Command("git", "clone", cloneURL, tmpDir)
|
||||||
|
cloneCmd.Stdout = os.Stdout
|
||||||
|
cloneCmd.Stderr = os.Stderr
|
||||||
|
if err := cloneCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone AUR repo: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
makepkgCmd := exec.Command("makepkg", "-si", "--noconfirm")
|
||||||
|
makepkgCmd.Stdout = os.Stdout
|
||||||
|
makepkgCmd.Stderr = os.Stderr
|
||||||
|
makepkgCmd.Dir = tmpDir
|
||||||
|
if err := makepkgCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("makepkg failed to build AUR package: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getInstalledCount() (int, error) {
|
func getInstalledCount() (int, error) {
|
||||||
cmd := exec.Command("pacman", "-Qq")
|
cmd := exec.Command("pacman", "-Qq")
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
@@ -177,3 +318,63 @@ func (p *Pac) CleanupOrphans() (int, error) {
|
|||||||
count := strings.Count(orphanList, "\n") + 1
|
count := strings.Count(orphanList, "\n") + 1
|
||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DryRun(packages []string) (*output.Result, error) {
|
||||||
|
p, err := New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
current, err := p.GetInstalledPackages()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
currentSet := make(map[string]bool)
|
||||||
|
for _, pkg := range current {
|
||||||
|
currentSet[pkg] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var toInstall []string
|
||||||
|
var aurPkgs []string
|
||||||
|
for _, pkg := range packages {
|
||||||
|
if !currentSet[pkg] {
|
||||||
|
info, err := p.ValidatePackage(pkg)
|
||||||
|
if err != nil || !info.Exists {
|
||||||
|
return nil, fmt.Errorf("package not found: %s", pkg)
|
||||||
|
}
|
||||||
|
if info.InAUR {
|
||||||
|
aurPkgs = append(aurPkgs, pkg)
|
||||||
|
} else {
|
||||||
|
toInstall = append(toInstall, pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
orphans, err := p.listOrphans()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &output.Result{
|
||||||
|
Installed: len(toInstall) + len(aurPkgs),
|
||||||
|
Removed: len(orphans),
|
||||||
|
ToInstall: append(toInstall, aurPkgs...),
|
||||||
|
ToRemove: orphans,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pac) listOrphans() ([]string, error) {
|
||||||
|
cmd := exec.Command("pacman", "-Qdtq")
|
||||||
|
orphans, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
list := strings.TrimSpace(string(orphans))
|
||||||
|
if list == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Split(list, "\n"), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"time"
|
"time"
|
||||||
@@ -10,6 +14,16 @@ import (
|
|||||||
|
|
||||||
var LockFile = "/var/lib/pacman/db.lock"
|
var LockFile = "/var/lib/pacman/db.lock"
|
||||||
|
|
||||||
|
const AURInfoURL = "https://aur.archlinux.org/rpc?v=5&type=info"
|
||||||
|
|
||||||
|
type AURResponse struct {
|
||||||
|
Results []AURResult `json:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AURResult struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
}
|
||||||
|
|
||||||
func Validate(packages []string) error {
|
func Validate(packages []string) error {
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return errors.New("no packages found")
|
return errors.New("no packages found")
|
||||||
@@ -46,29 +60,75 @@ func checkDBFreshness() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validatePackages(packages []string) error {
|
func validatePackages(packages []string) error {
|
||||||
|
var pacmanPkgs []string
|
||||||
|
var aurPkgs []string
|
||||||
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
if err := validatePackage(pkg); err != nil {
|
if inPacman(pkg) {
|
||||||
return err
|
pacmanPkgs = append(pacmanPkgs, pkg)
|
||||||
|
} else {
|
||||||
|
aurPkgs = append(aurPkgs, pkg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(aurPkgs) > 0 {
|
||||||
|
foundAUR := batchSearchAUR(aurPkgs)
|
||||||
|
for _, pkg := range aurPkgs {
|
||||||
|
if !foundAUR[pkg] {
|
||||||
|
return fmt.Errorf("package not found: %s", pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validatePackage(name string) error {
|
func inPacman(name string) bool {
|
||||||
cmd := exec.Command("pacman", "-Qip", name)
|
cmd := exec.Command("pacman", "-Qip", name)
|
||||||
if err := cmd.Run(); err == nil {
|
if err := cmd.Run(); err == nil {
|
||||||
return nil
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd = exec.Command("pacman", "-Sip", name)
|
cmd = exec.Command("pacman", "-Sip", name)
|
||||||
if err := cmd.Run(); err == nil {
|
if err := cmd.Run(); err == nil {
|
||||||
return nil
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd = exec.Command("aur", "search", name)
|
return false
|
||||||
if out, err := cmd.Output(); err == nil && len(out) > 0 {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("package not found: %s", name)
|
func batchSearchAUR(packages []string) map[string]bool {
|
||||||
|
result := make(map[string]bool)
|
||||||
|
|
||||||
|
if len(packages) == 0 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
v := url.Values{}
|
||||||
|
v.Set("type", "info")
|
||||||
|
for _, pkg := range packages {
|
||||||
|
v.Add("arg[]", pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(AURInfoURL + "&" + v.Encode())
|
||||||
|
if err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
var aurResp AURResponse
|
||||||
|
if err := json.Unmarshal(body, &aurResp); err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range aurResp.Results {
|
||||||
|
result[r.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user