pubgrub

package module
v0.3.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 13, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

README

PubGrub-Go

A comprehensive Go implementation of the PubGrub version solving algorithm with semantic versioning, version ranges, and enhanced error reporting.

Go Version License Algorithm

Features

  • Semantic Versioning - Full semver support with major.minor.patch ordering
  • 📊 Version Ranges - Complex constraints like >=1.0.0, <2.0.0 || >=3.0.0
  • 🔍 Enhanced Error Messages - Human-readable explanations of why resolution fails
  • 🚀 CDCL Solver - Conflict-driven clause learning with unit propagation
  • 🧪 Well Tested - Comprehensive test suite with strong coverage
  • Production Ready - Handles complex dependency graphs efficiently
  • 🪵 Structured Debug Logging - Plug in log/slog via WithLogger for rich solver traces

Origin

This is a derivative work based on the tinyrange/tinyrange experimental/pubgrub package (v0.2.6). The original implementation was removed from the tinyrange repository. This package preserves the original work and significantly extends it with:

  • Semantic versioning support
  • Complex version range constraints
  • Enhanced error reporting with derivation trees
  • Comprehensive test suite
  • Production-ready CDCL solver

Original Copyright: Copyright 2024 The University of Queensland Enhancements: Copyright 2025 Contriboss License: Apache 2.0

About PubGrub

PubGrub is a version solving algorithm designed for dependency resolution. This implementation features a production-ready CDCL (Conflict-Driven Clause Learning) solver with unit propagation, learned clauses, and intelligent backtracking for efficient dependency resolution. The solver includes optional incompatibility tracking for generating detailed, human-readable error messages with derivation trees.

Installation

Requirements: Go 1.25 or later

go get github.com/contriboss/pubgrub-go

Quick Start

Basic Usage
package main

import (
    "fmt"
    "github.com/contriboss/pubgrub-go"
)

func main() {
    // Create a root source with your initial requirements
    root := pubgrub.NewRootSource()
    root.AddPackage("mypackage", pubgrub.EqualsCondition{Version: pubgrub.SimpleVersion("1.0.0")})

    // Create an in-memory source for your package repository
    source := &pubgrub.InMemorySource{}
    source.AddPackage("mypackage", pubgrub.SimpleVersion("1.0.0"), []pubgrub.Term{
        pubgrub.NewTerm("dependency", pubgrub.EqualsCondition{Version: pubgrub.SimpleVersion("2.0.0")}),
    })
    source.AddPackage("dependency", pubgrub.SimpleVersion("2.0.0"), nil)

    // Create a solver and solve
    solver := pubgrub.NewSolver(root, source)
    solution, err := solver.Solve(root.Term())
    if err != nil {
        panic(err)
    }

    fmt.Println("Solution:", solution)
}
Using New Features: Version Ranges + Better Errors
package main

import (
    "fmt"
    "os"
    "github.com/contriboss/pubgrub-go"
)

func main() {
    // Use semantic versioning
    webVersion, _ := pubgrub.ParseSemanticVersion("1.0.0")

    // Create requirements with version ranges
    httpRange, _ := pubgrub.ParseVersionRange(">=2.0.0, <3.0.0")
    jsonRange, _ := pubgrub.ParseVersionRange(">=1.5.0")

    root := pubgrub.NewRootSource()
    root.AddPackage("web", pubgrub.EqualsCondition{Version: webVersion})

    source := &pubgrub.InMemorySource{}
    source.AddPackage("web", webVersion, []pubgrub.Term{
        pubgrub.NewTerm("http", pubgrub.NewVersionSetCondition(httpRange)),
        pubgrub.NewTerm("json", pubgrub.NewVersionSetCondition(jsonRange)),
    })

    http2, _ := pubgrub.ParseSemanticVersion("2.5.0")
    json15, _ := pubgrub.ParseSemanticVersion("1.5.0")
    source.AddPackage("http", http2, nil)
    source.AddPackage("json", json15, nil)

    // Enable enhanced error reporting
    solver := pubgrub.NewSolver(root, source).EnableIncompatibilityTracking()
    solution, err := solver.Solve(root.Term())

    if err != nil {
        if nsErr, ok := err.(*pubgrub.NoSolutionError); ok {
            fmt.Fprintln(os.Stderr, nsErr.Error())
        } else {
            fmt.Fprintln(os.Stderr, err)
        }
        os.Exit(1)
    }

    for _, nv := range solution {
        fmt.Printf("✓ %s: %s\n", nv.Name, nv.Version)
    }
}
Debug Logging
package main

import (
    "log/slog"
    "os"

    "github.com/contriboss/pubgrub-go"
)

func main() {
    logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelDebug,
    }))

    root := pubgrub.NewRootSource()
    source := &pubgrub.InMemorySource{}

    solver := pubgrub.NewSolverWithOptions(
        []pubgrub.Source{root, source},
        pubgrub.WithLogger(logger),
    )

    if _, err := solver.Solve(root.Term()); err != nil {
        logger.Error("resolution failed", "err", err)
    }
}

Core Concepts

Versions
// Simple string-based versions (original)
v1 := pubgrub.SimpleVersion("1.0.0")

// Semantic versioning (new)
v2, _ := pubgrub.ParseSemanticVersion("1.2.3")
v3, _ := pubgrub.ParseSemanticVersion("2.0.0-alpha.1")
Version Constraints
// Exact match (original)
cond := pubgrub.EqualsCondition{Version: v}

// Version ranges (new)
set, _ := pubgrub.ParseVersionRange(">=1.0.0, <2.0.0")
cond := pubgrub.NewVersionSetCondition(set)

// Operators: >=, >, <=, <, ==, !=
// Compound: ">=1.0.0, <2.0.0" (AND)
// Union: ">=1.0.0 || >=3.0.0" (OR)
// Wildcard: "*" (any version)
Custom Conditions

You can create custom version constraints by implementing the Condition interface and optionally the VersionSetConverter interface for CDCL solver support:

// Custom caret constraint (like npm's ^1.2.3)
type CaretCondition struct {
    Base *SemanticVersion
}

func (cc CaretCondition) String() string {
    return fmt.Sprintf("^%s", cc.Base)
}

func (cc CaretCondition) Satisfies(ver Version) bool {
    sv, ok := ver.(*SemanticVersion)
    if !ok { return false }
    return sv.Major == cc.Base.Major && sv.Sort(cc.Base) >= 0
}

// Implement VersionSetConverter to enable CDCL solver support
func (cc CaretCondition) ToVersionSet() VersionSet {
    rangeStr := fmt.Sprintf(">=%d.%d.%d, <%d.0.0",
        cc.Base.Major, cc.Base.Minor, cc.Base.Patch,
        cc.Base.Major+1)
    set, _ := ParseVersionRange(rangeStr)
    return set
}

// Use it with the solver
base, _ := ParseSemanticVersion("1.2.0")
condition := CaretCondition{Base: base}
root.AddPackage("mylib", condition)

The VersionSetConverter interface enables your custom condition to participate in set operations (union, intersection, complement) required by the CDCL solver. Without it, custom conditions work for simple resolution but may fail in complex scenarios requiring conflict analysis.

Error Reporting
// Simple errors (default)
solver := pubgrub.NewSolver(root, source)
_, err := solver.Solve(root.Term())

