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 ()) {