diff --git a/UET/Redpoint.Uet.SdkManagement/Sdk/MacSdkSetup.cs b/UET/Redpoint.Uet.SdkManagement/Sdk/MacSdkSetup.cs index fd5f55ff..92f14b99 100644 --- a/UET/Redpoint.Uet.SdkManagement/Sdk/MacSdkSetup.cs +++ b/UET/Redpoint.Uet.SdkManagement/Sdk/MacSdkSetup.cs @@ -2,11 +2,14 @@ { using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; + using Redpoint.Hashing; using Redpoint.PackageManagement; using Redpoint.ProcessExecution; using Redpoint.ProgressMonitor; using Redpoint.Uet.SdkManagement.Sdk.VersionNumbers; + using System.Globalization; using System.Runtime.Versioning; + using System.Text; public class MacSdkSetup : ISdkSetup { @@ -55,7 +58,18 @@ public async Task ComputeSdkPackageId(string unrealEnginePath, Cancellat { if (OperatingSystem.IsMacOS()) { - var versionNumber = await _versionNumberResolver.For(unrealEnginePath).GetXcodeVersion(unrealEnginePath).ConfigureAwait(false); + var resolver = _versionNumberResolver.For(unrealEnginePath); + var xcodeVersion = await resolver.GetXcodeVersion(unrealEnginePath).ConfigureAwait(false); + var portableToolchainInfo = await resolver.GetPortableToolchainVersion(unrealEnginePath, xcodeVersion).ConfigureAwait(false); + var versionNumber = xcodeVersion; + if (portableToolchainInfo != null) + { + versionNumber += '-' + Hash.XxHash64( + portableToolchainInfo.Value.ClangSubdir + "-" + + portableToolchainInfo.Value.OSSHeadersSubdir + "-" + + portableToolchainInfo.Value.PortableToolchainVersion, + Encoding.UTF8).Hash.ToString(CultureInfo.InvariantCulture); + } return $"{versionNumber}-iOS"; } else if (OperatingSystem.IsWindows()) @@ -70,7 +84,7 @@ public async Task ComputeSdkPackageId(string unrealEnginePath, Cancellat } [SupportedOSPlatform("macos")] - public async Task InstallXcode(string xcodeVersion, string sdkPackagePath, CancellationToken cancellationToken) + public async Task InstallXcode(string xcodeVersion, MacPortableToolchainInfo? portableToolchainInfo, string sdkPackagePath, CancellationToken cancellationToken) { // Check that the required environment variables have been set. var appleXcodeStoragePath = Environment.GetEnvironmentVariable("UET_APPLE_XCODE_STORAGE_PATH"); @@ -231,14 +245,54 @@ await monitor.MonitorAsync( { throw new SdkSetupPackageGenerationFailedException("Xcode was unable to install iOS platform support."); } + + // Create AutoSDK directory. + if (portableToolchainInfo.HasValue) + { + var clangBase = Path.GetDirectoryName(Path.GetDirectoryName(portableToolchainInfo.Value.ClangSubdir)!)!; + + var toolchainPath = Path.Combine(sdkPackagePath, "AutoSDK", portableToolchainInfo.Value.PortableToolchainVersion); + Directory.CreateDirectory(toolchainPath); + Directory.CreateDirectory(Path.Combine(toolchainPath, clangBase)); + Directory.CreateDirectory(Path.Combine(toolchainPath, portableToolchainInfo.Value.OSSHeadersSubdir, "usr")); + + var links = new Dictionary + { + { + Path.Combine(toolchainPath, portableToolchainInfo.Value.OSSHeadersSubdir, "usr", "include"), + Path.Combine(sdkPackagePath, "Xcode.app", "Contents", "Developer", "Platforms", "MacOSX.platform", "Developer", "SDKs", "MacOSX.sdk", "usr", "include") + }, + { + Path.Combine(toolchainPath, clangBase, "include"), + Path.Combine(sdkPackagePath, "Xcode.app", "Contents", "Developer", "Platforms", "MacOSX.platform", "Developer", "SDKs", "MacOSX.sdk", "usr", "include") + }, + { + Path.Combine(toolchainPath, clangBase, "lib"), + Path.Combine(sdkPackagePath, "Xcode.app", "Contents", "Developer", "Toolchains", "XcodeDefault.xctoolchain", "usr", "lib") + }, + { + Path.Combine(toolchainPath, clangBase, "bin"), + Path.Combine(sdkPackagePath, "Xcode.app", "Contents", "Developer", "Toolchains", "XcodeDefault.xctoolchain", "usr", "bin") + }, + }; + + foreach (var kv in links) + { + Directory.CreateSymbolicLink( + kv.Key, + Path.GetRelativePath(kv.Key, kv.Value).Substring(3)); + } + } } public async Task GenerateSdkPackage(string unrealEnginePath, string sdkPackagePath, CancellationToken cancellationToken) { if (OperatingSystem.IsMacOS()) { - var xcodeVersion = await _versionNumberResolver.For(unrealEnginePath).GetXcodeVersion(unrealEnginePath).ConfigureAwait(false); - await InstallXcode(xcodeVersion, sdkPackagePath, cancellationToken); + var versionResolver = _versionNumberResolver.For(unrealEnginePath); + var xcodeVersion = await versionResolver.GetXcodeVersion(unrealEnginePath).ConfigureAwait(false); + var portableToolchainVersion = await versionResolver.GetPortableToolchainVersion(unrealEnginePath, xcodeVersion).ConfigureAwait(false); + await InstallXcode(xcodeVersion, portableToolchainVersion, sdkPackagePath, cancellationToken); } else { @@ -248,7 +302,26 @@ public async Task GenerateSdkPackage(string unrealEnginePath, string sdkPackageP public Task GetAutoSdkMappingsForSdkPackage(string sdkPackagePath, CancellationToken cancellationToken) { - return Task.FromResult(Array.Empty()); + if (OperatingSystem.IsMacOS()) + { + return Task.FromResult(new[] + { + new AutoSdkMapping + { + RelativePathInsideAutoSdkPath = "Apple", + RelativePathInsideSdkPackagePath = "AutoSDK", + }, + new AutoSdkMapping + { + RelativePathInsideAutoSdkPath = "Xcode.app", + RelativePathInsideSdkPackagePath = "Xcode.app", + }, + }); + } + else + { + return Task.FromResult(Array.Empty()); + } } public async Task GetRuntimeEnvironmentForSdkPackage(string sdkPackagePath, CancellationToken cancellationToken) @@ -277,7 +350,7 @@ await _processExecutor.ExecuteAsync( // Emit the environment variable required to use Xcode from the package directory. var envs = new Dictionary { - { "DEVELOPER_DIR", Path.Combine(sdkPackagePath, "Xcode.app") } + { "DEVELOPER_DIR", Path.Combine(sdkPackagePath, "Xcode.app") }, }; var currentPath = Environment.GetEnvironmentVariable("PATH"); if (currentPath != null) diff --git a/UET/Redpoint.Uet.SdkManagement/Sdk/VersionNumbers/EmbeddedMacVersionNumbers.cs b/UET/Redpoint.Uet.SdkManagement/Sdk/VersionNumbers/EmbeddedMacVersionNumbers.cs index c9dc6255..ac2ecc61 100644 --- a/UET/Redpoint.Uet.SdkManagement/Sdk/VersionNumbers/EmbeddedMacVersionNumbers.cs +++ b/UET/Redpoint.Uet.SdkManagement/Sdk/VersionNumbers/EmbeddedMacVersionNumbers.cs @@ -47,6 +47,11 @@ public async Task GetXcodeVersion(string unrealEnginePath) return await ParseXcodeVersion(applePlatformSdk).ConfigureAwait(false); } + public Task GetPortableToolchainVersion(string unrealEnginePath, string xcodeVersion) + { + return Task.FromResult(null); + } + public async Task GetITunesVersion(string unrealEnginePath) { return string.Empty; diff --git a/UET/Redpoint.Uet.SdkManagement/Sdk/VersionNumbers/IMacVersionNumbers.cs b/UET/Redpoint.Uet.SdkManagement/Sdk/VersionNumbers/IMacVersionNumbers.cs index 8c75d897..f8279941 100644 --- a/UET/Redpoint.Uet.SdkManagement/Sdk/VersionNumbers/IMacVersionNumbers.cs +++ b/UET/Redpoint.Uet.SdkManagement/Sdk/VersionNumbers/IMacVersionNumbers.cs @@ -2,11 +2,21 @@ { using System.Runtime.Versioning; + public readonly record struct MacPortableToolchainInfo + { + public required readonly string PortableToolchainVersion { get; init; } + public required readonly string ClangSubdir { get; init; } + public required readonly string OSSHeadersSubdir { get; init; } + } + internal interface IMacVersionNumbers : IVersionNumbers { [SupportedOSPlatform("macos")] Task GetXcodeVersion(string unrealEnginePath); + [SupportedOSPlatform("macos")] + Task GetPortableToolchainVersion(string unrealEnginePath, string xcodeVersion); + [SupportedOSPlatform("windows")] Task GetITunesVersion(string unrealEnginePath); } diff --git a/UET/Redpoint.Uet.SdkManagement/Sdk/VersionNumbers/JsonMacVersionNumbers.cs b/UET/Redpoint.Uet.SdkManagement/Sdk/VersionNumbers/JsonMacVersionNumbers.cs index 8673f786..78f4f95b 100644 --- a/UET/Redpoint.Uet.SdkManagement/Sdk/VersionNumbers/JsonMacVersionNumbers.cs +++ b/UET/Redpoint.Uet.SdkManagement/Sdk/VersionNumbers/JsonMacVersionNumbers.cs @@ -279,6 +279,91 @@ await File.ReadAllTextAsync(engineBuildVersionPath), throw new InvalidOperationException("Unable to read Apple SDK versions from either 'MaxVersion' or 'MainVersion' in Apple_SDK.json!"); } + [SupportedOSPlatform("macos")] + public async Task GetPortableToolchainVersion(string unrealEnginePath, string xcodeVersion) + { + var jsonPath = Path.Combine( + unrealEnginePath, + "Engine", + "Config", + "Apple", + "AppleOpenSource_SDK.json"); + if (!File.Exists(jsonPath)) + { + return null; + } + + if (!Version.TryParse(xcodeVersion, out var xcodeParsedVersion) || xcodeParsedVersion == null) + { + throw new InvalidOperationException($"Unable to parse {xcodeVersion} to generic platform version!"); + } + + var json = await File.ReadAllTextAsync(jsonPath).ConfigureAwait(false); + var dictionary = JsonSerializer.Deserialize( + json, + new JsonConfigJsonSerializerContext(new JsonSerializerOptions + { + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip, + }).DictionaryStringJsonElement); + if (dictionary == null) + { + throw new InvalidOperationException("Unable to read Apple open source SDK versions from Apple_SDK.json!"); + } + + string? clangSubdir = null; + string? ossHeadersSubdir = null; + + if (dictionary.TryGetValue("ClangSubdir", out var clangSubdirElement) && + clangSubdirElement.ValueKind == JsonValueKind.String) + { + clangSubdir = clangSubdirElement.GetString(); + } + if (dictionary.TryGetValue("OSSHeadersSubdir", out var ossHeadersSubdirElement) && + ossHeadersSubdirElement.ValueKind == JsonValueKind.String) + { + ossHeadersSubdir = ossHeadersSubdirElement.GetString(); + } + + string? matchingVersion = null; + if (dictionary.TryGetValue("XcodeToToolchainVersion", out var versionMappings) && + versionMappings.ValueKind == JsonValueKind.Array) + { + var jsonArray = versionMappings.EnumerateArray().ToArray(); + for (int mappingIndex = jsonArray.Length - 1; mappingIndex >= 0; mappingIndex--) + { + var jsonElement = jsonArray[mappingIndex]; + if (jsonElement.ValueKind == JsonValueKind.String) + { + var split = jsonElement.GetString()!.Split('-'); + if (split.Length == 2) + { + if (Version.TryParse(split[0], out var mappingVersion) && xcodeParsedVersion >= mappingVersion) + { + _logger.LogInformation($"Selected Xcode version '{xcodeParsedVersion}' is within bounds '0'-'{mappingVersion}', with toolchain version '{split[1]}'."); + matchingVersion = split[1]; + break; + } + } + } + } + } + + if (!string.IsNullOrWhiteSpace(clangSubdir) && + !string.IsNullOrWhiteSpace(ossHeadersSubdir) && + !string.IsNullOrWhiteSpace(matchingVersion)) + { + return new MacPortableToolchainInfo + { + ClangSubdir = clangSubdir, + OSSHeadersSubdir = ossHeadersSubdir, + PortableToolchainVersion = matchingVersion, + }; + } + + return null; + } + [SupportedOSPlatform("windows")] public async Task GetITunesVersion(string unrealEnginePath) { diff --git a/UET/uet/Commands/Generate/GenerateCommand.cs b/UET/uet/Commands/Generate/GenerateCommand.cs index a50cbb17..1610972a 100644 --- a/UET/uet/Commands/Generate/GenerateCommand.cs +++ b/UET/uet/Commands/Generate/GenerateCommand.cs @@ -426,17 +426,24 @@ public async Task ExecuteAsync(ICommandInvocationContext context) } else if (OperatingSystem.IsMacOS()) { - var solutionPath = Path.Combine(workingDirectory, outputFilenameWithoutExtension + ".xcworkspace"); - if (File.Exists(solutionPath)) + var opened = false; + foreach (var candidateFilename in new[] { outputFilenameWithoutExtension, outputFilenameWithoutExtension + " (Mac)" }) { - _logger.LogInformation($"Opening generated project file due to --open|-o being passed: {solutionPath}"); - Process.Start(new ProcessStartInfo + var solutionPath = Path.Combine(workingDirectory, candidateFilename + ".xcworkspace"); + _logger.LogInformation($"Checking for project: {solutionPath}"); + if (File.Exists(solutionPath) || Directory.Exists(solutionPath)) { - UseShellExecute = true, - FileName = solutionPath, - }); + _logger.LogInformation($"Opening generated project file due to --open|-o being passed: {solutionPath}"); + Process.Start(new ProcessStartInfo + { + UseShellExecute = true, + FileName = solutionPath, + }); + opened = true; + break; + } } - else + if (!opened) { _logger.LogWarning("Unable to automatically open generated project file because it doesn't exist. This can happen if you've configured Unreal Engine to generate project files for something other than Xcode."); } diff --git a/UET/uet/Commands/InstallSdks/InstallSdksCommand.cs b/UET/uet/Commands/InstallSdks/InstallSdksCommand.cs index 0f82b031..62e8ac84 100644 --- a/UET/uet/Commands/InstallSdks/InstallSdksCommand.cs +++ b/UET/uet/Commands/InstallSdks/InstallSdksCommand.cs @@ -79,9 +79,9 @@ public InstallSdksCommandInstance( public async Task ExecuteAsync(ICommandInvocationContext context) { - if (!OperatingSystem.IsWindows()) + if (OperatingSystem.IsLinux()) { - _logger.LogError("This command is not currently supported on non-Windows platforms."); + _logger.LogError("This command is not currently supported on Linux."); return 1; } @@ -103,7 +103,9 @@ public async Task ExecuteAsync(ICommandInvocationContext context) _logger.LogInformation($" - {platform}"); } - var packagePath = UetPaths.UetDefaultWindowsSdkStoragePath; + var packagePath = OperatingSystem.IsWindows() + ? UetPaths.UetDefaultWindowsSdkStoragePath + : UetPaths.UetDefaultMacSdkStoragePath; Directory.CreateDirectory(packagePath); var envVars = await _localSdkManager.SetupEnvironmentForSdkSetups( @@ -112,21 +114,24 @@ public async Task ExecuteAsync(ICommandInvocationContext context) sdkSetups.ToHashSet(), context.GetCancellationToken()).ConfigureAwait(false); - if (!context.ParseResult.GetValueForOption(_options.SkipPermissionUpdate)) + if (OperatingSystem.IsWindows()) { - _logger.LogInformation("Updating permissions on SDK directories so all users have read/write access..."); - await _worldPermissionApplier.GrantEveryonePermissionAsync(packagePath, context.GetCancellationToken()).ConfigureAwait(false); - } + if (!context.ParseResult.GetValueForOption(_options.SkipPermissionUpdate)) + { + _logger.LogInformation("Updating permissions on SDK directories so all users have read/write access..."); + await _worldPermissionApplier.GrantEveryonePermissionAsync(packagePath, context.GetCancellationToken()).ConfigureAwait(false); + } - _logger.LogInformation("Setting environment variables to user scope..."); - foreach (var kv in envVars) - { - _logger.LogInformation($" {kv.Key} = {kv.Value}"); - if (Environment.GetEnvironmentVariable(kv.Key, EnvironmentVariableTarget.User) != kv.Value) + _logger.LogInformation("Setting environment variables to user scope..."); + foreach (var kv in envVars) { - Environment.SetEnvironmentVariable(kv.Key, kv.Value, EnvironmentVariableTarget.User); + _logger.LogInformation($" {kv.Key} = {kv.Value}"); + if (Environment.GetEnvironmentVariable(kv.Key, EnvironmentVariableTarget.User) != kv.Value) + { + Environment.SetEnvironmentVariable(kv.Key, kv.Value, EnvironmentVariableTarget.User); + } + context.GetCancellationToken().ThrowIfCancellationRequested(); } - context.GetCancellationToken().ThrowIfCancellationRequested(); } return 0; diff --git a/UET/uet/Commands/Internal/InstallXcode/InstallXcodeCommand.cs b/UET/uet/Commands/Internal/InstallXcode/InstallXcodeCommand.cs index 47d7ea45..687b3367 100644 --- a/UET/uet/Commands/Internal/InstallXcode/InstallXcodeCommand.cs +++ b/UET/uet/Commands/Internal/InstallXcode/InstallXcodeCommand.cs @@ -96,6 +96,7 @@ public async Task ExecuteAsync(ICommandInvocationContext context) await macSdkSetup.InstallXcode( version, + null, packageWorkingPath, context.GetCancellationToken()); await File.WriteAllTextAsync(Path.Combine(packageWorkingPath, "sdk-ready"), "ready", context.GetCancellationToken()).ConfigureAwait(false);