// Enhanced errors (opt-in)
solver.EnableIncompatibilityTracking()
if nsErr, ok := err.(*pubgrub.NoSolutionError); ok {
    fmt.Println(nsErr.Error()) // Human-readable explanation
}
Solver Configuration

You can tune the solver with functional options when constructing it, or update an existing instance:

solver := pubgrub.NewSolverWithOptions(
    []pubgrub.Source{root, source},
    pubgrub.WithIncompatibilityTracking(true),
    pubgrub.WithMaxSteps(10_000), // 0 disables the watchdog
)

// Adjust configuration later if needed
solver.Configure(pubgrub.WithMaxSteps(0))

WithIncompatibilityTracking toggles derivation tree generation, while WithMaxSteps caps (or disables) the internal propagation watchdog used to detect runaway scenarios.

Performance Optimization with Caching

For sources with expensive I/O operations (network, disk, database), wrap them with CachedSource:

// Wrap an expensive source (e.g., HTTP API, database)
expensiveSource := &MyRegistrySource{} // implements Source
cached := pubgrub.NewCachedSource(expensiveSource)

solver := pubgrub.NewSolver(root, cached)
solution, _ := solver.Solve(root.Term())

// Check cache performance
stats := cached.GetCacheStats()
fmt.Printf("Cache hit rate: %.1f%%\n", stats.OverallHitRate * 100)

When to use caching:

  • ✅ Network sources (package registries, APIs)
  • ✅ Database or file system sources
  • ✅ Multiple dependency resolutions with the same source
  • ❌ InMemorySource (already fast, adds ~3-5% overhead)
  • ❌ Single-shot resolutions

API Reference

Core Types
  • Name - Package name identifier
  • Version - Interface for version representation
  • Condition - Interface for version constraints
  • VersionSetConverter - Optional interface for custom conditions to enable CDCL solver support
  • Term - Package name with constraint
  • Source - Package version/dependency queries
  • Solution - Resolved package versions
  • VersionSet - Set of versions with operations
Implementations
  • SimpleVersion - String-based version (original)
  • SemanticVersion - Full semver support (new)
  • EqualsCondition - Exact match (original)
  • VersionSetCondition - Version ranges (new)
  • InMemorySource - In-memory repository
  • CachedSource - Caching wrapper for expensive sources (new)
  • CombinedSource - Multiple sources
  • RootSource - Initial requirements
Solver
  • NewSolver(sources...) - Create solver with defaults
  • NewSolverWithOptions([]Source, ...SolverOption) - Create solver with configuration options
  • Solve(root) - Solve dependencies
  • EnableIncompatibilityTracking() - Enable detailed errors
  • Configure(...SolverOption) - Adjust options after construction
  • GetIncompatibilities() - Get tracked conflicts
Error Types
  • ErrNoSolutionFound - Simple error (original)
  • NoSolutionError - Enhanced error (new)
  • ErrIterationLimit - Solver exceeded configured step limit
  • DefaultReporter / CollapsedReporter - Error formatters (new)

Examples

See runnable examples in test files:

go test -v -run Example
  • ExampleVersionSetCondition - Using version ranges with solver
  • ExampleParseVersionRange - Parsing and testing version ranges
  • ExampleSemanticVersion - Semantic version parsing
  • ExampleNoSolutionError_defaultReporter - Enhanced error messages
  • ExampleNoSolutionError_collapsedReporter - Compact error format

Documentation

  • API Documentation - Full API reference (when published)
  • See example tests for usage patterns (go test -v -run Example)

Testing

# Run all tests
go test -v

# Run with coverage
go test -cover  # 82.7% coverage

# Run specific test suites
go test -v -run TestBackwardCompatibility
go test -v -run TestSemanticVersion
go test -v -run TestVersionRange
go test -v -run TestSolver

# Run benchmarks
go test -bench=. -benchmem

Benchmarks

Performance characteristics on Apple M1 Max:

Scenario Time/op Memory/op Ops/sec
Simple linear chain (4 packages) 14.7μs 17.3KB 67,000
Diamond dependency 19.4μs 22.5KB 51,000
Complex graph (10 packages) 43.7μs 48.7KB 23,000
Deep chain (20 levels) 92.0μs 98.8KB 10,800
Conflict detection 14.9μs 17.5KB 67,000

Key Insights:

  • ⚡ Most resolutions complete in microseconds
  • 🎯 Conflict detection is as fast as successful resolution
  • 📊 Incompatibility tracking adds <1% overhead
  • 🚀 Scales linearly with dependency graph complexity

Run go test -bench=. -benchmem to see performance on your system.

Performance & Status

  • Test Coverage: Strong coverage with comprehensive test suite
  • Tests: All passing
  • Algorithm: CDCL (Conflict-Driven Clause Learning) with unit propagation
  • Memory: Minimal; learned clauses and optional tracking add derivation trees
  • Caching: Optional CachedSource wrapper for expensive I/O operations
  • Production Ready: Yes, handles complex dependency graphs efficiently

Contributing

Contributions welcome! Please:

  1. Add tests for new features
  2. Update documentation
  3. Follow existing code style
  4. Ensure all tests pass

Attribution

This package is derived from the tinyrange project:

  • Original Repository: https://github.com/tinyrange/tinyrange
  • Original Package: experimental/pubgrub
  • Version: v0.3.3 (Nov 1, 2025)
  • Original Copyright: Copyright 2024 The University of Queensland
  • Original License: Apache 2.0

Enhancements and extensions:

  • Semantic Versioning: Copyright 2025 Contriboss
  • Version Ranges: Copyright 2025 Contriboss
  • Error Reporting: Copyright 2025 Contriboss
  • License: Apache 2.0

License

Apache License 2.0 - See LICENSE file for details.

This project maintains the same Apache 2.0 license as the original tinyrange implementation and includes proper attribution as required by the license terms.

Credits

This implementation builds upon the following works:

  1. Weizenbaum, N. (2018). PubGrub: Next-Generation Version Solving. Medium. https://medium.com/@nex3/pubgrub-2fb6470504f

  2. University of Queensland (2024). tinyrange/pubgrub (v0.2.6). Original Go implementation. https://github.com/tinyrange/tinyrange

  3. Dart Team (2018-present). PubGrub Specification. https://github.com/dart-lang/pub/blob/master/doc/solver.md

  4. pubgrub-rs Contributors (2019-present). pubgrub-rs. Rust reference implementation providing CDCL patterns. https://github.com/pubgrub-rs/pubgrub

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type CacheStats

type CacheStats struct {
	VersionsCalls     int
	VersionsCacheHits int
	VersionsHitRate   float64

	DepsCalls     int
	DepsCacheHits int
	DepsHitRate   float64

	TotalCalls     int
	TotalCacheHits int
	OverallHitRate float64
}

CacheStats returns statistics about cache performance.

type CachedSource

type CachedSource struct {
	// contains filtered or unexported fields
}

CachedSource wraps a Source and caches GetVersions and GetDependencies calls to improve performance when the same queries are made repeatedly.

WHEN TO USE: CachedSource is most beneficial for: - Sources with expensive network I/O (package registries, APIs) - Sources with disk I/O or database queries - Running multiple dependency resolutions without recreating the source - Build systems that resolve dependencies repeatedly

WHEN NOT TO USE: CachedSource adds ~3-5% overhead for: - InMemorySource (already fast, no repeated queries in CDCL solver) - Simple, single-shot dependency resolutions - Sources where queries are naturally cached upstream

