From a0e49e80bce67465cfa02b041a0fdcd61afbda84 Mon Sep 17 00:00:00 2001 From: Geoff Bourne Date: Sat, 17 Jun 2023 11:28:04 -0500 Subject: [PATCH 1/2] Consistently resolve and write out VERSION and TYPE --- README.md | 38 ++ dev/paper.http | 2 + .../curseforge/CurseForgeInstaller.java | 56 ++- .../fabric/FabricLauncherInstaller.java | 429 ++++++------------ .../itzg/helpers/fabric/FabricManifest.java | 1 + .../itzg/helpers/fabric/FabricMetaClient.java | 122 +++++ .../fabric/InstallFabricLoaderCommand.java | 30 +- .../helpers/fabric/LoaderResponseEntry.java | 7 +- .../me/itzg/helpers/fabric/LocalFile.java | 2 - .../java/me/itzg/helpers/fabric/Origin.java | 1 - ...erResponseEntry.java => VersionEntry.java} | 4 +- .../java/me/itzg/helpers/fabric/Versions.java | 7 +- .../java/me/itzg/helpers/files/IoStreams.java | 18 +- .../java/me/itzg/helpers/files/Manifests.java | 22 +- .../itzg/helpers/files/ResultsFileWriter.java | 10 + .../me/itzg/helpers/forge/ForgeInstaller.java | 47 +- .../http/OutputToDirectoryFetchBuilder.java | 4 +- .../java/me/itzg/helpers/http/UriBuilder.java | 2 + .../InstallModrinthModpackCommand.java | 86 ++-- .../helpers/modrinth/ModrinthCommand.java | 37 +- .../modrinth/ModrinthModpackManifest.java | 3 + .../me/itzg/helpers/mvn/MavenRepoApi.java | 4 +- .../helpers/paper/InstallPaperCommand.java | 75 ++- .../itzg/helpers/paper/model/VersionMeta.java | 14 + .../helpers/purpur/InstallPurpurCommand.java | 46 +- .../helpers/purpur/model/VersionMeta.java | 14 + .../helpers/quilt/InstallQuiltCommand.java | 5 +- .../me/itzg/helpers/quilt/QuiltInstaller.java | 16 +- .../versions/MinecraftVersionsApi.java | 9 +- .../fabric/FabricLauncherInstallerTest.java | 130 +++--- .../java/me/itzg/helpers/fabric/research.http | 6 + .../__files/fabric-empty-launcher.jar | Bin 0 -> 562 bytes .../fabric/mappings/v2_versions_game.json | 13 + 33 files changed, 718 insertions(+), 542 deletions(-) create mode 100644 dev/paper.http create mode 100644 src/main/java/me/itzg/helpers/fabric/FabricMetaClient.java rename src/main/java/me/itzg/helpers/fabric/{InstallerResponseEntry.java => VersionEntry.java} (72%) create mode 100644 src/main/java/me/itzg/helpers/paper/model/VersionMeta.java create mode 100644 src/main/java/me/itzg/helpers/purpur/model/VersionMeta.java create mode 100644 src/test/resources/__files/fabric-empty-launcher.jar create mode 100644 src/test/resources/fabric/mappings/v2_versions_game.json diff --git a/README.md b/README.md index b32bd378..02445e06 100644 --- a/README.md +++ b/README.md @@ -408,6 +408,44 @@ Usage: mc-image-helper patch [-h] [--patch-env-prefix=] FILE_OR_DIR [See below](#patch-schemas) for a description of [PatchSet](#patchset) and [PatchDefinition](#patchdefinition) JSON schemas. +### install-quilt + +``` +Usage: mc-image-helper install-quilt [-h] [--force-reinstall] + [--loader-version=VERSION] + [--minecraft-version=VERSION] + [--output-directory=DIR] + [--repo-url=] + [--results-file=FILE] [--installer-url=URL + | --installer-version=VERSION] + [[--tls-handshake-timeout=DURATION] + [--connection-pool-max-idle-timeout=DURATIO + N] [--http-response-timeout=DURATION]] +Installs Quilt mod loader + --connection-pool-max-idle-timeout=DURATION + + --force-reinstall + -h, --help + --http-response-timeout=DURATION + The response timeout to apply to HTTP operations. + Parsed from ISO-8601 format. Default: PT30S + --installer-url=URL + --installer-version=VERSION + Default uses latest + --loader-version=VERSION + Default uses latest + --minecraft-version=VERSION + 'latest', 'snapshot', or specific version + --output-directory=DIR + --repo-url= Default: https://maven.quiltmc. + org/repository/release + --results-file=FILE A key=value file suitable for scripted environment + variables. Currently includes + SERVER: the entry point jar or script + --tls-handshake-timeout=DURATION + Default: PT30S +``` + ### sync-and-interpolate ``` diff --git a/dev/paper.http b/dev/paper.http new file mode 100644 index 00000000..294afce9 --- /dev/null +++ b/dev/paper.http @@ -0,0 +1,2 @@ +### +GET https://api.papermc.io/v2/projects/paper \ No newline at end of file diff --git a/src/main/java/me/itzg/helpers/curseforge/CurseForgeInstaller.java b/src/main/java/me/itzg/helpers/curseforge/CurseForgeInstaller.java index 18b01b9c..f3285b81 100644 --- a/src/main/java/me/itzg/helpers/curseforge/CurseForgeInstaller.java +++ b/src/main/java/me/itzg/helpers/curseforge/CurseForgeInstaller.java @@ -1,5 +1,28 @@ package me.itzg.helpers.curseforge; +import static java.util.Collections.emptySet; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; +import static me.itzg.helpers.singles.MoreCollections.safeStreamFrom; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -26,30 +49,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.Paths; -import java.nio.file.StandardCopyOption; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -import static java.util.Collections.emptySet; -import static java.util.Objects.requireNonNull; -import static java.util.Optional.ofNullable; -import static me.itzg.helpers.singles.MoreCollections.safeStreamFrom; - @RequiredArgsConstructor @Slf4j public class CurseForgeInstaller { @@ -385,7 +384,6 @@ private void finalizeExistingInstallation(CurseForgeManifest prevManifest) throw if (prevManifest.getLevelName() != null) { resultsFileWriter.write("LEVEL", prevManifest.getLevelName()); } - resultsFileWriter.write("VERSION", prevManifest.getMinecraftVersion()); } } } @@ -413,7 +411,6 @@ private void finalizeResults(InstallContext context, ModPackResults results, int if (results.getLevelName() != null) { resultsFileWriter.write("LEVEL", results.getLevelName()); } - resultsFileWriter.write("VERSION", results.getMinecraftVersion()); } } } @@ -841,7 +838,7 @@ private boolean isServerMod(CurseForgeFile file) { return !client; } - private void prepareModLoader(String id, String minecraftVersion) throws IOException { + private void prepareModLoader(String id, String minecraftVersion) { final String[] parts = id.split("-", 2); if (parts.length != 2) { throw new GenericException("Unknown modloader ID: " + id); @@ -858,8 +855,9 @@ private void prepareModLoader(String id, String minecraftVersion) throws IOExcep } } - private void prepareFabric(String minecraftVersion, String loaderVersion) throws IOException { - final FabricLauncherInstaller installer = new FabricLauncherInstaller(outputDir, resultsFile); + private void prepareFabric(String minecraftVersion, String loaderVersion) { + final FabricLauncherInstaller installer = new FabricLauncherInstaller(outputDir) + .setResultsFile(resultsFile); installer.installUsingVersions(minecraftVersion, loaderVersion, null); } diff --git a/src/main/java/me/itzg/helpers/fabric/FabricLauncherInstaller.java b/src/main/java/me/itzg/helpers/fabric/FabricLauncherInstaller.java index e83fbf9f..e2749041 100644 --- a/src/main/java/me/itzg/helpers/fabric/FabricLauncherInstaller.java +++ b/src/main/java/me/itzg/helpers/fabric/FabricLauncherInstaller.java @@ -1,32 +1,27 @@ package me.itzg.helpers.fabric; -import static me.itzg.helpers.http.Fetch.fetch; import static me.itzg.helpers.http.Fetch.sharedFetch; import java.io.IOException; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.TimeUnit; +import java.util.Properties; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import me.itzg.helpers.errors.GenericException; -import me.itzg.helpers.fabric.LoaderResponseEntry.Loader; -import me.itzg.helpers.files.Checksums; +import me.itzg.helpers.errors.InvalidParameterException; import me.itzg.helpers.files.IoStreams; import me.itzg.helpers.files.Manifests; import me.itzg.helpers.files.ResultsFileWriter; -import me.itzg.helpers.http.FailedRequestException; +import me.itzg.helpers.http.Fetch; import me.itzg.helpers.http.SharedFetch; import me.itzg.helpers.http.SharedFetch.Options; -import me.itzg.helpers.http.UriBuilder; +import org.jetbrains.annotations.Blocking; +import org.jetbrains.annotations.Nullable; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; @@ -34,334 +29,200 @@ @Slf4j public class FabricLauncherInstaller { - private static final String RESULT_LAUNCHER = "SERVER"; - public static final String MANIFEST_ID = "fabric"; - private static final long FABRIC_INSTALLER_TIMEOUT_SEC = 60; - + @NonNull private final Path outputDir; - private final Path resultsFile; + + @Setter + private Path resultsFile; @Getter @Setter private String fabricMetaBaseUrl = "https://meta.fabricmc.net"; - /** - * @param minecraftVersion required - * @param loaderVersion optional - * @param installerVersion optional - * @return the launcher's path - */ - public Path installUsingVersions(@NonNull String minecraftVersion, String loaderVersion, String installerVersion) - throws IOException { - Objects.requireNonNull(outputDir, "outputDir is required"); - - final UriBuilder uriBuilder = UriBuilder.withBaseUrl(fabricMetaBaseUrl); - + public void installUsingVersions( + @NonNull String minecraftVersion, + @Nullable String loaderVersion, + @Nullable String installerVersion + ) { try (SharedFetch sharedFetch = sharedFetch("fabric", Options.builder().build())) { - loaderVersion = resolveLoaderVersion(uriBuilder, sharedFetch, minecraftVersion, loaderVersion); - - installerVersion = resolveInstallerVersion(uriBuilder, sharedFetch, installerVersion); - - final FabricManifest manifest = Manifests.load(outputDir, MANIFEST_ID, FabricManifest.class); + final FabricMetaClient fabricMetaClient = new FabricMetaClient(sharedFetch, fabricMetaBaseUrl); + + fabricMetaClient.resolveMinecraftVersion(minecraftVersion) + .doOnNext(v -> log.debug("Resolved minecraft version {} from {}", v, minecraftVersion)) + .flatMap(resolvedMinecraftVersion -> + fabricMetaClient.resolveLoaderVersion(resolvedMinecraftVersion, loaderVersion) + .doOnNext(v -> log.debug("Resolved loader version {} from {}", v, loaderVersion)) + .flatMap(resolvedLoaderVersion -> + + fabricMetaClient.resolveInstallerVersion(installerVersion) + .doOnNext(v -> log.debug("Resolved installer version {} from {}", v, installerVersion)) + .flatMap(resolvedInstallerVersion -> downloadResolvedLauncher( + fabricMetaClient, + resolvedMinecraftVersion, + resolvedLoaderVersion, + resolvedInstallerVersion + )) + ) + ) + .block(); + } - final Versions versions = Versions.builder() - .game(minecraftVersion) - .loader(loaderVersion) - .installer(installerVersion) - .build(); - final boolean needsInstall = manifest == null || manifest.getOrigin() == null || - !manifest.getOrigin().equals(versions); + } - if (needsInstall) { - return processInstallUsingVersions(minecraftVersion, loaderVersion, installerVersion, uriBuilder, sharedFetch, - manifest, versions - ); - } - else { - return Paths.get(manifest.getLauncherPath()); - } + private Mono downloadResolvedLauncher(FabricMetaClient fabricMetaClient, + String minecraftVersion, String loaderVersion, String installerVersion + ) { + final FabricManifest prevManifest = Manifests.load(outputDir, FabricManifest.MANIFEST_ID, + FabricManifest.class + ); + + final Versions expectedVersions = Versions.builder() + .game(minecraftVersion) + .loader(loaderVersion) + .installer(installerVersion) + .build(); - } + final boolean needsInstall = + prevManifest == null + || prevManifest.getOrigin() == null + || !prevManifest.getOrigin().equals(expectedVersions) + || !Manifests.allFilesPresent(outputDir, prevManifest); - } + if (needsInstall) { + return fabricMetaClient.downloadLauncher( + outputDir, minecraftVersion, loaderVersion, installerVersion, + Fetch.loggingDownloadStatusHandler(log) + ) + .publishOn(Schedulers.boundedElastic()) + .flatMap(launcherPath -> + { + try { + //noinspection BlockingMethodInNonBlockingContext because IntelliJ is confused + return finalizeResultsFileAndManifest( + prevManifest, + expectedVersions, + launcherPath + ); + } catch (IOException e) { + return Mono.error( + new GenericException("Failed to finalize Fabric setup", e) + ); + } + } - private Path processInstallUsingVersions(String minecraftVersion, String loaderVersion, String installerVersion, - UriBuilder uriBuilder, - SharedFetch sharedFetch, FabricManifest manifest, Versions versions - ) throws IOException { - if (manifest != null && manifest.getOrigin() != null) { - log.info("Upgrading Fabric from {} to {}", manifest.getOrigin(), versions); + ); } else { - log.info("Installing Fabric {}", versions); - } - - final Path launcherPath = sharedFetch.fetch( - uriBuilder.resolve( - "/v2/versions/loader/{game_version}/{loader_version}/{installer_version}/server/jar", - minecraftVersion, loaderVersion, installerVersion - ) - ) - .toDirectory(outputDir) - .assemble() - .onErrorResume(FailedRequestException::isBadRequest, - throwable -> { - log.debug("Loader could not be downloaded for minecraft={} loader={} installer={}. Getting and using installer instead.", - minecraftVersion, loaderVersion, installerVersion); - // retrieve and run the installer to obtain the launcher - return installLauncherUsingInstaller(sharedFetch, minecraftVersion, loaderVersion, - installerVersion - ); - } - ) - .block(); - - if (launcherPath == null) { - throw new GenericException("Unable to resolve launcher path"); + log.info("Fabric launcher for minecraft {} loader {} is already available", + minecraftVersion, loaderVersion + ); + return Mono.empty(); } + } + @Blocking + private Mono finalizeResultsFileAndManifest(FabricManifest prevManifest, Versions versions, Path launcherPath) + throws IOException { if (resultsFile != null) { - try (ResultsFileWriter results = new ResultsFileWriter(resultsFile)) { - results.write(RESULT_LAUNCHER, launcherPath.toString()); - results.write("FAMILY", "FABRIC"); - } + writeResultsFile(launcherPath, versions.getGame()); } final FabricManifest newManifest = FabricManifest.builder() .origin(versions) .files( - Manifests.relativizeAll(outputDir, Collections.singletonList(launcherPath)) + Manifests.relativizeAll(outputDir, launcherPath) ) .launcherPath(launcherPath.toString()) .build(); - Manifests.cleanup(outputDir, manifest, newManifest, log); + Manifests.cleanup(outputDir, prevManifest, newManifest, log); - Manifests.save(outputDir, MANIFEST_ID, newManifest); + Manifests.save(outputDir, FabricManifest.MANIFEST_ID, newManifest); - return launcherPath; + return Mono.just(newManifest); } - private Mono installLauncherUsingInstaller(SharedFetch sharedFetch, - String minecraftVersion, String loaderVersion, String installerVersion - ) { - final Path installerFile = outputDir.resolve(String.format("fabric-installer-%s.jar", installerVersion)); - - final URI installerUri = UriBuilder.withNoBaseUrl() - .resolve("https://maven.fabricmc.net/net/fabricmc/fabric-installer/{version}/fabric-installer-{version}.jar", - installerVersion, installerVersion - ); - - if (log.isDebugEnabled()) { - log.debug("Downloading Fabric installer from {}", installerUri); - } - else { - log.info("Downloading Fabric installer"); - } - - return sharedFetch.fetch( - installerUri - ) - .toFile(installerFile) - .skipExisting(true) - .assemble() - .publishOn(Schedulers.boundedElastic()) - .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", - "-mcversion", minecraftVersion, - "-loader", loaderVersion - ) - .directory(outputDir.toFile()) - .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); - return Mono.error(new GenericException("Fabric installer took too long to run")); - } - - if (proc.exitValue() != 0) { - IoStreams.transfer(proc.getInputStream(), System.err); - return Mono.error(new GenericException("Fabric installer failed to run")); - } - } catch (IOException e) { - return Mono.error(new GenericException("Failed to run fabric installer", e)); - } catch (InterruptedException 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 Mono.just(outputDir.resolve("fabric-server-launch.jar")); - }); - } - - public void installGivenLauncherFile(Path launcher) throws IOException { - if (resultsFile != null) { - try (ResultsFileWriter results = new ResultsFileWriter(resultsFile)) { - results.write(RESULT_LAUNCHER, launcher.toString()); - results.write("FAMILY", "FABRIC"); - } + public void installUsingUri(URI loaderUri) throws IOException { + final Path launcherPath; + try (SharedFetch sharedFetch = sharedFetch("fabric", Options.builder().build())) { + launcherPath = sharedFetch.fetch(loaderUri) + .toDirectory(outputDir) + .skipUpToDate(true) + .handleStatus(Fetch.loggingDownloadStatusHandler(log)) + .assemble() + .block(); } - if (outputDir != null) { - // Do some manifest updates - - final FabricManifest manifest = Manifests.load(outputDir, MANIFEST_ID, FabricManifest.class); - final String previousChecksum; - if (manifest != null && manifest.getOrigin() instanceof LocalFile) { - previousChecksum = ((LocalFile) manifest.getOrigin()).getChecksum(); - } - else { - previousChecksum = null; - } - - final String newChecksum = Checksums.checksumLike(previousChecksum, launcher); - final FabricManifest newManifest = FabricManifest.builder() - .origin(LocalFile.builder() - .checksum(newChecksum) - .build()) - .launcherPath(launcher.toString()) - .build(); - - if (previousChecksum != null) { - if (!previousChecksum.equals(newChecksum)) { - log.info("Provided launcher has changed"); - } - } - else if (manifest != null) { - Manifests.cleanup(outputDir, manifest, newManifest, log); - - if (manifest.getOrigin() != null) { - log.info("Switching from {} to provided launcher", manifest.getOrigin()); - } - } - - Manifests.save(outputDir, MANIFEST_ID, newManifest); + if (launcherPath == null) { + throw new GenericException("Failed to download Fabric launcher"); } - } - - /** - * @return launcher's path - */ - public Path installUsingUri(URI loaderUri) throws IOException { - Objects.requireNonNull(outputDir, "outputDir is required"); - final Path launcher; - try { - launcher = fetch(loaderUri) - .toDirectory(outputDir) - .skipExisting(true) - .execute(); - } catch (IOException e) { - throw new GenericException("Failed to fetch launcher from " + loaderUri, e); + if (resultsFile != null) { + writeResultsFileFromLauncher(launcherPath); } - final RemoteFile newOrigin = RemoteFile.builder() - .uri(loaderUri.toString()) - .build(); - - final FabricManifest oldManifest = Manifests.load(outputDir, MANIFEST_ID, FabricManifest.class); - if (oldManifest != null) { - if (!Objects.equals(oldManifest.getOrigin(), newOrigin)) { - log.info("Switching from {} to {} downloaded from {}", - oldManifest.getOrigin(), launcher, loaderUri - ); - } - } - else { - log.info("Using {} downloaded from {}", launcher, loaderUri); - } + final FabricManifest prevManifest = Manifests.load(outputDir, FabricManifest.MANIFEST_ID, FabricManifest.class); final FabricManifest newManifest = FabricManifest.builder() - .origin(newOrigin) - .launcherPath(launcher.toString()) - .files(Manifests.relativizeAll(outputDir, Collections.singletonList(launcher))) + .origin( + RemoteFile.builder() + .uri(loaderUri.toString()) + .build() + ) + .files( + Manifests.relativizeAll(outputDir, launcherPath) + ) + .launcherPath(launcherPath.toString()) .build(); - Manifests.save(outputDir, MANIFEST_ID, newManifest); - Manifests.cleanup(outputDir, oldManifest, newManifest, log); - - if (resultsFile != null) { - try (ResultsFileWriter results = new ResultsFileWriter(resultsFile)) { - results.write(RESULT_LAUNCHER, launcher.toString()); - results.write("FAMILY", "FABRIC"); - } catch (IOException e) { - throw new GenericException("Failed to write results file", e); - } - } - - return launcher; + Manifests.save(outputDir, FabricManifest.MANIFEST_ID, newManifest); + Manifests.cleanup(outputDir, prevManifest, newManifest, log); } - private String resolveInstallerVersion(UriBuilder uriBuilder, SharedFetch sharedFetch, String installerVersion) { - if (nonEmptyString(installerVersion)) { - return installerVersion; + public void installUsingLocalFile(Path launcherFile) throws IOException { + if (!Files.exists(launcherFile)) { + throw new InvalidParameterException("The local Fabric launcher file does not exist: " + launcherFile); } - try { - final List installerEntries = sharedFetch.fetch(uriBuilder.resolve("/v2/versions/installer")) - .toObjectList(InstallerEntry.class) - .execute(); - - return installerEntries.stream() - .filter(InstallerEntry::isStable) - .findFirst() - .orElseThrow(() -> new GenericException("Failed to find stable installer from " + fabricMetaBaseUrl)) - .getVersion(); - } catch (IOException e) { - throw new GenericException("Failed to retrieve installer metadata from " + fabricMetaBaseUrl, e); + if (resultsFile != null) { + writeResultsFileFromLauncher(launcherFile); } + + Manifests.save(outputDir, FabricManifest.MANIFEST_ID, + FabricManifest.builder() + .origin(LocalFile.builder().build()) + .launcherPath(launcherFile.toString()) + .build() + ); } - private String resolveLoaderVersion(UriBuilder uriBuilder, SharedFetch sharedFetch, String minecraftVersion, - String loaderVersion - ) { - if (nonEmptyString(loaderVersion)) { - return loaderVersion; - } + private void writeResultsFileFromLauncher(Path launcherPath) throws IOException { + final Properties installProps = IoStreams.readFileFromZip(launcherPath, + "install.properties", in -> { + final Properties p = new Properties(); + p.load(in); + return p; + } + ); - final List loaderResponse; - try { - loaderResponse = sharedFetch.fetch( - uriBuilder.resolve("/v2/versions/loader/{game_version}", minecraftVersion)) - .toObjectList(LoaderResponseEntry.class) - .execute(); - } catch (IOException e) { - throw new GenericException("Failed to retrieve loader metadata from " + fabricMetaBaseUrl, e); + if (installProps == null) { + throw new GenericException("Failed to locate install.properties from launcher " + launcherPath); } - if (loaderResponse.isEmpty()) { - throw new GenericException("No loader entries provided from " + fabricMetaBaseUrl); + if (!installProps.containsKey("game-version")) { + throw new GenericException("Install properties from launcher " + launcherPath + " is missing game-version"); } - final Loader loader = loaderResponse.stream() - .filter(entry -> entry.getLoader() != null && entry.getLoader().isStable()) - .findFirst() - .orElseThrow(() -> new GenericException("No stable loaders found from " + fabricMetaBaseUrl)) - .getLoader(); - - return loader.getVersion(); + writeResultsFile(launcherPath, installProps.getProperty("game-version")); } - private static boolean nonEmptyString(String loaderVersion) { - return loaderVersion != null && !loaderVersion.isEmpty(); + private void writeResultsFile(Path launcherPath, String gameVersion) throws IOException { + try (ResultsFileWriter results = new ResultsFileWriter(resultsFile)) { + results.writeServer(launcherPath); + results.write("FAMILY", "FABRIC"); + results.writeType("FABRIC"); + results.writeVersion(gameVersion); + } } - } diff --git a/src/main/java/me/itzg/helpers/fabric/FabricManifest.java b/src/main/java/me/itzg/helpers/fabric/FabricManifest.java index 340d2612..297b6271 100644 --- a/src/main/java/me/itzg/helpers/fabric/FabricManifest.java +++ b/src/main/java/me/itzg/helpers/fabric/FabricManifest.java @@ -10,6 +10,7 @@ @Jacksonized public class FabricManifest extends BaseManifest { + public static final String MANIFEST_ID = "fabric"; /** * The path to the launcher. This should also be in {@link #getFiles()}, but provides a specific reference. */ diff --git a/src/main/java/me/itzg/helpers/fabric/FabricMetaClient.java b/src/main/java/me/itzg/helpers/fabric/FabricMetaClient.java new file mode 100644 index 00000000..bb5628c2 --- /dev/null +++ b/src/main/java/me/itzg/helpers/fabric/FabricMetaClient.java @@ -0,0 +1,122 @@ +package me.itzg.helpers.fabric; + +import java.nio.file.Path; +import java.util.List; +import java.util.function.Predicate; +import me.itzg.helpers.errors.GenericException; +import me.itzg.helpers.http.FileDownloadStatusHandler; +import me.itzg.helpers.http.SharedFetch; +import me.itzg.helpers.http.UriBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import reactor.core.publisher.Mono; + +public class FabricMetaClient { + + private final SharedFetch sharedFetch; + private final UriBuilder uriBuilder; + + public FabricMetaClient(SharedFetch sharedFetch, String fabricMetaBaseUrl) { + this.sharedFetch = sharedFetch; + uriBuilder = UriBuilder.withBaseUrl(fabricMetaBaseUrl); + } + + static boolean nonEmptyString(String loaderVersion) { + return loaderVersion != null && !loaderVersion.isEmpty(); + } + + /** + * @param version can be latest, snapshot or specific + */ + public Mono resolveMinecraftVersion(@Nullable String version) { + return sharedFetch.fetch( + uriBuilder.resolve("/v2/versions/game") + ) + .toObjectList(VersionEntry.class) + .assemble() + .flatMap(versionEntries -> { + if (version == null || version.equalsIgnoreCase("latest")) { + return findFirst(versionEntries, VersionEntry::isStable) + .switchIfEmpty(Mono.error(() -> new GenericException("Unable to find any stable versions"))); + } + else if (version.equalsIgnoreCase("snapshot")) { + return findFirst(versionEntries, versionEntry -> !versionEntry.isStable()) + .switchIfEmpty(Mono.error(() -> new GenericException("Unable to find any unstable versions"))); + } + else { + return findFirst(versionEntries, versionEntry -> versionEntry.getVersion().equalsIgnoreCase(version)) + .switchIfEmpty(Mono.error(() -> new GenericException("Unable to find requested version"))); + } + }); + } + + public Mono resolveLoaderVersion(String minecraftVersion, String loaderVersion) { + if (nonEmptyString(loaderVersion)) { + return Mono.just(loaderVersion); + } + + return sharedFetch.fetch( + uriBuilder.resolve("/v2/versions/loader/{game_version}", minecraftVersion)) + .toObjectList(LoaderResponseEntry.class) + .assemble() + .flatMap(loaderResponse -> { + if (loaderResponse.isEmpty()) { + return Mono.error(new GenericException("No loader entries provided from " + uriBuilder.getBaseUrl())); + } + + final VersionEntry loader = loaderResponse.stream() + .filter(entry -> entry.getLoader() != null && entry.getLoader().isStable()) + .findFirst() + .orElseThrow(() -> new GenericException("No stable loaders found from " + uriBuilder.getBaseUrl())) + .getLoader(); + + return Mono.just(loader.getVersion()); + }); + } + + public Mono resolveInstallerVersion(String installerVersion) { + if (nonEmptyString(installerVersion)) { + return Mono.just(installerVersion); + } + + return sharedFetch.fetch( + uriBuilder.resolve("/v2/versions/installer") + ) + .toObjectList(InstallerEntry.class) + .assemble() + .flatMap(installerEntries -> installerEntries.stream() + .filter(InstallerEntry::isStable) + .findFirst() + .map(installerEntry -> Mono.just(installerEntry.getVersion())) + .orElseGet( + () -> Mono.error(new GenericException("Failed to find stable installer from " + uriBuilder.getBaseUrl())) + ) + ); + } + + public Mono downloadLauncher( + Path outputDir, String minecraftVersion, String loaderVersion, String installerVersion, + FileDownloadStatusHandler statusHandler + ) { + return sharedFetch.fetch( + uriBuilder.resolve( + "/v2/versions/loader/{game_version}/{loader_version}/{installer_version}/server/jar", + minecraftVersion, loaderVersion, installerVersion + ) + ) + .toDirectory(outputDir) + .handleStatus(statusHandler) + .assemble(); + } + + @NotNull + private static Mono findFirst(List versionEntries, Predicate condition + ) { + return Mono.justOrEmpty( + versionEntries.stream() + .filter(condition) + .map(VersionEntry::getVersion) + .findFirst() + ); + } +} diff --git a/src/main/java/me/itzg/helpers/fabric/InstallFabricLoaderCommand.java b/src/main/java/me/itzg/helpers/fabric/InstallFabricLoaderCommand.java index 14b31bd2..9ea233d8 100644 --- a/src/main/java/me/itzg/helpers/fabric/InstallFabricLoaderCommand.java +++ b/src/main/java/me/itzg/helpers/fabric/InstallFabricLoaderCommand.java @@ -32,7 +32,7 @@ public class InstallFabricLoaderCommand implements Callable { static class OriginOptions { @ArgGroup(exclusive = false) - VersionOptions versionOptions; + VersionOptions versionOptions = new VersionOptions(); @Option(names = "--from-local-file", paramLabel = "FILE") Path launcherFile; @@ -42,7 +42,7 @@ static class OriginOptions { } static class VersionOptions { - @Option(names = "--minecraft-version", required = true, paramLabel = "VERSION") + @Option(names = "--minecraft-version", defaultValue = "latest", paramLabel = "VERSION") String minecraftVersion; @Option(names = "--installer-version", paramLabel = "VERSION", @@ -62,7 +62,7 @@ static class VersionOptions { private static class AllowedVersions implements ITypeConverter { @Override - public String convert(String value) throws Exception { + public String convert(String value) { if (value == null) { return null; @@ -80,24 +80,22 @@ else if (VERSION_PATTERN.matcher(value).matches()) { @Override public Integer call() throws Exception { - final FabricLauncherInstaller installer = new FabricLauncherInstaller(outputDirectory, resultsFile); - final Path launcher; - if (originOptions.versionOptions != null) { - launcher = installer.installUsingVersions( + final FabricLauncherInstaller installer = new FabricLauncherInstaller(outputDirectory) + .setResultsFile(resultsFile); + + if (originOptions.fromUri != null) { + installer.installUsingUri(originOptions.fromUri); + } + else if (originOptions.launcherFile != null) { + installer.installUsingLocalFile(originOptions.launcherFile); + } + else { + installer.installUsingVersions( originOptions.versionOptions.minecraftVersion, originOptions.versionOptions.loaderVersion, originOptions.versionOptions.installerVersion ); } - else if (originOptions.fromUri != null) { - launcher = installer.installUsingUri(originOptions.fromUri); - } - else { - installer.installGivenLauncherFile(originOptions.launcherFile); - launcher = originOptions.launcherFile; - } - - log.debug("Fabric launcher installed/reused at {}", launcher); return ExitCode.OK; } diff --git a/src/main/java/me/itzg/helpers/fabric/LoaderResponseEntry.java b/src/main/java/me/itzg/helpers/fabric/LoaderResponseEntry.java index 92b35f44..c80776ec 100644 --- a/src/main/java/me/itzg/helpers/fabric/LoaderResponseEntry.java +++ b/src/main/java/me/itzg/helpers/fabric/LoaderResponseEntry.java @@ -5,11 +5,6 @@ @Data public class LoaderResponseEntry { - Loader loader; + private VersionEntry loader; - @Data - public static class Loader { - boolean stable; - String version; - } } diff --git a/src/main/java/me/itzg/helpers/fabric/LocalFile.java b/src/main/java/me/itzg/helpers/fabric/LocalFile.java index 5d985a60..28cb3e6b 100644 --- a/src/main/java/me/itzg/helpers/fabric/LocalFile.java +++ b/src/main/java/me/itzg/helpers/fabric/LocalFile.java @@ -12,6 +12,4 @@ @EqualsAndHashCode(callSuper = false) @ToString public class LocalFile extends Origin { - - String checksum; } diff --git a/src/main/java/me/itzg/helpers/fabric/Origin.java b/src/main/java/me/itzg/helpers/fabric/Origin.java index 890f04b3..51f5a707 100644 --- a/src/main/java/me/itzg/helpers/fabric/Origin.java +++ b/src/main/java/me/itzg/helpers/fabric/Origin.java @@ -8,7 +8,6 @@ @JsonTypeInfo(use = Id.NAME) @JsonSubTypes({ @Type(value = Versions.class, name = "versions"), - @Type(value = LocalFile.class, name = "file"), @Type(value = RemoteFile.class, name = "remote") }) public abstract class Origin { diff --git a/src/main/java/me/itzg/helpers/fabric/InstallerResponseEntry.java b/src/main/java/me/itzg/helpers/fabric/VersionEntry.java similarity index 72% rename from src/main/java/me/itzg/helpers/fabric/InstallerResponseEntry.java rename to src/main/java/me/itzg/helpers/fabric/VersionEntry.java index ec34fe98..ceede721 100644 --- a/src/main/java/me/itzg/helpers/fabric/InstallerResponseEntry.java +++ b/src/main/java/me/itzg/helpers/fabric/VersionEntry.java @@ -3,7 +3,7 @@ import lombok.Data; @Data -public class InstallerResponseEntry { - boolean stable; +public class VersionEntry { String version; + boolean stable; } diff --git a/src/main/java/me/itzg/helpers/fabric/Versions.java b/src/main/java/me/itzg/helpers/fabric/Versions.java index ec3b4280..fb8da11a 100644 --- a/src/main/java/me/itzg/helpers/fabric/Versions.java +++ b/src/main/java/me/itzg/helpers/fabric/Versions.java @@ -13,8 +13,9 @@ @ToString public class Versions extends Origin { - String game; - String loader; - String installer; + private String game; + private String loader; + @EqualsAndHashCode.Exclude + private String installer; } diff --git a/src/main/java/me/itzg/helpers/files/IoStreams.java b/src/main/java/me/itzg/helpers/files/IoStreams.java index 0aa51fc9..856d4249 100644 --- a/src/main/java/me/itzg/helpers/files/IoStreams.java +++ b/src/main/java/me/itzg/helpers/files/IoStreams.java @@ -2,36 +2,32 @@ import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; +import org.jetbrains.annotations.Nullable; /** * Since Java 8 doesn't have InputStream.transferTo */ public class IoStreams { - public static void transfer(InputStream in, OutputStream out) throws IOException { - final byte[] buf = new byte[8192]; - int length; - while ((length = in.read(buf)) != -1) { - out.write(buf, 0, length); - } - } - @FunctionalInterface public interface EntryReader { T read(InputStream in) throws IOException; } - public static T readFileFromZip(Path zipFile, String entryName, EntryReader reader) throws IOException { + /** + * @return the result of entryReader or null if entry not present + */ + @Nullable + public static T readFileFromZip(Path zipFile, String entryName, EntryReader entryReader) throws IOException { try (ZipInputStream zipIn = new ZipInputStream(Files.newInputStream(zipFile))) { ZipEntry entry; while ((entry = zipIn.getNextEntry()) != null) { if (entry.getName().equals(entryName)) { - return reader.read(zipIn); + return entryReader.read(zipIn); } } } diff --git a/src/main/java/me/itzg/helpers/files/Manifests.java b/src/main/java/me/itzg/helpers/files/Manifests.java index 77c69cb9..afd2e9ac 100644 --- a/src/main/java/me/itzg/helpers/files/Manifests.java +++ b/src/main/java/me/itzg/helpers/files/Manifests.java @@ -6,8 +6,10 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.function.Consumer; import java.util.stream.Collectors; +import java.util.stream.Stream; import me.itzg.helpers.json.ObjectMappers; import org.slf4j.Logger; @@ -64,8 +66,26 @@ public static String relativize(Path basePath, Path path) { return basePath.relativize(path).toString(); } + /** + * @param paths paths to process where nulls are skipped + */ public static List relativizeAll(Path basePath, Collection paths) { - return paths.stream() + return relativizeAll(basePath, paths.stream()); + } + + /** + * @param paths paths to process where nulls are skipped + */ + public static List relativizeAll(Path basePath, Path... paths) { + return relativizeAll(basePath, Stream.of(paths)); + } + + /** + * @param pathStream paths to process where nulls are skipped + */ + public static List relativizeAll(Path basePath, Stream pathStream) { + return pathStream + .filter(Objects::nonNull) .map(p -> basePath.relativize(p) .toString() diff --git a/src/main/java/me/itzg/helpers/files/ResultsFileWriter.java b/src/main/java/me/itzg/helpers/files/ResultsFileWriter.java index 29615e00..b0ecf370 100644 --- a/src/main/java/me/itzg/helpers/files/ResultsFileWriter.java +++ b/src/main/java/me/itzg/helpers/files/ResultsFileWriter.java @@ -48,4 +48,14 @@ public void close() throws IOException { public ResultsFileWriter writeServer(Path serverJar) throws IOException { return write("SERVER", serverJar.toString()); } + + @SuppressWarnings("UnusedReturnValue") + public ResultsFileWriter writeVersion(String version) throws IOException { + return write("VERSION", version); + } + + @SuppressWarnings("UnusedReturnValue") + public ResultsFileWriter writeType(String type) throws IOException { + return write("TYPE", type); + } } diff --git a/src/main/java/me/itzg/helpers/forge/ForgeInstaller.java b/src/main/java/me/itzg/helpers/forge/ForgeInstaller.java index dfc5f0c3..099cad27 100644 --- a/src/main/java/me/itzg/helpers/forge/ForgeInstaller.java +++ b/src/main/java/me/itzg/helpers/forge/ForgeInstaller.java @@ -1,20 +1,9 @@ package me.itzg.helpers.forge; +import static me.itzg.helpers.http.Fetch.fetch; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.extern.slf4j.Slf4j; -import me.itzg.helpers.errors.GenericException; -import me.itzg.helpers.errors.InvalidParameterException; -import me.itzg.helpers.files.IoStreams; -import me.itzg.helpers.files.Manifests; -import me.itzg.helpers.files.ResultsFileWriter; -import me.itzg.helpers.forge.model.PromotionsSlim; -import me.itzg.helpers.http.FailedRequestException; -import me.itzg.helpers.http.Uris; -import me.itzg.helpers.json.ObjectMappers; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -32,8 +21,20 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; - -import static me.itzg.helpers.http.Fetch.fetch; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import me.itzg.helpers.errors.GenericException; +import me.itzg.helpers.errors.InvalidParameterException; +import me.itzg.helpers.files.IoStreams; +import me.itzg.helpers.files.Manifests; +import me.itzg.helpers.files.ResultsFileWriter; +import me.itzg.helpers.forge.model.PromotionsSlim; +import me.itzg.helpers.http.FailedRequestException; +import me.itzg.helpers.http.Uris; +import me.itzg.helpers.json.ObjectMappers; +import org.jetbrains.annotations.Nullable; @Slf4j public class ForgeInstaller { @@ -58,7 +59,8 @@ private static class VersionPair { * @param forgeInstaller when non-null, specifies a provided installer to use */ public void install(String minecraftVersion, String forgeVersion, - Path outputDir, Path resultsFile, + @NonNull Path outputDir, + @Nullable Path resultsFile, boolean forceReinstall, Path forgeInstaller ) { @@ -81,7 +83,7 @@ public void install(String minecraftVersion, String forgeVersion, try { resolvedForgeVersion = resolveForgeVersion(resolvedMinecraftVersion, forgeVersion, promotionsSlim); } catch (IOException e) { - throw new RuntimeException("Failed to resolve forge version", e); + throw new GenericException("Failed to resolve forge version", e); } } else { @@ -145,7 +147,10 @@ else if ( if (resultsFile != null && (newManifest != null || prevManifest != null)) { try { - populateResultsFile(resultsFile, (newManifest != null ? newManifest : prevManifest).getServerEntry()); + populateResultsFile( + resultsFile, (newManifest != null ? newManifest : prevManifest).getServerEntry(), + resolvedMinecraftVersion + ); } catch (IOException e) { throw new RuntimeException("Failed to populate results file", e); } @@ -245,12 +250,14 @@ private ForgeManifest installUsingExisting(String minecraftVersion, String forge .build(); } - private void populateResultsFile(Path resultsFile, String serverEntry) throws IOException { + private void populateResultsFile(Path resultsFile, String serverEntry, String minecraftVersion) throws IOException { log.debug("Populating results file {}", resultsFile); try (ResultsFileWriter results = new ResultsFileWriter(resultsFile)) { results.write("SERVER", serverEntry); results.write("FAMILY", "FORGE"); + results.writeVersion(minecraftVersion); + results.writeType("FORGE"); } } @@ -450,7 +457,7 @@ private static PromotionsSlim loadPromotions() { } private String resolveMinecraftVersion(String minecraftVersion, PromotionsSlim promotionsSlim) { - if (minecraftVersion.equalsIgnoreCase(LATEST)) { + if (minecraftVersion == null || minecraftVersion.equalsIgnoreCase(LATEST)) { return promotionsSlim.getPromos().entrySet().stream() .map(ForgeInstaller::parsePromoEntry) // pick off the last entry, where order is significant since JSON parsing retains ordering diff --git a/src/main/java/me/itzg/helpers/http/OutputToDirectoryFetchBuilder.java b/src/main/java/me/itzg/helpers/http/OutputToDirectoryFetchBuilder.java index 58ba7135..8ea464dd 100644 --- a/src/main/java/me/itzg/helpers/http/OutputToDirectoryFetchBuilder.java +++ b/src/main/java/me/itzg/helpers/http/OutputToDirectoryFetchBuilder.java @@ -92,6 +92,7 @@ public Mono assemble() { )); }) .publishOn(Schedulers.boundedElastic()) + .checkpoint("Fetch HEAD of requested file") .flatMap(fileToDownload -> assembleFileDownload(client, fileToDownload) ) @@ -172,7 +173,8 @@ private Mono assembleFileDownload(HttpClient client, FileToDownload fileTo return Mono.error(e); } }); - }); + }) + .checkpoint("Fetching file into directory"); } private String extractFilename(HttpClientResponse resp) { diff --git a/src/main/java/me/itzg/helpers/http/UriBuilder.java b/src/main/java/me/itzg/helpers/http/UriBuilder.java index 5ede83cc..47ce2547 100644 --- a/src/main/java/me/itzg/helpers/http/UriBuilder.java +++ b/src/main/java/me/itzg/helpers/http/UriBuilder.java @@ -1,9 +1,11 @@ package me.itzg.helpers.http; import java.net.URI; +import lombok.Getter; public class UriBuilder { + @Getter private final String baseUrl; protected UriBuilder(String baseUrl) { diff --git a/src/main/java/me/itzg/helpers/modrinth/InstallModrinthModpackCommand.java b/src/main/java/me/itzg/helpers/modrinth/InstallModrinthModpackCommand.java index 2ab848c3..b07e3c8f 100644 --- a/src/main/java/me/itzg/helpers/modrinth/InstallModrinthModpackCommand.java +++ b/src/main/java/me/itzg/helpers/modrinth/InstallModrinthModpackCommand.java @@ -39,6 +39,7 @@ import me.itzg.helpers.modrinth.model.VersionType; import me.itzg.helpers.quilt.QuiltInstaller; import org.jetbrains.annotations.Blocking; +import org.jetbrains.annotations.NotNull; import picocli.CommandLine; import picocli.CommandLine.ExitCode; import picocli.CommandLine.Option; @@ -148,39 +149,54 @@ private ModrinthModpackManifest processModpack(ProjectRef projectRef, ModrinthMo project, projectRef, loader != null ? loader.asLoader() : null, gameVersion, defaultVersionType ) .switchIfEmpty(Mono.defer(() -> Mono.error(new InvalidParameterException( - "Unable to find version given " + projectRef)))) - .doOnNext(version -> log.debug("Resolved version={} from projectRef={}", version, projectRef)) + "Unable to find version given " + projectRef))) + ) + .doOnNext(version -> log.debug("Resolved version={} from projectRef={}", version.getVersionNumber(), projectRef)) .publishOn(Schedulers.boundedElastic()) // since next item does I/O .filter(version -> needsInstall(prevManifest, project, version)) - .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 -> - processModpackZip(apiClient, zipPath, project, version) - .publishOn(Schedulers.boundedElastic()) - .doOnTerminate(() -> { - try { - Files.delete(zipPath); - } catch (IOException e) { - throw new RuntimeException(e); - } - }) - ); - }) + .flatMap(version -> processVersion(apiClient, project, version)) + .switchIfEmpty(Mono.defer(() -> { + try { + applyModLoader(prevManifest.getDependencies()); + } catch (IOException e) { + return Mono.error(new GenericException("Failed to re-apply mod loader", e)); + } + return Mono.just(prevManifest); + })) ) .block(); } } + @NotNull + private Mono processVersion(ModrinthApiClient apiClient, Project project, + Version 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 -> + processModpackZip(apiClient, zipPath, project, version) + .publishOn(Schedulers.boundedElastic()) + .doOnTerminate(() -> { + try { + Files.delete(zipPath); + } catch (IOException e) { + throw new RuntimeException(e); + } + }) + ); + } + @Blocking private boolean needsInstall(ModrinthModpackManifest prevManifest, Project project, Version version) { if (prevManifest != null) { if (prevManifest.getProjectSlug().equals(project.getSlug()) && prevManifest.getVersionId().equals(version.getId()) + && prevManifest.getDependencies() != null && Manifests.allFilesPresent(outputDirectory, prevManifest) ) { if (forceSynchronize) { @@ -230,33 +246,23 @@ private Mono processModpackZip(ModrinthApiClient apiCli .flatMap(Function.identity()) .collect(Collectors.toList()) ) - .handle((paths, sink) -> { - final String minecraftVersion; + .flatMap(paths -> { try { - minecraftVersion = applyModloader(modpackIndex.getDependencies()); + applyModLoader(modpackIndex.getDependencies()); } catch (IOException e) { - sink.error(new GenericException("Failed to apply mod loader", e)); - return; - } - - if (resultsFile != null) { - try (ResultsFileWriter resultsFileWriter = new ResultsFileWriter(resultsFile, true)) { - resultsFileWriter.write("VERSION", minecraftVersion); - } catch (IOException e) { - sink.error(new GenericException("Failed to write results file", e)); - return; - } + return Mono.error(new GenericException("Failed to apply mod loader", e)); } - sink.next(ModrinthModpackManifest.builder() + return Mono.just(ModrinthModpackManifest.builder() .files(Manifests.relativizeAll(outputDirectory, paths)) .projectSlug(project.getSlug()) .versionId(version.getId()) + .dependencies(modpackIndex.getDependencies()) .build()); }); } - private String applyModloader(Map dependencies) throws IOException { + private void applyModLoader(Map dependencies) throws IOException { log.debug("Applying mod loader from dependencies={}", dependencies); final String minecraftVersion = dependencies.get(DependencyId.minecraft); @@ -274,18 +280,19 @@ private String applyModloader(Map dependencies) throws IOE forceModloaderReinstall, null ); - return minecraftVersion; + return; } final String fabricVersion = dependencies.get(DependencyId.fabricLoader); if (fabricVersion != null) { - new FabricLauncherInstaller(outputDirectory, resultsFile) + new FabricLauncherInstaller(outputDirectory) + .setResultsFile(resultsFile) .installUsingVersions( minecraftVersion, fabricVersion, null ); - return minecraftVersion; + return; } final String quiltVersion = dependencies.get(DependencyId.quiltLoader); @@ -301,7 +308,6 @@ private String applyModloader(Map dependencies) throws IOE } } - return minecraftVersion; } @SuppressWarnings("SameParameterValue") diff --git a/src/main/java/me/itzg/helpers/modrinth/ModrinthCommand.java b/src/main/java/me/itzg/helpers/modrinth/ModrinthCommand.java index c4844f36..d81d87f3 100644 --- a/src/main/java/me/itzg/helpers/modrinth/ModrinthCommand.java +++ b/src/main/java/me/itzg/helpers/modrinth/ModrinthCommand.java @@ -1,6 +1,23 @@ package me.itzg.helpers.modrinth; +import static me.itzg.helpers.McImageHelper.OPTION_SPLIT_COMMAS; +import static me.itzg.helpers.http.Fetch.fetch; + import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; +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; @@ -19,24 +36,6 @@ import picocli.CommandLine.ExitCode; import picocli.CommandLine.Option; -import java.io.IOException; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.Callable; -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.McImageHelper.OPTION_SPLIT_COMMAS; -import static me.itzg.helpers.http.Fetch.fetch; - @Command(name = "modrinth", description = "Automates downloading of modrinth resources") @Slf4j public class ModrinthCommand implements Callable { @@ -145,7 +144,7 @@ private Stream expandDependencies(Version version) { depVersion = getVersion(dep.getVersionId()); } if (depVersion != null) { - log.debug("Resolved version={} for dep={}", depVersion, dep); + log.debug("Resolved version={} for dep={}", depVersion.getVersionNumber(), dep); return Stream.concat( Stream.of(depVersion), expandDependencies(depVersion) diff --git a/src/main/java/me/itzg/helpers/modrinth/ModrinthModpackManifest.java b/src/main/java/me/itzg/helpers/modrinth/ModrinthModpackManifest.java index 0abf08a8..d7e83101 100644 --- a/src/main/java/me/itzg/helpers/modrinth/ModrinthModpackManifest.java +++ b/src/main/java/me/itzg/helpers/modrinth/ModrinthModpackManifest.java @@ -1,9 +1,11 @@ package me.itzg.helpers.modrinth; +import java.util.Map; import lombok.Getter; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; import me.itzg.helpers.files.BaseManifest; +import me.itzg.helpers.modrinth.model.DependencyId; @SuperBuilder @Getter @@ -14,5 +16,6 @@ public class ModrinthModpackManifest extends BaseManifest { private String projectSlug; private String versionId; + private Map dependencies; } diff --git a/src/main/java/me/itzg/helpers/mvn/MavenRepoApi.java b/src/main/java/me/itzg/helpers/mvn/MavenRepoApi.java index debb830b..2b034bc0 100644 --- a/src/main/java/me/itzg/helpers/mvn/MavenRepoApi.java +++ b/src/main/java/me/itzg/helpers/mvn/MavenRepoApi.java @@ -41,10 +41,10 @@ public Mono download(Path outputDirectory, String groupId, String artifact final String groupPath = groupId.replace('.', '/'); Mono resolvedVersionMono; - if (version.equals("release")) { + if (version == null || version.equalsIgnoreCase("release")) { resolvedVersionMono = fetchMetadata(groupId, artifactId) .map(mavenMetadata -> mavenMetadata.getVersioning().getRelease()); - } else if (version.equals("latest")) { + } else if (version.equalsIgnoreCase("latest")) { resolvedVersionMono = fetchMetadata(groupId, artifactId) .map(mavenMetadata -> mavenMetadata.getVersioning().getLatest()); } else { diff --git a/src/main/java/me/itzg/helpers/paper/InstallPaperCommand.java b/src/main/java/me/itzg/helpers/paper/InstallPaperCommand.java index a9c93da9..5944d1c8 100644 --- a/src/main/java/me/itzg/helpers/paper/InstallPaperCommand.java +++ b/src/main/java/me/itzg/helpers/paper/InstallPaperCommand.java @@ -1,6 +1,7 @@ package me.itzg.helpers.paper; import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; +import java.io.IOException; import java.net.URI; import java.nio.file.Path; import java.util.Collections; @@ -11,12 +12,15 @@ import lombok.extern.slf4j.Slf4j; import me.itzg.helpers.errors.GenericException; import me.itzg.helpers.errors.InvalidParameterException; +import me.itzg.helpers.files.IoStreams; import me.itzg.helpers.files.ManifestException; 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.http.SharedFetchArgs; +import me.itzg.helpers.json.ObjectMappers; +import me.itzg.helpers.paper.model.VersionMeta; import me.itzg.helpers.sync.MultiCopyManifest; import picocli.CommandLine; import picocli.CommandLine.ArgGroup; @@ -26,6 +30,7 @@ import picocli.CommandLine.ParameterException; import picocli.CommandLine.Spec; import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; @Command(name = "install-paper", description = "Installs selected PaperMC") @Slf4j @@ -88,6 +93,7 @@ private static class Result { final PaperManifest newManifest; final Path serverJar; + final String version; } @Override @@ -109,6 +115,8 @@ public Integer call() throws Exception { if (resultsFile != null) { try (ResultsFileWriter results = new ResultsFileWriter(resultsFile)) { results.writeServer(result.serverJar); + results.writeType("PAPER"); + results.writeVersion(result.version); } } @@ -122,24 +130,24 @@ private Result useCoordinates(PaperDownloadsClient client, String project, Strin return resolveVersion(client, project, version) .flatMap(v -> resolveBuild(client, project, v, build) .flatMap(b -> { - log.info("Resolved {} to version {} build {}", project, v, b); - - return client.download(project, v, b, outputDirectory, Fetch.loggingDownloadStatusHandler(log)) - .map(serverJar -> - Result.builder() - .newManifest( - PaperManifest.builder() - .project(project) - .minecraftVersion(v) - .build(b) - .files(Collections.singleton(Manifests.relativize(outputDirectory, serverJar))) - .build() - ) - .serverJar(serverJar) - .build() - ); - } - ) + log.info("Resolved {} to version {} build {}", project, v, b); + + return client.download(project, v, b, outputDirectory, Fetch.loggingDownloadStatusHandler(log)) + .map(serverJar -> + Result.builder() + .newManifest( + PaperManifest.builder() + .project(project) + .minecraftVersion(v) + .build(b) + .files(Collections.singleton(Manifests.relativize(outputDirectory, serverJar))) + .build() + ) + .serverJar(serverJar) + .version(v) + .build() + ); + }) ) .block(); } @@ -152,8 +160,19 @@ private Result downloadCustom(URI downloadUrl) { .skipUpToDate(true) .handleStatus(Fetch.loggingDownloadStatusHandler(log)) .assemble() - .map(serverJar -> - Result.builder() + .publishOn(Schedulers.boundedElastic()) + .flatMap(serverJar -> { + final String version; + try { + version = extractVersionFromJar(serverJar); + + if (version == null) { + return Mono.error(new GenericException("Version metadata was not available from custom server jar")); + } + } catch (IOException e) { + return Mono.error(new GenericException("Failed to extract version from custom server jar", e)); + } + return Mono.just(Result.builder() .serverJar(serverJar) .newManifest( PaperManifest.builder() @@ -161,12 +180,24 @@ private Result downloadCustom(URI downloadUrl) { .files(Collections.singleton(Manifests.relativize(outputDirectory, serverJar))) .build() ) - .build() - ) + .version(version) + .build()); + }) .block(); } } + private String extractVersionFromJar(Path serverJar) throws IOException { + final VersionMeta versionMeta = IoStreams.readFileFromZip(serverJar, "version.json", in -> + ObjectMappers.defaultMapper().readValue(in, VersionMeta.class) + ); + if (versionMeta == null) { + return null; + } + + return versionMeta.getId(); + } + private PaperManifest loadOldManifest() { try { return Manifests.load(outputDirectory, PaperManifest.ID, PaperManifest.class); diff --git a/src/main/java/me/itzg/helpers/paper/model/VersionMeta.java b/src/main/java/me/itzg/helpers/paper/model/VersionMeta.java new file mode 100644 index 00000000..94266c38 --- /dev/null +++ b/src/main/java/me/itzg/helpers/paper/model/VersionMeta.java @@ -0,0 +1,14 @@ +package me.itzg.helpers.paper.model; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Data; + +/** + * Represents version.json content from server jar + */ +@Data +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class VersionMeta { + String id; +} diff --git a/src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java b/src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java index 8e180fda..20e5144e 100644 --- a/src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java +++ b/src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java @@ -3,6 +3,7 @@ import static me.itzg.helpers.errors.Validators.DESCRIPTION_MINECRAFT_VERSION; import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; +import java.io.IOException; import java.net.URI; import java.nio.file.Path; import java.util.Collections; @@ -12,12 +13,15 @@ import me.itzg.helpers.errors.GenericException; import me.itzg.helpers.errors.InvalidParameterException; import me.itzg.helpers.errors.Validators; +import me.itzg.helpers.files.IoStreams; import me.itzg.helpers.files.ManifestException; 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.http.SharedFetchArgs; +import me.itzg.helpers.json.ObjectMappers; +import me.itzg.helpers.purpur.model.VersionMeta; import me.itzg.helpers.sync.MultiCopyManifest; import picocli.CommandLine; import picocli.CommandLine.ArgGroup; @@ -26,6 +30,7 @@ import picocli.CommandLine.Option; import picocli.CommandLine.Spec; import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; @Command(name = "install-purpur", description = "Downloads latest or selected version of Purpur") @Slf4j @@ -83,6 +88,7 @@ private static class Result { final PurpurManifest newManifest; final Path serverJar; + final String version; } @Override @@ -102,8 +108,10 @@ public Integer call() throws Exception { } if (resultsFile != null) { - try (ResultsFileWriter results = new ResultsFileWriter(resultsFile)) { - results.writeServer(result.serverJar); + try (ResultsFileWriter resultsWriter = new ResultsFileWriter(resultsFile)) { + resultsWriter.writeServer(result.serverJar); + resultsWriter.writeType("PURPUR"); + resultsWriter.writeVersion(result.version); } } @@ -130,6 +138,7 @@ private Result useCoordinates(PurpurDownloadsClient client, String version, Stri .build() ) .serverJar(serverJar) + .version(v) .build() ); } @@ -146,8 +155,21 @@ private Result downloadCustom(URI downloadUrl) { .skipUpToDate(true) .handleStatus(Fetch.loggingDownloadStatusHandler(log)) .assemble() - .map(serverJar -> - Result.builder() + .publishOn(Schedulers.boundedElastic()) + .flatMap(serverJar -> { + @SuppressWarnings("DuplicatedCode") // same as Paper + final String version; + try { + version = extractVersionFromJar(serverJar); + + if (version == null) { + return Mono.error(new GenericException("Version metadata was not available from custom server jar")); + } + } catch (IOException e) { + return Mono.error(new GenericException("Failed to extract version from custom server jar", e)); + } + + return Mono.just(Result.builder() .serverJar(serverJar) .newManifest( PurpurManifest.builder() @@ -155,12 +177,24 @@ private Result downloadCustom(URI downloadUrl) { .files(Collections.singleton(Manifests.relativize(outputDirectory, serverJar))) .build() ) - .build() - ) + .version(version) + .build()); + }) .block(); } } + private String extractVersionFromJar(Path serverJar) throws IOException { + final VersionMeta versionMeta = IoStreams.readFileFromZip(serverJar, "version.json", in -> + ObjectMappers.defaultMapper().readValue(in, VersionMeta.class) + ); + if (versionMeta == null) { + return null; + } + + return versionMeta.getId(); + } + private PurpurManifest loadOldManifest() { try { return Manifests.load(outputDirectory, PurpurManifest.ID, PurpurManifest.class); diff --git a/src/main/java/me/itzg/helpers/purpur/model/VersionMeta.java b/src/main/java/me/itzg/helpers/purpur/model/VersionMeta.java new file mode 100644 index 00000000..3038ea2b --- /dev/null +++ b/src/main/java/me/itzg/helpers/purpur/model/VersionMeta.java @@ -0,0 +1,14 @@ +package me.itzg.helpers.purpur.model; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Data; + +/** + * Represents version.json content from server jar + */ +@Data +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class VersionMeta { + String id; +} diff --git a/src/main/java/me/itzg/helpers/quilt/InstallQuiltCommand.java b/src/main/java/me/itzg/helpers/quilt/InstallQuiltCommand.java index 4e3e9689..b8a96b36 100644 --- a/src/main/java/me/itzg/helpers/quilt/InstallQuiltCommand.java +++ b/src/main/java/me/itzg/helpers/quilt/InstallQuiltCommand.java @@ -13,6 +13,9 @@ @Command(name = "install-quilt", description = "Installs Quilt mod loader") public class InstallQuiltCommand implements Callable { + @Option(names = {"-h", "--help"}, usageHelp = true) + boolean showHelp; + @Option(names = "--minecraft-version", defaultValue = "latest", required = true, paramLabel = "VERSION", description = "'latest', 'snapshot', or specific version" ) @@ -51,7 +54,7 @@ static class Inputs { @Option(names = "--output-directory", defaultValue = ".", paramLabel = "DIR") Path outputDirectory; - @Option(names = "--force-reinstall") + @Option(names = "--force-reinstall", defaultValue = "${env:QUILT_FORCE_REINSTALL:-false}") boolean forceReinstall; @Override diff --git a/src/main/java/me/itzg/helpers/quilt/QuiltInstaller.java b/src/main/java/me/itzg/helpers/quilt/QuiltInstaller.java index 9323b87c..18145946 100644 --- a/src/main/java/me/itzg/helpers/quilt/QuiltInstaller.java +++ b/src/main/java/me/itzg/helpers/quilt/QuiltInstaller.java @@ -11,7 +11,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; -import java.util.Collections; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import me.itzg.helpers.errors.GenericException; @@ -68,7 +67,8 @@ public void installWithVersion(String installerVersion, String loaderVersion) { ) .flatMap(resolvedLoaderVersion -> mavenRepoApi.download(outputDir, QUILT_GROUP_ID, INSTALLER_ARTIFACT, - installerVersion != null ? installerVersion : "release", "jar", null + installerVersion, + "jar", null ) .switchIfEmpty( Mono.defer(() -> Mono.error(new GenericException("Unable to obtain Quilt installer")))) @@ -195,9 +195,11 @@ private QuiltManifest runInstaller(String minecraftVersion, Path installerPath, } if (resultsFile != null) { - try (ResultsFileWriter resultsFileWriter = new ResultsFileWriter(resultsFile, false)) { - resultsFileWriter.writeServer(resolvedLauncher); - resultsFileWriter.write("FAMILY", "FABRIC"); + try (ResultsFileWriter resultsWriter = new ResultsFileWriter(resultsFile, false)) { + resultsWriter.writeServer(resolvedLauncher); + resultsWriter.write("FAMILY", "FABRIC"); + resultsWriter.writeType("QUILT"); + resultsWriter.writeVersion(minecraftVersion); } catch (IOException e) { throw new GenericException("Failed to write results file", e); } @@ -206,12 +208,12 @@ private QuiltManifest runInstaller(String minecraftVersion, Path installerPath, return QuiltManifest.builder() .minecraftVersion(minecraftVersion) .loaderVersion(resolvedLoaderVersion) - .files(Manifests.relativizeAll(outputDir, Collections.singletonList(resolvedLauncher))) + .files(Manifests.relativizeAll(outputDir, resolvedLauncher)) .build(); } private Mono resolveLoaderVersion(String loaderVersion) { - if (loaderVersion != null) { + if (loaderVersion != null && !loaderVersion.equalsIgnoreCase("latest")) { return Mono.just(loaderVersion); } diff --git a/src/main/java/me/itzg/helpers/versions/MinecraftVersionsApi.java b/src/main/java/me/itzg/helpers/versions/MinecraftVersionsApi.java index e03b53dc..6656e43e 100644 --- a/src/main/java/me/itzg/helpers/versions/MinecraftVersionsApi.java +++ b/src/main/java/me/itzg/helpers/versions/MinecraftVersionsApi.java @@ -2,10 +2,13 @@ import java.net.URI; import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import me.itzg.helpers.errors.InvalidParameterException; import me.itzg.helpers.http.SharedFetch; import me.itzg.helpers.versions.VersionManifestV2.Version; import reactor.core.publisher.Mono; +@Slf4j public class MinecraftVersionsApi { @Setter @@ -29,7 +32,7 @@ public Mono resolve(String inputVersion) { .assemble() .flatMap(manifest -> { if (inputVersion == null - || inputVersion.equals("latest") + || inputVersion.equalsIgnoreCase("latest") || inputVersion.equalsIgnoreCase("release")) { return Mono.just(manifest.getLatest().getRelease()); } @@ -44,6 +47,8 @@ else if (inputVersion.equalsIgnoreCase("snapshot")) { .findFirst() ); } - }); + }) + .doOnNext(resolvedVersion -> log.debug("Resolved given Minecraft version {} to {}", inputVersion, resolvedVersion)) + .switchIfEmpty(Mono.error(() -> new InvalidParameterException(String.format("Minecraft version '%s' is not valid", inputVersion)))); } } diff --git a/src/test/java/me/itzg/helpers/fabric/FabricLauncherInstallerTest.java b/src/test/java/me/itzg/helpers/fabric/FabricLauncherInstallerTest.java index d7352d72..26fd4ce2 100644 --- a/src/test/java/me/itzg/helpers/fabric/FabricLauncherInstallerTest.java +++ b/src/test/java/me/itzg/helpers/fabric/FabricLauncherInstallerTest.java @@ -31,7 +31,6 @@ void testSaveManifest() { Versions.builder() .game("1.19.2") .loader("0.14.11") - .installer("0.11.1") .build() ) .files(Collections.singletonList("fabric-server-mc.1.19.2-loader.0.14.11-launcher.0.11.1.jar")) @@ -48,91 +47,74 @@ void testSaveManifest() { } @Test - void testInstallUsingVersions_onlyGameVersion(WireMockRuntimeInfo wmRuntimeInfo) throws IOException { + void testInstallUsingVersions_onlyGameVersion(WireMockRuntimeInfo wmRuntimeInfo) { final WireMock wm = wmRuntimeInfo.getWireMock(); wm.loadMappingsFrom("src/test/resources/fabric"); final Path resultsFile = tempDir.resolve("results.env"); final FabricLauncherInstaller installer = new FabricLauncherInstaller( - tempDir, resultsFile - ); + tempDir + ) + .setResultsFile(resultsFile); installer.setFabricMetaBaseUrl(wmRuntimeInfo.getHttpBaseUrl()); - final Path launcherPath = installer.installUsingVersions("1.19.3", null, null); + installer.installUsingVersions("1.19.3", null, null); - assertThat(launcherPath) - .isEqualTo(tempDir.resolve("fabric-server-mc.1.19.3-loader.0.14.12-launcher.0.11.1.jar")) + final Path expectedLauncherPath = tempDir.resolve("fabric-server-mc.1.19.3-loader.0.14.12-launcher.0.11.1.jar"); + assertThat(expectedLauncherPath) .isNotEmptyFile() .hasContent("fabric-server-mc.1.19.3-loader.0.14.12-launcher.0.11.1"); assertThat(resultsFile) .exists() - .hasContent("SERVER=\"" + launcherPath + "\"" + - "\nFAMILY=\"FABRIC\""); + .hasContent("SERVER=\"" + expectedLauncherPath + "\"" + + "\nFAMILY=\"FABRIC\"" + + "\nVERSION=\"1.19.3\"" + ); final Path expectedManifestFile = tempDir.resolve(".fabric-manifest.json"); assertThat(expectedManifestFile) .exists(); assertJson(expectedManifestFile.toFile()) - .at("/launcherPath").hasValue(launcherPath.toString()) + .at("/launcherPath").hasValue(expectedLauncherPath.toString()) .at("/origin/game").hasValue("1.19.3") .at("/origin/loader").hasValue("0.14.12") - .at("/origin/installer").hasValue("0.11.1") .at("/files").isArrayContaining("fabric-server-mc.1.19.3-loader.0.14.12-launcher.0.11.1.jar"); } - @Test - void testWithProvidedFile() throws IOException { - final Path resultsFile = tempDir.resolve("results.env"); - final FabricLauncherInstaller installer = new FabricLauncherInstaller(tempDir, resultsFile); - - final Path givenFile = Paths.get("src/test/resources/fabric/test-file.txt"); - installer.installGivenLauncherFile(givenFile); - - assertThat(resultsFile) - .exists() - .hasContent("SERVER=\"" + givenFile + "\"" - +"\nFAMILY=\"FABRIC\"" - ); - - final Path expectedManifestFile = tempDir.resolve(".fabric-manifest.json"); - assertThat(expectedManifestFile) - .exists(); - - assertJson(expectedManifestFile.toFile()) - .at("/launcherPath").isText(givenFile.toString()) - .at("/origin/@type").isText("file") - .at("/origin/checksum").isText("sha256:5c2d133f4e4263ee18630616a53579f561005bbe2777e59f298eaac05be0eaae") - .at("/files").isNull(); - } - @Test void testWithProvidedUri(WireMockRuntimeInfo wmRuntimeInfo) throws IOException { stubFor( - head(urlEqualTo("/fabric.jar")) + head(urlEqualTo("/fabric-launcher.jar")) .willReturn(aResponse().withStatus(200)) ); stubFor( - get(urlEqualTo("/fabric.jar")) + get(urlEqualTo("/fabric-launcher.jar")) .willReturn(aResponse() .withStatus(200) - .withBody("Just a test") + // can't use withBodyFile + .withBodyFile("fabric-empty-launcher.jar") ) ); - final FabricLauncherInstaller installer = new FabricLauncherInstaller(tempDir, null); - final URI loaderUri = URI.create(wmRuntimeInfo.getHttpBaseUrl() + "/fabric.jar"); + final Path expectedResultsPath = tempDir.resolve("results.env"); + final FabricLauncherInstaller installer = new FabricLauncherInstaller(tempDir) + .setResultsFile(expectedResultsPath); + final URI loaderUri = URI.create(wmRuntimeInfo.getHttpBaseUrl() + "/fabric-launcher.jar"); + + installer.installUsingUri(loaderUri); - // twice to ensure idempotent - for (int i = 0; i < 2; i++) { - installer.installUsingUri(loaderUri); + final Path expectedLauncherPath = tempDir.resolve("fabric-launcher.jar"); + assertThat(expectedLauncherPath) + .exists(); - assertThat(tempDir.resolve("fabric.jar")) - .exists() - .hasContent("Just a test"); + assertThat(expectedResultsPath) + .exists() + .hasContent("SERVER=\"" + expectedLauncherPath + "\"" + + "\nFAMILY=\"FABRIC\"" + + "\nVERSION=\"1.19.4\""); - } } @Test @@ -153,63 +135,77 @@ void testWithProvidedUri_contentDisposition(WireMockRuntimeInfo wmRuntimeInfo) t ) ); - final FabricLauncherInstaller installer = new FabricLauncherInstaller(tempDir, null); - final Path actualLauncherPath = installer.installUsingUri( + final FabricLauncherInstaller installer = new FabricLauncherInstaller(tempDir); + installer.installUsingUri( URI.create(wmRuntimeInfo.getHttpBaseUrl() + "/server") ); final Path expectedLauncherPath = tempDir.resolve("fabric-server-mc.1.19.3-loader.0.14.12-launcher.0.11.1.jar"); assertThat(expectedLauncherPath) .exists() - .isEqualTo(actualLauncherPath) .hasContent("testWithProvidedUri_contentDisposition"); } @Test - void testUpgradeFromVersionToVersion(WireMockRuntimeInfo wmRuntimeInfo) throws IOException { + void testWithLocalLauncherFile() throws IOException { + final Path expectedResultsPath = tempDir.resolve("results.env"); + final FabricLauncherInstaller installer = new FabricLauncherInstaller(tempDir) + .setResultsFile(expectedResultsPath); + + final Path launcherFile = Paths.get("src/test/resources/__files/fabric-empty-launcher.jar"); + installer.installUsingLocalFile( + launcherFile + ); + + assertThat(expectedResultsPath) + .exists() + .hasContent("SERVER=\"" + launcherFile + "\"" + + "\nFAMILY=\"FABRIC\"" + + "\nVERSION=\"1.19.4\""); + } + + @Test + void testUpgradeFromVersionToVersion(WireMockRuntimeInfo wmRuntimeInfo) { final WireMock wm = wmRuntimeInfo.getWireMock(); wm.loadMappingsFrom("src/test/resources/fabric"); final FabricLauncherInstaller installer = new FabricLauncherInstaller( - tempDir, null + tempDir ); installer.setFabricMetaBaseUrl(wmRuntimeInfo.getHttpBaseUrl()); - final Path launcherPath1192 = installer.installUsingVersions( + installer.installUsingVersions( "1.19.2", null, null ); - assertThat(launcherPath1192) - .isEqualTo(tempDir.resolve("fabric-server-mc.1.19.2-loader.0.14.12-launcher.0.11.1.jar")) + final Path expectedLauncher192 = tempDir.resolve("fabric-server-mc.1.19.2-loader.0.14.12-launcher.0.11.1.jar"); + assertThat(expectedLauncher192) .isNotEmptyFile() .hasContent("fabric-server-mc.1.19.2-loader.0.14.12-launcher.0.11.1"); // Now upgrade from 1.19.2 to 1.19.3 - final Path launcherPath1193 = installer.installUsingVersions( + installer.installUsingVersions( "1.19.3", null, null ); - assertThat(launcherPath1193) - .isEqualTo(tempDir.resolve("fabric-server-mc.1.19.3-loader.0.14.12-launcher.0.11.1.jar")) + final Path expectedLauncher193 = tempDir.resolve("fabric-server-mc.1.19.3-loader.0.14.12-launcher.0.11.1.jar"); + assertThat(expectedLauncher193) .isNotEmptyFile() .hasContent("fabric-server-mc.1.19.3-loader.0.14.12-launcher.0.11.1"); - assertThat(launcherPath1192) + assertThat(expectedLauncher192) .doesNotExist(); } @Test @EnabledIfSystemProperty(named = "testEnableManualTests", matches = "true", disabledReason = "For manual recording") - void forRecordingVersionDiscovery() throws IOException { + void forRecordingVersionDiscovery() { final Path resultsFile = tempDir.resolve("results.env"); - final FabricLauncherInstaller installer = new FabricLauncherInstaller(tempDir, resultsFile); + final FabricLauncherInstaller installer = new FabricLauncherInstaller(tempDir) + .setResultsFile(resultsFile); installer.setFabricMetaBaseUrl("http://localhost:8080"); - final Path installerPath = installer.installUsingVersions("1.19.3", null, null); - - assertThat(installerPath) - .exists() - .isNotEmptyFile(); + installer.installUsingVersions("1.19.3", null, null); } } \ No newline at end of file diff --git a/src/test/java/me/itzg/helpers/fabric/research.http b/src/test/java/me/itzg/helpers/fabric/research.http index adbbfafe..421fe442 100644 --- a/src/test/java/me/itzg/helpers/fabric/research.http +++ b/src/test/java/me/itzg/helpers/fabric/research.http @@ -4,11 +4,17 @@ GET https://meta.fabricmc.net/v2/versions/loader/1.19.2 ### GET https://meta.fabricmc.net/v2/versions/loader/1.19.3 +### +GET https://meta.fabricmc.net/v2/versions/loader/1.12.2 + ### GET https://meta.fabricmc.net/v2/versions/loader/{{game_version}}/{{loader_version}}/server/json ### GET https://meta.fabricmc.net/v2/versions/installer +### +GET https://meta.fabricmc.net/v2/versions/game + ### GET https://meta.fabricmc.net/v2/versions/loader/{{game_version}}/{{loader_version}}/{{installer_version}}/server/jar diff --git a/src/test/resources/__files/fabric-empty-launcher.jar b/src/test/resources/__files/fabric-empty-launcher.jar new file mode 100644 index 0000000000000000000000000000000000000000..dc5e4e1704510d609380cc5d5d68d15d43967b09 GIT binary patch literal 562 zcmWIWW@h1HVBp|jSml`*#sCCNAOZ+Df!NnI#8KDN&rP41Apk{T*|Gww0-%D)KrDi+ z(AUw=)6F$FM9FpM6VN74TqhGbT(acN8cE9}s>Fxc; zw=ekM_3WohR?XB|Tt0?p9$(9xVDU@)nR4~=S!ESrd7Eb>zh6>+`}~XrA~%j~Q1HH2 zX0qDzMB-nD#feWmJU~sT=h;DNz*8y5!2*d&2j7%b+puifEps+*+@Nf+cVP#-8 z5dWWyt{*jkk@af=4T9@O3T_OeU?G9*Od)Wz!kls%$iNmZtdMXC@MdKLDP;!2D?r+V H5yS%k Date: Sat, 17 Jun 2023 11:31:15 -0500 Subject: [PATCH 2/2] Fixed fabric tests --- .../me/itzg/helpers/fabric/FabricLauncherInstallerTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/me/itzg/helpers/fabric/FabricLauncherInstallerTest.java b/src/test/java/me/itzg/helpers/fabric/FabricLauncherInstallerTest.java index 26fd4ce2..077f0834 100644 --- a/src/test/java/me/itzg/helpers/fabric/FabricLauncherInstallerTest.java +++ b/src/test/java/me/itzg/helpers/fabric/FabricLauncherInstallerTest.java @@ -69,6 +69,7 @@ void testInstallUsingVersions_onlyGameVersion(WireMockRuntimeInfo wmRuntimeInfo) .exists() .hasContent("SERVER=\"" + expectedLauncherPath + "\"" + "\nFAMILY=\"FABRIC\"" + + "\nTYPE=\"FABRIC\"" + "\nVERSION=\"1.19.3\"" ); @@ -113,6 +114,7 @@ void testWithProvidedUri(WireMockRuntimeInfo wmRuntimeInfo) throws IOException { .exists() .hasContent("SERVER=\"" + expectedLauncherPath + "\"" + "\nFAMILY=\"FABRIC\"" + + "\nTYPE=\"FABRIC\"" + "\nVERSION=\"1.19.4\""); } @@ -161,6 +163,7 @@ void testWithLocalLauncherFile() throws IOException { .exists() .hasContent("SERVER=\"" + launcherFile + "\"" + "\nFAMILY=\"FABRIC\"" + + "\nTYPE=\"FABRIC\"" + "\nVERSION=\"1.19.4\""); }