diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/ForgeRunsProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/ForgeRunsProvider.java index c44b4537..3cc9b6bb 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/ForgeRunsProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/ForgeRunsProvider.java @@ -42,6 +42,7 @@ import dev.architectury.loom.forge.UserdevConfig; import org.gradle.api.NamedDomainObjectContainer; import org.gradle.api.NamedDomainObjectSet; import org.gradle.api.Project; +import org.gradle.api.artifacts.Dependency; import org.jetbrains.annotations.Nullable; import net.fabricmc.loom.LoomGradleExtension; @@ -49,6 +50,7 @@ import net.fabricmc.loom.api.ModSettings; import net.fabricmc.loom.configuration.ide.RunConfigSettings; import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.DependencyDownloader; +import net.fabricmc.loom.util.Version; import net.fabricmc.loom.util.gradle.SourceSetHelper; import net.fabricmc.loom.util.gradle.SourceSetReference; @@ -128,8 +130,7 @@ public class ForgeRunsProvider { // Use a set-valued multimap for deduplicating paths. Multimap modClasses = MultimapBuilder.hashKeys().linkedHashSetValues().build(); NamedDomainObjectContainer mods = extension.getMods(); - // Forge 49+ bootstrap-dev uses ; as a separator, instead of File.pathSeparator - String separator = extension.getForgeProvider().getVersion().getMajorVersion() >= Constants.Forge.MIN_BOOTSTRAP_DEV_VERSION ? ";" : File.pathSeparator; + String separator = getSourceRootsSeparator(); if (runConfig != null && !runConfig.getMods().isEmpty()) { mods = runConfig.getMods(); @@ -186,4 +187,21 @@ public class ForgeRunsProvider { private Set minecraftClasspath() { return DependencyDownloader.resolveFiles(project, project.getConfigurations().getByName(Constants.Configurations.FORGE_RUNTIME_LIBRARY), true); } + + private String getSourceRootsSeparator() { + // Some versions of Forge 49+ requires a different separator + if (!extension.isForge() || extension.getForgeProvider().getVersion().getMajorVersion() < Constants.Forge.MIN_BOOTSTRAP_DEV_VERSION) { + return File.pathSeparator; + } + + for (Dependency dependency : project.getConfigurations().getByName(Constants.Configurations.FORGE_DEPENDENCIES).getDependencies()) { + if (dependency.getGroup().equals("net.minecraftforge") && dependency.getName().equals("bootstrap-dev")) { + Version version = Version.parse(dependency.getVersion()); + return version.compareTo(Version.parse("2.1.4")) >= 0 ? File.pathSeparator : ";"; + } + } + + project.getLogger().warn("Failed to find bootstrap-dev in forge dependencies, using File.pathSeparator as separator"); + return File.pathSeparator; + } } diff --git a/src/main/java/net/fabricmc/loom/util/Version.java b/src/main/java/net/fabricmc/loom/util/Version.java new file mode 100644 index 00000000..9948622b --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/Version.java @@ -0,0 +1,115 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.common.base.Strings; +import com.google.common.collect.Ordering; +import org.gradle.util.internal.VersionNumber; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A simple version class that can be used to compare versions. + * This class allows for versions that are not strictly following the semver specification, + * but are still allowed in the context of gradle versioning. + *

+ * This class is intentionally very flexible and does not enforce any specific versioning scheme, + * and should be very similar to the versioning used by gradle itself. + */ +public record Version(int major, int minor, int micro, int patch, @Nullable String qualifier) implements Comparable { + private static final Pattern REGEX = Pattern.compile("(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?()(?:[.-]([^+\\s]*))?(?:\\+.*)?"); + private static final Pattern REGEX_WITH_PATCH = Pattern.compile("(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?(?:\\.(\\d+))?(?:[+.-]([^+\\s]*))?(?:\\+.*)?"); + public static final Version UNKNOWN = new Version(0, 0, 0, 0, null); + + public static Version parse(String version) { + return parse(version, false); + } + + public static Version parse(String version, boolean withPatch) { + Matcher matcher = (withPatch ? REGEX_WITH_PATCH : REGEX).matcher(version); + + if (!matcher.matches()) { + return UNKNOWN; + } + + int major = Integer.parseInt(matcher.group(1)); + int minor = !Strings.isNullOrEmpty(matcher.group(2)) ? Integer.parseInt(matcher.group(2)) : 0; + int micro = !Strings.isNullOrEmpty(matcher.group(3)) ? Integer.parseInt(matcher.group(3)) : 0; + int patch = !Strings.isNullOrEmpty(matcher.group(4)) ? Integer.parseInt(matcher.group(4)) : 0; + String qualifier = matcher.group(5); + + return new Version(major, minor, micro, patch, qualifier); + } + + public VersionNumber asBaseVersion() { + return new VersionNumber(this.major, this.minor, this.micro, this.patch, null); + } + + @Override + public int compareTo(@NotNull Version other) { + if (this.major != other.major) { + return this.major - other.major; + } else if (this.minor != other.minor) { + return this.minor - other.minor; + } else if (this.micro != other.micro) { + return this.micro - other.micro; + } else { + return this.patch != other.patch ? this.patch - other.patch + : Ordering.natural().nullsLast() + .compare(this.toLowerCase(this.qualifier), this.toLowerCase(other.qualifier)); + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj == null || this.getClass() != obj.getClass()) { + return false; + } else { + Version other = (Version) obj; + return this.major == other.major && this.minor == other.minor && this.micro == other.micro && this.patch == other.patch + && this.toLowerCase(this.qualifier).equals(this.toLowerCase(other.qualifier)); + } + } + + @Override + public int hashCode() { + int result = this.major; + result = 31 * result + this.minor; + result = 31 * result + this.micro; + result = 31 * result + this.patch; + result = 31 * result + this.toLowerCase(this.qualifier).hashCode(); + return result; + } + + @Nullable + private String toLowerCase(@Nullable String string) { + return string == null ? null : string.toLowerCase(); + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/VersionTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/VersionTest.groovy new file mode 100644 index 00000000..d8726dbe --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/VersionTest.groovy @@ -0,0 +1,57 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.test.unit + + +import spock.lang.Specification + +import net.fabricmc.loom.util.Version + +class VersionTest extends Specification { + def "version comparison"() { + when: + def compare = Version.parse(s1) <=> Version.parse(s2) + // turns compare into 1 or 0 or -1 + compare = compare <=> 0 + + then: + compare == expected + + where: + s1 | s2 | expected + "1.1.1" | "1.1.1" | 0 + "1.1.1" | "1.1.0" | 1 + "1.1.0" | "1.1" | 0 + "1.0.0" | "1" | 0 + "1-" | "1" | -1 + "1.1.1" | "1.1.2" | -1 + "1.1.1" | "1.1.1-" | 1 + "1.1.1-beta" | "1.1.1-alpha" | 1 + "1.1.1-alpha" | "1.1.1-beta" | -1 + "1.1.1-beta.1" | "1.1.1-beta.2" | -1 + "1.1.1-beta.1" | "1.1.1-beta.10" | -1 + "1.1.1+123" | "1.1.1+567" | 0 + } +}