The cache is maintained for the lifetime of the CachedSource instance and assumes that version lists and dependencies are immutable during solving.

func NewCachedSource

func NewCachedSource(source Source) *CachedSource

NewCachedSource creates a new caching wrapper around the given source.

func (*CachedSource) ClearCache

func (c *CachedSource) ClearCache()

ClearCache clears all cached data while preserving the underlying source.

func (*CachedSource) GetCacheStats

func (c *CachedSource) GetCacheStats() CacheStats

GetCacheStats returns cache performance statistics.

func (*CachedSource) GetDependencies

func (c *CachedSource) GetDependencies(name Name, version Version) ([]Term, error)

GetDependencies returns dependencies for a specific package version, caching the result.

func (*CachedSource) GetVersions

func (c *CachedSource) GetVersions(name Name) ([]Version, error)

GetVersions returns all available versions for a package, caching the result.

type CollapsedReporter

type CollapsedReporter struct{}

CollapsedReporter produces a more compact error format

func (*CollapsedReporter) Report

func (r *CollapsedReporter) Report(incomp *Incompatibility) string

Report implements Reporter with a collapsed format

type CombinedSource

type CombinedSource []Source

CombinedSource aggregates multiple package sources into a single source. When querying for versions or dependencies, it tries each source in order and combines the results.

This is useful for:

  • Combining local and remote package sources
  • Implementing package source fallbacks
  • Testing with mixed source types

Example:

local := &InMemorySource{}
remote := &RegistrySource{}
combined := CombinedSource{local, remote}
solver := NewSolver(root, combined)

func (CombinedSource) GetDependencies

func (s CombinedSource) GetDependencies(name Name, version Version) ([]Term, error)

GetDependencies queries sources in order and returns dependencies from the first source that has the specified package version.

func (CombinedSource) GetVersions

func (s CombinedSource) GetVersions(name Name) ([]Version, error)

GetVersions queries all sources and returns the combined set of versions in sorted order. Returns an error only if all sources fail with non-NotFound errors.

type Condition

type Condition interface {
	// String returns a human-readable representation of the condition.
	String() string

	// Satisfies returns true if the given version meets the condition.
	Satisfies(ver Version) bool
}

Condition represents a constraint on package versions. Basic conditions like equality are built-in, but custom conditions can be implemented by satisfying this interface.

Built-in implementations:

  • EqualsCondition: Exact version match
  • VersionSetCondition: Version range constraints

Example custom condition:

type MinVersionCondition struct {
    MinVersion Version
}

func (mvc MinVersionCondition) String() string {
    return fmt.Sprintf(">=%s", mvc.MinVersion)
}

func (mvc MinVersionCondition) Satisfies(ver Version) bool {
    return ver.Sort(mvc.MinVersion) >= 0
}

type DefaultReporter

type DefaultReporter struct{}

DefaultReporter produces readable error messages with hierarchical structure

func (*DefaultReporter) Report

func (r *DefaultReporter) Report(incomp *Incompatibility) string

Report implements Reporter

type DependencyError

type DependencyError struct {
	Package Name
	Version Version
	Err     error
}

DependencyError represents an error while fetching dependencies

func (*DependencyError) Error

func (e *DependencyError) Error() string

Error implements the error interface

func (*DependencyError) Unwrap

func (e *DependencyError) Unwrap() error

Unwrap returns the underlying error

type EqualsCondition

type EqualsCondition struct {
	Version Version
}

EqualsCondition represents an exact version match constraint. This is the most basic condition type, requiring a version to exactly match the specified version.

For more flexible constraints like version ranges, use VersionSetCondition with ParseVersionRange.

Example:

cond := EqualsCondition{Version: SimpleVersion("1.0.0")}
fmt.Println(cond.Satisfies(SimpleVersion("1.0.0"))) // true
fmt.Println(cond.Satisfies(SimpleVersion("1.0.1"))) // false

func (EqualsCondition) Satisfies

func (c EqualsCondition) Satisfies(ver Version) bool

Satisfies returns true if the given version exactly matches the constraint.

func (EqualsCondition) String

func (c EqualsCondition) String() string

String returns a human-readable representation of the condition.

type ErrIterationLimit

type ErrIterationLimit struct {
	Steps int
}

ErrIterationLimit is returned when the solver exceeds its maximum iteration count. This prevents infinite loops in pathological cases. Configure with WithMaxSteps(0) to disable the limit (not recommended for untrusted inputs).

Example:

solver := NewSolverWithOptions(
    []Source{root, source},
    WithMaxSteps(1000),
)
_, err := solver.Solve(root.Term())
if iterErr, ok := err.(ErrIterationLimit); ok {
    log.Printf("Solver exceeded %d steps", iterErr.Steps)
}

func (ErrIterationLimit) Error

func (e ErrIterationLimit) Error() string

Error implements the error interface.

type ErrNoSolutionFound

type ErrNoSolutionFound struct {
	Term Term
}

ErrNoSolutionFound is a simple error returned when solving fails without incompatibility tracking. For detailed error messages with derivation trees, enable WithIncompatibilityTracking and use NoSolutionError.

Example:

solver := NewSolver(root, source) // Tracking disabled by default
_, err := solver.Solve(root.Term())
if err != nil {
    if errors.Is(err, ErrNoSolutionFound{}) {
        // Handle no solution case
    }
}

func (ErrNoSolutionFound) Error

func (e ErrNoSolutionFound) Error() string

Error implements the error interface.

type InMemorySource

type InMemorySource struct {
	Packages map[Name]map[Version][]Term
}

InMemorySource provides an in-memory implementation of Source for testing and simple use cases. It stores all package versions and dependencies in memory without any I/O operations.

This is the simplest source implementation and is useful for:

  • Testing dependency resolution scenarios
  • Building example dependency graphs
  • Prototyping before implementing a real package source

For production use cases with network or database access, consider wrapping your source with CachedSource for performance.

Example:

source := &InMemorySource{}
source.AddPackage("lodash", SimpleVersion("1.0.0"), []Term{
    NewTerm("core-js", EqualsCondition{Version: SimpleVersion("2.0.0")}),
})
source.AddPackage("core-js", SimpleVersion("2.0.0"), nil)

func (*InMemorySource) AddPackage

func (s *InMemorySource) AddPackage(name Name, version Version, deps []Term)

AddPackage adds a package version with its dependencies to the source. If the package map is nil, it will be initialized automatically.

func (*InMemorySource) GetDependencies

func (s *InMemorySource) GetDependencies(name Name, version Version) ([]Term, error)

GetDependencies returns the dependency terms for a specific package version.

func (*InMemorySource) GetVersions

func (s *InMemorySource) GetVersions(name Name) ([]Version, error)

GetVersions returns all available versions of a package in sorted order.

type Incompatibility

type Incompatibility struct {
	// Terms that are incompatible
	Terms []Term
	// Kind of incompatibility
	Kind IncompatibilityKind
	// Cause1 and Cause2 are set for derived incompatibilities (Kind == KindConflict)
	Cause1 *Incompatibility
	Cause2 *Incompatibility
	// Package and Version for KindFromDependency
	Package Name
	Version Version
}

Incompatibility represents a set of package requirements that cannot all be satisfied

func NewIncompatibilityConflict

func NewIncompatibilityConflict(terms []Term, cause1, cause2 *Incompatibility) *Incompatibility

