Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generics-based resolver #19

Merged
merged 5 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 91 additions & 64 deletions resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,21 @@ type Dependency interface {
}

// Release represents a release, it must provide methods to return Name, Version and Dependencies
type Release interface {
type Release[D Dependency] interface {
GetName() string
GetVersion() *Version
GetDependencies() []Dependency
GetDependencies() []D
}

func match(r Release, dep Dependency) bool {
return r.GetName() == dep.GetName() && dep.GetConstraint().Match(r.GetVersion())
}

// Releases is a list of Release
type Releases []Release
// Releases is a list of Release of the same package (all releases with
// the same Name but different Version)
type Releases[R Release[D], D Dependency] []R

// FilterBy return a subset of the Releases matching the provided Dependency
func (set Releases) FilterBy(dep Dependency) Releases {
res := []Release{}
// FilterBy return a subset of the Releases matching the provided Constraint
func (set Releases[R, D]) FilterBy(c Constraint) Releases[R, D] {
var res Releases[R, D]
for _, r := range set {
if match(r, dep) {
if c.Match(r.GetVersion()) {
res = append(res, r)
}
}
Expand All @@ -41,108 +38,138 @@ func (set Releases) FilterBy(dep Dependency) Releases {

// SortDescent sort the Releases in this set in descending order (the lastest
// release is the first)
func (set Releases) SortDescent() {
func (set Releases[R, D]) SortDescent() {
sort.Slice(set, func(i, j int) bool {
return set[i].GetVersion().GreaterThan(set[j].GetVersion())
})
}

// Archive contains all Releases set to consider for dependency resolution
type Archive struct {
Releases map[string]Releases
// Resolver is a container with references to all Releases to consider for
// dependency resolution
type Resolver[R Release[D], D Dependency] struct {
releases map[string]Releases[R, D]

// resolver state
solution map[string]R
depsToProcess []D
problematicDeps map[dependencyHash]int
}

// Resolve will try to depp-resolve dependencies from the Release passed as
// arguent using a backtracking algorithm.
func (ar *Archive) Resolve(release Release) []Release {
mainDep := &bareDependency{
name: release.GetName(),
version: release.GetVersion(),
// NewResolver creates a new archive
func NewResolver[R Release[D], D Dependency]() *Resolver[R, D] {
return &Resolver[R, D]{
releases: map[string]Releases[R, D]{},
}
return ar.resolve(map[string]Release{}, []Dependency{mainDep}, map[Dependency]int{})
}

type bareDependency struct {
name string
version *Version
// AddRelease adds a release to this archive
func (ar *Resolver[R, D]) AddRelease(rel R) {
relName := rel.GetName()
ar.releases[relName] = append(ar.releases[relName], rel)
}

func (b *bareDependency) GetName() string {
return b.name
// AddReleases adds all the releases to this archive
func (ar *Resolver[R, D]) AddReleases(rels ...R) {
for _, rel := range rels {
relName := rel.GetName()
ar.releases[relName] = append(ar.releases[relName], rel)
}
}

func (b *bareDependency) GetConstraint() Constraint {
return &Equals{Version: b.version}
// Resolve will try to depp-resolve dependencies from the Release passed as
// arguent using a backtracking algorithm. This function is NOT thread-safe.
func (ar *Resolver[R, D]) Resolve(release R) Releases[R, D] {
// Initial empty state of the resolver
ar.solution = map[string]R{}
ar.depsToProcess = []D{}
ar.problematicDeps = map[dependencyHash]int{}

// Check if the release is in the archive
if len(ar.releases[release.GetName()].FilterBy(&Equals{Version: release.GetVersion()})) == 0 {
return nil
}

// Add the requested release to the solution and proceed
// with the dependencies resolution
ar.solution[release.GetName()] = release
ar.depsToProcess = append(ar.depsToProcess, release.GetDependencies()...)
return ar.resolve()
}

func (b *bareDependency) String() string {
return b.GetName() + b.GetConstraint().String()
type dependencyHash string

func hashDependency[D Dependency](dep D) dependencyHash {
return dependencyHash(dep.GetName() + "/" + dep.GetConstraint().String())
}

func (ar *Archive) resolve(solution map[string]Release, depsToProcess []Dependency, problematicDeps map[Dependency]int) []Release {
debug("deps to process: %s", depsToProcess)
if len(depsToProcess) == 0 {
func (ar *Resolver[R, D]) resolve() Releases[R, D] {
debug("deps to process: %s", ar.depsToProcess)
if len(ar.depsToProcess) == 0 {
debug("All dependencies have been resolved.")
res := []Release{}
for _, v := range solution {
var res Releases[R, D]
for _, v := range ar.solution {
res = append(res, v)
}
return res
}

// Pick the first dependency in the deps to process
dep := depsToProcess[0]
dep := ar.depsToProcess[0]
depName := dep.GetName()
debug("Considering next dep: %s", depName)

// If a release is already picked in the solution check if it match the dep
if existingRelease, has := solution[depName]; has {
if match(existingRelease, dep) {
if existingRelease, has := ar.solution[depName]; has {
if dep.GetConstraint().Match(existingRelease.GetVersion()) {
debug("%s already in solution and matching", existingRelease)
return ar.resolve(solution, depsToProcess[1:], problematicDeps)
oldDepsToProcess := ar.depsToProcess
ar.depsToProcess = ar.depsToProcess[1:]
if res := ar.resolve(); res != nil {
return res
}
ar.depsToProcess = oldDepsToProcess
return nil
}
debug("%s already in solution do not match... rollingback", existingRelease)
return nil
}

// Otherwise start backtracking the dependency
releases := ar.Releases[dep.GetName()].FilterBy(dep)
releases := ar.releases[depName].FilterBy(dep.GetConstraint())

// Consider the latest versions first
releases.SortDescent()

findMissingDeps := func(deps []Dependency) Dependency {
for _, dep := range deps {
if _, ok := ar.Releases[dep.GetName()]; !ok {
return dep
}
}
return nil
}

debug("releases matching criteria: %s", releases)

backtracking_loop:
for _, release := range releases {
deps := release.GetDependencies()
debug("try with %s %s", release, deps)
releaseDeps := release.GetDependencies()
debug("try with %s %s", release, releaseDeps)

if missingDep := findMissingDeps(deps); missingDep != nil {
debug("%s did not work, becuase his dependency %s does not exists", release, missingDep.GetName())
continue
for _, releaseDep := range releaseDeps {
if _, ok := ar.releases[releaseDep.GetName()]; !ok {
debug("%s did not work, becuase his dependency %s does not exists", release, releaseDep.GetName())
continue backtracking_loop
}
}

solution[depName] = release
newDepsToProcess := append(depsToProcess[1:], deps...)
ar.solution[depName] = release
oldDepsToProcess := ar.depsToProcess
ar.depsToProcess = append(ar.depsToProcess[1:], releaseDeps...)
// bubble up problematics deps so they are processed first
sort.Slice(newDepsToProcess, func(i, j int) bool {
return problematicDeps[newDepsToProcess[i]] > problematicDeps[newDepsToProcess[j]]
sort.Slice(ar.depsToProcess, func(i, j int) bool {
ci := hashDependency(ar.depsToProcess[i])
cj := hashDependency(ar.depsToProcess[j])
return ar.problematicDeps[ci] > ar.problematicDeps[cj]
})
if res := ar.resolve(solution, newDepsToProcess, problematicDeps); res != nil {
if res := ar.resolve(); res != nil {
return res
}
ar.depsToProcess = oldDepsToProcess
debug("%s did not work...", release)
delete(solution, depName)
delete(ar.solution, depName)
}

problematicDeps[dep]++
ar.problematicDeps[hashDependency(dep)]++
return nil
}
43 changes: 25 additions & 18 deletions resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ type customDep struct {
cond Constraint
}

// GetName return the name of the dependency (implements the Dependency interface)
func (c *customDep) GetName() string {
return c.name
}

// GetConstraint return the version contraints of the dependency (implements the Dependency interface)
func (c *customDep) GetConstraint() Constraint {
return c.cond
}
Expand All @@ -34,26 +36,28 @@ func (c *customDep) String() string {
type customRel struct {
name string
vers *Version
deps []Dependency
deps []*customDep
}

// GetName return the name of the release (implements the Release interface)
func (r *customRel) GetName() string {
return r.name
}

// GetVersion return the version of the release (implements the Release interface)
func (r *customRel) GetVersion() *Version {
return r.vers
}

func (r *customRel) GetDependencies() []Dependency {
func (r *customRel) GetDependencies() []*customDep {
return r.deps
}

func (r *customRel) String() string {
return r.name + "@" + r.vers.String()
}

func d(dep string) Dependency {
func d(dep string) *customDep {
name := dep[0:1]
cond, err := ParseConstraint(dep[1:])
if err != nil {
Expand All @@ -62,15 +66,15 @@ func d(dep string) Dependency {
return &customDep{name: name, cond: cond}
}

func deps(deps ...string) []Dependency {
res := []Dependency{}
func deps(deps ...string) []*customDep {
var res []*customDep
for _, dep := range deps {
res = append(res, d(dep))
}
return res
}

func rel(name, ver string, deps []Dependency) Release {
func rel(name, ver string, deps []*customDep) *customRel {
return &customRel{name: name, vers: v(ver), deps: deps}
}

Expand Down Expand Up @@ -119,18 +123,18 @@ func TestResolver(t *testing.T) {
i160 := rel("I", "1.6.0", deps())
i170 := rel("I", "1.7.0", deps())
i180 := rel("I", "1.8.0", deps())
arch := &Archive{
Releases: map[string]Releases{
"A": {a100, a110, a111, a120, a121},
"B": {b131, b130, b121, b120, b111, b110, b100},
"C": {c200, c120, c111, c110, c102, c101, c100, c021, c020, c010},
"D": {d100, d120},
"E": {e100, e101},
"G": {g130, g140, g150, g160, g170, g180},
"H": {h130, h140, h150, h160, h170, h180},
"I": {i130, i140, i150, i160, i170, i180},
},
}
arch := NewResolver[*customRel, *customDep]()
arch.AddReleases(
a100, a110, a111, a120, a121,
b131, b130, b121, b120, b111, b110, b100,
c200, c120, c111, c110, c102, c101, c100, c021, c020, c010,
d100, d120,
g130, g140, g150, g160, g170, g180,
h130, h140, h150, h160, h170, h180,
i130, i140, i150, i160, i170, i180,
)
arch.AddRelease(e100) // use this method for 100% code coverage
arch.AddRelease(e101)

a130 := rel("A", "1.3.0", deps())
r0 := arch.Resolve(a130) // Non-existent in archive
Expand Down Expand Up @@ -179,4 +183,7 @@ func TestResolver(t *testing.T) {
case <-time.After(time.Second):
require.FailNow(t, "test didn't complete in the allocated time")
}

r7 := arch.Resolve(e101)
require.Nil(t, r7)
}
Loading