Skip to content

Commit c2a9851

Browse files
committed
Add version comparison methods to SharedUtil
These methods use copies of `StaticVersionComparator` and `VersionParser` from Gradle 9.0.0 to avoid using internal classes at runtime.
1 parent 3b200a6 commit c2a9851

File tree

3 files changed

+275
-1
lines changed

3 files changed

+275
-1
lines changed

gradleutils-shared/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ dependencies {
4040
license {
4141
header = rootProject.file('LICENSE-header.txt')
4242
newLine = false
43-
exclude '**/*.properties'
43+
exclude '**/*.properties', '**/StaticVersionComparator.java'
4444
}
4545

4646
tasks.named('jar', Jar) {

gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/SharedUtil.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import java.text.MessageFormat;
4949
import java.util.ArrayList;
5050
import java.util.Collection;
51+
import java.util.Comparator;
5152
import java.util.List;
5253
import java.util.Map;
5354
import java.util.Objects;
@@ -423,6 +424,21 @@ public static void forEachClasspathEagerly(NamedDomainObjectSet<Configuration> c
423424
}
424425
//endregion
425426

427+
//region Dependency Versioning
428+
429+
static int versionCompare(String v1, String v2) {
430+
return StaticVersionComparator.compareNow(v1, v2);
431+
}
432+
433+
static Comparator<String> versionComparator() {
434+
return StaticVersionComparator.INSTANCE;
435+
}
436+
437+
static Class<? extends Comparator<String>> versionComparatorClass() {
438+
return StaticVersionComparator.class;
439+
}
440+
//endergion
441+
426442
//region Domain Object Handling
427443

428444
/// Iterates through the given collection using the given action.
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
/*
2+
* Copyright 2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package net.minecraftforge.gradleutils.shared;
18+
19+
import java.io.Serial;
20+
import java.io.Serializable;
21+
import java.util.ArrayList;
22+
import java.util.Comparator;
23+
import java.util.List;
24+
import java.util.Locale;
25+
import java.util.Map;
26+
27+
// https://github.com/gradle/gradle/blob/v9.0.0/platforms/software/dependency-management/src/main/java/org/gradle/api/internal/artifacts/ivyservice/ivyresolve/strategy/StaticVersionComparator.java
28+
@SuppressWarnings("ObjectInstantiationInEqualsHashCode")
29+
class StaticVersionComparator implements Comparator<String>, Serializable {
30+
private static final @Serial long serialVersionUID = 6082715929370009879L;
31+
static final StaticVersionComparator INSTANCE = new StaticVersionComparator();
32+
33+
static final Map<String, Integer> SPECIAL_MEANINGS = Map.of(
34+
"dev", -1,
35+
"rc", 1,
36+
"snapshot", 2,
37+
"final", 3, "ga", 4, "release", 5,
38+
"sp", 6);
39+
40+
@Override
41+
public int compare(String o1, String o2) {
42+
return compareNow(o1, o2);
43+
}
44+
45+
static int compareNow(String o1, String o2) {
46+
return compare(Version.parse(o1), Version.parse(o2));
47+
}
48+
49+
/**
50+
* Compares 2 versions. Algorithm is inspired by PHP version_compare one.
51+
*/
52+
private static int compare(Version version1, Version version2) {
53+
if (version1.equals(version2)) {
54+
return 0;
55+
}
56+
57+
String[] parts1 = version1.getParts();
58+
String[] parts2 = version2.getParts();
59+
Long[] numericParts1 = version1.getNumericParts();
60+
Long[] numericParts2 = version2.getNumericParts();
61+
62+
int i = 0;
63+
for (; i < parts1.length && i < parts2.length; i++) {
64+
String part1 = parts1[i];
65+
String part2 = parts2[i];
66+
67+
Long numericPart1 = numericParts1[i];
68+
Long numericPart2 = numericParts2[i];
69+
70+
boolean is1Number = numericPart1 != null;
71+
boolean is2Number = numericPart2 != null;
72+
73+
if (part1.equals(part2)) {
74+
continue;
75+
}
76+
if (is1Number && !is2Number) {
77+
return 1;
78+
}
79+
if (is2Number && !is1Number) {
80+
return -1;
81+
}
82+
if (is1Number && is2Number) {
83+
int result = numericPart1.compareTo(numericPart2);
84+
if (result == 0) {
85+
continue;
86+
}
87+
return result;
88+
}
89+
// both are strings, we compare them taking into account special meaning
90+
Integer sm1 = SPECIAL_MEANINGS.get(part1.toLowerCase(Locale.US));
91+
Integer sm2 = SPECIAL_MEANINGS.get(part2.toLowerCase(Locale.US));
92+
if (sm1 != null) {
93+
sm2 = sm2 == null ? 0 : sm2;
94+
return sm1 - sm2;
95+
}
96+
if (sm2 != null) {
97+
return -sm2;
98+
}
99+
return part1.compareTo(part2);
100+
}
101+
if (i < parts1.length) {
102+
return numericParts1[i] == null ? -1 : 1;
103+
}
104+
if (i < parts2.length) {
105+
return numericParts2[i] == null ? 1 : -1;
106+
}
107+
108+
return 0;
109+
}
110+
111+
// https://github.com/gradle/gradle/blob/v9.0.0/platforms/software/dependency-management/src/main/java/org/gradle/api/internal/artifacts/ivyservice/ivyresolve/strategy/Version.java
112+
private interface Version {
113+
/**
114+
* Returns the original {@link String} representation of the version.
115+
*/
116+
String getSource();
117+
118+
/**
119+
* Returns all the parts of this version. e.g. 1.2.3 returns [1,2,3] or 1.2-beta4 returns [1,2,beta,4].
120+
*/
121+
String[] getParts();
122+
123+
/**
124+
* Returns all the numeric parts of this version as {@link Long}, with nulls in non-numeric positions. eg. 1.2.3 returns [1,2,3] or 1.2-beta4 returns [1,2,null,4].
125+
*/
126+
Long[] getNumericParts();
127+
128+
/**
129+
* Returns the base version for this version, which removes any qualifiers. Generally this is the first '.' separated parts of this version.
130+
* e.g. 1.2.3-beta-4 returns 1.2.3, or 7.0.12beta5 returns 7.0.12.
131+
*/
132+
Version getBaseVersion();
133+
134+
/**
135+
* Returns true if this version is qualified in any way. For example, 1.2.3 is not qualified, 1.2-beta-3 is.
136+
*/
137+
boolean isQualified();
138+
139+
// https://github.com/gradle/gradle/blob/328772c6bae126949610a8beb59cb227ee580241/platforms/software/dependency-management/src/main/java/org/gradle/api/internal/artifacts/ivyservice/ivyresolve/strategy/VersionParser.java#L41-L88
140+
static Version parse(String original) {
141+
List<String> parts = new ArrayList<>();
142+
boolean digit = false;
143+
int startPart = 0;
144+
int pos = 0;
145+
int endBase = 0;
146+
int endBaseStr = 0;
147+
for (; pos < original.length(); pos++) {
148+
char ch = original.charAt(pos);
149+
if (ch == '.' || ch == '_' || ch == '-' || ch == '+') {
150+
parts.add(original.substring(startPart, pos));
151+
startPart = pos + 1;
152+
digit = false;
153+
if (ch != '.' && endBaseStr == 0) {
154+
endBase = parts.size();
155+
endBaseStr = pos;
156+
}
157+
} else if (ch >= '0' && ch <= '9') {
158+
if (!digit && pos > startPart) {
159+
if (endBaseStr == 0) {
160+
endBase = parts.size() + 1;
161+
endBaseStr = pos;
162+
}
163+
parts.add(original.substring(startPart, pos));
164+
startPart = pos;
165+
}
166+
digit = true;
167+
} else {
168+
if (digit) {
169+
if (endBaseStr == 0) {
170+
endBase = parts.size() + 1;
171+
endBaseStr = pos;
172+
}
173+
parts.add(original.substring(startPart, pos));
174+
startPart = pos;
175+
}
176+
digit = false;
177+
}
178+
}
179+
if (pos > startPart) {
180+
parts.add(original.substring(startPart, pos));
181+
}
182+
DefaultVersion base = null;
183+
if (endBaseStr > 0) {
184+
base = new DefaultVersion(original.substring(0, endBaseStr), parts.subList(0, endBase), null);
185+
}
186+
return new DefaultVersion(original, parts, base);
187+
}
188+
}
189+
190+
// https://github.com/gradle/gradle/blob/328772c6bae126949610a8beb59cb227ee580241/platforms/software/dependency-management/src/main/java/org/gradle/api/internal/artifacts/ivyservice/ivyresolve/strategy/VersionParser.java#L90-L152
191+
private static class DefaultVersion implements Version {
192+
private final String source;
193+
private final String[] parts;
194+
private final Long[] numericParts;
195+
private final DefaultVersion baseVersion;
196+
197+
public DefaultVersion(String source, List<String> parts, DefaultVersion baseVersion) {
198+
this.source = source;
199+
this.parts = parts.toArray(new String[0]);
200+
this.numericParts = new Long[this.parts.length];
201+
for (int i = 0; i < parts.size(); i++) {
202+
Long part = null;
203+
try {
204+
part = Long.parseLong(this.parts[i]);
205+
} catch (NumberFormatException ignored) { }
206+
this.numericParts[i] = part;
207+
}
208+
this.baseVersion = baseVersion == null ? this : baseVersion;
209+
}
210+
211+
@Override
212+
public String toString() {
213+
return source;
214+
}
215+
216+
@Override
217+
public boolean equals(Object obj) {
218+
if (obj == this) {
219+
return true;
220+
}
221+
if (obj == null || obj.getClass() != getClass()) {
222+
return false;
223+
}
224+
DefaultVersion other = (DefaultVersion) obj;
225+
return source.equals(other.source);
226+
}
227+
228+
@Override
229+
public int hashCode() {
230+
return source.hashCode();
231+
}
232+
233+
@Override
234+
public boolean isQualified() {
235+
return baseVersion != this;
236+
}
237+
238+
@Override
239+
public Version getBaseVersion() {
240+
return baseVersion;
241+
}
242+
243+
@Override
244+
public String[] getParts() {
245+
return parts;
246+
}
247+
248+
@Override
249+
public Long[] getNumericParts() {
250+
return numericParts;
251+
}
252+
253+
@Override
254+
public String getSource() {
255+
return source;
256+
}
257+
}
258+
}

0 commit comments

Comments
 (0)