NewIncompatibilityConflict creates a derived incompatibility from two causes

func NewIncompatibilityFromDependency

func NewIncompatibilityFromDependency(pkg Name, ver Version, dependency Term) *Incompatibility

NewIncompatibilityFromDependency creates an incompatibility from a dependency Represents: package@version depends on dependency Per PubGrub spec: "foo ^1.0.0 depends on bar ^2.0.0" → {foo ^1.0.0, not bar ^2.0.0}

func NewIncompatibilityNoVersions

func NewIncompatibilityNoVersions(term Term) *Incompatibility

NewIncompatibilityNoVersions creates an incompatibility for when no versions exist

func (*Incompatibility) String

func (inc *Incompatibility) String() string

String returns a string representation of the incompatibility

type IncompatibilityKind

type IncompatibilityKind int

IncompatibilityKind represents the type/origin of an incompatibility

const (
	// KindNoVersions means no versions satisfy the constraint
	KindNoVersions IncompatibilityKind = iota
	// KindFromDependency means incompatibility from a package dependency
	KindFromDependency
	// KindConflict means derived from conflict resolution
	KindConflict
)

type Name

type Name = unique.Handle[string]

Name represents a package name using value interning for memory efficiency. Multiple instances of the same package name share the same underlying memory.

Name uses Go's unique.Handle for efficient string interning, enabling:

  • Fast equality comparisons (pointer comparison instead of string comparison)
  • Reduced memory usage when the same package names appear frequently
  • Safe concurrent access (interning is thread-safe)

func EmptyName

func EmptyName() Name

EmptyName returns an empty name (interned empty string). Useful for creating placeholder or root package names.

func MakeName

func MakeName(s string) Name

MakeName creates an interned Name from a string. Equal strings will return the same Name value, enabling fast comparisons.

Example:

pkg1 := MakeName("lodash")
pkg2 := MakeName("lodash")
// pkg1 == pkg2 (fast pointer comparison)

type NameVersion

type NameVersion struct {
	Name    Name
	Version Version
}

NameVersion represents a resolved package with its selected version. This is the fundamental unit of a dependency resolution solution.

func (NameVersion) String

func (n NameVersion) String() string

String returns a human-readable representation of the package-version pair.

type NoSolutionError

type NoSolutionError struct {
	// Incompatibility is the root cause of the failure
	Incompatibility *Incompatibility
	// Reporter is used to format the error message (defaults to DefaultReporter)
	Reporter Reporter
}

NoSolutionError is returned when version solving fails with detailed explanation

Example (CollapsedReporter)

Example demonstrating error reporting with collapsed reporter

// Same scenario but with collapsed reporter
source := &InMemorySource{}
source.AddPackage(MakeName("dropdown"), SimpleVersion("2.0.0"), []Term{
	NewTerm(MakeName("icons"), EqualsCondition{Version: SimpleVersion("2.0.0")}),
})
source.AddPackage(MakeName("icons"), SimpleVersion("1.0.0"), nil)
// Note: icons 2.0.0 doesn't exist

root := NewRootSource()
root.AddPackage(MakeName("dropdown"), EqualsCondition{Version: SimpleVersion("2.0.0")})

solver := NewSolver(root, source).EnableIncompatibilityTracking()
_, err := solver.Solve(root.Term())

if nsErr, ok := err.(*NoSolutionError); ok {
	// Use collapsed reporter for more compact output
	customErr := nsErr.WithReporter(&CollapsedReporter{})
	fmt.Println("Error:")
	fmt.Println(customErr.Error())
}
Output:

Error:
no versions of icons == 2.0.0 satisfy the constraint
And because dropdown 2.0.0 depends on icons == 2.0.0
And because dropdown == 2.0.0 is forbidden
And because $$root 1 depends on dropdown == 2.0.0
And because $$root == 1 is forbidden
Example (DefaultReporter)

Example demonstrating error reporting with derivation tree

// Create a conflict scenario:
// Package A v1.0 depends on B v1.0
// Package C v1.0 depends on B v2.0
// Root depends on both A and C
source := &InMemorySource{}
source.AddPackage(MakeName("A"), SimpleVersion("1.0.0"), []Term{
	NewTerm(MakeName("B"), EqualsCondition{Version: SimpleVersion("1.0.0")}),
})
source.AddPackage(MakeName("B"), SimpleVersion("1.0.0"), nil)
source.AddPackage(MakeName("B"), SimpleVersion("2.0.0"), nil)
source.AddPackage(MakeName("C"), SimpleVersion("1.0.0"), []Term{
	NewTerm(MakeName("B"), EqualsCondition{Version: SimpleVersion("2.0.0")}),
})

root := NewRootSource()
root.AddPackage(MakeName("A"), EqualsCondition{Version: SimpleVersion("1.0.0")})
root.AddPackage(MakeName("C"), EqualsCondition{Version: SimpleVersion("1.0.0")})

// Enable incompatibility tracking for detailed errors
solver := NewSolver(root, source).EnableIncompatibilityTracking()
_, err := solver.Solve(root.Term())

if err != nil {
	fmt.Println("Error:")
	fmt.Println(err.Error())
}
Output:

Error:
Because:
  Because:
    Because:
      Because C 1.0.0 depends on B == 2.0.0
    and:
      Because A 1.0.0 depends on B == 1.0.0
    these constraints conflict: C == 1.0.0 and A == 1.0.0
  and:
    Because $$root 1 depends on C == 1.0.0
  these constraints conflict: A == 1.0.0 and $$root == 1
and:
  Because $$root 1 depends on A == 1.0.0
$$root == 1 is forbidden.

func NewNoSolutionError

func NewNoSolutionError(incomp *Incompatibility) *NoSolutionError

NewNoSolutionError creates a new NoSolutionError from an incompatibility

func (*NoSolutionError) Error

func (e *NoSolutionError) Error() string

Error implements the error interface

func (*NoSolutionError) Unwrap

func (e *NoSolutionError) Unwrap() error

Unwrap returns the underlying error (for errors.Is/As compatibility)

func (*NoSolutionError) WithReporter

func (e *NoSolutionError) WithReporter(reporter Reporter) *NoSolutionError

WithReporter returns a new error with a custom reporter

type PackageNotFoundError

type PackageNotFoundError struct {
	Package Name
}

PackageNotFoundError indicates that a package is absent from the source.

func (*PackageNotFoundError) Error

func (e *PackageNotFoundError) Error() string

Error implements the error interface.

type PackageVersionNotFoundError

type PackageVersionNotFoundError struct {
	Package Name
	Version Version
}

PackageVersionNotFoundError indicates a specific version is unavailable.

func (*PackageVersionNotFoundError) Error

Error implements the error interface.

type Reporter

type Reporter interface {
	// Report generates a human-readable error message from an incompatibility
	Report(incomp *Incompatibility) string
}

Reporter is an interface for formatting incompatibilities into error messages

type RootSource

type RootSource []Term

RootSource provides a special source for initial dependency requirements. It creates a virtual "$$root" package that the solver uses as the starting point for dependency resolution.

In PubGrub, the root package has a single version ("1") whose dependencies are the user's initial requirements. This design allows the solver to treat the root requirements uniformly with other package dependencies.

Example:

root := NewRootSource()
root.AddPackage("lodash", EqualsCondition{Version: SimpleVersion("1.0.0")})
root.AddPackage("moment", EqualsCondition{Version: SimpleVersion("2.0.0")})
solver := NewSolver(root, otherSources...)
solution, _ := solver.Solve(root.Term())

