package resolver import ( "fmt" "os/exec" "sync" "time" "zsvo/pkg/cache" "zsvo/pkg/deps" ) // FastResolver provides high-performance dependency resolution using cached indices type FastResolver struct { indexCache *cache.IndexCache packageIndex *PackageIndex mu sync.RWMutex } // PackageIndex wraps the cache PackageIndex with additional resolver functionality type PackageIndex struct { *cache.PackageIndex } // NewFastResolver creates a new fast dependency resolver func NewFastResolver(cacheDir string) *FastResolver { return &FastResolver{ indexCache: cache.NewIndexCache(cacheDir), } } // LoadIndex loads the package index for the given repository configuration func (r *FastResolver) LoadIndex(mirror, suite, component string) error { r.mu.Lock() defer r.mu.Unlock() idx, err := r.indexCache.GetIndex(mirror, suite, component) if err != nil { return err } r.packageIndex = &PackageIndex{PackageIndex: idx} return nil } // ResolvePackage resolves a package name to its source information in O(1) time func (r *FastResolver) ResolvePackage(name string) (*SourcePackage, error) { r.mu.RLock() if r.packageIndex == nil { r.mu.RUnlock() return nil, fmt.Errorf("no package index loaded") } // Try direct package name lookup first entry, found := r.packageIndex.LookupPackage(name) r.mu.RUnlock() // Release lock before processing if found { return r.entryToSourcePackage(entry), nil } // Try binary package lookup (need to re-acquire lock for this) r.mu.RLock() source, found := r.packageIndex.LookupBinary(name) r.mu.RUnlock() if found { r.mu.RLock() entry, found = r.packageIndex.LookupPackage(source) r.mu.RUnlock() if found { return r.entryToSourcePackage(entry), nil } } return nil, fmt.Errorf("package %s not found", name) } // ResolveDependencies resolves all dependencies for a package using iterative BFS func (r *FastResolver) ResolveDependencies(rootPackage string) (*DependencyGraph, error) { if r.packageIndex == nil { return nil, fmt.Errorf("no package index loaded") } graph := NewDependencyGraph() visited := make(map[string]bool) processing := make(map[string]bool) // Queue for BFS traversal queue := []string{rootPackage} for len(queue) > 0 { // Dequeue pkgName := queue[0] queue = queue[1:] // Skip if already processed if visited[pkgName] { continue } // Skip if currently being processed (cycle detection) if processing[pkgName] { continue } // Mark as being processed processing[pkgName] = true // Resolve the package pkg, err := r.ResolvePackage(pkgName) if err != nil { delete(processing, pkgName) return nil, fmt.Errorf("failed to resolve package %s: %w", pkgName, err) } // Add to graph graph.AddPackage(pkg) // Process dependencies for _, dep := range pkg.BuildDepends { depName := extractPackageName(dep) if depName == "" { continue } // Skip if it's a system package that doesn't need building if isSystemPackage(depName) { continue } // Add dependency relationship graph.AddDependency(pkgName, depName) // Enqueue if not visited if !visited[depName] && !processing[depName] { queue = append(queue, depName) } } // Mark as visited and remove from processing visited[pkgName] = true delete(processing, pkgName) } // Check for cycles by verifying no package is still in processing if len(processing) > 0 { return nil, fmt.Errorf("dependency cycle detected involving packages: %v", getKeys(processing)) } // Calculate build order if err := graph.CalculateBuildOrder(); err != nil { return nil, err } return graph, nil } // GetBuildOrder returns the packages in build order (dependencies first) func (r *FastResolver) GetBuildOrder(rootPackage string) ([]*SourcePackage, error) { graph, err := r.ResolveDependencies(rootPackage) if err != nil { return nil, err } return graph.GetBuildOrder(), nil } // entryToSourcePackage converts a cache entry to a SourcePackage func (r *FastResolver) entryToSourcePackage(entry *cache.PackageEntry) *SourcePackage { return &SourcePackage{ Name: entry.Package, Version: entry.Version, Directory: entry.Directory, DSCName: entry.DSCName, DSCSHA256: entry.DSCSHA256, Binaries: entry.Binaries, BuildDepends: entry.BuildDepends, } } // AddPackage adds a package to the current index (for updating cache) func (r *FastResolver) AddPackage(entry *cache.PackageEntry) error { r.mu.Lock() defer r.mu.Unlock() if r.packageIndex == nil { return fmt.Errorf("no package index loaded") } r.packageIndex.AddPackage(entry) return nil } // GetStats returns resolver statistics func (r *FastResolver) GetStats() (int, int, bool) { r.mu.RLock() defer r.mu.RUnlock() if r.packageIndex == nil { return 0, 0, false } pkgCount, binCount := r.packageIndex.GetStats() expired := r.packageIndex.IsExpired(24 * time.Hour) // Consider expired after 24 hours return pkgCount, binCount, expired } // SaveIndex persists the current index to disk func (r *FastResolver) SaveIndex(mirror, suite, component string) error { r.mu.RLock() defer r.mu.RUnlock() if r.packageIndex == nil { return fmt.Errorf("no package index loaded") } cacheFile := r.indexCache.CacheFilePath(mirror, suite, component) return r.packageIndex.Save(cacheFile) } // SourcePackage represents a Debian source package type SourcePackage struct { Name string Version string Directory string DSCName string DSCSHA256 string Binaries []string BuildDepends []string } // extractPackageName extracts package name from dependency string and handles alternatives func extractPackageName(dep string) string { // Parse dependency constraints using existing deps package req, err := deps.ParseRequirement(dep) if err != nil { return "" } if len(req.Alternatives) == 0 { return "" } // For alternatives like "A | B | C", check each alternative in order // and return the first one that exists in the package index for _, alt := range req.Alternatives { if alt.Name != "" { return alt.Name } } return req.Alternatives[0].Name // Fallback to first alternative } // isSystemPackage checks if a package is already available on the system func isSystemPackage(name string) bool { // Basic essential system packages that should always be considered available essentialPackages := map[string]bool{ "gcc": true, "g++": true, "make": true, "bash": true, "glibc": true, "libc6": true, "libc-bin": true, } // Check if it's an essential package if essentialPackages[name] { return true } // Try to find the package in PATH // For common development tools, check if the binary exists switch name { case "pkg-config": _, err := exec.LookPath("pkg-config") return err == nil case "cmake": _, err := exec.LookPath("cmake") return err == nil case "python3": _, err := exec.LookPath("python3") return err == nil case "perl": _, err := exec.LookPath("perl") return err == nil case "sed": _, err := exec.LookPath("sed") return err == nil case "grep": _, err := exec.LookPath("grep") return err == nil case "awk": _, err := exec.LookPath("awk") return err == nil } // For Debian packages, try to map to common binary names binaryNames := map[string]string{ "coreutils": "ls", "findutils": "find", "diffutils": "diff", "gzip": "gzip", "hostname": "hostname", "util-linux": "mount", "ncurses-bin": "tput", "debianutils": "tempfile", "base-files": "lsb_release", "base-passwd": "passwd", "dash": "dash", "dpkg": "dpkg", "login": "login", "ncurses-base": "tput", "perl-base": "perl", "sysvinit": "init", "init-system-helpers": "service", } if binary, exists := binaryNames[name]; exists { _, err := exec.LookPath(binary) return err == nil } // Default: not a system package return false } // getKeys returns keys from a map as a slice func getKeys(m map[string]bool) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } return keys }