zsvo/pkg/deps/resolver.go
itexpert228 b2d7e12ac1
parasha
2026-03-14 12:23:04 +03:00

455 lines
12 KiB
Go

package deps
import (
"fmt"
"strings"
)
// PackageRepository provides access to installed packages
type PackageRepository interface {
GetInstalled() (map[string]*PackageInfo, error)
GetPackage(name string) (*PackageInfo, error)
SearchPackages(query string) ([]*PackageInfo, error) // New method for heuristic search
}
// PackageFetcher provides ability to download packages
type PackageFetcher interface {
FetchPackage(pkgName string, version string) error
}
// PackageInfo represents package metadata
type PackageInfo struct {
Name string
Version string
Dependencies []string
Description string // Optional for better heuristics
}
// DependencyResolver handles dependency resolution
type DependencyResolver struct {
repo PackageRepository
fetcher PackageFetcher
}
// NewDependencyResolver creates a new resolver
func NewDependencyResolver(repo PackageRepository, fetcher PackageFetcher) *DependencyResolver {
return &DependencyResolver{
repo: repo,
fetcher: fetcher,
}
}
// ResolveOrder determines installation order using topological sort
func (r *DependencyResolver) ResolveOrder(packages []*PackageInfo) ([]*PackageInfo, error) {
// Create name to package map
pkgMap := make(map[string]*PackageInfo)
for _, pkg := range packages {
pkgMap[pkg.Name] = pkg
}
// Build dependency graph
indegree := make(map[string]int)
graph := make(map[string][]string)
// Initialize
for _, pkg := range packages {
indegree[pkg.Name] = 0
graph[pkg.Name] = []string{}
}
// Calculate indegrees
for _, pkg := range packages {
reqs, err := ParseRequirements(pkg.Dependencies)
if err != nil {
return nil, fmt.Errorf("invalid dependencies in %s: %w", pkg.Name, err)
}
for _, req := range reqs {
for _, alt := range req.Alternatives {
if depPkg, exists := pkgMap[alt.Name]; exists {
// Check if version constraint is satisfied
if alt.MatchesVersion(depPkg.Version) {
graph[alt.Name] = append(graph[alt.Name], pkg.Name)
indegree[pkg.Name]++
break // Only one alternative needed
}
}
}
}
}
// Topological sort using Kahn's algorithm
queue := make([]string, 0)
for name, deg := range indegree {
if deg == 0 {
queue = append(queue, name)
}
}
result := make([]*PackageInfo, 0)
for len(queue) > 0 {
// Get first element
current := queue[0]
queue = queue[1:]
// Add to result
result = append(result, pkgMap[current])
// Remove edges
for _, neighbor := range graph[current] {
indegree[neighbor]--
if indegree[neighbor] == 0 {
queue = append(queue, neighbor)
}
}
}
if len(result) != len(packages) {
return nil, fmt.Errorf("dependency cycle detected")
}
return result, nil
}
// ResolveDependencies recursively resolves and downloads dependencies
func (r *DependencyResolver) ResolveDependencies(rootPackages []*PackageInfo) ([]*PackageInfo, error) {
processed := make(map[string]bool)
result := make([]*PackageInfo, 0)
for _, rootPkg := range rootPackages {
pkgs, err := r.resolveRecursive(rootPkg, processed)
if err != nil {
return nil, fmt.Errorf("failed to resolve dependencies for %s: %w", rootPkg.Name, err)
}
result = append(result, pkgs...)
}
// Remove duplicates and sort by dependency order
return r.deduplicateAndSort(result)
}
// resolveRecursive recursively resolves dependencies with fetching
func (r *DependencyResolver) resolveRecursive(pkg *PackageInfo, processed map[string]bool) ([]*PackageInfo, error) {
if processed[pkg.Name] {
return nil, nil // Already processed
}
processed[pkg.Name] = true
result := []*PackageInfo{pkg}
// Parse dependencies
reqs, err := ParseRequirements(pkg.Dependencies)
if err != nil {
return result, fmt.Errorf("invalid dependencies in %s: %w", pkg.Name, err)
}
for i := range reqs {
depPkg, err := r.resolveRequirement(&reqs[i], processed)
if err != nil {
// Log warning but continue - don't fail installation
fmt.Printf("Warning: failed to resolve dependency %s for %s: %v\n",
reqs[i].Raw, pkg.Name, err)
continue
}
if depPkg != nil {
result = append(result, depPkg)
}
}
return result, nil
}
// resolveRequirement resolves a single dependency requirement
func (r *DependencyResolver) resolveRequirement(req *Requirement, processed map[string]bool) (*PackageInfo, error) {
// First check if already installed
installed, err := r.repo.GetInstalled()
if err != nil {
return nil, fmt.Errorf("failed to get installed packages: %w", err)
}
// Check if any alternative is already installed and satisfies constraint
for i := range req.Alternatives {
if installedPkg, exists := installed[req.Alternatives[i].Name]; exists {
if req.Alternatives[i].MatchesVersion(installedPkg.Version) {
return nil, nil // Already satisfied
}
}
}
// Try to find and fetch the dependency
for i := range req.Alternatives {
depPkg, err := r.findAndFetchDependency(&req.Alternatives[i], processed)
if err == nil && depPkg != nil {
return depPkg, nil
}
// Try next alternative if this one fails
}
// If all alternatives fail, try heuristic search
heuristicPkg, err := r.findHeuristicDependency(req.Raw)
if err == nil && heuristicPkg != nil {
return heuristicPkg, nil
}
return nil, fmt.Errorf("no suitable dependency found for %s", req.Raw)
}
// findAndFetchDependency finds and fetches a dependency
func (r *DependencyResolver) findAndFetchDependency(alt *Constraint, processed map[string]bool) (*PackageInfo, error) {
// Try to get package from repository
pkg, err := r.repo.GetPackage(alt.Name)
if err != nil {
// Package not found, try to fetch it
if r.fetcher != nil {
fetchErr := r.fetcher.FetchPackage(alt.Name, "")
if fetchErr != nil {
return nil, fmt.Errorf("failed to fetch %s: %w", alt.Name, fetchErr)
}
// Try again after fetching
pkg, err = r.repo.GetPackage(alt.Name)
if err != nil {
return nil, fmt.Errorf("package %s not found after fetch", alt.Name)
}
} else {
return nil, fmt.Errorf("package %s not found and no fetcher available", alt.Name)
}
}
// Check version constraint
if !alt.MatchesVersion(pkg.Version) {
var opStr string
switch alt.Op {
case OpAny:
opStr = ""
case OpEqual:
opStr = "="
case OpGreater:
opStr = ">"
case OpGreaterOrEqual:
opStr = ">="
case OpLess:
opStr = "<"
case OpLessOrEqual:
opStr = "<="
default:
opStr = ""
}
return nil, fmt.Errorf("version mismatch for %s: need %s%s, have %s",
alt.Name, opStr, alt.Version, pkg.Version)
}
// Recursively resolve its dependencies
if !processed[pkg.Name] {
_, err = r.resolveRecursive(pkg, processed)
if err != nil {
return nil, fmt.Errorf("failed to resolve dependencies of %s: %w", pkg.Name, err)
}
}
return pkg, nil
}
// findHeuristicDependency tries to find dependency using heuristics
func (r *DependencyResolver) findHeuristicDependency(depName string) (*PackageInfo, error) {
// Try different naming patterns
patterns := []string{
depName, // Exact match
"lib" + depName, // lib prefix
depName + "-dev", // dev suffix
depName + "-devel", // devel suffix
depName + "-libs", // libs suffix
strings.ToLower(depName), // lowercase
strings.Title(depName), // title case
}
// Try common library name transformations
libPatterns := r.generateLibraryPatterns(depName)
patterns = append(patterns, libPatterns...)
for _, pattern := range patterns {
if r.repo == nil {
continue
}
// Search for packages
pkgs, err := r.repo.SearchPackages(pattern)
if err != nil {
continue
}
// Return the best match
if len(pkgs) > 0 {
return r.findBestMatch(depName, pkgs), nil
}
}
return nil, fmt.Errorf("heuristic search failed for %s", depName)
}
// generateLibraryPatterns generates common library naming patterns
func (r *DependencyResolver) generateLibraryPatterns(name string) []string {
patterns := []string{}
// Remove common prefixes/suffixes and add lib prefix
cleanName := strings.TrimPrefix(name, "lib")
cleanName = strings.TrimSuffix(cleanName, "-dev")
cleanName = strings.TrimSuffix(cleanName, "-devel")
patterns = append(patterns,
"lib"+cleanName,
"lib"+cleanName+"-dev",
"lib"+cleanName+"-devel",
"lib"+cleanName+"-libs",
)
// Common library transformations
libMappings := map[string]string{
"ssl": "openssl",
"crypto": "libcrypto",
"z": "zlib",
"png": "libpng",
"jpeg": "libjpeg",
"tiff": "libtiff",
"xml": "libxml2",
"xslt": "libxslt",
"ffi": "libffi",
"readline": "readline",
"ncurses": "ncurses",
"curl": "libcurl",
"sqlite": "sqlite3",
"db": "db",
"bz2": "libbz2",
"lzma": "liblzma",
"iconv": "libiconv",
"intl": "libintl",
"uuid": "libuuid",
"expat": "libexpat",
"pcre": "libpcre",
"pcre2": "libpcre2",
"gcrypt": "libgcrypt",
"gpg": "libgpg",
"tls": "gnutls",
"ssl2": "libssl",
}
if mapped, exists := libMappings[cleanName]; exists {
patterns = append(patterns, mapped)
}
return patterns
}
// findBestMatch finds the best matching package from search results
func (r *DependencyResolver) findBestMatch(originalName string, candidates []*PackageInfo) *PackageInfo {
if len(candidates) == 1 {
return candidates[0]
}
bestScore := -1
var bestPkg *PackageInfo
for _, pkg := range candidates {
score := r.calculateMatchScore(originalName, pkg)
if score > bestScore {
bestScore = score
bestPkg = pkg
}
}
return bestPkg
}
// calculateMatchScore calculates how well a package matches the requested dependency
func (r *DependencyResolver) calculateMatchScore(originalName string, pkg *PackageInfo) int {
score := 0
pkgName := strings.ToLower(pkg.Name)
originalLower := strings.ToLower(originalName)
// Exact match gets highest score
if pkgName == originalLower {
score += 100
}
// Name contains original
if strings.Contains(pkgName, originalLower) || strings.Contains(originalLower, pkgName) {
score += 50
}
// Common prefix/suffix matches
if strings.HasPrefix(pkgName, "lib") && strings.Contains(pkgName, originalLower) {
score += 30
}
if strings.HasSuffix(pkgName, "dev") && strings.Contains(pkgName, originalLower) {
score += 20
}
// Description match (if available)
if pkg.Description != "" {
descLower := strings.ToLower(pkg.Description)
if strings.Contains(descLower, originalLower) {
score += 10
}
}
// Favor packages with shorter names (likely more specific)
score += 10 / (len(pkgName) + 1)
return score
}
// deduplicateAndSort removes duplicates and sorts by dependency order
func (r *DependencyResolver) deduplicateAndSort(packages []*PackageInfo) ([]*PackageInfo, error) {
// Remove duplicates
seen := make(map[string]bool)
unique := make([]*PackageInfo, 0)
for _, pkg := range packages {
if !seen[pkg.Name] {
seen[pkg.Name] = true
unique = append(unique, pkg)
}
}
// Sort by dependency order
return r.ResolveOrder(unique)
}
// CheckDependencies verifies if dependencies are satisfied (non-fatal)
func (r *DependencyResolver) CheckDependencies(pkg *PackageInfo) []string {
installed, err := r.repo.GetInstalled()
if err != nil {
return []string{fmt.Sprintf("failed to get installed packages: %v", err)}
}
missing := r.findMissingDependencies(pkg, installed)
return missing
}
func (r *DependencyResolver) findMissingDependencies(pkg *PackageInfo, installed map[string]*PackageInfo) []string {
var missing []string
reqs, err := ParseRequirements(pkg.Dependencies)
if err != nil {
return []string{err.Error()}
}
for _, req := range reqs {
satisfied := false
for _, alt := range req.Alternatives {
if installedPkg, exists := installed[alt.Name]; exists {
if alt.MatchesVersion(installedPkg.Version) {
satisfied = true
break
}
}
}
if !satisfied {
missing = append(missing, req.Raw)
}
}
return missing
}