Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions UET/Redpoint.Uet.SdkManagement.Tests/MacVersionTests.cs
Original file line number Diff line number Diff line change
@@ -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<object?[]>
{
private readonly List<object?[]> _data = new List<object?[]>
{
new object?[] { null, "15.2", "15.2.0", "26.9.0", Array.Empty<string>() },
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<object[]> 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());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -30,6 +31,132 @@ public bool CanUse(string unrealEnginePath)
"Apple_SDK.json"));
}

private static GenericPlatformVersion? ParseVersionFromDictionary(Dictionary<string, JsonElement> 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<FileInfo> 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<string> availableXcodes,
ILogger? logger = null)
{
var candidateVersionRegex = new Regex("^Xcode_(?<version>.*)\\.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<string> GetXcodeVersion(string unrealEnginePath)
{
Expand Down Expand Up @@ -59,63 +186,39 @@ public async Task<string> 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("^(?<major>[0-9]+)\\.(?<minor>[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)
{
Expand Down
Loading