mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
refactor(deps): Improve PyPI dependency resolution and add custom resolver support
This commit is contained in:
@@ -0,0 +1,255 @@
|
|||||||
|
diff --git a/packagemanager/dependency_resolver.go b/packagemanager/dependency_resolver.go
|
||||||
|
index 2d48a8f..58e0330 100644
|
||||||
|
--- a/packagemanager/dependency_resolver.go
|
||||||
|
+++ b/packagemanager/dependency_resolver.go
|
||||||
|
@@ -13,7 +13,9 @@ import (
|
||||||
|
|
||||||
|
// Contract for a function that implements ecosystem specific version
|
||||||
|
// resolver from a version range specification.
|
||||||
|
-type versionSpecResolver func(packageName, version string) string
|
||||||
|
+type versionSpecResolverFn func(packageName, version string) string
|
||||||
|
+
|
||||||
|
+type dependencyResolverFn func(packageName, version string) (*packageregistry.PackageDependencyList, error)
|
||||||
|
|
||||||
|
type dependencyResolverConfig struct {
|
||||||
|
IncludeDevDependencies bool
|
||||||
|
@@ -24,14 +26,15 @@ type dependencyResolverConfig struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type dependencyResolver struct {
|
||||||
|
- client packageregistry.Client
|
||||||
|
- config dependencyResolverConfig
|
||||||
|
- mutex sync.Mutex
|
||||||
|
- versionSpecResolver versionSpecResolver
|
||||||
|
+ client packageregistry.Client
|
||||||
|
+ config dependencyResolverConfig
|
||||||
|
+ mutex sync.Mutex
|
||||||
|
+ versionSpecResolver versionSpecResolverFn
|
||||||
|
+ packageDependencyResolver dependencyResolverFn
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDependencyResolver(client packageregistry.Client, config dependencyResolverConfig,
|
||||||
|
- versionSpecResolver versionSpecResolver) *dependencyResolver {
|
||||||
|
+ versionSpecResolver versionSpecResolverFn, packageDependencyResolver dependencyResolverFn) *dependencyResolver {
|
||||||
|
if config.MaxConcurrency <= 0 {
|
||||||
|
config.MaxConcurrency = 10
|
||||||
|
}
|
||||||
|
@@ -44,9 +47,10 @@ func newDependencyResolver(client packageregistry.Client, config dependencyResol
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dependencyResolver{
|
||||||
|
- client: client,
|
||||||
|
- config: config,
|
||||||
|
- versionSpecResolver: versionSpecResolver,
|
||||||
|
+ client: client,
|
||||||
|
+ config: config,
|
||||||
|
+ versionSpecResolver: versionSpecResolver,
|
||||||
|
+ packageDependencyResolver: packageDependencyResolver,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -122,7 +126,14 @@ func (r *dependencyResolver) resolvePackageDependenciesConcurrent(
|
||||||
|
log.Debugf("resolving dependencies for %s@%s", packageVersion.Package.Name, packageVersion.Version)
|
||||||
|
|
||||||
|
// Get dependencies for the current package
|
||||||
|
- dependencyList, err := pd.GetPackageDependencies(packageVersion.Package.Name, packageVersion.Version)
|
||||||
|
+ var dependencyList *packageregistry.PackageDependencyList
|
||||||
|
+ var err error
|
||||||
|
+ if r.packageDependencyResolver != nil {
|
||||||
|
+ dependencyList, err = r.packageDependencyResolver(packageVersion.Package.Name, packageVersion.Version)
|
||||||
|
+ } else {
|
||||||
|
+ dependencyList, err = pd.GetPackageDependencies(packageVersion.Package.Name, packageVersion.Version)
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
if err != nil {
|
||||||
|
return ff(fmt.Errorf("failed to get package dependencies: %w", err))
|
||||||
|
}
|
||||||
|
@@ -186,7 +197,6 @@ func (r *dependencyResolver) resolvePackageDependenciesConcurrent(
|
||||||
|
return ff(fmt.Errorf("failed to resolve transitive dependency: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
-
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
diff --git a/packagemanager/npm_resolver.go b/packagemanager/npm_resolver.go
|
||||||
|
index dd46c2c..1d244dc 100644
|
||||||
|
--- a/packagemanager/npm_resolver.go
|
||||||
|
+++ b/packagemanager/npm_resolver.go
|
||||||
|
@@ -80,7 +80,7 @@ func (r *npmDependencyResolver) ResolveDependencies(ctx context.Context,
|
||||||
|
MaxConcurrency: r.config.MaxConcurrency,
|
||||||
|
}, func(packageName, version string) string {
|
||||||
|
return npmCleanVersion(version)
|
||||||
|
- })
|
||||||
|
+ }, nil)
|
||||||
|
|
||||||
|
return resolver.resolveDependencies(ctx, packageVersion)
|
||||||
|
}
|
||||||
|
diff --git a/packagemanager/pypi_resolver.go b/packagemanager/pypi_resolver.go
|
||||||
|
index eded678..84873a9 100644
|
||||||
|
--- a/packagemanager/pypi_resolver.go
|
||||||
|
+++ b/packagemanager/pypi_resolver.go
|
||||||
|
@@ -56,20 +56,41 @@ func NewPypiDependencyResolver(config PyPiDependencyResolverConfig) (*pypiDepend
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pypiDependencyResolver) ResolveDependencies(ctx context.Context, pkg *packagev1.PackageVersion) ([]*packagev1.PackageVersion, error) {
|
||||||
|
- resolver := newDependencyResolver(p.registry, dependencyResolverConfig{
|
||||||
|
- IncludeDevDependencies: p.config.IncludeDevDependencies,
|
||||||
|
- IncludeTransitiveDependencies: p.config.IncludeTransitiveDependencies,
|
||||||
|
- TransitiveDepth: p.config.TransitiveDepth,
|
||||||
|
- FailFast: p.config.FailFast,
|
||||||
|
- MaxConcurrency: p.config.MaxConcurrency,
|
||||||
|
- }, func(packageName, version string) string {
|
||||||
|
+ pypiVersionSpecResolverFn := func(packageName, version string) string {
|
||||||
|
ver, err := pipGetMatchingVersion(packageName, version)
|
||||||
|
+ fmt.Printf("Resolved %s for %s to %s\n", version, packageName, ver)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("error getting matching version for %s@%s", packageName, version)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return ver
|
||||||
|
- })
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ pypiDependencyResolverFn := func(packageName, version string) (*packageregistry.PackageDependencyList, error) {
|
||||||
|
+ resolvedDependencies, err := getPypiPackageDependencies(packageName, version)
|
||||||
|
+ if err != nil {
|
||||||
|
+ return nil, err
|
||||||
|
+ }
|
||||||
|
+ dependencies := make([]packageregistry.PackageDependencyInfo, 0)
|
||||||
|
+ for _, dep := range resolvedDependencies {
|
||||||
|
+ dependencies = append(dependencies, packageregistry.PackageDependencyInfo{
|
||||||
|
+ Name: dep.PackageNameExtra,
|
||||||
|
+ VersionSpec: dep.VersionSpec,
|
||||||
|
+ })
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ return &packageregistry.PackageDependencyList{
|
||||||
|
+ Dependencies: dependencies,
|
||||||
|
+ }, nil
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ resolver := newDependencyResolver(p.registry, dependencyResolverConfig{
|
||||||
|
+ IncludeDevDependencies: p.config.IncludeDevDependencies,
|
||||||
|
+ IncludeTransitiveDependencies: p.config.IncludeTransitiveDependencies,
|
||||||
|
+ TransitiveDepth: p.config.TransitiveDepth,
|
||||||
|
+ FailFast: p.config.FailFast,
|
||||||
|
+ MaxConcurrency: p.config.MaxConcurrency,
|
||||||
|
+ }, pypiVersionSpecResolverFn, pypiDependencyResolverFn)
|
||||||
|
|
||||||
|
return resolver.resolveDependencies(ctx, pkg)
|
||||||
|
}
|
||||||
|
@@ -124,7 +145,7 @@ type pypiPackageInfo struct {
|
||||||
|
RequiresDist []string `json:"requires_dist"`
|
||||||
|
}
|
||||||
|
|
||||||
|
-func GetPackageDependencies(packageName, version string) ([]PyPIDependencySpec, error) {
|
||||||
|
+func getPypiPackageDependencies(packageName, version string) ([]PyPIDependencySpec, error) {
|
||||||
|
url := fmt.Sprintf("https://pypi.org/pypi/%s/%s/json", packageName, version)
|
||||||
|
|
||||||
|
res, err := http.Get(url)
|
||||||
|
@@ -147,14 +168,19 @@ func GetPackageDependencies(packageName, version string) ([]PyPIDependencySpec,
|
||||||
|
return nil, ErrFailedToParsePackage
|
||||||
|
}
|
||||||
|
|
||||||
|
- pkgDeps := make([]PyPIDependencySpec, len(pypipkg.Info.RequiresDist))
|
||||||
|
+ pkgDeps := make([]PyPIDependencySpec, 0, len(pypipkg.Info.RequiresDist))
|
||||||
|
+
|
||||||
|
for _, dep := range pypipkg.Info.RequiresDist {
|
||||||
|
name, version, extra := pypiParseDependency(dep)
|
||||||
|
- pkgDeps = append(pkgDeps, PyPIDependencySpec{
|
||||||
|
- PackageNameExtra: name,
|
||||||
|
- VersionSpec: version,
|
||||||
|
- Extra: extra,
|
||||||
|
- })
|
||||||
|
+
|
||||||
|
+ // Skip dependencies with extras/conditions to avoid resolution issues
|
||||||
|
+ if extra == "" {
|
||||||
|
+ pkgDeps = append(pkgDeps, PyPIDependencySpec{
|
||||||
|
+ PackageNameExtra: name,
|
||||||
|
+ VersionSpec: version,
|
||||||
|
+ Extra: extra,
|
||||||
|
+ })
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkgDeps, nil
|
||||||
|
@@ -164,32 +190,33 @@ func GetPackageDependencies(packageName, version string) ([]PyPIDependencySpec,
|
||||||
|
// and conditional dependencies. Keeps extras as part of the package name.
|
||||||
|
// Example: "uvicorn[standard]>=0.12.0; extra == \"all\"" returns ("uvicorn[standard]", ">=0.12.0", "all")
|
||||||
|
func pypiParseDependency(input string) (string, string, string) {
|
||||||
|
- var name string
|
||||||
|
- var version string
|
||||||
|
-
|
||||||
|
// Split line by ';' to separate version and markers
|
||||||
|
parts := strings.SplitN(input, ";", 2)
|
||||||
|
mainPart := strings.TrimSpace(parts[0])
|
||||||
|
|
||||||
|
- // Find last occurrence of version operators
|
||||||
|
- operators := []string{"==", ">=", "<=", "!=", ">", "<", "~="}
|
||||||
|
- versionIndex := -1
|
||||||
|
-
|
||||||
|
- for _, op := range operators {
|
||||||
|
- if idx := strings.LastIndex(mainPart, op); idx != -1 {
|
||||||
|
- if idx > versionIndex {
|
||||||
|
- versionIndex = idx
|
||||||
|
- }
|
||||||
|
- }
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- if versionIndex != -1 {
|
||||||
|
- name = strings.TrimSpace(mainPart[:versionIndex])
|
||||||
|
- version = strings.TrimSpace(mainPart[versionIndex:])
|
||||||
|
+ // Regex to match the first occurrence of version operators
|
||||||
|
+ // Using lookahead to ensure we match standalone operators
|
||||||
|
+ operatorRegex := regexp.MustCompile(`(==|>=|<=|!=|>|<|~=)(?:\d|$)`)
|
||||||
|
+ match := operatorRegex.FindStringIndex(mainPart)
|
||||||
|
+
|
||||||
|
+ var name, version string
|
||||||
|
+ if match != nil {
|
||||||
|
+ // Everything before the operator is the name
|
||||||
|
+ name = strings.TrimSpace(mainPart[:match[0]])
|
||||||
|
+ // Remove trailing parentheses from name if present
|
||||||
|
+ name = strings.TrimRight(name, " (")
|
||||||
|
+
|
||||||
|
+ // Everything from the operator onwards is the version spec
|
||||||
|
+ version = strings.TrimSpace(mainPart[match[0]:])
|
||||||
|
+ // Remove parentheses from version spec if present
|
||||||
|
+ version = strings.Trim(version, "()")
|
||||||
|
} else {
|
||||||
|
+ // No version operator found
|
||||||
|
name = mainPart
|
||||||
|
+ version = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
+ // Extract extra marker if present
|
||||||
|
var extra string
|
||||||
|
if len(parts) == 2 {
|
||||||
|
extraRe := regexp.MustCompile(`extra\s*==\s*["']([^"']+)["']`)
|
||||||
|
@@ -211,6 +238,26 @@ func pipGetMatchingVersion(packageName, versionConstraint string) (string, error
|
||||||
|
if strings.HasPrefix(versionConstraint, "~=") {
|
||||||
|
versionConstraint = pipConvertCompatibleRelease(versionConstraint)
|
||||||
|
}
|
||||||
|
+ // Handle empty version constraint
|
||||||
|
+ if versionConstraint == "" {
|
||||||
|
+ // Get latest version
|
||||||
|
+ registry, err := packageregistry.NewPypiAdapter()
|
||||||
|
+ if err != nil {
|
||||||
|
+ return "", fmt.Errorf("failed to create pypi adapter: %w", err)
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ pd, err := registry.PackageDiscovery()
|
||||||
|
+ if err != nil {
|
||||||
|
+ return "", fmt.Errorf("failed to get package discovery: %w", err)
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ pkg, err := pd.GetPackage(packageName)
|
||||||
|
+ if err != nil {
|
||||||
|
+ return "", err
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ return pkg.LatestVersion, nil
|
||||||
|
+ }
|
||||||
|
|
||||||
|
registry, err := packageregistry.NewPypiAdapter()
|
||||||
|
if err != nil {
|
||||||
@@ -13,7 +13,9 @@ import (
|
|||||||
|
|
||||||
// Contract for a function that implements ecosystem specific version
|
// Contract for a function that implements ecosystem specific version
|
||||||
// resolver from a version range specification.
|
// resolver from a version range specification.
|
||||||
type versionSpecResolver func(packageName, version string) string
|
type versionSpecResolverFn func(packageName, version string) string
|
||||||
|
|
||||||
|
type dependencyResolverFn func(packageName, version string) (*packageregistry.PackageDependencyList, error)
|
||||||
|
|
||||||
type dependencyResolverConfig struct {
|
type dependencyResolverConfig struct {
|
||||||
IncludeDevDependencies bool
|
IncludeDevDependencies bool
|
||||||
@@ -24,14 +26,15 @@ type dependencyResolverConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type dependencyResolver struct {
|
type dependencyResolver struct {
|
||||||
client packageregistry.Client
|
client packageregistry.Client
|
||||||
config dependencyResolverConfig
|
config dependencyResolverConfig
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
versionSpecResolver versionSpecResolver
|
versionSpecResolver versionSpecResolverFn
|
||||||
|
packageDependencyResolver dependencyResolverFn
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDependencyResolver(client packageregistry.Client, config dependencyResolverConfig,
|
func newDependencyResolver(client packageregistry.Client, config dependencyResolverConfig,
|
||||||
versionSpecResolver versionSpecResolver) *dependencyResolver {
|
versionSpecResolver versionSpecResolverFn, packageDependencyResolver dependencyResolverFn) *dependencyResolver {
|
||||||
if config.MaxConcurrency <= 0 {
|
if config.MaxConcurrency <= 0 {
|
||||||
config.MaxConcurrency = 10
|
config.MaxConcurrency = 10
|
||||||
}
|
}
|
||||||
@@ -44,9 +47,10 @@ func newDependencyResolver(client packageregistry.Client, config dependencyResol
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &dependencyResolver{
|
return &dependencyResolver{
|
||||||
client: client,
|
client: client,
|
||||||
config: config,
|
config: config,
|
||||||
versionSpecResolver: versionSpecResolver,
|
versionSpecResolver: versionSpecResolver,
|
||||||
|
packageDependencyResolver: packageDependencyResolver,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +126,14 @@ func (r *dependencyResolver) resolvePackageDependenciesConcurrent(
|
|||||||
log.Debugf("resolving dependencies for %s@%s", packageVersion.Package.Name, packageVersion.Version)
|
log.Debugf("resolving dependencies for %s@%s", packageVersion.Package.Name, packageVersion.Version)
|
||||||
|
|
||||||
// Get dependencies for the current package
|
// Get dependencies for the current package
|
||||||
dependencyList, err := pd.GetPackageDependencies(packageVersion.Package.Name, packageVersion.Version)
|
var dependencyList *packageregistry.PackageDependencyList
|
||||||
|
var err error
|
||||||
|
if r.packageDependencyResolver != nil {
|
||||||
|
dependencyList, err = r.packageDependencyResolver(packageVersion.Package.Name, packageVersion.Version)
|
||||||
|
} else {
|
||||||
|
dependencyList, err = pd.GetPackageDependencies(packageVersion.Package.Name, packageVersion.Version)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ff(fmt.Errorf("failed to get package dependencies: %w", err))
|
return ff(fmt.Errorf("failed to get package dependencies: %w", err))
|
||||||
}
|
}
|
||||||
@@ -186,7 +197,6 @@ func (r *dependencyResolver) resolvePackageDependenciesConcurrent(
|
|||||||
return ff(fmt.Errorf("failed to resolve transitive dependency: %w", err))
|
return ff(fmt.Errorf("failed to resolve transitive dependency: %w", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ func (r *npmDependencyResolver) ResolveDependencies(ctx context.Context,
|
|||||||
MaxConcurrency: r.config.MaxConcurrency,
|
MaxConcurrency: r.config.MaxConcurrency,
|
||||||
}, func(packageName, version string) string {
|
}, func(packageName, version string) string {
|
||||||
return npmCleanVersion(version)
|
return npmCleanVersion(version)
|
||||||
})
|
}, nil)
|
||||||
|
|
||||||
return resolver.resolveDependencies(ctx, packageVersion)
|
return resolver.resolveDependencies(ctx, packageVersion)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,20 +56,41 @@ func NewPypiDependencyResolver(config PyPiDependencyResolverConfig) (*pypiDepend
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *pypiDependencyResolver) ResolveDependencies(ctx context.Context, pkg *packagev1.PackageVersion) ([]*packagev1.PackageVersion, error) {
|
func (p *pypiDependencyResolver) ResolveDependencies(ctx context.Context, pkg *packagev1.PackageVersion) ([]*packagev1.PackageVersion, error) {
|
||||||
|
pypiVersionSpecResolverFn := func(packageName, version string) string {
|
||||||
|
ver, err := pipGetMatchingVersion(packageName, version)
|
||||||
|
fmt.Printf("Resolved %s for %s to %s\n", version, packageName, ver)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("error getting matching version for %s@%s", packageName, version)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return ver
|
||||||
|
}
|
||||||
|
|
||||||
|
pypiDependencyResolverFn := func(packageName, version string) (*packageregistry.PackageDependencyList, error) {
|
||||||
|
resolvedDependencies, err := getPypiPackageDependencies(packageName, version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dependencies := make([]packageregistry.PackageDependencyInfo, 0)
|
||||||
|
for _, dep := range resolvedDependencies {
|
||||||
|
dependencies = append(dependencies, packageregistry.PackageDependencyInfo{
|
||||||
|
Name: dep.PackageNameExtra,
|
||||||
|
VersionSpec: dep.VersionSpec,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &packageregistry.PackageDependencyList{
|
||||||
|
Dependencies: dependencies,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
resolver := newDependencyResolver(p.registry, dependencyResolverConfig{
|
resolver := newDependencyResolver(p.registry, dependencyResolverConfig{
|
||||||
IncludeDevDependencies: p.config.IncludeDevDependencies,
|
IncludeDevDependencies: p.config.IncludeDevDependencies,
|
||||||
IncludeTransitiveDependencies: p.config.IncludeTransitiveDependencies,
|
IncludeTransitiveDependencies: p.config.IncludeTransitiveDependencies,
|
||||||
TransitiveDepth: p.config.TransitiveDepth,
|
TransitiveDepth: p.config.TransitiveDepth,
|
||||||
FailFast: p.config.FailFast,
|
FailFast: p.config.FailFast,
|
||||||
MaxConcurrency: p.config.MaxConcurrency,
|
MaxConcurrency: p.config.MaxConcurrency,
|
||||||
}, func(packageName, version string) string {
|
}, pypiVersionSpecResolverFn, pypiDependencyResolverFn)
|
||||||
ver, err := pipGetMatchingVersion(packageName, version)
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("error getting matching version for %s@%s", packageName, version)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return ver
|
|
||||||
})
|
|
||||||
|
|
||||||
return resolver.resolveDependencies(ctx, pkg)
|
return resolver.resolveDependencies(ctx, pkg)
|
||||||
}
|
}
|
||||||
@@ -124,7 +145,7 @@ type pypiPackageInfo struct {
|
|||||||
RequiresDist []string `json:"requires_dist"`
|
RequiresDist []string `json:"requires_dist"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPackageDependencies(packageName, version string) ([]PyPIDependencySpec, error) {
|
func getPypiPackageDependencies(packageName, version string) ([]PyPIDependencySpec, error) {
|
||||||
url := fmt.Sprintf("https://pypi.org/pypi/%s/%s/json", packageName, version)
|
url := fmt.Sprintf("https://pypi.org/pypi/%s/%s/json", packageName, version)
|
||||||
|
|
||||||
res, err := http.Get(url)
|
res, err := http.Get(url)
|
||||||
@@ -147,14 +168,19 @@ func GetPackageDependencies(packageName, version string) ([]PyPIDependencySpec,
|
|||||||
return nil, ErrFailedToParsePackage
|
return nil, ErrFailedToParsePackage
|
||||||
}
|
}
|
||||||
|
|
||||||
pkgDeps := make([]PyPIDependencySpec, len(pypipkg.Info.RequiresDist))
|
pkgDeps := make([]PyPIDependencySpec, 0, len(pypipkg.Info.RequiresDist))
|
||||||
|
|
||||||
for _, dep := range pypipkg.Info.RequiresDist {
|
for _, dep := range pypipkg.Info.RequiresDist {
|
||||||
name, version, extra := pypiParseDependency(dep)
|
name, version, extra := pypiParseDependency(dep)
|
||||||
pkgDeps = append(pkgDeps, PyPIDependencySpec{
|
|
||||||
PackageNameExtra: name,
|
// Skip dependencies with extras/conditions to avoid resolution issues
|
||||||
VersionSpec: version,
|
if extra == "" {
|
||||||
Extra: extra,
|
pkgDeps = append(pkgDeps, PyPIDependencySpec{
|
||||||
})
|
PackageNameExtra: name,
|
||||||
|
VersionSpec: version,
|
||||||
|
Extra: extra,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return pkgDeps, nil
|
return pkgDeps, nil
|
||||||
@@ -164,32 +190,33 @@ func GetPackageDependencies(packageName, version string) ([]PyPIDependencySpec,
|
|||||||
// and conditional dependencies. Keeps extras as part of the package name.
|
// and conditional dependencies. Keeps extras as part of the package name.
|
||||||
// Example: "uvicorn[standard]>=0.12.0; extra == \"all\"" returns ("uvicorn[standard]", ">=0.12.0", "all")
|
// Example: "uvicorn[standard]>=0.12.0; extra == \"all\"" returns ("uvicorn[standard]", ">=0.12.0", "all")
|
||||||
func pypiParseDependency(input string) (string, string, string) {
|
func pypiParseDependency(input string) (string, string, string) {
|
||||||
var name string
|
|
||||||
var version string
|
|
||||||
|
|
||||||
// Split line by ';' to separate version and markers
|
// Split line by ';' to separate version and markers
|
||||||
parts := strings.SplitN(input, ";", 2)
|
parts := strings.SplitN(input, ";", 2)
|
||||||
mainPart := strings.TrimSpace(parts[0])
|
mainPart := strings.TrimSpace(parts[0])
|
||||||
|
|
||||||
// Find last occurrence of version operators
|
// Regex to match the first occurrence of version operators
|
||||||
operators := []string{"==", ">=", "<=", "!=", ">", "<", "~="}
|
// Using lookahead to ensure we match standalone operators
|
||||||
versionIndex := -1
|
operatorRegex := regexp.MustCompile(`(==|>=|<=|!=|>|<|~=)(?:\d|$)`)
|
||||||
|
match := operatorRegex.FindStringIndex(mainPart)
|
||||||
|
|
||||||
for _, op := range operators {
|
var name, version string
|
||||||
if idx := strings.LastIndex(mainPart, op); idx != -1 {
|
if match != nil {
|
||||||
if idx > versionIndex {
|
// Everything before the operator is the name
|
||||||
versionIndex = idx
|
name = strings.TrimSpace(mainPart[:match[0]])
|
||||||
}
|
// Remove trailing parentheses from name if present
|
||||||
}
|
name = strings.TrimRight(name, " (")
|
||||||
}
|
|
||||||
|
|
||||||
if versionIndex != -1 {
|
// Everything from the operator onwards is the version spec
|
||||||
name = strings.TrimSpace(mainPart[:versionIndex])
|
version = strings.TrimSpace(mainPart[match[0]:])
|
||||||
version = strings.TrimSpace(mainPart[versionIndex:])
|
// Remove parentheses from version spec if present
|
||||||
|
version = strings.Trim(version, "()")
|
||||||
} else {
|
} else {
|
||||||
|
// No version operator found
|
||||||
name = mainPart
|
name = mainPart
|
||||||
|
version = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract extra marker if present
|
||||||
var extra string
|
var extra string
|
||||||
if len(parts) == 2 {
|
if len(parts) == 2 {
|
||||||
extraRe := regexp.MustCompile(`extra\s*==\s*["']([^"']+)["']`)
|
extraRe := regexp.MustCompile(`extra\s*==\s*["']([^"']+)["']`)
|
||||||
@@ -211,6 +238,26 @@ func pipGetMatchingVersion(packageName, versionConstraint string) (string, error
|
|||||||
if strings.HasPrefix(versionConstraint, "~=") {
|
if strings.HasPrefix(versionConstraint, "~=") {
|
||||||
versionConstraint = pipConvertCompatibleRelease(versionConstraint)
|
versionConstraint = pipConvertCompatibleRelease(versionConstraint)
|
||||||
}
|
}
|
||||||
|
// Handle empty version constraint
|
||||||
|
if versionConstraint == "" {
|
||||||
|
// Get latest version
|
||||||
|
registry, err := packageregistry.NewPypiAdapter()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create pypi adapter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pd, err := registry.PackageDiscovery()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get package discovery: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pkg, err := pd.GetPackage(packageName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkg.LatestVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
registry, err := packageregistry.NewPypiAdapter()
|
registry, err := packageregistry.NewPypiAdapter()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user