func NewRootSource

func NewRootSource() *RootSource

NewRootSource creates a new empty root source.

func (*RootSource) AddPackage

func (s *RootSource) AddPackage(name Name, condition Condition)

AddPackage adds a single requirement to the root source. Each requirement becomes a dependency of the virtual root package.

func (RootSource) GetDependencies

func (s RootSource) GetDependencies(name Name, version Version) ([]Term, error)

GetDependencies returns the user's initial requirements for the root package.

func (RootSource) GetVersions

func (s RootSource) GetVersions(name Name) ([]Version, error)

GetVersions returns a single version for the root package only.

func (*RootSource) Term

func (s *RootSource) Term() Term

Term returns the term representing the root package itself. This is the starting term passed to Solver.Solve().

type SemanticVersion

type SemanticVersion struct {
	Major      int
	Minor      int
	Patch      int
	Prerelease string
	Build      string
}

SemanticVersion represents a semantic version (major.minor.patch[-prerelease][+build])

Example

ExampleSemanticVersion demonstrates semantic version parsing and comparison

// Parse semantic versions
v1, _ := pubgrub.ParseSemanticVersion("1.2.3")
v2, _ := pubgrub.ParseSemanticVersion("1.2.4")
v3, _ := pubgrub.ParseSemanticVersion("2.0.0-alpha")

// Compare versions
fmt.Println("v1 < v2:", v1.Sort(v2) < 0)
fmt.Println("v2 > v1:", v2.Sort(v1) > 0)
fmt.Println("v3 (prerelease) < 2.0.0:", v3.Sort(pubgrub.NewSemanticVersion(2, 0, 0)) < 0)
Output:

v1 < v2: true
v2 > v1: true
v3 (prerelease) < 2.0.0: true

func NewSemanticVersion

func NewSemanticVersion(major, minor, patch int) *SemanticVersion

NewSemanticVersion creates a new SemanticVersion with the given major, minor, and patch versions

func NewSemanticVersionWithPrerelease

func NewSemanticVersionWithPrerelease(major, minor, patch int, prerelease string) *SemanticVersion

NewSemanticVersionWithPrerelease creates a new SemanticVersion with prerelease info

func ParseSemanticVersion

func ParseSemanticVersion(s string) (*SemanticVersion, error)

ParseSemanticVersion parses a semantic version string Supports formats like: "1.2.3", "1.2.3-alpha", "1.2.3-alpha.1", "1.2.3+build", "1.2.3-alpha+build"

func (*SemanticVersion) Sort

func (sv *SemanticVersion) Sort(other Version) int

Sort implements Version.Sort Returns:

-1 if sv < other
 0 if sv == other
 1 if sv > other

Comparison follows semantic versioning rules: 1. Compare major, minor, patch numerically 2. Pre-release versions have lower precedence than normal versions 3. Build metadata is ignored for comparison

func (*SemanticVersion) String

func (sv *SemanticVersion) String() string

String returns the string representation of the semantic version

type SimpleVersion

type SimpleVersion string

SimpleVersion provides a basic string-based version implementation. Versions are compared lexicographically using string comparison.

For semantic versioning support, use SemanticVersion instead.

Example:

v1 := SimpleVersion("1.0.0")
v2 := SimpleVersion("2.0.0")
fmt.Println(v1.Sort(v2)) // prints negative number (v1 < v2)

func (SimpleVersion) Sort

func (v SimpleVersion) Sort(other Version) int

Sort implements Version by performing lexicographic string comparison. Returns:

  • negative if v < other
  • zero if v == other
  • positive if v > other

func (SimpleVersion) String

func (v SimpleVersion) String() string

String returns the string representation of the version.

type Solution

type Solution []NameVersion

Solution represents the complete set of resolved package versions. A solution maps package names to their selected versions, ensuring all dependency constraints are satisfied.

Example:

solution, err := solver.Solve(root.Term())
if err != nil {
    log.Fatal(err)
}
for pkg := range solution.All() {
    fmt.Printf("%s: %s\n", pkg.Name.Value(), pkg.Version)
}

func (Solution) All

func (s Solution) All() iter.Seq[NameVersion]

All returns an iterator over all package-version pairs in the solution. This enables using range-over-function syntax:

for pkg := range solution.All() {
    fmt.Printf("%s: %s\n", pkg.Name, pkg.Version)
}

func (Solution) GetVersion

func (s Solution) GetVersion(name Name) (Version, bool)

GetVersion retrieves the resolved version for a given package name. Returns the version and true if found, or nil and false if the package is not in the solution.

type Solver

type Solver struct {
	Source Source
	// contains filtered or unexported fields
}

Solver implements the PubGrub dependency resolution algorithm with CDCL.

The solver uses Conflict-Driven Clause Learning (CDCL) to efficiently find valid package version assignments that satisfy all dependencies and constraints. It maintains learned incompatibilities to avoid repeating failed resolution attempts.

Basic usage:

root := NewRootSource()
root.AddPackage("myapp", EqualsCondition{Version: SimpleVersion("1.0.0")})

source := &InMemorySource{}
// ... populate source with packages ...

solver := NewSolver(root, source)
solution, err := solver.Solve(root.Term())

With options:

solver := NewSolverWithOptions(
    []Source{root, source},
    WithIncompatibilityTracking(true),
    WithMaxSteps(10000),
)
Example (WithoutTracking)

Example showing backward compatibility without tracking

source := &InMemorySource{}
source.AddPackage(MakeName("foo"), SimpleVersion("1.0.0"), []Term{
	NewTerm(MakeName("bar"), EqualsCondition{Version: SimpleVersion("2.0.0")}),
})
source.AddPackage(MakeName("bar"), SimpleVersion("1.0.0"), nil)

root := NewRootSource()
root.AddPackage(MakeName("foo"), EqualsCondition{Version: SimpleVersion("1.0.0")})

// Without tracking, get simple error (backward compatible)
solver := NewSolver(root, source) // tracking disabled by default
_, err := solver.Solve(root.Term())

if err != nil {
	// Will be ErrNoSolutionFound, not NoSolutionError
	fmt.Printf("Error type: %T\n", err)
	// Error message will vary based on where solving fails
	if _, ok := err.(ErrNoSolutionFound); ok {
		fmt.Println("Got simple ErrNoSolutionFound (backward compatible)")
	}
}
Output:

Error type: pubgrub.ErrNoSolutionFound
Got simple ErrNoSolutionFound (backward compatible)

func NewSolver

func NewSolver(sources ...Source) *Solver

NewSolver creates a new solver with default options from multiple sources. The sources are combined into a single CombinedSource that tries each source in order.

Example:

root := NewRootSource()
source := &InMemorySource{}
solver := NewSolver(root, source)

func NewSolverWithOptions

func NewSolverWithOptions(sources []Source, opts ...SolverOption) *Solver

func (*Solver) ClearIncompatibilities

func (s *Solver) ClearIncompatibilities()

func (*Solver) Configure

func (s *Solver) Configure(opts ...SolverOption) *Solver

func (*Solver) DisableIncompatibilityTracking

func (s *Solver) DisableIncompatibilityTracking() *Solver

func (*Solver) EnableIncompatibilityTracking

func (s *Solver) EnableIncompatibilityTracking() *Solver

