332 lines
8.7 KiB
Go
332 lines
8.7 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/Khan/genqlient/graphql"
|
|
"github.com/Masterminds/semver/v3"
|
|
"github.com/pkg/errors"
|
|
"github.com/satisfactorymodding/ficsit-cli/ficsit"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
const smlDownloadTemplate = `https://github.com/satisfactorymodding/SatisfactoryModLoader/releases/download/%s/SML.zip`
|
|
|
|
type DependencyResolver struct {
|
|
apiClient graphql.Client
|
|
}
|
|
|
|
func NewDependencyResolver(apiClient graphql.Client) DependencyResolver {
|
|
return DependencyResolver{apiClient: apiClient}
|
|
}
|
|
|
|
func (d DependencyResolver) ResolveModDependencies(constraints map[string]string, lockFile *LockFile, gameVersion int) (LockFile, error) {
|
|
smlVersionsDB, err := ficsit.SMLVersions(context.TODO(), d.apiClient)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed fetching SMl versions")
|
|
}
|
|
|
|
copied := make(map[string][]string, len(constraints))
|
|
for k, v := range constraints {
|
|
copied[k] = []string{v}
|
|
}
|
|
|
|
instance := &resolvingInstance{
|
|
Resolver: d,
|
|
InputLock: lockFile,
|
|
ToResolve: copied,
|
|
OutputLock: make(LockFile),
|
|
SMLVersions: smlVersionsDB,
|
|
GameVersion: gameVersion,
|
|
}
|
|
|
|
if err := instance.Step(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return instance.OutputLock, nil
|
|
}
|
|
|
|
type resolvingInstance struct {
|
|
Resolver DependencyResolver
|
|
|
|
InputLock *LockFile
|
|
|
|
ToResolve map[string][]string
|
|
|
|
OutputLock LockFile
|
|
|
|
SMLVersions *ficsit.SMLVersionsResponse
|
|
GameVersion int
|
|
}
|
|
|
|
func (r *resolvingInstance) Step() error {
|
|
if len(r.ToResolve) > 0 {
|
|
if err := r.LockStep(make(map[string]bool)); err != nil {
|
|
return err
|
|
}
|
|
|
|
converted := make([]ficsit.ModVersionConstraint, 0)
|
|
for id, constraint := range r.ToResolve {
|
|
if id != "SML" {
|
|
converted = append(converted, ficsit.ModVersionConstraint{
|
|
ModIdOrReference: id,
|
|
Version: constraint[0],
|
|
})
|
|
} else {
|
|
if existingSML, ok := r.OutputLock[id]; ok {
|
|
for _, cs := range constraint {
|
|
smlVersionConstraint, _ := semver.NewConstraint(cs)
|
|
if !smlVersionConstraint.Check(semver.MustParse(existingSML.Version)) {
|
|
return errors.Errorf("mod %s version %s does not match constraint %s",
|
|
id,
|
|
existingSML.Version,
|
|
constraint,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
var chosenSMLVersion *semver.Version
|
|
for _, version := range r.SMLVersions.SmlVersions.Sml_versions {
|
|
if version.Satisfactory_version > r.GameVersion {
|
|
continue
|
|
}
|
|
|
|
currentVersion := semver.MustParse(version.Version)
|
|
|
|
matches := true
|
|
for _, cs := range constraint {
|
|
smlVersionConstraint, _ := semver.NewConstraint(cs)
|
|
|
|
if !smlVersionConstraint.Check(currentVersion) {
|
|
matches = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if matches {
|
|
if chosenSMLVersion == nil || currentVersion.GreaterThan(chosenSMLVersion) {
|
|
chosenSMLVersion = currentVersion
|
|
}
|
|
}
|
|
}
|
|
|
|
if chosenSMLVersion == nil {
|
|
return errors.Errorf("could not find an SML version that matches constraint %s and game version %d", constraint, r.GameVersion)
|
|
}
|
|
|
|
r.OutputLock[id] = LockedMod{
|
|
Version: chosenSMLVersion.String(),
|
|
Link: fmt.Sprintf(smlDownloadTemplate, chosenSMLVersion.String()),
|
|
Dependencies: map[string]string{},
|
|
}
|
|
}
|
|
}
|
|
|
|
nextResolve := make(map[string][]string)
|
|
|
|
// TODO Cache
|
|
dependencies, err := ficsit.ResolveModDependencies(context.TODO(), r.Resolver.apiClient, converted)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed resolving mod dependencies")
|
|
}
|
|
|
|
for _, mod := range dependencies.Mods {
|
|
modVersions := make([]ModVersion, len(mod.Versions))
|
|
for i, version := range mod.Versions {
|
|
versionDependencies := make([]VersionDependency, len(version.Dependencies))
|
|
|
|
for j, dependency := range version.Dependencies {
|
|
versionDependencies[j] = VersionDependency{
|
|
ModReference: dependency.Mod_id,
|
|
Constraint: dependency.Condition,
|
|
Optional: dependency.Optional,
|
|
}
|
|
}
|
|
|
|
modVersions[i] = ModVersion{
|
|
ID: version.Id,
|
|
Version: version.Version,
|
|
Link: viper.GetString("api-base") + version.Link,
|
|
Hash: version.Hash,
|
|
Dependencies: versionDependencies,
|
|
}
|
|
}
|
|
|
|
sort.Slice(modVersions, func(i, j int) bool {
|
|
a := semver.MustParse(modVersions[i].Version)
|
|
b := semver.MustParse(modVersions[j].Version)
|
|
return b.LessThan(a)
|
|
})
|
|
|
|
// Pick latest version
|
|
// TODO Clone and branch
|
|
var selectedVersion ModVersion
|
|
for _, version := range modVersions {
|
|
matches := true
|
|
|
|
for _, cs := range r.ToResolve[mod.Mod_reference] {
|
|
resolvingConstraint, _ := semver.NewConstraint(cs)
|
|
if !resolvingConstraint.Check(semver.MustParse(version.Version)) {
|
|
matches = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if matches {
|
|
selectedVersion = version
|
|
break
|
|
}
|
|
}
|
|
|
|
if selectedVersion.Version == "" {
|
|
return errors.Errorf("no version of %s matches constraints", mod.Mod_reference)
|
|
}
|
|
|
|
if _, ok := r.OutputLock[mod.Mod_reference]; ok {
|
|
if r.OutputLock[mod.Mod_reference].Version != selectedVersion.Version {
|
|
return errors.New("failed resolving dependencies. requires different versions of " + mod.Mod_reference)
|
|
}
|
|
}
|
|
|
|
modDependencies := make(map[string]string)
|
|
for _, dependency := range selectedVersion.Dependencies {
|
|
if !dependency.Optional {
|
|
modDependencies[dependency.ModReference] = dependency.Constraint
|
|
}
|
|
}
|
|
|
|
r.OutputLock[mod.Mod_reference] = LockedMod{
|
|
Version: selectedVersion.Version,
|
|
Hash: selectedVersion.Hash,
|
|
Link: selectedVersion.Link,
|
|
Dependencies: modDependencies,
|
|
}
|
|
|
|
for _, dependency := range selectedVersion.Dependencies {
|
|
if previousSelectedVersion, ok := r.OutputLock[dependency.ModReference]; ok {
|
|
constraint, _ := semver.NewConstraint(dependency.Constraint)
|
|
if !constraint.Check(semver.MustParse(previousSelectedVersion.Version)) {
|
|
return errors.Errorf("mod %s version %s does not match constraint %s",
|
|
dependency.ModReference,
|
|
previousSelectedVersion.Version,
|
|
dependency.Constraint,
|
|
)
|
|
}
|
|
}
|
|
|
|
if resolving, ok := nextResolve[dependency.ModReference]; ok {
|
|
constraint, _ := semver.NewConstraint(dependency.Constraint)
|
|
|
|
for _, cs := range resolving {
|
|
resolvingConstraint, _ := semver.NewConstraint(cs)
|
|
intersects, _ := constraint.Intersects(resolvingConstraint)
|
|
if !intersects {
|
|
return errors.Errorf("mod %s constraint %s does not intersect with %s",
|
|
dependency.ModReference,
|
|
resolving,
|
|
dependency.Constraint,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
if dependency.Optional {
|
|
continue
|
|
}
|
|
|
|
nextResolve[dependency.ModReference] = append(nextResolve[dependency.ModReference], dependency.Constraint)
|
|
}
|
|
}
|
|
|
|
r.ToResolve = nextResolve
|
|
|
|
for _, constraint := range converted {
|
|
if _, ok := r.OutputLock[constraint.ModIdOrReference]; !ok {
|
|
return errors.New("failed resolving dependency: " + constraint.ModIdOrReference)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(r.ToResolve) > 0 {
|
|
if err := r.Step(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *resolvingInstance) LockStep(viewed map[string]bool) error {
|
|
added := false
|
|
if r.InputLock != nil {
|
|
for modReference, constraints := range r.ToResolve {
|
|
if _, ok := viewed[modReference]; ok {
|
|
continue
|
|
}
|
|
|
|
viewed[modReference] = true
|
|
|
|
if locked, ok := (*r.InputLock)[modReference]; ok {
|
|
passes := true
|
|
|
|
for _, cs := range constraints {
|
|
constraint, _ := semver.NewConstraint(cs)
|
|
if !constraint.Check(semver.MustParse(locked.Version)) {
|
|
passes = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if passes {
|
|
delete(r.ToResolve, modReference)
|
|
r.OutputLock[modReference] = locked
|
|
for k, v := range locked.Dependencies {
|
|
if alreadyResolving, ok := r.ToResolve[k]; ok {
|
|
newConstraint, _ := semver.NewConstraint(v)
|
|
for _, resolvingConstraint := range alreadyResolving {
|
|
cs2, _ := semver.NewConstraint(resolvingConstraint)
|
|
intersects, _ := newConstraint.Intersects(cs2)
|
|
if !intersects {
|
|
return errors.Errorf("mod %s constraint %s does not intersect with %s",
|
|
k,
|
|
v,
|
|
alreadyResolving,
|
|
)
|
|
}
|
|
}
|
|
|
|
r.ToResolve[k] = append(r.ToResolve[k], v)
|
|
|
|
continue
|
|
}
|
|
|
|
if outVersion, ok := r.OutputLock[k]; ok {
|
|
constraint, _ := semver.NewConstraint(v)
|
|
if !constraint.Check(semver.MustParse(outVersion.Version)) {
|
|
return errors.Errorf("mod %s version %s does not match constraint %s",
|
|
k,
|
|
outVersion.Version,
|
|
v,
|
|
)
|
|
}
|
|
continue
|
|
}
|
|
|
|
r.ToResolve[k] = append(r.ToResolve[k], v)
|
|
added = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if added {
|
|
if err := r.LockStep(viewed); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|