diff --git a/build.gradle b/build.gradle index 3f9fb4b1..70aeb97f 100644 --- a/build.gradle +++ b/build.gradle @@ -81,12 +81,8 @@ dependencies { def picocliVersion = '4.7.4' implementation(platform('com.fasterxml.jackson:jackson-bom:2.15.2')) - implementation(platform('ch.qos.logback:logback-parent:1.3.5')) { - because '1.4.+ requires Java 11 and we are stuck on 8' - version { - reject '1.4.+' - } - } + // 1.4.+ requires Java 11 and we are stuck on 8 + implementation(platform('ch.qos.logback:logback-parent:[1.3.5,1.4)')) implementation 'ch.qos.logback:logback-core' implementation 'ch.qos.logback:logback-classic' @@ -104,6 +100,7 @@ dependencies { implementation 'commons-codec:commons-codec:1.15' // for RFC5987 parsing of content-disposition filename* implementation 'org.apache.cxf:cxf-core:4.0.2' + implementation 'org.jetbrains:annotations:24.0.1' testImplementation 'org.assertj:assertj-core:3.24.2' // https://github.com/webcompere/model-assert diff --git a/src/main/java/me/itzg/helpers/McImageHelper.java b/src/main/java/me/itzg/helpers/McImageHelper.java index f6ad1d09..88de502a 100644 --- a/src/main/java/me/itzg/helpers/McImageHelper.java +++ b/src/main/java/me/itzg/helpers/McImageHelper.java @@ -37,6 +37,7 @@ import me.itzg.helpers.vanillatweaks.VanillaTweaksCommand; import me.itzg.helpers.versions.CompareVersionsCommand; import me.itzg.helpers.versions.JavaReleaseCommand; +import me.itzg.helpers.versions.ResolveMinecraftVersionCommand; import org.slf4j.LoggerFactory; import picocli.CommandLine; import picocli.CommandLine.ArgGroup; @@ -68,6 +69,7 @@ MulitCopyCommand.class, NetworkInterfacesCommand.class, PatchCommand.class, + ResolveMinecraftVersionCommand.class, Sync.class, SyncAndInterpolate.class, YamlPathCmd.class, diff --git a/src/main/java/me/itzg/helpers/curseforge/CurseForgeApiClient.java b/src/main/java/me/itzg/helpers/curseforge/CurseForgeApiClient.java index 4fc03b2f..3888f362 100644 --- a/src/main/java/me/itzg/helpers/curseforge/CurseForgeApiClient.java +++ b/src/main/java/me/itzg/helpers/curseforge/CurseForgeApiClient.java @@ -2,15 +2,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; -import lombok.extern.slf4j.Slf4j; -import me.itzg.helpers.curseforge.model.*; -import me.itzg.helpers.errors.GenericException; -import me.itzg.helpers.errors.InvalidParameterException; -import me.itzg.helpers.http.*; -import me.itzg.helpers.json.ObjectMappers; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; - import java.io.IOException; import java.net.URI; import java.nio.file.Files; @@ -18,6 +9,26 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import me.itzg.helpers.curseforge.model.Category; +import me.itzg.helpers.curseforge.model.CurseForgeFile; +import me.itzg.helpers.curseforge.model.CurseForgeMod; +import me.itzg.helpers.curseforge.model.CurseForgeResponse; +import me.itzg.helpers.curseforge.model.GetCategoriesResponse; +import me.itzg.helpers.curseforge.model.GetModFileResponse; +import me.itzg.helpers.curseforge.model.GetModFilesResponse; +import me.itzg.helpers.curseforge.model.GetModResponse; +import me.itzg.helpers.curseforge.model.ModsSearchResponse; +import me.itzg.helpers.errors.GenericException; +import me.itzg.helpers.errors.InvalidParameterException; +import me.itzg.helpers.http.FailedRequestException; +import me.itzg.helpers.http.Fetch; +import me.itzg.helpers.http.FileDownloadStatusHandler; +import me.itzg.helpers.http.SharedFetch; +import me.itzg.helpers.http.UriBuilder; +import me.itzg.helpers.json.ObjectMappers; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; @Slf4j public class CurseForgeApiClient implements AutoCloseable { @@ -48,7 +59,7 @@ public CategoryInfo loadCategoryInfo(Set applicableClassIdSlugs, String .fetch(uriBuilder.resolve("/categories?gameId={gameId}&classesOnly=true", gameId)) .toObject(GetCategoriesResponse.class) .assemble() - .map(resp -> { + .flatMap(resp -> { final Map contentClassIds = new HashMap<>(); Integer modpackClassId = null; @@ -62,10 +73,10 @@ public CategoryInfo loadCategoryInfo(Set applicableClassIdSlugs, String } if (modpackClassId == null) { - throw new GenericException("Unable to lookup classId for modpacks"); + return Mono.error(new GenericException("Unable to lookup classId for modpacks")); } - return new CategoryInfo(contentClassIds, modpackClassId); + return Mono.just(new CategoryInfo(contentClassIds, modpackClassId)); } ) .block(); diff --git a/src/main/java/me/itzg/helpers/errors/Validators.java b/src/main/java/me/itzg/helpers/errors/Validators.java new file mode 100644 index 00000000..9996d6e9 --- /dev/null +++ b/src/main/java/me/itzg/helpers/errors/Validators.java @@ -0,0 +1,20 @@ +package me.itzg.helpers.errors; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParameterException; + +public class Validators { + public static final Pattern VERSION_OR_LATEST = Pattern.compile("latest|\\d+(\\.\\d+)+(-.+)?", Pattern.CASE_INSENSITIVE); + public static final String DESCRIPTION_MINECRAFT_VERSION = "May be 'latest' or specific version"; + + public static String validateMinecraftVersion(CommandSpec spec, String input) { + final Matcher m = VERSION_OR_LATEST.matcher(input); + if (m.matches()) { + return input.toLowerCase(); + } + + throw new ParameterException(spec.commandLine(), "Invalid value for minecraft version: " + input); + } +} diff --git a/src/main/java/me/itzg/helpers/fabric/FabricLauncherInstaller.java b/src/main/java/me/itzg/helpers/fabric/FabricLauncherInstaller.java index c2aec6c6..e83fbf9f 100644 --- a/src/main/java/me/itzg/helpers/fabric/FabricLauncherInstaller.java +++ b/src/main/java/me/itzg/helpers/fabric/FabricLauncherInstaller.java @@ -167,12 +167,13 @@ private Mono installLauncherUsingInstaller(SharedFetch sharedFetch, .skipExisting(true) .assemble() .publishOn(Schedulers.boundedElastic()) - .map(path -> { + .flatMap(path -> { log.info("Running Fabric installer for Minecraft {} and loader version {}", minecraftVersion, loaderVersion ); try { + //noinspection BlockingMethodInNonBlockingContext because IntelliJ is confused final Process proc = new ProcessBuilder( "java", "-jar", path.toString(), "server", @@ -183,29 +184,31 @@ private Mono installLauncherUsingInstaller(SharedFetch sharedFetch, .redirectErrorStream(true) .start(); + //noinspection BlockingMethodInNonBlockingContext because IntelliJ is confused final boolean success = proc.waitFor(FABRIC_INSTALLER_TIMEOUT_SEC, TimeUnit.SECONDS); if (!success) { IoStreams.transfer(proc.getInputStream(), System.err); - throw new GenericException("Fabric installer took too long to run"); + return Mono.error(new GenericException("Fabric installer took too long to run")); } if (proc.exitValue() != 0) { IoStreams.transfer(proc.getInputStream(), System.err); - throw new GenericException("Fabric installer failed to run"); + return Mono.error(new GenericException("Fabric installer failed to run")); } } catch (IOException e) { - throw new GenericException("Failed to run fabric installer", e); + return Mono.error(new GenericException("Failed to run fabric installer", e)); } catch (InterruptedException e) { - throw new GenericException("While running fabric installer", e); + return Mono.error(new GenericException("While running fabric installer", e)); } finally { try { + //noinspection BlockingMethodInNonBlockingContext because IntelliJ is confused Files.delete(path); } catch (IOException e) { log.warn("Failed to delete fabric installer at {}", path, e); } } - return outputDir.resolve("fabric-server-launch.jar"); + return Mono.just(outputDir.resolve("fabric-server-launch.jar")); }); } diff --git a/src/main/java/me/itzg/helpers/http/OutputToDirectoryFetchBuilder.java b/src/main/java/me/itzg/helpers/http/OutputToDirectoryFetchBuilder.java index 489b1f20..58ba7135 100644 --- a/src/main/java/me/itzg/helpers/http/OutputToDirectoryFetchBuilder.java +++ b/src/main/java/me/itzg/helpers/http/OutputToDirectoryFetchBuilder.java @@ -16,6 +16,7 @@ import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; import me.itzg.helpers.errors.GenericException; +import org.jetbrains.annotations.Blocking; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import reactor.netty.http.client.HttpClient; @@ -71,6 +72,7 @@ private static class FileToDownload { } public Mono assemble() { + //noinspection BlockingMethodInNonBlockingContext return useReactiveClient(client -> client .followRedirect(true) @@ -96,9 +98,7 @@ public Mono assemble() { ); } - /** - * NOTE: contains blocking call - */ + @Blocking private Mono assembleFileDownload(HttpClient client, FileToDownload fileToDownload) { final Path outputFile = fileToDownload.outputFile; diff --git a/src/main/java/me/itzg/helpers/modrinth/InstallModrinthModpackCommand.java b/src/main/java/me/itzg/helpers/modrinth/InstallModrinthModpackCommand.java index f6969a4e..2ab848c3 100644 --- a/src/main/java/me/itzg/helpers/modrinth/InstallModrinthModpackCommand.java +++ b/src/main/java/me/itzg/helpers/modrinth/InstallModrinthModpackCommand.java @@ -1,5 +1,23 @@ package me.itzg.helpers.modrinth; +import static me.itzg.helpers.modrinth.ModrinthApiClient.pickVersionFile; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; import lombok.extern.slf4j.Slf4j; import me.itzg.helpers.errors.GenericException; import me.itzg.helpers.errors.InvalidParameterException; @@ -20,6 +38,7 @@ import me.itzg.helpers.modrinth.model.VersionFile; import me.itzg.helpers.modrinth.model.VersionType; import me.itzg.helpers.quilt.QuiltInstaller; +import org.jetbrains.annotations.Blocking; import picocli.CommandLine; import picocli.CommandLine.ExitCode; import picocli.CommandLine.Option; @@ -27,25 +46,6 @@ import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.Callable; -import java.util.function.Function; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -import static me.itzg.helpers.modrinth.ModrinthApiClient.pickVersionFile; - @CommandLine.Command(name = "install-modrinth-modpack", description = "Supports installation of Modrinth modpacks along with the associated mod loader", mixinStandardHelpOptions = true @@ -155,6 +155,7 @@ private ModrinthModpackManifest processModpack(ProjectRef projectRef, ModrinthMo .flatMap(version -> { final VersionFile versionFile = pickVersionFile(version); log.info("Installing version {} of {}", version.getVersionNumber(), project.getTitle()); + //noinspection BlockingMethodInNonBlockingContext because IntelliJ is confused return apiClient.downloadMrPack(versionFile) .publishOn(Schedulers.boundedElastic()) .flatMap(zipPath -> @@ -175,6 +176,7 @@ private ModrinthModpackManifest processModpack(ProjectRef projectRef, ModrinthMo } } + @Blocking private boolean needsInstall(ModrinthModpackManifest prevManifest, Project project, Version version) { if (prevManifest != null) { if (prevManifest.getProjectSlug().equals(project.getSlug()) @@ -194,6 +196,7 @@ private boolean needsInstall(ModrinthModpackManifest prevManifest, Project proje return true; } + @Blocking private Mono processModpackZip(ModrinthApiClient apiClient, Path zipFile, Project project, Version version ) { @@ -287,14 +290,15 @@ private String applyModloader(Map dependencies) throws IOE final String quiltVersion = dependencies.get(DependencyId.quiltLoader); if (quiltVersion != null) { - new QuiltInstaller(QuiltInstaller.DEFAULT_REPO_URL, + try (QuiltInstaller installer = new QuiltInstaller(QuiltInstaller.DEFAULT_REPO_URL, sharedFetchArgs.options(), outputDirectory, minecraftVersion ) - .setResultsFile(resultsFile) - .setLoaderVersion(quiltVersion) - .install(); + .setResultsFile(resultsFile)) { + + installer.installWithVersion(null, quiltVersion); + } } return minecraftVersion; diff --git a/src/main/java/me/itzg/helpers/mvn/MavenRepoApi.java b/src/main/java/me/itzg/helpers/mvn/MavenRepoApi.java index 99b46923..debb830b 100644 --- a/src/main/java/me/itzg/helpers/mvn/MavenRepoApi.java +++ b/src/main/java/me/itzg/helpers/mvn/MavenRepoApi.java @@ -1,22 +1,19 @@ package me.itzg.helpers.mvn; import com.fasterxml.jackson.dataformat.xml.XmlMapper; -import me.itzg.helpers.http.Fetch; +import java.nio.file.Path; import me.itzg.helpers.http.SharedFetch; -import me.itzg.helpers.http.SharedFetch.Options; import me.itzg.helpers.http.UriBuilder; import reactor.core.publisher.Mono; -import java.nio.file.Path; - public class MavenRepoApi { private final UriBuilder uriBuilder; private final SharedFetch sharedFetch; private final XmlMapper xmlMapper; - public MavenRepoApi(String repoUrl, String forCommand, Options options) { + public MavenRepoApi(String repoUrl, SharedFetch sharedFetch) { uriBuilder = UriBuilder.withBaseUrl(repoUrl); - sharedFetch = Fetch.sharedFetch(forCommand, options); + this.sharedFetch = sharedFetch; xmlMapper = new XmlMapper(); } diff --git a/src/main/java/me/itzg/helpers/patch/PatchSetProcessor.java b/src/main/java/me/itzg/helpers/patch/PatchSetProcessor.java index 61db134b..4f014e83 100644 --- a/src/main/java/me/itzg/helpers/patch/PatchSetProcessor.java +++ b/src/main/java/me/itzg/helpers/patch/PatchSetProcessor.java @@ -4,12 +4,6 @@ import com.fasterxml.jackson.databind.node.TextNode; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; -import lombok.extern.slf4j.Slf4j; -import me.itzg.helpers.CharsetDetector; -import me.itzg.helpers.env.Interpolator; -import me.itzg.helpers.env.MissingVariablesException; -import me.itzg.helpers.patch.model.*; - import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; @@ -17,6 +11,15 @@ import java.util.List; import java.util.Map; import java.util.function.Consumer; +import lombok.extern.slf4j.Slf4j; +import me.itzg.helpers.CharsetDetector; +import me.itzg.helpers.env.Interpolator; +import me.itzg.helpers.env.MissingVariablesException; +import me.itzg.helpers.patch.model.PatchDefinition; +import me.itzg.helpers.patch.model.PatchOperation; +import me.itzg.helpers.patch.model.PatchPutOperation; +import me.itzg.helpers.patch.model.PatchSet; +import me.itzg.helpers.patch.model.PatchSetOperation; @Slf4j public class PatchSetProcessor { @@ -160,7 +163,7 @@ private void processTextNode(TextNode value, String valueType, consumer.accept(convertedValue); } - else if (!interpolateResult.getMissingVariables().isEmpty()) { + else { throw new MissingVariablesException(interpolateResult.getMissingVariables()); } } diff --git a/src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java b/src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java index 430a4695..8e180fda 100644 --- a/src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java +++ b/src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java @@ -1,16 +1,17 @@ package me.itzg.helpers.purpur; +import static me.itzg.helpers.errors.Validators.DESCRIPTION_MINECRAFT_VERSION; + import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; import java.net.URI; import java.nio.file.Path; import java.util.Collections; import java.util.concurrent.Callable; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import lombok.Builder; import lombok.extern.slf4j.Slf4j; import me.itzg.helpers.errors.GenericException; import me.itzg.helpers.errors.InvalidParameterException; +import me.itzg.helpers.errors.Validators; import me.itzg.helpers.files.ManifestException; import me.itzg.helpers.files.Manifests; import me.itzg.helpers.files.ResultsFileWriter; @@ -23,7 +24,6 @@ import picocli.CommandLine.Command; import picocli.CommandLine.ExitCode; import picocli.CommandLine.Option; -import picocli.CommandLine.ParameterException; import picocli.CommandLine.Spec; import reactor.core.publisher.Mono; @@ -47,20 +47,10 @@ static class Coordinates { @Spec CommandLine.Model.CommandSpec spec; - private static final Pattern ALLOWED_VERSIONS = Pattern.compile("latest|\\d+\\.\\d+(\\.\\d+)?", - Pattern.CASE_INSENSITIVE - ); - - @Option(names = "--version", defaultValue = "latest", description = "May be 'latest' or specific version") + @Option(names = "--version", defaultValue = "latest", description = DESCRIPTION_MINECRAFT_VERSION) public void setVersion(String version) { - final Matcher m = ALLOWED_VERSIONS.matcher(version); - if (!m.matches()) { - throw new ParameterException(spec.commandLine(), "Invalid value for minecraft version: " + version); - } - this.version = version.toLowerCase(); - + this.version = Validators.validateMinecraftVersion(spec, version); } - String version; @Option(names = "--build") diff --git a/src/main/java/me/itzg/helpers/quilt/InstallQuiltCommand.java b/src/main/java/me/itzg/helpers/quilt/InstallQuiltCommand.java index 893eb9aa..4e3e9689 100644 --- a/src/main/java/me/itzg/helpers/quilt/InstallQuiltCommand.java +++ b/src/main/java/me/itzg/helpers/quilt/InstallQuiltCommand.java @@ -1,5 +1,8 @@ package me.itzg.helpers.quilt; +import java.net.URI; +import java.nio.file.Path; +import java.util.concurrent.Callable; import me.itzg.helpers.files.ResultsFileWriter; import me.itzg.helpers.http.SharedFetchArgs; import picocli.CommandLine.ArgGroup; @@ -7,37 +10,46 @@ import picocli.CommandLine.ExitCode; import picocli.CommandLine.Option; -import java.nio.file.Path; -import java.util.concurrent.Callable; - @Command(name = "install-quilt", description = "Installs Quilt mod loader") public class InstallQuiltCommand implements Callable { - @ArgGroup(exclusive = false) - SharedFetchArgs sharedFetchArgs = new SharedFetchArgs(); - @Option(names = "--repo-url", defaultValue = QuiltInstaller.DEFAULT_REPO_URL, - description = "Default: ${DEFAULT-VALUE}" + @Option(names = "--minecraft-version", defaultValue = "latest", required = true, paramLabel = "VERSION", + description = "'latest', 'snapshot', or specific version" ) - String repoUrl; + String minecraftVersion; - @Option(names = "--output-directory", defaultValue = ".", paramLabel = "DIR") - Path outputDirectory; + @Option(names = "--loader-version", paramLabel = "VERSION", + description = "Default uses latest" + ) + String loaderVersion; - @Option(names = "--minecraft-version", required = true, paramLabel = "VERSION") - String minecraftVersion; + @ArgGroup + Inputs inputs = new Inputs(); + + static class Inputs { + + @Option(names = "--installer-url", paramLabel = "URL") + URI installerUrl; + + @Option(names = "--installer-version", paramLabel = "VERSION", + description = "Default uses latest" + ) + String installerVersion; + } @Option(names = "--results-file", description = ResultsFileWriter.OPTION_DESCRIPTION, paramLabel = "FILE") Path resultsFile; - @Option(names = "--installer-version", paramLabel = "VERSION", - description = "Default uses latest" - ) - String installerVersion; + @ArgGroup(exclusive = false) + SharedFetchArgs sharedFetchArgs = new SharedFetchArgs(); - @Option(names = "--loader-version", paramLabel = "VERSION", - description = "Default uses latest" + @Option(names = "--repo-url", defaultValue = QuiltInstaller.DEFAULT_REPO_URL, + description = "Default: ${DEFAULT-VALUE}" ) - String loaderVersion; + String repoUrl; + + @Option(names = "--output-directory", defaultValue = ".", paramLabel = "DIR") + Path outputDirectory; @Option(names = "--force-reinstall") boolean forceReinstall; @@ -45,12 +57,23 @@ public class InstallQuiltCommand implements Callable { @Override public Integer call() throws Exception { - new QuiltInstaller(repoUrl, sharedFetchArgs.options(), outputDirectory, minecraftVersion) - .setInstallerVersion(installerVersion) - .setLoaderVersion(loaderVersion) + try (QuiltInstaller installer = new QuiltInstaller( + repoUrl, sharedFetchArgs.options(), outputDirectory, + minecraftVersion + ) .setResultsFile(resultsFile) .setForceReinstall(forceReinstall) - .install(); + ) { + if (inputs.installerUrl != null) { + installer.installFromUrl(inputs.installerUrl, loaderVersion); + } + else { + installer.installWithVersion( + inputs.installerVersion, + loaderVersion + ); + } + } return ExitCode.OK; } diff --git a/src/main/java/me/itzg/helpers/quilt/QuiltInstaller.java b/src/main/java/me/itzg/helpers/quilt/QuiltInstaller.java index 697b78e2..9323b87c 100644 --- a/src/main/java/me/itzg/helpers/quilt/QuiltInstaller.java +++ b/src/main/java/me/itzg/helpers/quilt/QuiltInstaller.java @@ -1,9 +1,12 @@ package me.itzg.helpers.quilt; +import static java.util.Objects.requireNonNull; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.lang.ProcessBuilder.Redirect; +import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -14,13 +17,16 @@ import me.itzg.helpers.errors.GenericException; import me.itzg.helpers.files.Manifests; import me.itzg.helpers.files.ResultsFileWriter; +import me.itzg.helpers.http.Fetch; import me.itzg.helpers.http.SharedFetch; import me.itzg.helpers.mvn.MavenRepoApi; +import me.itzg.helpers.versions.MinecraftVersionsApi; +import org.jetbrains.annotations.Blocking; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; @Slf4j -public class QuiltInstaller { +public class QuiltInstaller implements AutoCloseable { public static final String LOADER_ARTIFACT_ID = "quilt-loader"; public static final String INSTALLER_ARTIFACT = "quilt-installer"; @@ -30,12 +36,8 @@ public class QuiltInstaller { private final MavenRepoApi mavenRepoApi; private final Path outputDir; private final String minecraftVersion; - - @Setter - private String loaderVersion; - - @Setter - private String installerVersion; + private final MinecraftVersionsApi minecraftVersionsApi; + private final SharedFetch sharedFetch; @Setter private Path resultsFile; @@ -44,44 +46,65 @@ public class QuiltInstaller { private boolean forceReinstall; public QuiltInstaller(String repoUrl, SharedFetch.Options fetchOptions, Path outputDir, String minecraftVersion) { - this.outputDir = outputDir; - this.minecraftVersion = minecraftVersion; - mavenRepoApi = new MavenRepoApi(repoUrl, "quilt-installer", fetchOptions); + + this.outputDir = requireNonNull(outputDir, "Missing outputDir"); + this.minecraftVersion = requireNonNull(minecraftVersion, "Missing minecraftVersion"); + sharedFetch = Fetch.sharedFetch("quilt-installer", fetchOptions); + mavenRepoApi = new MavenRepoApi(repoUrl, sharedFetch); + minecraftVersionsApi = new MinecraftVersionsApi(sharedFetch); } - public void install() { - final QuiltManifest prevManifest = Manifests.load(outputDir, QuiltManifest.ID, QuiltManifest.class); + /** + * @param installerVersion specific version or null to use latest + * @param loaderVersion specific version or null to use latest + */ + public void installWithVersion(String installerVersion, String loaderVersion) { + handleInstall(prevManifest -> + minecraftVersionsApi.resolve(minecraftVersion) + .flatMap(resolvedMinecraftVersion -> + resolveLoaderVersion(loaderVersion) + .filter(resolvedLoaderVersion -> + needsInstall(prevManifest, resolvedMinecraftVersion, resolvedLoaderVersion) + ) + .flatMap(resolvedLoaderVersion -> + mavenRepoApi.download(outputDir, QUILT_GROUP_ID, INSTALLER_ARTIFACT, + installerVersion != null ? installerVersion : "release", "jar", null + ) + .switchIfEmpty( + Mono.defer(() -> Mono.error(new GenericException("Unable to obtain Quilt installer")))) + .publishOn(Schedulers.boundedElastic()) + .map(installerPath -> runInstaller(resolvedMinecraftVersion, installerPath, resolvedLoaderVersion)) + ) + ) + ); + } - final QuiltManifest newManifest = resolveLoaderVersion() - .filter(resolvedLoaderVersion -> { - if ( - prevManifest != null - && prevManifest.getMinecraftVersion().equals(minecraftVersion) - && prevManifest.getLoaderVersion().equals(resolvedLoaderVersion) - && Manifests.allFilesPresent(outputDir, prevManifest) - ) { - if (forceReinstall) { - log.info("Quilt {} loader {} is already installed, but force reinstall requested", - minecraftVersion, resolvedLoaderVersion - ); - } else { - log.info("Quilt {} loader {} is already installed", - minecraftVersion, resolvedLoaderVersion - ); - return false; - } - } - return true; - } - ) - .flatMap(resolvedLoaderVersion -> - mavenRepoApi.download(outputDir, QUILT_GROUP_ID, INSTALLER_ARTIFACT, - installerVersion != null ? installerVersion : "release", "jar", null + public void installFromUrl(URI installerUrl, String loaderVersion) { + handleInstall(prevManifest -> + minecraftVersionsApi.resolve(minecraftVersion) + .flatMap(resolvedMinecraftVersion -> + resolveLoaderVersion(loaderVersion) + .filter(resolvedLoaderVersion -> + needsInstall(prevManifest, resolvedMinecraftVersion, resolvedLoaderVersion) ) - .publishOn(Schedulers.boundedElastic()) - .switchIfEmpty( - Mono.defer(() -> Mono.error(new GenericException("Unable to obtain Quilt installer")))) - .map(installerPath -> runInstaller(installerPath, resolvedLoaderVersion))) + .flatMap(resolvedLoaderVersion -> + downloadFromCustomInstallerUrl(installerUrl) + .publishOn(Schedulers.boundedElastic()) + .map(installerPath -> runInstaller(resolvedMinecraftVersion, installerPath, resolvedLoaderVersion)) + ) + ) + ); + } + + @FunctionalInterface + private interface ResolveAndInstaller { + Mono assemble(QuiltManifest prevManifest); + } + + protected void handleInstall(ResolveAndInstaller resolveAndInstaller) { + final QuiltManifest prevManifest = Manifests.load(outputDir, QuiltManifest.ID, QuiltManifest.class); + + final QuiltManifest newManifest = resolveAndInstaller.assemble(prevManifest) .block(); if (newManifest != null) { @@ -94,7 +117,37 @@ public void install() { } } - private QuiltManifest runInstaller(Path installerPath, String resolvedLoaderVersion) { + private Mono downloadFromCustomInstallerUrl(URI installerUrl) { + return sharedFetch.fetch(installerUrl) + .toDirectory(outputDir) + .skipUpToDate(true) + .assemble(); + } + + private boolean needsInstall(QuiltManifest prevManifest, String resolvedMinecraftVersion, String resolvedLoaderVersion) { + if ( + prevManifest != null + && prevManifest.getMinecraftVersion().equals(resolvedMinecraftVersion) + && prevManifest.getLoaderVersion().equals(resolvedLoaderVersion) + && Manifests.allFilesPresent(outputDir, prevManifest) + ) { + if (forceReinstall) { + log.info("Quilt {} loader {} is already installed, but force reinstall requested", + resolvedMinecraftVersion, resolvedLoaderVersion + ); + } + else { + log.info("Quilt {} loader {} is already installed", + resolvedMinecraftVersion, resolvedLoaderVersion + ); + return false; + } + } + return true; + } + + @Blocking + private QuiltManifest runInstaller(String minecraftVersion, Path installerPath, String resolvedLoaderVersion) { log.info("Installing Quilt version {} with loader {}", minecraftVersion, resolvedLoaderVersion); try { @@ -157,7 +210,7 @@ private QuiltManifest runInstaller(Path installerPath, String resolvedLoaderVers .build(); } - private Mono resolveLoaderVersion() { + private Mono resolveLoaderVersion(String loaderVersion) { if (loaderVersion != null) { return Mono.just(loaderVersion); } @@ -165,4 +218,9 @@ private Mono resolveLoaderVersion() { return mavenRepoApi.fetchMetadata(QUILT_GROUP_ID, LOADER_ARTIFACT_ID) .map(mavenMetadata -> mavenMetadata.getVersioning().getLatest()); } + + @Override + public void close() { + sharedFetch.close(); + } } diff --git a/src/main/java/me/itzg/helpers/sync/MulitCopyCommand.java b/src/main/java/me/itzg/helpers/sync/MulitCopyCommand.java index f74dbfd9..4559229d 100644 --- a/src/main/java/me/itzg/helpers/sync/MulitCopyCommand.java +++ b/src/main/java/me/itzg/helpers/sync/MulitCopyCommand.java @@ -20,6 +20,7 @@ import me.itzg.helpers.http.Fetch; import me.itzg.helpers.http.SharedFetch; import me.itzg.helpers.http.Uris; +import org.jetbrains.annotations.Blocking; import org.reactivestreams.Publisher; import picocli.CommandLine.Command; import picocli.CommandLine.ExitCode; @@ -151,6 +152,7 @@ private Mono processFile(Path source) { * * @param scopedDest allows for sub-directory destinations */ + @Blocking private Path processFileImmediate(Path source, Path scopedDest) { if (!Files.exists(source)) { throw new InvalidParameterException("Source file does not exist: " + source); @@ -206,9 +208,10 @@ private Flux processDirectory(Path srcDir) { try { final ArrayList results = new ArrayList<>(); - //noinspection BlockingMethodInNonBlockingContext since IntelliJ flags incorrectly + //noinspection BlockingMethodInNonBlockingContext because IntelliJ is confused try (DirectoryStream files = Files.newDirectoryStream(srcDir, fileGlob)) { for (final Path file : files) { + //noinspection BlockingMethodInNonBlockingContext because IntelliJ is confused results.add(processFileImmediate(file, dest)); } } diff --git a/src/main/java/me/itzg/helpers/vanillatweaks/VanillaTweaksCommand.java b/src/main/java/me/itzg/helpers/vanillatweaks/VanillaTweaksCommand.java index 2b78d30f..a738a905 100644 --- a/src/main/java/me/itzg/helpers/vanillatweaks/VanillaTweaksCommand.java +++ b/src/main/java/me/itzg/helpers/vanillatweaks/VanillaTweaksCommand.java @@ -39,6 +39,7 @@ import me.itzg.helpers.vanillatweaks.model.PackDefinition; import me.itzg.helpers.vanillatweaks.model.Type; import me.itzg.helpers.vanillatweaks.model.ZipLinkResponse; +import org.jetbrains.annotations.Blocking; import org.reactivestreams.Publisher; import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Command; @@ -261,10 +262,8 @@ private Path dataPacksDir() throws IOException { return Files.createDirectories(worldPath.resolve("datapacks")); } - /** - * Must be within {@link Schedulers#boundedElastic()} - */ @SuppressWarnings("BlockingMethodInNonBlockingContext") + @Blocking private Flux unzipInto(Path zipPath, Path outDir) throws IOException { final List result = new ArrayList<>(); try (InputStream in = Files.newInputStream(zipPath); @@ -318,6 +317,7 @@ private Publisher downloadVanillaTweaksZip(SharedFetch sharedFetch, String .publishOn(Schedulers.boundedElastic()) .flatMapMany(downloaded -> { try { + //noinspection BlockingMethodInNonBlockingContext because IntelliJ is confused return unzipInto(tempZip, dataPacksDir()); } catch (IOException e) { return Flux.error(e); diff --git a/src/main/java/me/itzg/helpers/versions/MinecraftVersionsApi.java b/src/main/java/me/itzg/helpers/versions/MinecraftVersionsApi.java new file mode 100644 index 00000000..e03b53dc --- /dev/null +++ b/src/main/java/me/itzg/helpers/versions/MinecraftVersionsApi.java @@ -0,0 +1,49 @@ +package me.itzg.helpers.versions; + +import java.net.URI; +import lombok.Setter; +import me.itzg.helpers.http.SharedFetch; +import me.itzg.helpers.versions.VersionManifestV2.Version; +import reactor.core.publisher.Mono; + +public class MinecraftVersionsApi { + + @Setter + private URI manifestUrl = URI.create("https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"); + + private final SharedFetch sharedFetch; + + public MinecraftVersionsApi(SharedFetch sharedFetch) { + this.sharedFetch = sharedFetch; + } + + /** + * @param inputVersion latest, release, snapshot or a specific version + * @return the resolved version or empty if not valid/present + */ + public Mono resolve(String inputVersion) { + return sharedFetch.fetch( + manifestUrl + ) + .toObject(VersionManifestV2.class) + .assemble() + .flatMap(manifest -> { + if (inputVersion == null + || inputVersion.equals("latest") + || inputVersion.equalsIgnoreCase("release")) { + return Mono.just(manifest.getLatest().getRelease()); + } + else if (inputVersion.equalsIgnoreCase("snapshot")) { + return Mono.just(manifest.getLatest().getSnapshot()); + } + else { + return Mono.justOrEmpty( + manifest.getVersions().stream() + .map(Version::getId) + .filter(id -> id.equalsIgnoreCase(inputVersion)) + .findFirst() + ); + } + }); + } +} diff --git a/src/main/java/me/itzg/helpers/versions/ResolveMinecraftVersionCommand.java b/src/main/java/me/itzg/helpers/versions/ResolveMinecraftVersionCommand.java new file mode 100644 index 00000000..35387a33 --- /dev/null +++ b/src/main/java/me/itzg/helpers/versions/ResolveMinecraftVersionCommand.java @@ -0,0 +1,37 @@ +package me.itzg.helpers.versions; + +import java.util.concurrent.Callable; +import me.itzg.helpers.http.Fetch; +import me.itzg.helpers.http.SharedFetch; +import me.itzg.helpers.http.SharedFetchArgs; +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Command; +import picocli.CommandLine.ExitCode; +import picocli.CommandLine.Parameters; + +@Command(name = "resolve-minecraft-version", description = "Resolves and validate latest, snapshot, and specific versions") +public class ResolveMinecraftVersionCommand implements Callable { + + @Parameters(arity = "1") + String inputVersion; + + @ArgGroup(exclusive = false) + SharedFetchArgs sharedFetchArgs = new SharedFetchArgs(); + + @Override + public Integer call() throws Exception { + try (SharedFetch sharedFetch = Fetch.sharedFetch("resolve-minecraft-version", sharedFetchArgs.options())) { + final String resolved = new MinecraftVersionsApi(sharedFetch) + .resolve(inputVersion) + .block(); + if (resolved == null) { + System.err.println("Unable to resolve version from "+inputVersion); + return ExitCode.USAGE; + } + + System.out.println(resolved); + } + + return ExitCode.OK; + } +} diff --git a/src/main/java/me/itzg/helpers/versions/VersionManifestV2.java b/src/main/java/me/itzg/helpers/versions/VersionManifestV2.java new file mode 100644 index 00000000..d8061f8b --- /dev/null +++ b/src/main/java/me/itzg/helpers/versions/VersionManifestV2.java @@ -0,0 +1,32 @@ +package me.itzg.helpers.versions; + +import java.net.URI; +import java.util.List; +import lombok.Data; + +@Data +public class VersionManifestV2 { + public enum VersionType { + release, + snapshot, + old_beta, + old_alpha + } + + @Data + public static class Latest { + private String release; + private String snapshot; + } + + @Data + public static class Version { + private String id; + private VersionType type; + private URI url; + } + + private Latest latest; + + private List versions; +}