func (*Solver) GetIncompatibilities

func (s *Solver) GetIncompatibilities() []*Incompatibility
Example

Example demonstrating incompatibility tracking

source := &InMemorySource{}
source.AddPackage(MakeName("foo"), SimpleVersion("1.0.0"), []Term{
	NewTerm(MakeName("bar"), EqualsCondition{Version: SimpleVersion("2.0.0")}),
})
source.AddPackage(MakeName("bar"), SimpleVersion("1.0.0"), nil)

root := NewRootSource()
root.AddPackage(MakeName("foo"), EqualsCondition{Version: SimpleVersion("1.0.0")})

solver := NewSolver(root, source).EnableIncompatibilityTracking()
_, err := solver.Solve(root.Term())

if err != nil {
	fmt.Printf("Solving failed: %v\n", err)

	// Get all tracked incompatibilities
	incomps := solver.GetIncompatibilities()
	fmt.Printf("Tracked %d incompatibilities during solving\n", len(incomps))

	for i, incomp := range incomps {
		fmt.Printf("  [%d] %s (kind: %d)\n", i+1, incomp.String(), incomp.Kind)
	}
}
Output:

Solving failed: Because:
  Because:
    No versions of bar == 2.0.0 satisfy the constraint
  and:
    Because foo 1.0.0 depends on bar == 2.0.0
  foo == 1.0.0 is forbidden.
and:
  Because $$root 1 depends on foo == 1.0.0
$$root == 1 is forbidden.
Tracked 4 incompatibilities during solving
  [1] $$root 1 depends on foo == 1.0.0 (kind: 1)
  [2] foo 1.0.0 depends on bar == 2.0.0 (kind: 1)
  [3] foo == 1.0.0 is forbidden (kind: 2)
  [4] foo == 1.0.0 is forbidden (kind: 2)

func (*Solver) Solve

func (s *Solver) Solve(root Term) (Solution, error)

type SolverOption

type SolverOption func(*SolverOptions)

SolverOption is a functional option for configuring the solver.

func WithIncompatibilityTracking

func WithIncompatibilityTracking(enabled bool) SolverOption

WithIncompatibilityTracking enables or disables incompatibility tracking. When enabled, the solver collects learned clauses and provides detailed error messages with derivation trees.

Example:

solver := NewSolverWithOptions(
    []Source{root, source},
    WithIncompatibilityTracking(true),
)

func WithLogger

func WithLogger(logger *slog.Logger) SolverOption

WithLogger sets a structured logger for solver diagnostics. The logger receives debug messages during solving, useful for understanding the solver's decision-making process.

Example:

logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))
solver := NewSolverWithOptions(
    []Source{root, source},
    WithLogger(logger),
)

func WithMaxSteps

func WithMaxSteps(steps int) SolverOption

WithMaxSteps sets the maximum number of solver iterations. Use 0 to disable the limit (allows unbounded execution).

The iteration limit prevents infinite loops in pathological cases. Most real-world dependency graphs resolve in thousands of steps.

Example:

solver := NewSolverWithOptions(
    []Source{root, source},
    WithMaxSteps(10000), // Limit to 10k iterations
)

func WithPreferHighestVersions added in v0.3.4

func WithPreferHighestVersions(enabled bool) SolverOption

WithPreferHighestVersions forces the solver to choose the highest allowed version. This matches Bundler-style "latest" resolution behavior.

type SolverOptions

type SolverOptions struct {
	// TrackIncompatibilities enables collecting learned clauses for error reporting.
	// When enabled, NoSolutionError will include a detailed derivation tree.
	// When disabled, returns simple ErrNoSolutionFound.
	TrackIncompatibilities bool

	// MaxSteps limits the number of solver iterations.
	// Set to 0 to disable the limit (not recommended for untrusted inputs).
	// Default: 100000
	MaxSteps int

	// Logger enables debug logging of solver operations.
	// When nil, no logging is performed.
	Logger *slog.Logger

	// PreferHighestVersions forces the solver to pick the highest allowed version.
	// When false, the solver uses a dependency-flexibility heuristic.
	PreferHighestVersions bool
}

SolverOptions configures the behavior of the dependency solver.

Options control:

  • Incompatibility tracking for enhanced error reporting
  • Maximum iteration limits to prevent infinite loops
  • Debug logging for solver diagnostics

type Source

type Source interface {
	// GetVersions returns all versions of a package in sorted order.
	// Versions should be sorted from lowest to highest, as the solver
	// selects from the highest available version.
	GetVersions(name Name) ([]Version, error)

	// GetDependencies returns the dependency terms for a specific package version.
	GetDependencies(name Name, version Version) ([]Term, error)
}

Source provides access to package versions and their dependencies. Implementations can fetch from in-memory stores, network registries, file systems, or any other package source.

Built-in implementations:

  • InMemorySource: Simple in-memory storage for testing
  • CombinedSource: Aggregates multiple sources
  • RootSource: Special source for initial requirements
  • CachedSource: Wraps a source with caching for performance

Example custom source:

type RegistrySource struct {
    BaseURL string
    Client  *http.Client
}

func (rs *RegistrySource) GetVersions(name Name) ([]Version, error) {
    resp, err := rs.Client.Get(rs.BaseURL + "/packages/" + name.Value() + "/versions")
    // ... parse response ...
}

func (rs *RegistrySource) GetDependencies(name Name, version Version) ([]Term, error) {
    resp, err := rs.Client.Get(rs.BaseURL + "/packages/" + name.Value() + "/" + version.String())
    // ... parse response ...
}

type Term

type Term struct {
	Name      Name
	Condition Condition
	Positive  bool
}

Term represents a dependency constraint, either positive or negative. A positive term (e.g., "lodash >=1.0.0") asserts that a package must satisfy the condition. A negative term (e.g., "not lodash ==1.5.0") excludes versions that match the condition.

Terms are the building blocks of dependency resolution, combining package names with version constraints and polarity.

func NewNegativeTerm

func NewNegativeTerm(name Name, condition Condition) Term

NewNegativeTerm creates a negative term excluding versions matching the condition.

func NewTerm

func NewTerm(name Name, condition Condition) Term

NewTerm creates a positive term requiring the package to satisfy the condition.

func (Term) IsPositive

func (t Term) IsPositive() bool

IsPositive reports whether the term asserts a positive constraint.

func (Term) Negate

func (t Term) Negate() Term

Negate returns the logical negation of the term. A positive term becomes negative and vice versa.

func (Term) SatisfiedBy

func (t Term) SatisfiedBy(ver Version) bool

SatisfiedBy reports whether the provided version satisfies the term. A nil version indicates the package is not selected.

For positive terms, returns true if the version matches the condition. For negative terms, returns true if the version does NOT match the condition.

func (Term) String

func (t Term) String() string

String returns a human-readable representation of the term.

type Version

type Version interface {
	// String returns a human-readable representation of the version.
	String() string

	// Sort compares this version to another.
	// Returns:
	//   - negative if this version < other
	//   - zero if this version == other
	//   - positive if this version > other
	Sort(other Version) int
}

Version represents a package version in the dependency resolution system. Implementations must provide string representation and comparison.

The PubGrub algorithm is version-type agnostic - any type can be used as long as it implements this interface. Built-in implementations include:

  • SimpleVersion: Lexicographic string comparison
  • SemanticVersion: Full semver with major.minor.patch ordering

