From 580183d112eae298d523f84f783679c17e2ac249 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 20 May 2026 10:59:06 +0200 Subject: [PATCH 1/3] [msbuild] Improve diagnostics when simulator runtimes aren't available Apple's Xcode tools (actool, ibtool, etc.) require the simulator runtime even when building for physical devices. When the runtime is missing or the wrong version, these tools fail with unhelpful errors or hang indefinitely. After a tool failure, run 'xcrun simctl --json-output= list runtimes' to check whether the required simulator runtime is installed. If missing, emit a clear error (E7170) with instructions on how to install it. Additionally, detect simulator runtime version mismatch errors in tool output (e.g. 'No simulator runtime version from [...] available to use with iphonesimulator SDK version ...') and emit E7172 suggesting the user update their simulator runtime. The simctl check uses a 1-minute timeout (via a linked CancellationTokenSource) to avoid hanging, and results are cached per platform/SdkDevPath. New diagnostics: - E7170: Simulator runtime not installed (post-failure simctl check) - W7171: Unable to determine simulator runtime availability - E7172: Tool error indicates incompatible simulator runtime version Fixes https://github.com/dotnet/macios/issues/25298 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../MSBStrings.resx | 18 +++ .../Tasks/XcodeCompilerToolTask.cs | 103 +++++++++++++++++- 2 files changed, 120 insertions(+), 1 deletion(-) diff --git a/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx b/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx index f85052143906..132f8acd487b 100644 --- a/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx +++ b/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx @@ -1627,4 +1627,22 @@ The task '{0}' requires the property '{1}' to be set. Please file an issue at https://github.com/dotnet/macios/issues/new/choose. + + + 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 e86af43215b6..b87e3ab6155b 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,87 @@ 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.E7170, simPlatform); + return; + } + + var jsonOutputFile = Path.GetTempFileName (); + try { + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource (cancellationTokenSource.Token); + timeoutCts.CancelAfter (TimeSpan.FromMinutes (1)); + var args = new List { "simctl", "--json-output=" + jsonOutputFile, "list", "runtimes" }; + var rv = ExecuteAsync ("xcrun", args, showErrorIfFailure: false, cancellationToken: timeoutCts.Token).Result; + + if (rv.ExitCode != 0) { + Log.LogWarning (MSBStrings.W7171, simPlatform); + simulatorRuntimeCache [cacheKey] = true; + 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.E7170, 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.W7171, simPlatform); + } catch (AggregateException ae) when (ae.InnerException is OperationCanceledException) { + // Timeout + Log.LogWarning (MSBStrings.W7171, simPlatform); + } catch (Exception ex) { + Log.LogWarning (MSBStrings.W7171, 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 (); @@ -220,6 +303,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 +371,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.E7172, simPlatform); + } + } } } @@ -298,6 +390,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 ()) { From 64b247f3e2007370200efab14061cd63cf83171c Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 20 May 2026 11:35:31 +0200 Subject: [PATCH 2/3] Don't cache result when simctl itself fails If simctl exits with a non-zero exit code, don't cache anything so the check will be retried on the next failure. Caching 'true' would suppress future diagnostics even if the runtime is genuinely missing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- msbuild/Xamarin.MacDev.Tasks/Tasks/XcodeCompilerToolTask.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/XcodeCompilerToolTask.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/XcodeCompilerToolTask.cs index b87e3ab6155b..2e4fc01a42ea 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/XcodeCompilerToolTask.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/XcodeCompilerToolTask.cs @@ -206,7 +206,6 @@ void CheckSimulatorRuntimeAvailable () if (rv.ExitCode != 0) { Log.LogWarning (MSBStrings.W7171, simPlatform); - simulatorRuntimeCache [cacheKey] = true; return; } From 826229b22d9213f5b8d0189f736815b483706235 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Fri, 22 May 2026 10:26:33 +0200 Subject: [PATCH 3/3] Fix arguments. --- .../Tasks/XcodeCompilerToolTask.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/XcodeCompilerToolTask.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/XcodeCompilerToolTask.cs index 2e4fc01a42ea..b7cc40ef8675 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/XcodeCompilerToolTask.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/XcodeCompilerToolTask.cs @@ -201,7 +201,13 @@ void CheckSimulatorRuntimeAvailable () try { using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource (cancellationTokenSource.Token); timeoutCts.CancelAfter (TimeSpan.FromMinutes (1)); - var args = new List { "simctl", "--json-output=" + jsonOutputFile, "list", "runtimes" }; + 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) { @@ -275,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); @@ -288,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 {