diff --git a/UET/Redpoint.Uet.SdkManagement.Tests/MacVersionTests.cs b/UET/Redpoint.Uet.SdkManagement.Tests/MacVersionTests.cs new file mode 100644 index 00000000..0c17a631 --- /dev/null +++ b/UET/Redpoint.Uet.SdkManagement.Tests/MacVersionTests.cs @@ -0,0 +1,51 @@ +namespace Redpoint.Uet.SdkManagement.Tests +{ + using Redpoint.Uet.SdkManagement.Sdk.GenericPlatform; + using Redpoint.Uet.SdkManagement.Sdk.VersionNumbers; + using System; + using System.Collections; + using System.Collections.Generic; + using System.Text; + + public class MacVersionTestDataGenerator : IEnumerable + { + private readonly List _data = new List + { + new object?[] { null, "15.2", "15.2.0", "26.9.0", Array.Empty() }, + new object?[] { "15.2", "15.2", "15.2.0", "26.9.0", new[] { "Xcode_15.2.xip" } }, + new object?[] { "15.2", "15.2", "15.2.0", "26.9.0", new[] { "Xcode_15.2.xip", "Xcode_15.4.xip", "Xcode_16.0.xip", "Xcode_16.3.xip" } }, + new object?[] { "16.3", null, "15.2.0", "26.9.0", new[] { "Xcode_15.2.xip", "Xcode_15.4.xip", "Xcode_16.0.xip", "Xcode_16.3.xip" } }, + new object?[] { "15.4", "15.3", "15.2.0", "26.9.0", new[] { "Xcode_15.2.xip", "Xcode_15.4.xip", "Xcode_16.0.xip", "Xcode_16.3.xip" } }, + new object?[] { "16.0", null, "15.2.0", "16.2.0", new[] { "Xcode_15.2.xip", "Xcode_15.4.xip", "Xcode_16.0.xip", "Xcode_16.3.xip" } }, + new object?[] { "16.3", null, "16.1", "26.9.0", new[] { "Xcode_15.2.xip", "Xcode_15.4.xip", "Xcode_16.0.xip", "Xcode_16.3.xip" } }, + new object?[] { "16.0", "16.1", "15.9", "26.9.0", new[] { "Xcode_15.2.xip", "Xcode_15.4.xip", "Xcode_16.0.xip", "Xcode_16.3.xip" } }, + }; + + public IEnumerator GetEnumerator() => _data.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + public class MacVersionTests + { + [Theory] + [ClassData(typeof(MacVersionTestDataGenerator))] + public void GetBestAvailableVersionFromAvailableXcodes(string? expectedVersion, string? mainVersion, string? minVersion, string? maxVersion, string[] availableXcodes) + { + var selectedVersion = JsonMacVersionNumbers.GetBestAvailableVersionFromAvailableXcodes( + mainVersion != null ? GenericPlatformVersion.Parse(mainVersion) : null, + minVersion != null ? GenericPlatformVersion.Parse(minVersion) : null, + maxVersion != null ? GenericPlatformVersion.Parse(maxVersion) : null, + availableXcodes); + if (expectedVersion == null) + { + Assert.Null(selectedVersion); + } + else + { + Assert.NotNull(selectedVersion); + Assert.Equal(expectedVersion, selectedVersion.ToString()); + } + } + } +} diff --git a/UET/Redpoint.Uet.SdkManagement/Sdk/VersionNumbers/JsonMacVersionNumbers.cs b/UET/Redpoint.Uet.SdkManagement/Sdk/VersionNumbers/JsonMacVersionNumbers.cs index ff2f3cbd..96de6cf5 100644 --- a/UET/Redpoint.Uet.SdkManagement/Sdk/VersionNumbers/JsonMacVersionNumbers.cs +++ b/UET/Redpoint.Uet.SdkManagement/Sdk/VersionNumbers/JsonMacVersionNumbers.cs @@ -1,6 +1,7 @@ namespace Redpoint.Uet.SdkManagement.Sdk.VersionNumbers { using Microsoft.Extensions.Logging; + using Redpoint.Uet.SdkManagement.Sdk.GenericPlatform; using System.Runtime.Versioning; using System.Text.Json; using System.Text.RegularExpressions; @@ -30,6 +31,132 @@ public bool CanUse(string unrealEnginePath) "Apple_SDK.json")); } + private static GenericPlatformVersion? ParseVersionFromDictionary(Dictionary dictionary, string key) + { + if (dictionary.TryGetValue(key, out var version) && + version.ValueKind == JsonValueKind.String) + { + var versionString = version.GetString(); + if (versionString != null) + { + return GenericPlatformVersion.Parse(versionString); + } + } + return null; + } + + private static GenericPlatformVersion? ClampVersion(GenericPlatformVersion? version, int major, int minor) + { + if (version != null) + { + if (version.Major < major || (version.Major == major && version.Minor < minor)) + { + return GenericPlatformVersion.Parse($"{major}.{minor}"); + } + } + return version; + } + + internal static GenericPlatformVersion? GetBestAvailableVersionFromAvailableXcodes( + GenericPlatformVersion? mainVersion, + GenericPlatformVersion? minVersion, + GenericPlatformVersion? maxVersion, + IEnumerable availableXcodes, + ILogger? logger = null) + { + return GetBestAvailableVersionFromAvailableXcodes( + mainVersion, + minVersion, + maxVersion, + availableXcodes.Select(x => x.Name), + logger); + } + + internal static GenericPlatformVersion? GetBestAvailableVersionFromAvailableXcodes( + GenericPlatformVersion? mainVersion, + GenericPlatformVersion? minVersion, + GenericPlatformVersion? maxVersion, + IEnumerable availableXcodes, + ILogger? logger = null) + { + var candidateVersionRegex = new Regex("^Xcode_(?.*)\\.xip$"); + var absoluteMaximumVersion = GenericPlatformVersion.Parse("999")!; + GenericPlatformVersion? selectedVersion = null; + long selectedVersionDistance = long.MaxValue; + long candidateCount = 0; + foreach (var availableXcode in availableXcodes) + { + var candidateVersionMatch = candidateVersionRegex.Match(availableXcode); + if (candidateVersionMatch.Success) + { + var candidateVersion = GenericPlatformVersion.Parse(candidateVersionMatch.Groups["version"].Value); + if (candidateVersion != null) + { + candidateCount++; + var allowed = true; + if (minVersion != null && candidateVersion - minVersion < 0) + { + if (allowed) + { + logger?.LogInformation($"Candidate Xcode version '{candidateVersion}' will not be selected because it is lower than the minimum version '{minVersion}'."); + } + allowed = false; + } + if (maxVersion != null && maxVersion - candidateVersion < 0) + { + if (allowed) + { + logger?.LogInformation($"Candidate Xcode version '{candidateVersion}' will not be selected because it is higher than the maximum version '{maxVersion}'."); + } + allowed = false; + } + if (allowed) + { + long distance; + if (mainVersion != null) + { + // How close is it to the main version? + distance = candidateVersion - mainVersion; + if (distance > 0) + { + // The candidate version is higher than the main version; prefer + // a higher version one minor up than a lower version one minor down, in case the engine relies on compiler features introduced in the main version that aren't actually compatible with min version. + distance /= 2; + } + distance = Math.Abs(distance); + } + else if (maxVersion != null) + { + // How close is it in the max version. + distance = Math.Abs(maxVersion - candidateVersion); + } + else + { + // How high is the version. + distance = Math.Abs(absoluteMaximumVersion - candidateVersion); + } + if (selectedVersion == null || distance < selectedVersionDistance) + { + selectedVersion = candidateVersion; + selectedVersionDistance = distance; + } + } + } + } + } + + if (selectedVersion != null) + { + logger?.LogInformation($"Candidate Xcode version '{selectedVersion}' was selected as the best available Xcode version."); + return selectedVersion; + } + else + { + logger?.LogWarning($"Unable to find a candidate Xcode version that met the constraints of MinVersion={minVersion},MaxVersion={maxVersion},MainVersion={mainVersion} with {candidateCount} candidates available."); + return null; + } + } + [SupportedOSPlatform("macos")] public async Task GetXcodeVersion(string unrealEnginePath) { @@ -59,63 +186,39 @@ public async Task GetXcodeVersion(string unrealEnginePath) _logger.LogWarning("UET_APPLE_XCODE_STORAGE_PATH is not set, so we can not determine the highest available Xcode version to use from 'MaxVersion' and 'MinVersion' in Apple_SDK.json. The value of 'MainVersion' will always be used."); } } - if (dictionary.TryGetValue("MaxVersion", out var maxVersion) && - dictionary.TryGetValue("MinVersion", out var minVersion) && - !string.IsNullOrWhiteSpace(appleXcodeStoragePath)) + + var mainVersion = ParseVersionFromDictionary(dictionary, "MainVersion"); + var minVersion = ParseVersionFromDictionary(dictionary, "MinVersion"); + var maxVersion = ParseVersionFromDictionary(dictionary, "MaxVersion"); + + // Fab compiles with 15.4 even for engines that specify 15.2 as their MainVersion, at least + // for Unreal Engine 5.5. I am waiting on Fab support to give me a full list of engine version -> Xcode + // versions that they use, given that it doesn't seem to be based on the SDK version files + // in the engine itself. + mainVersion = ClampVersion(mainVersion, 15, 4); + minVersion = ClampVersion(mainVersion, 15, 4); + + if (!string.IsNullOrWhiteSpace(appleXcodeStoragePath)) { - var regex = new Regex("^(?[0-9]+)\\.(?[0-9]+)\\."); - var maxMatch = regex.Match(maxVersion.ToString()); - var minMatch = regex.Match(minVersion.ToString()); - if (maxMatch.Success && - minMatch.Success && - int.TryParse(maxMatch.Groups["major"].Value, out var major) && - int.TryParse(maxMatch.Groups["minor"].Value, out var minor) && - int.TryParse(minMatch.Groups["major"].Value, out var minMajor) && - int.TryParse(minMatch.Groups["minor"].Value, out var minMinor)) + var selectedVersion = GetBestAvailableVersionFromAvailableXcodes( + mainVersion, + minVersion, + maxVersion, + new DirectoryInfo(appleXcodeStoragePath).GetFiles("Xcode_*.xip"), + _hasEmittedLogs ? null : _logger); + if (selectedVersion != null) { if (!_hasEmittedLogs) { - _logger.LogInformation($"The maximum Apple SDK version permitted for this version of Unreal Engine is: Xcode {major}.{minor}"); - _logger.LogInformation($"The minimum Apple SDK version permitted for this version of Unreal Engine is: Xcode {minMajor}.{minMinor}"); - } - - // The version number for MaxVersion can be ahead of the released Xcode version. For example, it could be 16.9 while - // the latest release is 16.3. - // - // To figure out the actual version, check the files in UET_APPLE_XCODE_STORAGE_PATH and see what is available, decrementing - // the version number until we find a file that exists. - var foundVersion = false; - do - { - var xipSourcePath = Path.Combine(appleXcodeStoragePath, $"Xcode_{major}.{minor}.xip"); - if (File.Exists(xipSourcePath)) - { - if (!_hasEmittedLogs) - { - _logger.LogInformation($"Detected that the Apple SDK to use for this version of Unreal Engine is: Xcode {major}.{minor}"); - } - foundVersion = true; - break; - } - minor--; - if (minor == -1) - { - // No idea what the maximum minor version can be, so just overestimate. - minor = 20; - major--; - } + _logger.LogInformation($"Detected that the Apple SDK to use from 'MainVersion' for this version of Unreal Engine is: Xcode {selectedVersion}"); } - while ((major == minMajor && minor >= minMinor) || major > minMajor); - if (foundVersion) - { - _hasEmittedLogs = true; - return $"{major}.{minor}"; - } + _hasEmittedLogs = true; + return $"{selectedVersion.Major}.{selectedVersion.Minor}"; } } - if (dictionary.TryGetValue("MainVersion", out var mainVersion)) + if (mainVersion != null) { if (!_hasEmittedLogs) {