From f0cb3f1a9ba163a24f463e2f3c73668707b37422 Mon Sep 17 00:00:00 2001 From: Kharkunov Eugene Date: Thu, 2 Apr 2026 20:23:38 +0300 Subject: [PATCH 1/6] Introduced separate immutable field to lock access to currentCacheDir in Cocoapods service --- .../services/cocoapods/CocoaPodsService.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/com/defold/extender/services/cocoapods/CocoaPodsService.java b/server/src/main/java/com/defold/extender/services/cocoapods/CocoaPodsService.java index 084b5bb9..32a412bc 100644 --- a/server/src/main/java/com/defold/extender/services/cocoapods/CocoaPodsService.java +++ b/server/src/main/java/com/defold/extender/services/cocoapods/CocoaPodsService.java @@ -65,6 +65,7 @@ private class InstalledPods { private static final Logger LOGGER = LoggerFactory.getLogger(CocoaPodsService.class); private static final String CURRENT_CACHE_DIR_FILE = "current_pod_cache.txt"; private static final String OLD_CACHE_DIR_FILE = "old_pod_caches.txt"; + private final Object syncLock = new Object(); private final TemplateExecutor templateExecutor = new TemplateExecutor(); private final String podfileTemplateContents; @@ -88,13 +89,13 @@ public void runAfterStartup() { // initialize cache directory Path currentCacheDir = readCurrentCacheDir(); if (currentCacheDir != null && currentCacheDir.startsWith(this.homeDirPrefix)) { - synchronized(this.currentCacheDir) { + synchronized(this.syncLock) { this.currentCacheDir = currentCacheDir; } updateSpecRepo(); } else { LOGGER.info("Cocoapods has no current cache dir or prefix is changed. Created..."); - synchronized(this.currentCacheDir) { + synchronized(this.syncLock) { this.currentCacheDir = generateCacheDirPath(); storeCurrentCacheDir(this.currentCacheDir); } @@ -216,7 +217,7 @@ private InstalledPods installPods(ExtenderBuildState buildState, CocoaPodsServic LOGGER.info("Installing pods"); Path cacheDir; // store current cache dir into local variable to use the same value for all 'pod' runs - synchronized(currentCacheDir) { + synchronized(syncLock) { cacheDir = currentCacheDir; } InstalledPods installedPods = new InstalledPods(); @@ -501,7 +502,7 @@ private void storeCurrentCacheDir(Path currentCacheDir) { private void initializeTrunkRepo() { try { Path cacheDir; - synchronized(currentCacheDir) { + synchronized(syncLock) { cacheDir = currentCacheDir; } String log = ProcessUtils.execCommand(List.of( @@ -524,7 +525,7 @@ public void rotatePodCacheDirectory() { LOGGER.info("Rotate pod cache directory"); Path newCacheDir = generateCacheDirPath(); Path cacheDir; - synchronized(this.currentCacheDir) { + synchronized(this.syncLock) { cacheDir = this.currentCacheDir; } try { @@ -541,7 +542,7 @@ public void rotatePodCacheDirectory() { } catch(IOException exc) { LOGGER.warn("Error while writing to old cache paths file", exc); } - synchronized(this.currentCacheDir) { + synchronized(this.syncLock) { this.currentCacheDir = newCacheDir; storeCurrentCacheDir(currentCacheDir); } @@ -578,7 +579,7 @@ public void updateSpecRepo() { try { LOGGER.info("Run pod spec update"); Path cacheDir; - synchronized(currentCacheDir) { + synchronized(this.syncLock) { cacheDir = currentCacheDir; } String log = ProcessUtils.execCommand(List.of( From 8d2639a998f93866a394ea0351159e087a831308 Mon Sep 17 00:00:00 2001 From: Kharkunov Eugene Date: Wed, 15 Apr 2026 15:26:42 +0300 Subject: [PATCH 2/6] Fixed formating placeholders. Removed redundant condition --- .../java/com/defold/extender/client/ExtenderClientCache.java | 2 +- .../src/main/java/com/defold/extender/ExtenderController.java | 2 +- .../main/java/com/defold/extender/process/ProcessExecutor.java | 2 +- .../com/defold/extender/services/cocoapods/XCConfigParser.java | 2 -- .../defold/extender/services/cocoapods/XCConfigParserTest.java | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/client/src/main/java/com/defold/extender/client/ExtenderClientCache.java b/client/src/main/java/com/defold/extender/client/ExtenderClientCache.java index 82b876cd..a5e1d827 100644 --- a/client/src/main/java/com/defold/extender/client/ExtenderClientCache.java +++ b/client/src/main/java/com/defold/extender/client/ExtenderClientCache.java @@ -180,7 +180,7 @@ private static String hash(ExtenderResource extenderResource) throws ExtenderCli md.update(data); return hashToString(md.digest()); } catch(Exception e){ - throw new ExtenderClientException(String.format("Failed to hash resource: ", extenderResource.getPath()), e); + throw new ExtenderClientException(String.format("Failed to hash resource: %s", extenderResource.getPath()), e); } } diff --git a/server/src/main/java/com/defold/extender/ExtenderController.java b/server/src/main/java/com/defold/extender/ExtenderController.java index 59719542..5c6173ce 100644 --- a/server/src/main/java/com/defold/extender/ExtenderController.java +++ b/server/src/main/java/com/defold/extender/ExtenderController.java @@ -437,6 +437,6 @@ private RemoteInstanceConfig getRemoteBuilderConfig(String platform, String plat } else if (this.remoteBuilderPlatformMappings.containsKey(fallbackKey)) { return this.remoteBuilderPlatformMappings.get(fallbackKey); } - throw new ExtenderException(String.format("No suitable remote builder found for %", fullKey)); + throw new ExtenderException(String.format("No suitable remote builder found for %s", fullKey)); } } diff --git a/server/src/main/java/com/defold/extender/process/ProcessExecutor.java b/server/src/main/java/com/defold/extender/process/ProcessExecutor.java index 8e65ea71..c91cd5e4 100644 --- a/server/src/main/java/com/defold/extender/process/ProcessExecutor.java +++ b/server/src/main/java/com/defold/extender/process/ProcessExecutor.java @@ -46,7 +46,7 @@ public int execute(List args) throws IOException, InterruptedException { if (DM_DEBUG_COMMANDS) { StringBuffer debugBuffer = new StringBuffer(); debugBuffer.append(String.format("CMD %d: %s\n", commandId, String.join(" ", args))); - debugBuffer.append(String.format("\tWorking dir: \n", this.cwd == null ? "(null)" : this.cwd.toString())); + debugBuffer.append(String.format("\tWorking dir: %s\n", this.cwd == null ? "(null)" : this.cwd.toString())); debugBuffer.append("\tEnvironment:\n"); for (Map.Entry envEntry : this.env.entrySet()) { debugBuffer.append(String.format("\t%s=%s\n", envEntry.getKey(), envEntry.getValue())); diff --git a/server/src/main/java/com/defold/extender/services/cocoapods/XCConfigParser.java b/server/src/main/java/com/defold/extender/services/cocoapods/XCConfigParser.java index 60af93b7..ec471630 100644 --- a/server/src/main/java/com/defold/extender/services/cocoapods/XCConfigParser.java +++ b/server/src/main/java/com/defold/extender/services/cocoapods/XCConfigParser.java @@ -144,8 +144,6 @@ Pair parseLine(String line) { || (c >= 'A' && c <= 'Z') || c == '_') { varBuilder.append(c); - } else if (c == '[') { - currentMode = ParseMode.FLAVOUR_START; } else if (c == '=') { currentMode = ParseMode.ASSIGMENT_OPERATOR; } else { diff --git a/server/src/test/java/com/defold/extender/services/cocoapods/XCConfigParserTest.java b/server/src/test/java/com/defold/extender/services/cocoapods/XCConfigParserTest.java index 96f35295..ce0a55ac 100644 --- a/server/src/test/java/com/defold/extender/services/cocoapods/XCConfigParserTest.java +++ b/server/src/test/java/com/defold/extender/services/cocoapods/XCConfigParserTest.java @@ -72,7 +72,7 @@ private static Stream postProcessData() { private static Stream parsingData() { String podsConfigurationBuildDir = String.format("%s/%s%s", PODS_BUILD_DIR, "Debug", "iphoneos"); - String podsXCFrameworksBuildDir = String.format("%s/XCFrameworkIntermediates", podsConfigurationBuildDir, "Debug", "iphoneos"); + String podsXCFrameworksBuildDir = String.format("%s/XCFrameworkIntermediates", podsConfigurationBuildDir); // Arguments struct // * Pod name // * path to xcconfig From 134180252832667f6da0a7db3e00fda1c7bc746d Mon Sep 17 00:00:00 2001 From: Kharkunov Eugene Date: Wed, 15 Apr 2026 15:45:04 +0300 Subject: [PATCH 3/6] Manifest merge tool argument parsing was refactored. Fixed codeql reports in that area --- .../manifestmergetool/ManifestMergeTool.java | 89 ++++++++++++------- .../ManifestMergeToolTest.java | 87 ++++++++++++++++++ 2 files changed, 144 insertions(+), 32 deletions(-) diff --git a/server/manifestmergetool/src/main/java/com/defold/manifestmergetool/ManifestMergeTool.java b/server/manifestmergetool/src/main/java/com/defold/manifestmergetool/ManifestMergeTool.java index 2a9c4a88..f9744cf4 100644 --- a/server/manifestmergetool/src/main/java/com/defold/manifestmergetool/ManifestMergeTool.java +++ b/server/manifestmergetool/src/main/java/com/defold/manifestmergetool/ManifestMergeTool.java @@ -92,48 +92,73 @@ public static void merge(Platform platform, File main, File output, List l logger.log(Level.FINE, "Merging done"); } - /** - * Merges a main manifest with several stubs - */ - public static void main(String[] args) throws Exception { - + static class ParsedArgs { + Platform platform = Platform.UNKNOWN; File main = null; File output = null; List libraries = new ArrayList<>(); + } - Platform platform = Platform.UNKNOWN; + static ParsedArgs parseArgs(String[] args) { + if ((args.length % 2) != 0) { + throw new IllegalArgumentException("Expected key/value argument pairs, got odd number of arguments: " + args.length); + } - int index = 0; - for (int i = 0; i < args.length; ++i) { - if (args[i].equals("--main") && (index+1) < args.length) { - main = new File(args[++i]); - } - else if (args[i].equals("--out") && (index+1) < args.length) { - output = new File(args[++i]); - } - else if (args[i].equals("--lib") && (index+1) < args.length) { - libraries.add(new File(args[++i])); + ParsedArgs parsed = new ParsedArgs(); + for (int i = 0; i < args.length; i += 2) { + String key = args[i]; + String value = args[i + 1]; + switch (key) { + case "--main": + parsed.main = new File(value); + break; + case "--out": + parsed.output = new File(value); + break; + case "--lib": + parsed.libraries.add(new File(value)); + break; + case "--platform": + switch (value) { + case "android": + parsed.platform = Platform.ANDROID; + break; + case "ios": + parsed.platform = Platform.IOS; + break; + case "osx": + parsed.platform = Platform.OSX; + break; + case "web": + parsed.platform = Platform.WEB; + break; + default: + throw new IllegalArgumentException(String.format("Unsupported platform: %s", value)); + } + break; + default: + throw new IllegalArgumentException(String.format("Unknown argument: %s", key)); } + } + return parsed; + } - if (args[i].equals("--platform") && (index+1) < args.length) { - ++i; - if (args[i].equals("android")) { - platform = Platform.ANDROID; - } else if (args[i].equals("ios")) { - platform = Platform.IOS; - } else if (args[i].equals("osx")) { - platform = Platform.OSX; - } else if (args[i].equals("web")) { - platform = Platform.WEB; - } else { - ManifestMergeTool.logger.log(Level.SEVERE, String.format("Unsupported platform: %s", args[i])); - System.exit(1); - } - } + /** + * Merges a main manifest with several stubs + */ + public static void main(String[] args) throws Exception { + + ParsedArgs parsed; + try { + parsed = parseArgs(args); + } catch (IllegalArgumentException e) { + ManifestMergeTool.logger.log(Level.SEVERE, e.getMessage()); + System.exit(1); + return; } try { - merge(platform, main, output, libraries); + merge(parsed.platform, parsed.main, parsed.output, parsed.libraries); } catch(Exception e) { ManifestMergeTool.logger.log(Level.SEVERE, e.toString()); System.exit(1); diff --git a/server/manifestmergetool/src/test/java/com/defold/manifestmergetool/ManifestMergeToolTest.java b/server/manifestmergetool/src/test/java/com/defold/manifestmergetool/ManifestMergeToolTest.java index 9ae6fe97..bbab0768 100644 --- a/server/manifestmergetool/src/test/java/com/defold/manifestmergetool/ManifestMergeToolTest.java +++ b/server/manifestmergetool/src/test/java/com/defold/manifestmergetool/ManifestMergeToolTest.java @@ -9,6 +9,8 @@ import java.nio.file.Files; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import org.apache.commons.io.FileUtils; @@ -850,4 +852,89 @@ public void testMergeHTML5() throws IOException { assertEquals(expected, merged); } + + @Test + public void testParseArgsEmpty() { + ManifestMergeTool.ParsedArgs parsed = ManifestMergeTool.parseArgs(new String[]{}); + assertEquals(Platform.UNKNOWN, parsed.platform); + assertNull(parsed.main); + assertNull(parsed.output); + assertTrue(parsed.libraries.isEmpty()); + } + + @Test + public void testParseArgsAllFlags() { + String[] args = { + "--platform", "android", + "--main", "AndroidManifest.xml", + "--lib", "lib1.xml", + "--lib", "lib2.xml", + "--out", "merged.xml", + }; + ManifestMergeTool.ParsedArgs parsed = ManifestMergeTool.parseArgs(args); + assertEquals(Platform.ANDROID, parsed.platform); + assertEquals(new File("AndroidManifest.xml"), parsed.main); + assertEquals(new File("merged.xml"), parsed.output); + assertEquals(2, parsed.libraries.size()); + assertEquals(new File("lib1.xml"), parsed.libraries.get(0)); + assertEquals(new File("lib2.xml"), parsed.libraries.get(1)); + } + + @Test + public void testParseArgsPlatformIos() { + ManifestMergeTool.ParsedArgs parsed = ManifestMergeTool.parseArgs(new String[]{"--platform", "ios"}); + assertEquals(Platform.IOS, parsed.platform); + } + + @Test + public void testParseArgsPlatformOsx() { + ManifestMergeTool.ParsedArgs parsed = ManifestMergeTool.parseArgs(new String[]{"--platform", "osx"}); + assertEquals(Platform.OSX, parsed.platform); + } + + @Test + public void testParseArgsPlatformWeb() { + ManifestMergeTool.ParsedArgs parsed = ManifestMergeTool.parseArgs(new String[]{"--platform", "web"}); + assertEquals(Platform.WEB, parsed.platform); + } + + @Test + public void testParseArgsLibrariesOrderPreserved() { + String[] args = { + "--lib", "a.xml", + "--lib", "b.xml", + "--lib", "c.xml", + }; + ManifestMergeTool.ParsedArgs parsed = ManifestMergeTool.parseArgs(args); + assertEquals(3, parsed.libraries.size()); + assertEquals(new File("a.xml"), parsed.libraries.get(0)); + assertEquals(new File("b.xml"), parsed.libraries.get(1)); + assertEquals(new File("c.xml"), parsed.libraries.get(2)); + } + + @Test + public void testParseArgsUnsupportedPlatform() { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> ManifestMergeTool.parseArgs(new String[]{"--platform", "windows"})); + assertTrue(e.getMessage().contains("windows")); + } + + @Test + public void testParseArgsUnknownFlag() { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> ManifestMergeTool.parseArgs(new String[]{"--bogus", "value"})); + assertTrue(e.getMessage().contains("--bogus")); + } + + @Test + public void testParseArgsOddNumberOfArgs() { + assertThrows(IllegalArgumentException.class, + () -> ManifestMergeTool.parseArgs(new String[]{"--main"})); + } + + @Test + public void testParseArgsOddNumberOfArgsTrailing() { + assertThrows(IllegalArgumentException.class, + () -> ManifestMergeTool.parseArgs(new String[]{"--main", "a.xml", "--lib"})); + } } From 697276ac314789e8d5349fa5a5f0e0d7abdcde4e Mon Sep 17 00:00:00 2001 From: Kharkunov Eugene Date: Wed, 15 Apr 2026 15:49:57 +0300 Subject: [PATCH 4/6] Removed unused boxed value usage --- .../extender/services/cocoapods/PodfileParser.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/defold/extender/services/cocoapods/PodfileParser.java b/server/src/main/java/com/defold/extender/services/cocoapods/PodfileParser.java index fedd65a4..6902dfd7 100644 --- a/server/src/main/java/com/defold/extender/services/cocoapods/PodfileParser.java +++ b/server/src/main/java/com/defold/extender/services/cocoapods/PodfileParser.java @@ -52,11 +52,13 @@ static int compareVersions(String version1, String version2) { String[] parts2 = version2.split("\\."); int length = Math.max(parts1.length, parts2.length); for (int i = 0; i < length; i++) { - Integer v1 = i < parts1.length ? Integer.parseInt(parts1[i]) : 0; - Integer v2 = i < parts2.length ? Integer.parseInt(parts2[i]) : 0; - int compare = v1.compareTo(v2); - if (compare != 0) { - result = compare; + int v1 = i < parts1.length ? Integer.parseInt(parts1[i]) : 0; + int v2 = i < parts2.length ? Integer.parseInt(parts2[i]) : 0; + if (v1 < v2) { + result = -1; + break; + } else if (v1 > v2) { + result = 1; break; } } From bdc814700de1f22df2e911a8695b7ef8dd2de562 Mon Sep 17 00:00:00 2001 From: Kharkunov Eugene Date: Wed, 15 Apr 2026 15:52:14 +0300 Subject: [PATCH 5/6] Removed unused boxed value usage --- .../java/com/defold/extender/client/ExtenderClientCache.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/main/java/com/defold/extender/client/ExtenderClientCache.java b/client/src/main/java/com/defold/extender/client/ExtenderClientCache.java index a5e1d827..5830607e 100644 --- a/client/src/main/java/com/defold/extender/client/ExtenderClientCache.java +++ b/client/src/main/java/com/defold/extender/client/ExtenderClientCache.java @@ -35,10 +35,10 @@ public ExtenderClientCache(File cacheDir) throws IOException { */ public String getHash(ExtenderResource extenderResource) throws ExtenderClientException { String path = extenderResource.getPath(); - Long fileTimestamp = extenderResource.getLastModified(); + long fileTimestamp = extenderResource.getLastModified(); Long timestamp = this.timestamps.get(path); - if (timestamp != null && fileTimestamp.equals(timestamp) ) { + if (timestamp != null && timestamp.longValue() == fileTimestamp) { String hash = this.hashes.get(path); if (hash != null) { return hash; From 35520d97674ddd3d3024d0cc75042efe34a705e1 Mon Sep 17 00:00:00 2001 From: Kharkunov Eugene Date: Wed, 15 Apr 2026 19:27:54 +0300 Subject: [PATCH 6/6] Handled NumberFormattingException. Stripped symbolic postfix from pod version when doing comparasion --- .../services/cocoapods/PodfileParser.java | 42 ++++++++++++++----- .../cocoapods/PodfileParsingException.java | 4 ++ .../services/cocoapods/PodfileParserTest.java | 9 +++- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/server/src/main/java/com/defold/extender/services/cocoapods/PodfileParser.java b/server/src/main/java/com/defold/extender/services/cocoapods/PodfileParser.java index 6902dfd7..edfe0442 100644 --- a/server/src/main/java/com/defold/extender/services/cocoapods/PodfileParser.java +++ b/server/src/main/java/com/defold/extender/services/cocoapods/PodfileParser.java @@ -31,6 +31,7 @@ public ParseResult mergeWith(ParseResult other) throws PodfileParsingException { minVersion = other.minVersion; } + if (platform == null) { platform = other.platform; } else if (other.platform != null && !platform.equals(other.platform)) { @@ -45,21 +46,40 @@ public ParseResult mergeWith(ParseResult other) throws PodfileParsingException { } } + // Strip semver pre-release and build metadata (everything from the first '-' or '+'), + // e.g. "1.0.0-beta" -> "1.0.0", "2.0.0+build.7" -> "2.0.0". Comparison ignores these segments. + private static String stripVersionSuffix(String version) { + int cut = version.length(); + for (int i = 0; i < version.length(); i++) { + char c = version.charAt(i); + if (c == '-' || c == '+') { + cut = i; + break; + } + } + return version.substring(0, cut); + } + // https://www.baeldung.com/java-comparing-versions#customSolution - static int compareVersions(String version1, String version2) { + static int compareVersions(String version1, String version2) throws PodfileParsingException { int result = 0; - String[] parts1 = version1.split("\\."); - String[] parts2 = version2.split("\\."); + String[] parts1 = stripVersionSuffix(version1).split("\\."); + String[] parts2 = stripVersionSuffix(version2).split("\\."); int length = Math.max(parts1.length, parts2.length); for (int i = 0; i < length; i++) { - int v1 = i < parts1.length ? Integer.parseInt(parts1[i]) : 0; - int v2 = i < parts2.length ? Integer.parseInt(parts2[i]) : 0; - if (v1 < v2) { - result = -1; - break; - } else if (v1 > v2) { - result = 1; - break; + try { + int v1 = i < parts1.length && !parts1[i].isEmpty() ? Integer.parseInt(parts1[i]) : 0; + int v2 = i < parts2.length && !parts2[i].isEmpty() ? Integer.parseInt(parts2[i]) : 0; + if (v1 < v2) { + result = -1; + break; + } else if (v1 > v2) { + result = 1; + break; + } + } catch (NumberFormatException exc) { + throw new PodfileParsingException( + String.format("Failed to compare pod versions '%s' and '%s'", version1, version2), exc); } } return result; diff --git a/server/src/main/java/com/defold/extender/services/cocoapods/PodfileParsingException.java b/server/src/main/java/com/defold/extender/services/cocoapods/PodfileParsingException.java index 6b57e82f..8c73398f 100644 --- a/server/src/main/java/com/defold/extender/services/cocoapods/PodfileParsingException.java +++ b/server/src/main/java/com/defold/extender/services/cocoapods/PodfileParsingException.java @@ -6,4 +6,8 @@ public class PodfileParsingException extends ExtenderException { public PodfileParsingException(String reason) { super(reason); } + + public PodfileParsingException(String reason, Exception cause) { + super(cause, reason); + } } diff --git a/server/src/test/java/com/defold/extender/services/cocoapods/PodfileParserTest.java b/server/src/test/java/com/defold/extender/services/cocoapods/PodfileParserTest.java index a94a8dd8..8ad0c03b 100644 --- a/server/src/test/java/com/defold/extender/services/cocoapods/PodfileParserTest.java +++ b/server/src/test/java/com/defold/extender/services/cocoapods/PodfileParserTest.java @@ -69,14 +69,19 @@ public void testPodfileParserAll() throws IOException, PodfileParsingException { } @Test - public void testCompareVersions() { + public void testCompareVersions() throws PodfileParsingException { assertThrows(NullPointerException.class, () -> { PodfileParser.compareVersions(null, null); }); assertEquals(0, PodfileParser.compareVersions("12.0", "12.0")); assertEquals(0, PodfileParser.compareVersions("12.0", "12.0.")); assertTrue(PodfileParser.compareVersions("13.0.1", "13.0") > 0); assertTrue(PodfileParser.compareVersions("9.3", "12.6.1") < 0); - assertThrows(NumberFormatException.class, () -> { PodfileParser.compareVersions("9.3", "unknown"); }); + assertThrows(PodfileParsingException.class, () -> { PodfileParser.compareVersions("9.3", "unknown"); }); assertThrows(NullPointerException.class, () -> { PodfileParser.compareVersions("9.3", null); }); + // Semver pre-release and build metadata are ignored during comparison + assertEquals(0, PodfileParser.compareVersions("1.0.0-beta", "1.0.0")); + assertEquals(0, PodfileParser.compareVersions("2.0.0-rc.1", "2.0.0")); + assertEquals(0, PodfileParser.compareVersions("1.2.3+build.7", "1.2.3")); + assertTrue(PodfileParser.compareVersions("1.0.1-beta", "1.0.0") > 0); } private static Stream mergeVersionsData() {