Example custom version:

type DateVersion time.Time

func (dv DateVersion) String() string {
    return time.Time(dv).Format("2006-01-02")
}

func (dv DateVersion) Sort(other Version) int {
    otherDate, ok := other.(DateVersion)
    if !ok {
        return strings.Compare(dv.String(), other.String())
    }
    return time.Time(dv).Compare(time.Time(otherDate))
}

type VersionError

type VersionError struct {
	Package Name
	Message string
}

VersionError represents an error related to version constraints

func (*VersionError) Error

func (e *VersionError) Error() string

Error implements the error interface

type VersionIntervalSet

type VersionIntervalSet struct {
	// contains filtered or unexported fields
}

VersionIntervalSet implements VersionSet using sorted, disjoint intervals. This representation efficiently handles common version constraints like ranges and unions.

Intervals are stored in normalized form: sorted, non-empty, non-overlapping, and with no adjacent intervals that could be merged. This ensures efficient set operations and canonical string representations.

Example:

set := &VersionIntervalSet{}
set1 := ParseVersionRange(">=1.0.0, <2.0.0")
set2 := ParseVersionRange(">=1.5.0, <3.0.0")
union := set1.Union(set2) // >=1.0.0, <3.0.0

func (*VersionIntervalSet) Complement

func (s *VersionIntervalSet) Complement() VersionSet

Complement returns the set of versions NOT in this set.

func (*VersionIntervalSet) Contains

func (s *VersionIntervalSet) Contains(version Version) bool

Contains tests if a specific version is in the set.

func (*VersionIntervalSet) Empty

func (s *VersionIntervalSet) Empty() VersionSet

Empty returns a VersionSet containing no versions.

func (*VersionIntervalSet) Full

func (s *VersionIntervalSet) Full() VersionSet

Full returns a VersionSet containing all possible versions.

func (*VersionIntervalSet) Intersection

func (s *VersionIntervalSet) Intersection(other VersionSet) VersionSet

Intersection returns the set of versions in both this set and the other.

func (*VersionIntervalSet) Intervals

func (s *VersionIntervalSet) Intervals() iter.Seq[versionInterval]

Intervals returns an iterator over the internal version intervals. This enables using range-over-function syntax:

for interval := range versionSet.Intervals() {
    fmt.Printf("Range: %v to %v\n", interval.lower, interval.upper)
}

func (*VersionIntervalSet) IsDisjoint

func (s *VersionIntervalSet) IsDisjoint(other VersionSet) bool

IsDisjoint returns true if this set and the other set have no versions in common.

func (*VersionIntervalSet) IsEmpty

func (s *VersionIntervalSet) IsEmpty() bool

IsEmpty returns true if the set contains no versions.

func (*VersionIntervalSet) IsSubset

func (s *VersionIntervalSet) IsSubset(other VersionSet) bool

IsSubset returns true if all versions in this set are also in the other set.

func (*VersionIntervalSet) Singleton

func (s *VersionIntervalSet) Singleton(version Version) VersionSet

Singleton returns a VersionSet containing exactly one version.

func (*VersionIntervalSet) String

func (s *VersionIntervalSet) String() string

String returns a human-readable representation of the set. Empty sets display as "∅", full sets as "*", and intervals use standard operators.

func (*VersionIntervalSet) Union

func (s *VersionIntervalSet) Union(other VersionSet) VersionSet

Union returns the set of versions in either this set or the other.

type VersionSet

type VersionSet interface {
	// Empty returns a VersionSet containing no versions.
	Empty() VersionSet

	// Full returns a VersionSet containing all possible versions.
	Full() VersionSet

	// Singleton returns a VersionSet containing exactly one version.
	Singleton(version Version) VersionSet

	// Union returns the set of versions in either this set or the other.
	Union(other VersionSet) VersionSet

	// Intersection returns the set of versions in both this set and the other.
	Intersection(other VersionSet) VersionSet

	// Complement returns the set of versions NOT in this set.
	Complement() VersionSet

	// Contains tests if a specific version is in the set.
	Contains(version Version) bool

	// IsEmpty returns true if the set contains no versions.
	IsEmpty() bool

	// IsSubset returns true if all versions in this set are also in the other set.
	IsSubset(other VersionSet) bool

	// IsDisjoint returns true if this set and the other set have no versions in common.
	IsDisjoint(other VersionSet) bool

	// String returns a human-readable representation of the set.
	String() string
}

VersionSet represents a set of versions that can be used in version constraints. Implementations must be immutable – all operations return new instances.

VersionSet enables algebraic operations on version constraints, supporting:

  • Union: combining multiple version ranges
  • Intersection: finding common versions between constraints
  • Complement: inverting version constraints
  • Subset/Disjoint testing: analyzing constraint relationships

The primary implementation is VersionIntervalSet, which efficiently represents version ranges as sorted, non-overlapping intervals.

Example usage:

// Parse a version range from string
set1, _ := ParseVersionRange(">=1.0.0, <2.0.0")
set2, _ := ParseVersionRange(">=1.5.0, <3.0.0")

// Combine constraints
union := set1.Union(set2)        // >=1.0.0, <3.0.0
intersection := set1.Intersection(set2) // >=1.5.0, <2.0.0
complement := set1.Complement()   // <1.0.0 || >=2.0.0

func EmptyVersionSet

func EmptyVersionSet() VersionSet

EmptyVersionSet returns a VersionSet that contains no versions. Useful for creating impossible constraints or complement operations.

func FullVersionSet

func FullVersionSet() VersionSet

FullVersionSet returns a VersionSet that contains all possible versions. Equivalent to "*" or "any version" constraint.

func NewLowerBoundVersionSet added in v0.2.7

func NewLowerBoundVersionSet(version Version, inclusive bool) VersionSet

NewLowerBoundVersionSet creates a VersionSet with only a lower bound. Examples: ">= 1.0.0" (inclusive=true), "> 1.0.0" (inclusive=false)

Example:

v, _ := ParseSemanticVersion("1.0.0")
set := NewLowerBoundVersionSet(v, true) // >=1.0.0

func NewUpperBoundVersionSet added in v0.2.7

func NewUpperBoundVersionSet(version Version, inclusive bool) VersionSet

NewUpperBoundVersionSet creates a VersionSet with only an upper bound. Examples: "<= 2.0.0" (inclusive=true), "< 2.0.0" (inclusive=false)

Example:

v, _ := ParseSemanticVersion("2.0.0")
set := NewUpperBoundVersionSet(v, false) // <2.0.0

func NewVersionRangeSet added in v0.2.7

func NewVersionRangeSet(lower Version, lowerInclusive bool, upper Version, upperInclusive bool) VersionSet

NewVersionRangeSet creates a VersionSet from lower and upper bounds. This helper allows custom Version implementations to create intervals without relying on ParseVersionRange which uses SemanticVersion.

Example:

// Create range [1.0.0, 2.0.0)
lower, _ := ParseSemanticVersion("1.0.0")
upper, _ := ParseSemanticVersion("2.0.0")
set := NewVersionRangeSet(lower, true, upper, false)

func ParseVersionRange

func ParseVersionRange(s string) (VersionSet, error)

ParseVersionRange parses a version range string and returns a VersionSet.

