Compare commits
5
Commits
11a5e57516
...
01338cb671
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01338cb671 | ||
|
|
b4b12b39b6 | ||
|
|
720be92d82 | ||
|
|
4143a23c91 | ||
|
|
536a8d5ccb |
+2
-3
@@ -18,7 +18,6 @@ strategy without conflict resolution.
|
||||
- **WHEN** stdin contains packages not in state files
|
||||
- **THEN** those packages shall be added to the final state
|
||||
|
||||
#### Scenario: Duplicate packages accumulate
|
||||
#### Scenario: Duplicate packages are deduplicated
|
||||
- **WHEN** the same package appears in multiple inputs
|
||||
- **THEN** it shall be included multiple times in the final state (pacman
|
||||
handles duplicates)
|
||||
- **THEN** it shall be included once in the final state (map deduplication)
|
||||
+1
-1
@@ -17,7 +17,7 @@ After syncing, the system SHALL remove packages that are no longer required
|
||||
|
||||
#### Scenario: Orphan cleanup after sync
|
||||
- **WHEN** sync operation completes successfully
|
||||
- **THEN** system shall run pacman -Rsu to remove unneeded dependencies
|
||||
- **THEN** system shall run pacman -Rns to remove unneeded dependencies
|
||||
- **AND** report the number of packages removed
|
||||
|
||||
#### Scenario: Orphan cleanup respects explicitly installed
|
||||
+1
-1
@@ -10,7 +10,7 @@ The system SHALL execute pacman operations to install and upgrade all declared p
|
||||
|
||||
#### Scenario: No partial upgrades
|
||||
- **WHEN** running pacman commands
|
||||
- **THEN** system shall use -Syu flag (full system upgrade) ensuring all packages are latest
|
||||
- **THEN** system shall use -S --needed flag (sync with skip if up-to-date) ensuring declared packages are present
|
||||
|
||||
#### Scenario: Package availability check
|
||||
- **WHEN** a package from input is not in pacman repositories
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-17
|
||||
@@ -0,0 +1,44 @@
|
||||
## Context
|
||||
|
||||
Current `Resolve` in `pkg/fetch/fetch.go`:
|
||||
1. Initialize all packages with `Exists: true`
|
||||
2. Check local DB, set `Installed: true` if found
|
||||
3. Check sync DBs, set `Installed: false`
|
||||
4. Check AUR, set `InAUR: true`
|
||||
5. Error if not found anywhere
|
||||
|
||||
Bug: wrong default + wrong order + conflates two concerns.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Fix Resolve algorithm to correctly classify packages
|
||||
- Separate "available" from "installed" concerns
|
||||
|
||||
**Non-Goals:**
|
||||
- No new APIs or features
|
||||
- No refactoring outside Resolve function
|
||||
|
||||
## Decisions
|
||||
|
||||
**Decision 1: Start with Exists: false**
|
||||
- Default `Exists: false` (unknown) instead of `true`
|
||||
- Only set true when confirmed in sync DBs
|
||||
|
||||
**Decision 2: Check sync DBs first**
|
||||
- Order: sync → local → AUR
|
||||
- First determine if package exists (anywhere)
|
||||
- Then determine if installed (local only)
|
||||
|
||||
**Decision 3: Two-phase classification**
|
||||
|
||||
| Phase | Check | Sets |
|
||||
|------|-------|-----|
|
||||
| 1. Availability | sync DBs | Exists: true/false |
|
||||
| 2. Installation | local DB | Installed: true/false |
|
||||
| 3. Fallback | AUR | InAUR: true if not in sync |
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- Minimal risk: localized fix only
|
||||
- Trade-off: slight performance cost (check sync before local) - acceptable for correctness
|
||||
@@ -0,0 +1,26 @@
|
||||
## Why
|
||||
|
||||
The `Resolve` function in `pkg/fetch/fetch.go` has incorrect logic flow. It
|
||||
initializes all packages with `Exists: true`, then checks local DB first, then
|
||||
sync DBs, then AUR. This wrong order causes incorrect package state
|
||||
classification - packages that exist only in AUR may be incorrectly marked as
|
||||
found.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Fix initialization: packages should start with `Exists: false` (unknown), not `true`
|
||||
- Fix order: check sync DBs BEFORE local DB to determine availability
|
||||
- Separate independent concerns: "available" (sync/AUR) from "installed" (local)
|
||||
- All packages must validate: either `Exists: true` or `InAUR: true`
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `package-resolve-logic`: Correct resolution algorithm for pacman packages
|
||||
|
||||
### Modified Capabilities
|
||||
- None
|
||||
|
||||
## Impact
|
||||
|
||||
- `pkg/fetch/fetch.go`: `Resolve` function
|
||||
@@ -0,0 +1,7 @@
|
||||
## 1. Fix Resolve Function Logic
|
||||
|
||||
- [x] 1.1 Change initialization: start packages with Exists: false instead of true
|
||||
- [x] 1.2 Reorder to check sync DBs BEFORE local DB
|
||||
- [x] 1.3 Separate availability check (sync DBs) from installation check (local DB)
|
||||
- [x] 1.4 Add AUR fallback for packages not in sync DBs
|
||||
- [x] 1.5 Validate all packages have either Exists or InAUR before returning
|
||||
@@ -0,0 +1,45 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Package existence check via sync DBs
|
||||
|
||||
When resolving packages, the system SHALL first check sync databases (core, extra, multilib) to determine if a package exists in official repositories. If found in sync DBs, `Exists` SHALL be set to true.
|
||||
|
||||
#### Scenario: Package found in sync DB
|
||||
- **WHEN** package name exists in any sync database
|
||||
- **THEN** set `Exists: true`, `InAUR: false`
|
||||
|
||||
#### Scenario: Package not found in sync DB
|
||||
- **WHEN** package name does not exist in any sync database
|
||||
- **THEN** keep `Exists: false` for further lookup
|
||||
|
||||
### Requirement: Package installation check via local database
|
||||
|
||||
After checking sync DBs, the system SHALL check the local package database to determine if a package is installed. This is independent of existence check.
|
||||
|
||||
#### Scenario: Package installed locally
|
||||
- **WHEN** package is installed on the system
|
||||
- **THEN** set `Installed: true`
|
||||
|
||||
#### Scenario: Package not installed locally
|
||||
- **WHEN** package is not installed on the system
|
||||
- **THEN** set `Installed: false`
|
||||
|
||||
### Requirement: AUR fallback check
|
||||
|
||||
If package is not found in sync DBs, the system SHALL check the AUR as a fallback.
|
||||
|
||||
#### Scenario: Package found in AUR
|
||||
- **WHEN** package exists in AUR but not in sync DBs
|
||||
- **THEN** set `InAUR: true`
|
||||
|
||||
#### Scenario: Package not found anywhere
|
||||
- **WHEN** package not in sync DBs, not in local DB, not in AUR
|
||||
- **THEN** return error "package not found"
|
||||
|
||||
### Requirement: Validation at resolution end
|
||||
|
||||
After all checks complete, the system SHALL ensure every package has either `Exists: true` or `InAUR: true`. No package SHALL leave the resolver in an ambiguous state.
|
||||
|
||||
#### Scenario: All packages valid
|
||||
- **WHEN** all packages resolved successfully
|
||||
- **THEN** every package has Exists=true OR InAUR=true
|
||||
+42
-43
@@ -189,7 +189,19 @@ func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error) {
|
||||
|
||||
result := make(map[string]*PackageInfo)
|
||||
for _, pkg := range packages {
|
||||
result[pkg] = &PackageInfo{Name: pkg, Exists: true}
|
||||
result[pkg] = &PackageInfo{Name: pkg, Exists: false}
|
||||
}
|
||||
|
||||
syncPkgs, err := f.checkSyncDBs(packages)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "[debug] Resolve: sync db check done (%.2fs)\n", time.Since(start).Seconds())
|
||||
|
||||
for pkg, syncPkg := range syncPkgs {
|
||||
result[pkg].Exists = true
|
||||
result[pkg].InAUR = false
|
||||
result[pkg].syncPkg = syncPkg
|
||||
}
|
||||
|
||||
localPkgs, err := f.buildLocalPkgMap()
|
||||
@@ -198,53 +210,30 @@ func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error) {
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "[debug] Resolve: local pkgs built (%.2fs)\n", time.Since(start).Seconds())
|
||||
|
||||
var notInLocal []string
|
||||
for pkg := range localPkgs {
|
||||
if info, ok := result[pkg]; ok {
|
||||
info.Installed = true
|
||||
}
|
||||
}
|
||||
|
||||
var notInSync []string
|
||||
for _, pkg := range packages {
|
||||
if localPkg, ok := localPkgs[pkg]; ok {
|
||||
result[pkg] = &PackageInfo{
|
||||
Name: pkg,
|
||||
Exists: true,
|
||||
InAUR: false,
|
||||
Installed: true,
|
||||
syncPkg: localPkg,
|
||||
}
|
||||
} else {
|
||||
notInLocal = append(notInLocal, pkg)
|
||||
if !result[pkg].Exists {
|
||||
notInSync = append(notInSync, pkg)
|
||||
}
|
||||
}
|
||||
|
||||
if len(notInLocal) > 0 {
|
||||
syncPkgs, err := f.checkSyncDBs(notInLocal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f.ensureAURCache(packages)
|
||||
if len(notInSync) > 0 {
|
||||
f.ensureAURCache(notInSync)
|
||||
|
||||
for _, pkg := range packages {
|
||||
info := result[pkg]
|
||||
if info == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if info.Installed {
|
||||
if aurInfo, ok := f.aurCache[pkg]; ok {
|
||||
info.InAUR = true
|
||||
info.AURInfo = &aurInfo
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if syncPkg, ok := syncPkgs[pkg]; ok {
|
||||
info.InAUR = false
|
||||
info.Installed = false
|
||||
info.syncPkg = syncPkg
|
||||
if info.Exists {
|
||||
continue
|
||||
}
|
||||
|
||||
if aurInfo, ok := f.aurCache[pkg]; ok {
|
||||
info.InAUR = true
|
||||
info.Installed = false
|
||||
info.AURInfo = &aurInfo
|
||||
continue
|
||||
}
|
||||
@@ -253,6 +242,13 @@ func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, pkg := range packages {
|
||||
info := result[pkg]
|
||||
if !info.Exists && !info.InAUR {
|
||||
return nil, fmt.Errorf("package not validated: %s", pkg)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "[debug] Resolve: done (%.2fs)\n", time.Since(start).Seconds())
|
||||
return result, nil
|
||||
}
|
||||
@@ -277,18 +273,21 @@ func (f *Fetcher) ensureAURCache(packages []string) {
|
||||
return
|
||||
}
|
||||
|
||||
f.fetchAURInfo(uncached)
|
||||
_, err := f.fetchAURInfo(uncached)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[debug] ensureAURCache: fetch error: %v\n", err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "[debug] ensureAURCache: done (%.2fs)\n", time.Since(start).Seconds())
|
||||
}
|
||||
|
||||
func (f *Fetcher) fetchAURInfo(packages []string) map[string]AURPackage {
|
||||
func (f *Fetcher) fetchAURInfo(packages []string) (map[string]AURPackage, error) {
|
||||
start := time.Now()
|
||||
fmt.Fprintf(os.Stderr, "[debug] fetchAURInfo: starting...\n")
|
||||
|
||||
result := make(map[string]AURPackage)
|
||||
|
||||
if len(packages) == 0 {
|
||||
return result
|
||||
return result, nil
|
||||
}
|
||||
|
||||
v := url.Values{}
|
||||
@@ -298,18 +297,18 @@ func (f *Fetcher) fetchAURInfo(packages []string) map[string]AURPackage {
|
||||
|
||||
resp, err := http.Get(AURInfoURL + "&" + v.Encode())
|
||||
if err != nil {
|
||||
return result
|
||||
return result, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return result
|
||||
return result, err
|
||||
}
|
||||
|
||||
var aurResp AURResponse
|
||||
if err := json.Unmarshal(body, &aurResp); err != nil {
|
||||
return result
|
||||
return result, err
|
||||
}
|
||||
|
||||
for _, r := range aurResp.Results {
|
||||
@@ -318,7 +317,7 @@ func (f *Fetcher) fetchAURInfo(packages []string) map[string]AURPackage {
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "[debug] fetchAURInfo: done (%.2fs)\n", time.Since(start).Seconds())
|
||||
return result
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) ListOrphans() ([]string, error) {
|
||||
|
||||
+17
-4
@@ -20,11 +20,24 @@ func MarkAllAsDeps() error {
|
||||
start := time.Now()
|
||||
fmt.Fprintf(os.Stderr, "[debug] MarkAllAsDeps: starting...\n")
|
||||
|
||||
cmd := exec.Command("pacman", "-D", "--asdeps")
|
||||
listCmd := exec.Command("pacman", "-Qq")
|
||||
output, err := listCmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list packages: %w", err)
|
||||
}
|
||||
|
||||
packages := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||
if len(packages) == 0 || packages[0] == "" {
|
||||
fmt.Fprintf(os.Stderr, "[debug] MarkAllAsDeps: no packages to mark (%.2fs)\n", time.Since(start).Seconds())
|
||||
return nil
|
||||
}
|
||||
|
||||
args := append([]string{"-D", "--asdeps"}, packages...)
|
||||
cmd := exec.Command("pacman", args...)
|
||||
state.Write([]byte("MarkAllAsDeps...\n"))
|
||||
cmd.Stdout = io.MultiWriter(os.Stdout, state.GetLogWriter())
|
||||
cmd.Stderr = io.MultiWriter(os.Stderr, state.GetLogWriter())
|
||||
err := cmd.Run()
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
state.Write([]byte(fmt.Sprintf("error: %v\n", err)))
|
||||
}
|
||||
@@ -137,7 +150,7 @@ func categorizePackages(f *fetch.Fetcher, packages []string) (pacmanPkgs, aurPkg
|
||||
|
||||
for _, pkg := range packages {
|
||||
info := resolved[pkg]
|
||||
if info == nil || !info.Exists {
|
||||
if info == nil || (!info.Exists && !info.InAUR) {
|
||||
fmt.Fprintf(os.Stderr, "error: package not found: %s\n", pkg)
|
||||
continue
|
||||
}
|
||||
@@ -335,7 +348,7 @@ func DryRun(packages []string) (*output.Result, error) {
|
||||
var aurPkgs []string
|
||||
for _, pkg := range packages {
|
||||
info := resolved[pkg]
|
||||
if info == nil || !info.Exists {
|
||||
if info == nil || (!info.Exists && !info.InAUR) {
|
||||
return nil, fmt.Errorf("package not found: %s", pkg)
|
||||
}
|
||||
if info.InAUR {
|
||||
|
||||
Reference in New Issue
Block a user