diff --git a/src/main/java/net/neoforged/neoform/runtime/artifacts/ArtifactManager.java b/src/main/java/net/neoforged/neoform/runtime/artifacts/ArtifactManager.java index 76ab577..ed965a1 100644 --- a/src/main/java/net/neoforged/neoform/runtime/artifacts/ArtifactManager.java +++ b/src/main/java/net/neoforged/neoform/runtime/artifacts/ArtifactManager.java @@ -126,10 +126,28 @@ public Artifact get(MavenCoordinate mavenCoordinate) throws IOException { return externalArtifact; } + // Yet another special case: dynamic versions! + // Used in 1.12.1, for example. And yes, this will be very slow. + if (mavenCoordinate.isDynamicVersion()) { + var availableVersions = MavenMetadata.gatherVersions( + downloadManager, + repositoryBaseUrls, + mavenCoordinate.groupId(), + mavenCoordinate.artifactId() + ); + for (var availableVersion : availableVersions) { + if (mavenCoordinate.matchesVersion(availableVersion.version())) { + var concreteMavenCoordinate = mavenCoordinate.withVersion(availableVersion.version()); + return get(concreteMavenCoordinate, availableVersion.repositoryUrl()); + } + } + } + var finalLocation = artifactsCache.resolve(mavenCoordinate.toRelativeRepositoryPath()); // Special case: NeoForge reference libraries that are only available via the Mojang download server - if (mavenCoordinate.groupId().equals("com.mojang") && mavenCoordinate.artifactId().equals("logging")) { + if (mavenCoordinate.groupId().equals("com.mojang") && mavenCoordinate.artifactId().equals("logging") + || mavenCoordinate.groupId().equals("net.minecraft") && mavenCoordinate.artifactId().equals("launchwrapper")) { return get(mavenCoordinate, MINECRAFT_LIBRARIES_URI); } @@ -219,8 +237,8 @@ public Artifact downloadFromManifest(MinecraftVersionManifest versionManifest, S var downloadSpec = versionManifest.downloads().get(type); if (downloadSpec == null) { throw new IllegalArgumentException("Minecraft version manifest " + versionManifest.id() - + " does not declare a download for " + type + ". Available: " - + versionManifest.downloads().keySet()); + + " does not declare a download for " + type + ". Available: " + + versionManifest.downloads().keySet()); } var extension = FilenameUtil.getExtension(downloadSpec.uri().getPath()); @@ -261,11 +279,22 @@ public interface DownloadAction { private Artifact getFromExternalManifest(MavenCoordinate artifactCoordinate) { artifactCoordinate = normalizeExtension(artifactCoordinate); + // Try direct match first var artifact = externallyProvided.get(artifactCoordinate); if (artifact != null) { return artifact; } + // See if it's a dynamic version and match against all entries + if (artifactCoordinate.isDynamicVersion()) { + for (var entry : externallyProvided.entrySet()) { + if (artifactCoordinate.matchesVersion(entry.getKey().version()) + && entry.getKey().withVersion(artifactCoordinate.version()).equals(artifactCoordinate)) { + return entry.getValue(); + } + } + } + // Fall back to looking up a wildcard version for dependency replacement in includeBuild scenarios if (!"*".equals(artifactCoordinate.version())) { artifact = externallyProvided.get(artifactCoordinate.withVersion("*")); diff --git a/src/main/java/net/neoforged/neoform/runtime/artifacts/MavenMetadata.java b/src/main/java/net/neoforged/neoform/runtime/artifacts/MavenMetadata.java new file mode 100644 index 0000000..b42979e --- /dev/null +++ b/src/main/java/net/neoforged/neoform/runtime/artifacts/MavenMetadata.java @@ -0,0 +1,92 @@ +package net.neoforged.neoform.runtime.artifacts; + +import net.neoforged.neoform.runtime.downloads.DownloadManager; +import net.neoforged.neoform.runtime.utils.Logger; +import org.w3c.dom.Element; + +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +/** + * Support class for querying maven metadata from a remote repository. + * The format is documented here: https://maven.apache.org/repositories/metadata.html + * We only deal with A-level metadata since we're interested in listing the versions of + * a specific artifact. + */ +final class MavenMetadata { + private static final Logger LOG = Logger.create(); + + private MavenMetadata() { + } + + static List gatherVersions(DownloadManager downloadManager, + List repositoryBaseUrls, + String groupId, + String artifactId) throws IOException { + var versions = new ArrayList(); + for (var repositoryBaseUrl : repositoryBaseUrls) { + versions.addAll(gatherVersions(downloadManager, repositoryBaseUrl, groupId, artifactId)); + } + return versions; + } + + static List gatherVersions(DownloadManager downloadManager, + URI repositoryBaseUrl, + String groupId, + String artifactId) throws IOException { + var metadataUri = repositoryBaseUrl.toString(); + if (!metadataUri.endsWith("/")) { + metadataUri += "/"; + } + metadataUri += groupId.replace('.', '/') + "/" + artifactId + "/maven-metadata.xml"; + + byte[] metadataContent; + + var tempFile = Files.createTempFile("maven-metadata", ".xml"); + try { + Files.deleteIfExists(tempFile); // The downloader should assume it does not exist yet + downloadManager.download(URI.create(metadataUri), tempFile); + metadataContent = Files.readAllBytes(tempFile); + } catch (FileNotFoundException fnf) { + return List.of(); // Repository doesn't have artifact + } finally { + Files.deleteIfExists(tempFile); + } + + try (var in = new ByteArrayInputStream(metadataContent)) { + var result = new ArrayList(); + var documentBuilder = DocumentBuilderFactory.newDefaultInstance().newDocumentBuilder(); + var document = documentBuilder.parse(in).getDocumentElement(); + var nodes = document.getChildNodes(); + for (var i = 0; i < nodes.getLength(); i++) { + if (nodes.item(i) instanceof Element versioningEl && "versioning".equals(versioningEl.getTagName())) { + for (var versions = versioningEl.getFirstChild(); versions != null; versions = versions.getNextSibling()) { + if (versions instanceof Element versionsEl && "versions".equals(versionsEl.getTagName())) { + for (var child = versionsEl.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child instanceof Element childEl && "version".equals(childEl.getTagName())) { + result.add(new AvailableVersion( + repositoryBaseUrl, + childEl.getTextContent().trim() + )); + } + } + } + } + } + } + return result; + } catch (Exception e) { + LOG.println("Failed to parse Maven metadata from " + metadataUri + ": " + e); + throw new RuntimeException(e); + } + } + + record AvailableVersion(URI repositoryUrl, String version) { + } +} diff --git a/src/main/java/net/neoforged/neoform/runtime/utils/MavenCoordinate.java b/src/main/java/net/neoforged/neoform/runtime/utils/MavenCoordinate.java index 25315b2..b6169f8 100644 --- a/src/main/java/net/neoforged/neoform/runtime/utils/MavenCoordinate.java +++ b/src/main/java/net/neoforged/neoform/runtime/utils/MavenCoordinate.java @@ -138,4 +138,19 @@ public MavenCoordinate withVersion(String version) { version ); } + + public boolean isDynamicVersion() { + // We only support extremely simple cases right now. + return version.endsWith("+"); + } + + public boolean matchesVersion(String version) { + // "+" acts as a prefix match according to Gradle + // https://docs.gradle.org/current/userguide/dependency_versions.html#sec:single-version-declarations + if (this.version.endsWith("+")) { + return version.startsWith(this.version.substring(0, this.version.length() - 1)); + } else { + return this.version.equals(version); + } + } }