b6592fe185
* feat: parallel apply view * chore: cleaner readme * chore: lint * chore: remove debug logging * chore: lint
209 lines
6.2 KiB
Go
209 lines
6.2 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"slices"
|
|
|
|
"github.com/mircearoata/pubgrub-go/pubgrub"
|
|
"github.com/mircearoata/pubgrub-go/pubgrub/helpers"
|
|
"github.com/mircearoata/pubgrub-go/pubgrub/semver"
|
|
"github.com/pkg/errors"
|
|
"github.com/puzpuzpuz/xsync/v3"
|
|
"github.com/spf13/viper"
|
|
|
|
"github.com/satisfactorymodding/ficsit-cli/cli/provider"
|
|
"github.com/satisfactorymodding/ficsit-cli/ficsit"
|
|
)
|
|
|
|
const (
|
|
rootPkg = "$$root$$"
|
|
smlPkg = "SML"
|
|
factoryGamePkg = "FactoryGame"
|
|
)
|
|
|
|
type DependencyResolver struct {
|
|
provider provider.Provider
|
|
}
|
|
|
|
func NewDependencyResolver(provider provider.Provider) DependencyResolver {
|
|
return DependencyResolver{provider}
|
|
}
|
|
|
|
type ficsitAPISource struct {
|
|
provider provider.Provider
|
|
lockfile *LockFile
|
|
toInstall map[string]semver.Constraint
|
|
modVersionInfo *xsync.MapOf[string, ficsit.AllVersionsResponse]
|
|
gameVersion semver.Version
|
|
smlVersions []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion
|
|
}
|
|
|
|
func (f *ficsitAPISource) GetPackageVersions(pkg string) ([]pubgrub.PackageVersion, error) {
|
|
if pkg == rootPkg {
|
|
return []pubgrub.PackageVersion{{Version: semver.Version{}, Dependencies: f.toInstall}}, nil
|
|
}
|
|
if pkg == factoryGamePkg {
|
|
return []pubgrub.PackageVersion{{Version: f.gameVersion}}, nil
|
|
}
|
|
if pkg == smlPkg {
|
|
versions := make([]pubgrub.PackageVersion, len(f.smlVersions))
|
|
for i, smlVersion := range f.smlVersions {
|
|
v, err := semver.NewVersion(smlVersion.Version)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to parse version %s", smlVersion.Version)
|
|
}
|
|
gameConstraint, err := semver.NewConstraint(fmt.Sprintf(">=%d", smlVersion.Satisfactory_version))
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to parse constraint %s", fmt.Sprintf(">=%d", smlVersion.Satisfactory_version))
|
|
}
|
|
versions[i] = pubgrub.PackageVersion{
|
|
Version: v,
|
|
Dependencies: map[string]semver.Constraint{
|
|
factoryGamePkg: gameConstraint,
|
|
},
|
|
}
|
|
}
|
|
return versions, nil
|
|
}
|
|
response, err := f.provider.ModVersionsWithDependencies(context.TODO(), pkg)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to fetch mod %s", pkg)
|
|
}
|
|
if !response.Success {
|
|
if response.Error != nil {
|
|
return nil, errors.Errorf("mod %s not found: %s", pkg, response.Error.Message)
|
|
}
|
|
return nil, errors.Errorf("mod %s not found", pkg)
|
|
}
|
|
f.modVersionInfo.Store(pkg, *response)
|
|
versions := make([]pubgrub.PackageVersion, len(response.Data))
|
|
for i, modVersion := range response.Data {
|
|
v, err := semver.NewVersion(modVersion.Version)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to parse version %s", modVersion.Version)
|
|
}
|
|
dependencies := make(map[string]semver.Constraint)
|
|
optionalDependencies := make(map[string]semver.Constraint)
|
|
for _, dependency := range modVersion.Dependencies {
|
|
c, err := semver.NewConstraint(dependency.Condition)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to parse constraint %s", dependency.Condition)
|
|
}
|
|
if dependency.Optional {
|
|
optionalDependencies[dependency.ModID] = c
|
|
} else {
|
|
dependencies[dependency.ModID] = c
|
|
}
|
|
}
|
|
versions[i] = pubgrub.PackageVersion{
|
|
Version: v,
|
|
Dependencies: dependencies,
|
|
OptionalDependencies: optionalDependencies,
|
|
}
|
|
}
|
|
return versions, nil
|
|
}
|
|
|
|
func (f *ficsitAPISource) PickVersion(pkg string, versions []semver.Version) semver.Version {
|
|
if f.lockfile != nil {
|
|
if existing, ok := f.lockfile.Mods[pkg]; ok {
|
|
v, err := semver.NewVersion(existing.Version)
|
|
if err == nil {
|
|
if slices.ContainsFunc(versions, func(version semver.Version) bool {
|
|
return v.Compare(version) == 0
|
|
}) {
|
|
return v
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return helpers.StandardVersionPriority(versions)
|
|
}
|
|
|
|
func (d DependencyResolver) ResolveModDependencies(constraints map[string]string, lockFile *LockFile, gameVersion int) (*LockFile, error) {
|
|
smlVersionsDB, err := d.provider.SMLVersions(context.TODO())
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed fetching SML versions")
|
|
}
|
|
|
|
gameVersionSemver, err := semver.NewVersion(fmt.Sprintf("%d", gameVersion))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed parsing game version")
|
|
}
|
|
|
|
toInstall := make(map[string]semver.Constraint, len(constraints))
|
|
for k, v := range constraints {
|
|
c, err := semver.NewConstraint(v)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to parse constraint %s", v)
|
|
}
|
|
toInstall[k] = c
|
|
}
|
|
|
|
ficsitSource := &ficsitAPISource{
|
|
provider: d.provider,
|
|
smlVersions: smlVersionsDB.SmlVersions.Sml_versions,
|
|
gameVersion: gameVersionSemver,
|
|
lockfile: lockFile,
|
|
toInstall: toInstall,
|
|
modVersionInfo: xsync.NewMapOf[string, ficsit.AllVersionsResponse](),
|
|
}
|
|
|
|
result, err := pubgrub.Solve(helpers.NewCachingSource(ficsitSource), rootPkg)
|
|
if err != nil {
|
|
finalError := err
|
|
var solverErr pubgrub.SolvingError
|
|
if errors.As(err, &solverErr) {
|
|
finalError = DependencyResolverError{SolvingError: solverErr, provider: d.provider, smlVersions: smlVersionsDB.SmlVersions.Sml_versions, gameVersion: gameVersion}
|
|
}
|
|
return nil, errors.Wrap(finalError, "failed to solve dependencies")
|
|
}
|
|
delete(result, rootPkg)
|
|
delete(result, factoryGamePkg)
|
|
|
|
outputLock := MakeLockfile()
|
|
for k, v := range result {
|
|
if k == smlPkg {
|
|
for _, version := range ficsitSource.smlVersions {
|
|
if version.Version == v.String() {
|
|
targets := make(map[string]LockedModTarget)
|
|
for _, target := range version.Targets {
|
|
targets[string(target.TargetName)] = LockedModTarget{
|
|
Link: target.Link,
|
|
}
|
|
}
|
|
|
|
outputLock.Mods[k] = LockedMod{
|
|
Version: v.String(),
|
|
Targets: targets,
|
|
}
|
|
break
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
value, _ := ficsitSource.modVersionInfo.Load(k)
|
|
versions := value.Data
|
|
for _, ver := range versions {
|
|
if ver.Version == v.RawString() {
|
|
targets := make(map[string]LockedModTarget)
|
|
for _, target := range ver.Targets {
|
|
targets[target.TargetName] = LockedModTarget{
|
|
Link: viper.GetString("api-base") + "/v1/version/" + ver.ID + "/" + target.TargetName + "/download",
|
|
Hash: target.Hash,
|
|
}
|
|
}
|
|
|
|
outputLock.Mods[k] = LockedMod{
|
|
Version: v.String(),
|
|
Targets: targets,
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return outputLock, nil
|
|
}
|