mirror of
https://github.com/packwiz/packwiz.git
synced 2025-04-19 13:06:30 +02:00
Implement dependency support for Modrinth (fixes #56)
This commit is contained in:
parent
89f189256e
commit
503232a3fa
@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/sahilm/fuzzy"
|
"github.com/sahilm/fuzzy"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -156,13 +157,7 @@ var installCmd = &cobra.Command{
|
|||||||
// Remove installed IDs from dep queue
|
// Remove installed IDs from dep queue
|
||||||
i := 0
|
i := 0
|
||||||
for _, id := range depIDPendingQueue {
|
for _, id := range depIDPendingQueue {
|
||||||
contains := false
|
contains := slices.Contains(installedIDList, id)
|
||||||
for _, id2 := range installedIDList {
|
|
||||||
if id == id2 {
|
|
||||||
contains = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, data := range depsInstallable {
|
for _, data := range depsInstallable {
|
||||||
if id == data.ID {
|
if id == data.ID {
|
||||||
contains = true
|
contains = true
|
||||||
@ -217,7 +212,7 @@ var installCmd = &cobra.Command{
|
|||||||
fmt.Println(v.Name)
|
fmt.Println(v.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Print("Would you like to install them? [Y/n]: ")
|
fmt.Print("Would you like to add them? [Y/n]: ")
|
||||||
answer, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
answer, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package modrinth
|
package modrinth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
modrinthApi "codeberg.org/jmansfield/go-modrinth/modrinth"
|
modrinthApi "codeberg.org/jmansfield/go-modrinth/modrinth"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
@ -31,6 +33,12 @@ var installCmd = &cobra.Command{
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
index, err := pack.LoadIndex()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
if len(args) == 0 || len(args[0]) == 0 {
|
if len(args) == 0 || len(args[0]) == 0 {
|
||||||
fmt.Println("You must specify a mod.")
|
fmt.Println("You must specify a mod.")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -38,7 +46,7 @@ var installCmd = &cobra.Command{
|
|||||||
|
|
||||||
// If there are more than 1 argument, go straight to searching - URLs/Slugs should not have spaces!
|
// If there are more than 1 argument, go straight to searching - URLs/Slugs should not have spaces!
|
||||||
if len(args) > 1 {
|
if len(args) > 1 {
|
||||||
err = installViaSearch(strings.Join(args, " "), pack)
|
err = installViaSearch(strings.Join(args, " "), pack, &index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed installing mod: %s\n", err)
|
fmt.Printf("Failed installing mod: %s\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -49,7 +57,7 @@ var installCmd = &cobra.Command{
|
|||||||
//Try interpreting the arg as a version url
|
//Try interpreting the arg as a version url
|
||||||
matches := versionSiteRegex.FindStringSubmatch(args[0])
|
matches := versionSiteRegex.FindStringSubmatch(args[0])
|
||||||
if matches != nil && len(matches) == 3 {
|
if matches != nil && len(matches) == 3 {
|
||||||
err = installVersionById(matches[2], pack)
|
err = installVersionById(matches[2], pack, &index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed installing mod: %s\n", err)
|
fmt.Printf("Failed installing mod: %s\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -74,7 +82,7 @@ var installCmd = &cobra.Command{
|
|||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
//We found a mod with that id/slug
|
//We found a mod with that id/slug
|
||||||
err = installMod(mod, pack)
|
err = installMod(mod, pack, &index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed installing mod: %s\n", err)
|
fmt.Printf("Failed installing mod: %s\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -84,7 +92,7 @@ var installCmd = &cobra.Command{
|
|||||||
//This wasn't a valid modid/slug, try to search for it instead:
|
//This wasn't a valid modid/slug, try to search for it instead:
|
||||||
//Don't bother to search if it looks like a url though
|
//Don't bother to search if it looks like a url though
|
||||||
if matches == nil {
|
if matches == nil {
|
||||||
err = installViaSearch(args[0], pack)
|
err = installViaSearch(args[0], pack, &index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed installing mod: %s\n", err)
|
fmt.Printf("Failed installing mod: %s\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -97,7 +105,7 @@ var installCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func installViaSearch(query string, pack core.Pack) error {
|
func installViaSearch(query string, pack core.Pack, index *core.Index) error {
|
||||||
mcVersion, err := pack.GetMCVersion()
|
mcVersion, err := pack.GetMCVersion()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -133,13 +141,13 @@ func installViaSearch(query string, pack core.Pack) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return installMod(mod, pack)
|
return installMod(mod, pack, index)
|
||||||
})
|
})
|
||||||
|
|
||||||
return menu.Run()
|
return menu.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func installMod(mod *modrinthApi.Project, pack core.Pack) error {
|
func installMod(mod *modrinthApi.Project, pack core.Pack, index *core.Index) error {
|
||||||
fmt.Printf("Found mod %s: '%s'.\n", *mod.Title, *mod.Description)
|
fmt.Printf("Found mod %s: '%s'.\n", *mod.Title, *mod.Description)
|
||||||
|
|
||||||
latestVersion, err := getLatestVersion(*mod.ID, pack)
|
latestVersion, err := getLatestVersion(*mod.ID, pack)
|
||||||
@ -150,20 +158,164 @@ func installMod(mod *modrinthApi.Project, pack core.Pack) error {
|
|||||||
return errors.New("mod is not available for this Minecraft version (use the acceptable-game-versions option to accept more) or mod loader")
|
return errors.New("mod is not available for this Minecraft version (use the acceptable-game-versions option to accept more) or mod loader")
|
||||||
}
|
}
|
||||||
|
|
||||||
return installVersion(mod, latestVersion, pack)
|
return installVersion(mod, latestVersion, pack, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
func installVersion(mod *modrinthApi.Project, version *modrinthApi.Version, pack core.Pack) error {
|
const maxCycles = 20
|
||||||
var files = version.Files
|
|
||||||
|
|
||||||
if len(files) == 0 {
|
type depMetadataStore struct {
|
||||||
|
projectInfo *modrinthApi.Project
|
||||||
|
versionInfo *modrinthApi.Version
|
||||||
|
fileInfo *modrinthApi.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func installVersion(mod *modrinthApi.Project, version *modrinthApi.Version, pack core.Pack, index *core.Index) error {
|
||||||
|
if len(version.Files) == 0 {
|
||||||
return errors.New("version doesn't have any files attached")
|
return errors.New("version doesn't have any files attached")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(version.Dependencies) > 0 {
|
||||||
|
// TODO: could get installed version IDs, and compare to install the newest - i.e. preferring pinned versions over getting absolute latest?
|
||||||
|
installedProjects := getInstalledProjectIDs(index)
|
||||||
|
|
||||||
|
var depMetadata []depMetadataStore
|
||||||
|
var depProjectIDPendingQueue []string
|
||||||
|
var depVersionIDPendingQueue []string
|
||||||
|
|
||||||
|
for _, dep := range version.Dependencies {
|
||||||
|
// TODO: recommend optional dependencies?
|
||||||
|
if dep.DependencyType != nil && *dep.DependencyType == "required" {
|
||||||
|
if dep.ProjectID != nil {
|
||||||
|
depProjectIDPendingQueue = append(depProjectIDPendingQueue, *dep.ProjectID)
|
||||||
|
}
|
||||||
|
if dep.VersionID != nil {
|
||||||
|
depVersionIDPendingQueue = append(depVersionIDPendingQueue, *dep.VersionID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(depProjectIDPendingQueue)+len(depVersionIDPendingQueue) > 0 {
|
||||||
|
fmt.Println("Finding dependencies...")
|
||||||
|
|
||||||
|
cycles := 0
|
||||||
|
for len(depProjectIDPendingQueue)+len(depVersionIDPendingQueue) > 0 && cycles < maxCycles {
|
||||||
|
// Look up version IDs
|
||||||
|
if len(depVersionIDPendingQueue) > 0 {
|
||||||
|
depVersions, err := mrDefaultClient.Versions.GetMultiple(depVersionIDPendingQueue)
|
||||||
|
if err == nil {
|
||||||
|
for _, v := range depVersions {
|
||||||
|
// Add project ID to queue
|
||||||
|
depProjectIDPendingQueue = append(depProjectIDPendingQueue, *v.ProjectID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Error retrieving dependency data: %s\n", err.Error())
|
||||||
|
}
|
||||||
|
depVersionIDPendingQueue = depVersionIDPendingQueue[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove installed project IDs from dep queue
|
||||||
|
i := 0
|
||||||
|
for _, id := range depProjectIDPendingQueue {
|
||||||
|
contains := slices.Contains(installedProjects, id)
|
||||||
|
for _, dep := range depMetadata {
|
||||||
|
if *dep.projectInfo.ID == id {
|
||||||
|
contains = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !contains {
|
||||||
|
depProjectIDPendingQueue[i] = id
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
depProjectIDPendingQueue = depProjectIDPendingQueue[:i]
|
||||||
|
|
||||||
|
if len(depProjectIDPendingQueue) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
depProjects, err := mrDefaultClient.Projects.GetMultiple(depProjectIDPendingQueue)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error retrieving dependency data: %s\n", err.Error())
|
||||||
|
}
|
||||||
|
depProjectIDPendingQueue = depProjectIDPendingQueue[:0]
|
||||||
|
|
||||||
|
for _, project := range depProjects {
|
||||||
|
if project.ID == nil {
|
||||||
|
return errors.New("failed to get dependency data: invalid response")
|
||||||
|
}
|
||||||
|
// Get latest version - could reuse version lookup data but it's not as easy (particularly since the version won't necessarily be the latest)
|
||||||
|
latestVersion, err := getLatestVersion(*project.ID, pack)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get latest version of dependency %v: %v", *project.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dep := range version.Dependencies {
|
||||||
|
// TODO: recommend optional dependencies?
|
||||||
|
if dep.DependencyType != nil && *dep.DependencyType == "required" {
|
||||||
|
if dep.ProjectID != nil {
|
||||||
|
depProjectIDPendingQueue = append(depProjectIDPendingQueue, *dep.ProjectID)
|
||||||
|
}
|
||||||
|
if dep.VersionID != nil {
|
||||||
|
depVersionIDPendingQueue = append(depVersionIDPendingQueue, *dep.VersionID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add some way to allow users to pick which file to install?
|
||||||
|
var file = latestVersion.Files[0]
|
||||||
|
// Prefer the primary file
|
||||||
|
for _, v := range latestVersion.Files {
|
||||||
|
if *v.Primary {
|
||||||
|
file = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depMetadata = append(depMetadata, depMetadataStore{
|
||||||
|
projectInfo: project,
|
||||||
|
versionInfo: latestVersion,
|
||||||
|
fileInfo: file,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cycles++
|
||||||
|
}
|
||||||
|
if cycles >= maxCycles {
|
||||||
|
return errors.New("dependencies recurse too deeply, try increasing maxCycles")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(depMetadata) > 0 {
|
||||||
|
fmt.Println("Dependencies found:")
|
||||||
|
for _, v := range depMetadata {
|
||||||
|
fmt.Println(*v.projectInfo.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: --yes argument
|
||||||
|
fmt.Print("Would you like to add them? [Y/n]: ")
|
||||||
|
answer, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ansNormal := strings.ToLower(strings.TrimSpace(answer))
|
||||||
|
if !(len(ansNormal) > 0 && ansNormal[0] == 'n') {
|
||||||
|
for _, v := range depMetadata {
|
||||||
|
err = createFileMeta(v.projectInfo, v.versionInfo, v.fileInfo, index)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("Dependency \"%s\" successfully added! (%s)\n", *v.projectInfo.Title, *v.fileInfo.Filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("All dependencies are already added!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: add some way to allow users to pick which file to install?
|
// TODO: add some way to allow users to pick which file to install?
|
||||||
var file = files[0]
|
var file = version.Files[0]
|
||||||
// Prefer the primary file
|
// Prefer the primary file
|
||||||
for _, v := range files {
|
for _, v := range version.Files {
|
||||||
if *v.Primary {
|
if *v.Primary {
|
||||||
file = v
|
file = v
|
||||||
}
|
}
|
||||||
@ -171,13 +323,31 @@ func installVersion(mod *modrinthApi.Project, version *modrinthApi.Version, pack
|
|||||||
|
|
||||||
//Install the file
|
//Install the file
|
||||||
fmt.Printf("Installing %s from version %s\n", *file.Filename, *version.VersionNumber)
|
fmt.Printf("Installing %s from version %s\n", *file.Filename, *version.VersionNumber)
|
||||||
index, err := pack.LoadIndex()
|
|
||||||
|
err := createFileMeta(mod, version, file, index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = index.Write()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = pack.UpdateIndexHash()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = pack.Write()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFileMeta(mod *modrinthApi.Project, version *modrinthApi.Version, file *modrinthApi.File, index *core.Index) error {
|
||||||
updateMap := make(map[string]map[string]interface{})
|
updateMap := make(map[string]map[string]interface{})
|
||||||
|
|
||||||
|
var err error
|
||||||
updateMap["modrinth"], err = mrUpdateData{
|
updateMap["modrinth"], err = mrUpdateData{
|
||||||
ModID: *mod.ID,
|
ModID: *mod.ID,
|
||||||
InstalledVersion: *version.ID,
|
InstalledVersion: *version.ID,
|
||||||
@ -227,26 +397,10 @@ func installVersion(mod *modrinthApi.Project, version *modrinthApi.Version, pack
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = index.RefreshFileWithHash(path, format, hash, true)
|
return index.RefreshFileWithHash(path, format, hash, true)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = index.Write()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = pack.UpdateIndexHash()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = pack.Write()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func installVersionById(versionId string, pack core.Pack) error {
|
func installVersionById(versionId string, pack core.Pack, index *core.Index) error {
|
||||||
version, err := mrDefaultClient.Versions.Get(versionId)
|
version, err := mrDefaultClient.Versions.Get(versionId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to fetch version %s: %v", versionId, err)
|
return fmt.Errorf("failed to fetch version %s: %v", versionId, err)
|
||||||
@ -257,7 +411,7 @@ func installVersionById(versionId string, pack core.Pack) error {
|
|||||||
return fmt.Errorf("failed to fetch mod %s: %v", *version.ProjectID, err)
|
return fmt.Errorf("failed to fetch mod %s: %v", *version.ProjectID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return installVersion(mod, version, pack)
|
return installVersion(mod, version, pack, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -138,3 +138,22 @@ func getBestHash(v *modrinthApi.File) (string, string) {
|
|||||||
//No hashes were present
|
//No hashes were present
|
||||||
return "", ""
|
return "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getInstalledProjectIDs(index *core.Index) []string {
|
||||||
|
var installedProjects []string
|
||||||
|
for _, modPath := range index.GetAllMods() {
|
||||||
|
mod, err := core.LoadMod(modPath)
|
||||||
|
if err == nil {
|
||||||
|
data, ok := mod.GetParsedUpdateData("modrinth")
|
||||||
|
if ok {
|
||||||
|
updateData, ok := data.(mrUpdateData)
|
||||||
|
if ok {
|
||||||
|
if len(updateData.ModID) > 0 {
|
||||||
|
installedProjects = append(installedProjects, updateData.ModID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return installedProjects
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user