diff --git a/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx b/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx index 5f68eea415e..5de6bb55b80 100644 --- a/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx +++ b/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx @@ -1643,4 +1643,22 @@ The settings file '{0}' is deprecated, and will be ignored. Please use the 'DEVELOPER_DIR' environment variable or the 'XcodeLocation' MSBuild property to choose which Xcode to use. + + + The {0} simulator runtime is not installed. This is required by Apple's development tools (even when building for physical devices). Install it by running 'xcodebuild -downloadPlatform {0}' from the command line, or from Xcode (Settings > Components). + Shown when the required simulator runtime is not installed. +{0} - The platform name (e.g. "iOS" or "tvOS"). + + + + Unable to determine if the {0} simulator runtime is installed. If the build fails or hangs, install the {0} simulator runtime by running 'xcodebuild -downloadPlatform {0}' from the command line. + Shown when we're unable to check for simulator runtime availability. +{0} - The platform name (e.g. "iOS" or "tvOS"). + + + + The installed {0} simulator runtime is not compatible with the current Xcode version. Update the simulator runtime by running 'xcodebuild -downloadPlatform {0}' from the command line, or from Xcode (Settings > Components). + Shown when the tool reports a simulator runtime version mismatch. +{0} - The platform name (e.g. "iOS" or "tvOS"). + diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/XcodeCompilerToolTask.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/XcodeCompilerToolTask.cs index 419963539f9..c189dde986e 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/XcodeCompilerToolTask.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/XcodeCompilerToolTask.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Text; @@ -13,6 +14,7 @@ using Xamarin.Localization.MSBuild; using Xamarin.MacDev; +using Xamarin.MacDev.Models; using Xamarin.Messaging.Build.Client; using Xamarin.Utils; @@ -154,6 +156,92 @@ static bool IsTranslated () return translated.Value; } + // Cache simulator runtime check results to avoid running simctl multiple times. + // Key includes SdkDevPath because different Xcode installations may have different runtimes. + static ConcurrentDictionary simulatorRuntimeCache = new (); + + /// + /// Returns the platform name used by simctl for the current build platform, or null if no simulator is needed. + /// + string? GetSimulatorPlatformName () + { + switch (Platform) { + case ApplePlatform.iOS: + case ApplePlatform.MacCatalyst: + // Mac Catalyst uses the iOS-based toolchain, so it also needs the iOS simulator runtime. + return "iOS"; + case ApplePlatform.TVOS: + return "tvOS"; + case ApplePlatform.MacOSX: + default: + return null; + } + } + + /// + /// Checks if the required simulator runtime is installed and emits a diagnostic if not. + /// Apple's Xcode tools (actool, ibtool, etc.) require the simulator runtime to function, + /// even when building for physical devices. Call this after a tool failure to provide + /// actionable guidance to the user. + /// + void CheckSimulatorRuntimeAvailable () + { + var simPlatform = GetSimulatorPlatformName (); + if (simPlatform is null) + return; + + var cacheKey = $"{simPlatform}:{SdkDevPath}"; + if (simulatorRuntimeCache.TryGetValue (cacheKey, out var cachedResult)) { + if (!cachedResult) + Log.LogError (MSBStrings.E7175, simPlatform); + return; + } + + var jsonOutputFile = Path.GetTempFileName (); + try { + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource (cancellationTokenSource.Token); + timeoutCts.CancelAfter (TimeSpan.FromMinutes (1)); + var args = new List { + "simctl", + "list", + "runtimes", + "-j", + "--json-output=" + jsonOutputFile + }; + var rv = ExecuteAsync ("xcrun", args, showErrorIfFailure: false, cancellationToken: timeoutCts.Token).Result; + + if (rv.ExitCode != 0) { + Log.LogWarning (MSBStrings.W7176, simPlatform); + return; + } + + var json = File.ReadAllText (jsonOutputFile); + var runtimes = SimctlOutputParser.ParseRuntimes (json); + + var hasRuntime = runtimes.Any (r => + string.Equals (r.Platform, simPlatform, StringComparison.OrdinalIgnoreCase) && r.IsAvailable); + + simulatorRuntimeCache [cacheKey] = hasRuntime; + if (!hasRuntime) + Log.LogError (MSBStrings.E7175, simPlatform); + } catch (OperationCanceledException) when (cancellationTokenSource.IsCancellationRequested) { + // User cancelled - don't emit diagnostics + } catch (AggregateException ae) when (ae.InnerException is OperationCanceledException && cancellationTokenSource.IsCancellationRequested) { + // User cancelled - don't emit diagnostics + } catch (OperationCanceledException) { + // Timeout + Log.LogWarning (MSBStrings.W7176, simPlatform); + } catch (AggregateException ae) when (ae.InnerException is OperationCanceledException) { + // Timeout + Log.LogWarning (MSBStrings.W7176, simPlatform); + } catch (Exception ex) { + Log.LogWarning (MSBStrings.W7176, simPlatform); + Log.LogMessage (MessageImportance.Low, "Exception while checking simulator runtime: {0}", ex.Message); + } finally { + File.Delete (jsonOutputFile); + } + } + protected int Compile (ITaskItem [] items, string output, ITaskItem manifest) { var environment = new Dictionary (); @@ -193,7 +281,7 @@ protected int Compile (ITaskItem [] items, string output, ITaskItem manifest) if (Log.HasLoggedErrors) return 1; - var rv = ExecuteAsync (executable, args, environment: environment, cancellationToken: cancellationTokenSource.Token).Result; + var rv = ExecuteAsync (executable, args, showErrorIfFailure: true, environment: environment, cancellationToken: cancellationTokenSource.Token).Result; var exitCode = rv.ExitCode; var messages = rv.Output.StandardOutput; File.WriteAllText (manifest.ItemSpec, messages); @@ -206,8 +294,6 @@ protected int Compile (ITaskItem [] items, string output, ITaskItem manifest) if (errors.Length > 0) Log.LogError (null, null, null, items [0].ItemSpec, 0, 0, 0, 0, "{0}", errors); - Log.LogError (MSBStrings.E0117, ToolName, exitCode); - // Note: If the log file exists and is parseable, log those warnings/errors as well... if (File.Exists (manifest.ItemSpec)) { try { @@ -220,6 +306,9 @@ protected int Compile (ITaskItem [] items, string output, ITaskItem manifest) File.Delete (manifest.ItemSpec); } + + // Check if the failure might be caused by a missing simulator runtime. + CheckSimulatorRuntimeAvailable (); } return exitCode; @@ -285,8 +374,14 @@ protected void LogWarningsAndErrors (PDictionary plist, ITaskItem file) if (plist.TryGetValue (string.Format ("com.apple.{0}.errors", ToolName), out array)) { foreach (var item in array.OfType ()) { - if (item.TryGetValue ("description", out message)) + if (item.TryGetValue ("description", out message)) { Log.LogError (ToolName, null, null, file.ItemSpec, 0, 0, 0, 0, "{0}", message.Value); + if (IsSimulatorRuntimeVersionError (message.Value)) { + var simPlatform = GetSimulatorPlatformName (); + if (simPlatform is not null) + Log.LogError (MSBStrings.E7177, simPlatform); + } + } } } @@ -298,6 +393,15 @@ protected void LogWarningsAndErrors (PDictionary plist, ITaskItem file) } } + /// + /// Detects error messages like "No simulator runtime version from [...] available to use with ... SDK version ..." + /// which indicate an incompatible or missing simulator runtime version. + /// + static bool IsSimulatorRuntimeVersionError (string message) + { + return message.IndexOf ("simulator runtime", StringComparison.OrdinalIgnoreCase) >= 0; + } + public void Cancel () { if (ShouldExecuteRemotely ()) {