Supported syntax:

  • Comparison operators: >=, >, <=, <, ==, !=, =
  • Comma-separated conjunctions (AND): ">=1.0.0, <2.0.0"
  • Double-pipe disjunctions (OR): ">=1.0.0 || >=2.0.0"
  • Wildcard "*" for any version

Examples:

ParseVersionRange(">=1.0.0, <2.0.0")     // [1.0.0, 2.0.0)
ParseVersionRange(">=1.0.0 || >=3.0.0")  // >=1.0.0 OR >=3.0.0
ParseVersionRange("*")                   // Any version
ParseVersionRange("==1.5.0")             // Exactly 1.5.0
ParseVersionRange("!=1.5.0")             // Not 1.5.0

The parser tries to interpret versions as SemanticVersion first, falling back to SimpleVersion if parsing fails. This allows mixing version types within a constraint string.

Example

ExampleParseVersionRange demonstrates parsing various version range formats

// Simple range
range1, _ := pubgrub.ParseVersionRange(">=1.0.0")
fmt.Println("Range 1:", range1.String())

// Compound range (AND)
range2, _ := pubgrub.ParseVersionRange(">=1.0.0, <2.0.0")
fmt.Println("Range 2:", range2.String())

// Union range (OR)
range3, _ := pubgrub.ParseVersionRange(">=1.0.0, <2.0.0 || >=3.0.0")
fmt.Println("Range 3:", range3.String())

// Test if a version is in the range
v150, _ := pubgrub.ParseSemanticVersion("1.5.0")
fmt.Println("1.5.0 in range2:", range2.Contains(v150))
Output:

Range 1: >=1.0.0
Range 2: >=1.0.0, <2.0.0
Range 3: >=1.0.0, <2.0.0 || >=3.0.0
1.5.0 in range2: true

type VersionSetCondition

type VersionSetCondition struct {
	Set VersionSet
}

VersionSetCondition implements Condition using a VersionSet. This enables using complex version constraints (ranges, unions, complements) as dependency conditions.

Example:

// Create a version range condition
set, _ := ParseVersionRange(">=1.0.0, <2.0.0")
condition := NewVersionSetCondition(set)
term := NewTerm("lodash", condition)
Example

ExampleVersionSetCondition demonstrates how to use version ranges with the pubgrub solver

// Create an in-memory package source
source := &pubgrub.InMemorySource{}

// Add package versions with semantic versioning
v100, _ := pubgrub.ParseSemanticVersion("1.0.0")
v110, _ := pubgrub.ParseSemanticVersion("1.1.0")
v200, _ := pubgrub.ParseSemanticVersion("2.0.0")
v210, _ := pubgrub.ParseSemanticVersion("2.1.0")

// Create version range conditions
range1x, _ := pubgrub.ParseVersionRange(">=1.0.0, <2.0.0")
range2x, _ := pubgrub.ParseVersionRange(">=2.0.0")

// Package A has multiple versions
source.AddPackage(pubgrub.MakeName("A"), v100, []pubgrub.Term{})
source.AddPackage(pubgrub.MakeName("A"), v110, []pubgrub.Term{
	pubgrub.NewTerm(pubgrub.MakeName("B"), pubgrub.NewVersionSetCondition(range2x)),
})

// Package B has multiple versions
source.AddPackage(pubgrub.MakeName("B"), v200, []pubgrub.Term{})
source.AddPackage(pubgrub.MakeName("B"), v210, []pubgrub.Term{})

// Create a root source that requires package A with version range
root := pubgrub.NewRootSource()
root.AddPackage(pubgrub.MakeName("A"), pubgrub.NewVersionSetCondition(range1x))

// Create a solver and solve
solver := pubgrub.NewSolver(root, source)
solution, err := solver.Solve(root.Term())

if err != nil {
	fmt.Printf("Error: %v\n", err)
	return
}

// Print the solution (sorted by name for consistency)
for _, nv := range solution {
	if nv.Name != pubgrub.MakeName("$$root") {
		fmt.Printf("%s = %s\n", nv.Name.Value(), nv.Version)
	}
}
Output:

A = 1.1.0
B = 2.1.0

func NewVersionSetCondition

func NewVersionSetCondition(set VersionSet) *VersionSetCondition

NewVersionSetCondition creates a new VersionSetCondition from a VersionSet.

func (*VersionSetCondition) Satisfies

func (vsc *VersionSetCondition) Satisfies(ver Version) bool

Satisfies returns true if the given version satisfies the condition.

func (*VersionSetCondition) String

func (vsc *VersionSetCondition) String() string

String returns a human-readable representation of the condition.

func (*VersionSetCondition) ToVersionSet added in v0.2.7

func (vsc *VersionSetCondition) ToVersionSet() VersionSet

ToVersionSet implements VersionSetConverter, enabling CDCL solver support.

type VersionSetConverter added in v0.2.0

type VersionSetConverter interface {
	// ToVersionSet converts the condition to a VersionSet for algebraic operations.
	ToVersionSet() VersionSet
}

VersionSetConverter is an optional interface that Condition implementations can provide to enable conversion to VersionSet for use with the CDCL solver.

The CDCL solver needs to perform set operations (intersection, union, complement) on version constraints. Conditions that implement this interface can participate in these operations, enabling them to work with unit propagation and conflict resolution.

Built-in conditions (EqualsCondition, VersionSetCondition) are already handled by the solver. Custom condition types should implement this interface to enable solver support.

Example custom condition:

type SemverCaretCondition struct {
    Base *SemanticVersion
}

func (sc SemverCaretCondition) String() string {
    return fmt.Sprintf("^%s", sc.Base)
}

func (sc SemverCaretCondition) Satisfies(ver Version) bool {
    sv, ok := ver.(*SemanticVersion)
    if !ok {
        return false
    }
    return sv.Major == sc.Base.Major &&
           sv.Sort(sc.Base) >= 0 &&
           sv.Major == sc.Base.Major
}

func (sc SemverCaretCondition) ToVersionSet() VersionSet {
    // Convert ^1.2.3 to >=1.2.3 <2.0.0
    upper := &SemanticVersion{Major: sc.Base.Major + 1}
    return NewVersionRangeSet(sc.Base, true, upper, false)
}
Example

Example showing how to use a custom condition with the solver

// Define a custom caret condition ^1.2.0
base, _ := pubgrub.ParseSemanticVersion("1.2.0")
caretCondition := CaretCondition{Base: base}

// Create a source with several versions
source := &pubgrub.InMemorySource{}
v1_2_0, _ := pubgrub.ParseSemanticVersion("1.2.0")
v1_3_0, _ := pubgrub.ParseSemanticVersion("1.3.0")
v2_0_0, _ := pubgrub.ParseSemanticVersion("2.0.0")

source.AddPackage(pubgrub.MakeName("mylib"), v1_2_0, nil)
source.AddPackage(pubgrub.MakeName("mylib"), v1_3_0, nil)
source.AddPackage(pubgrub.MakeName("mylib"), v2_0_0, nil)

// Use the custom condition
root := pubgrub.NewRootSource()
root.AddPackage(pubgrub.MakeName("mylib"), caretCondition)

// Solve
solver := pubgrub.NewSolver(root, source)
solution, _ := solver.Solve(root.Term())

// The solver picks the highest compatible version (1.3.0, not 2.0.0)
for _, nv := range solution {
	if nv.Name.Value() == "mylib" {
		fmt.Printf("Selected version: %s\n", nv.Version)
	}
}
Output:

Selected version: 1.3.0

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL