diff --git a/Xamarin.MacDev/EnvironmentChecker.cs b/Xamarin.MacDev/EnvironmentChecker.cs
new file mode 100644
index 0000000..022ca07
--- /dev/null
+++ b/Xamarin.MacDev/EnvironmentChecker.cs
@@ -0,0 +1,198 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Xamarin.MacDev.Models;
+
+#nullable enable
+
+namespace Xamarin.MacDev;
+
+///
+/// Performs a comprehensive check of the Apple development environment.
+/// Aggregates results from ,
+/// , and .
+///
+public class EnvironmentChecker {
+
+ static readonly string XcrunPath = "/usr/bin/xcrun";
+
+ readonly ICustomLogger log;
+
+ public EnvironmentChecker (ICustomLogger log)
+ {
+ this.log = log ?? throw new ArgumentNullException (nameof (log));
+ }
+
+ ///
+ /// Runs a full environment check and returns the results.
+ ///
+ public EnvironmentCheckResult Check ()
+ {
+ var result = new EnvironmentCheckResult ();
+
+ result.Xcode = GetBestXcode ();
+
+ if (result.Xcode is not null) {
+ log.LogInfo ("Xcode {0} found at '{1}'.", result.Xcode.Version, result.Xcode.Path);
+
+ if (IsXcodeLicenseAccepted ())
+ log.LogInfo ("Xcode license is accepted.");
+ else
+ log.LogInfo ("Xcode license may not be accepted. Run 'sudo xcodebuild -license accept'.");
+
+ result.Platforms = GetPlatforms (result.Xcode.Path);
+ } else {
+ log.LogInfo ("No Xcode installation found.");
+ }
+
+ try {
+ result.CommandLineTools = CheckCommandLineTools ();
+ } catch (Exception ex) {
+ log.LogInfo ("Could not check Command Line Tools: {0}", ex.Message);
+ }
+
+ try {
+ result.Runtimes = ListRuntimes ();
+ } catch (Exception ex) {
+ log.LogInfo ("Could not check runtimes: {0}", ex.Message);
+ }
+
+ result.DeriveStatus ();
+
+ log.LogInfo ("Environment check complete. Status: {0}.", result.Status);
+ return result;
+ }
+
+ ///
+ /// Returns the best available Xcode installation, or null if none found.
+ ///
+ protected virtual XcodeInfo? GetBestXcode ()
+ {
+ var xcodeManager = new XcodeManager (log);
+ return xcodeManager.GetBest ();
+ }
+
+ ///
+ /// Checks Command Line Tools installation status.
+ ///
+ protected virtual CommandLineToolsInfo CheckCommandLineTools ()
+ {
+ var clt = new CommandLineTools (log);
+ return clt.Check ();
+ }
+
+ ///
+ /// Lists available simulator runtimes.
+ ///
+ protected virtual List ListRuntimes ()
+ {
+ var runtimeService = new RuntimeService (log);
+ return runtimeService.List (availableOnly: true);
+ }
+
+ ///
+ /// Checks whether the Xcode license has been accepted by running
+ /// xcrun xcodebuild -license check.
+ ///
+ public virtual bool IsXcodeLicenseAccepted ()
+ {
+ try {
+ var (exitCode, _, _) = ProcessUtils.Exec (XcrunPath, "xcodebuild", "-license", "check");
+ return exitCode == 0;
+ } catch (System.ComponentModel.Win32Exception) {
+ return false;
+ } catch (InvalidOperationException) {
+ return false;
+ }
+ }
+
+ ///
+ /// Runs xcrun xcodebuild -runFirstLaunch to ensure packages are installed.
+ /// Returns true if the command succeeded.
+ ///
+ public bool RunFirstLaunch ()
+ {
+ try {
+ log.LogInfo ("Running xcodebuild -runFirstLaunch...");
+ var (exitCode, _, stderr) = ProcessUtils.Exec (XcrunPath, "xcodebuild", "-runFirstLaunch");
+ if (exitCode != 0) {
+ log.LogInfo ("xcodebuild -runFirstLaunch failed (exit {0}): {1}", exitCode, stderr.Trim ());
+ return false;
+ }
+
+ log.LogInfo ("xcodebuild -runFirstLaunch completed successfully.");
+ return true;
+ } catch (System.ComponentModel.Win32Exception ex) {
+ log.LogInfo ("Could not run xcodebuild: {0}", ex.Message);
+ return false;
+ } catch (InvalidOperationException ex) {
+ log.LogInfo ("Could not run xcodebuild: {0}", ex.Message);
+ return false;
+ }
+ }
+
+ ///
+ /// Gets the list of available platform SDK directories in the Xcode bundle.
+ ///
+ protected virtual List GetPlatforms (string xcodePath)
+ {
+ var platformsDir = Path.Combine (xcodePath, "Contents", "Developer", "Platforms");
+
+ if (!Directory.Exists (platformsDir))
+ return new List ();
+
+ try {
+ var directoryNames = new List ();
+ foreach (var dir in Directory.GetDirectories (platformsDir, "*.platform"))
+ directoryNames.Add (Path.GetFileNameWithoutExtension (dir));
+
+ return MapDirectoryNamesToPlatforms (directoryNames);
+ } catch (UnauthorizedAccessException ex) {
+ log.LogInfo ("Could not read platforms directory: {0}", ex.Message);
+ return new List ();
+ }
+ }
+
+ ///
+ /// Maps a list of Apple platform directory names (e.g. "iPhoneOS", "MacOSX")
+ /// to deduplicated friendly names (e.g. "iOS", "macOS").
+ ///
+ public static List MapDirectoryNamesToPlatforms (IEnumerable directoryNames)
+ {
+ var platforms = new List ();
+ foreach (var name in directoryNames) {
+ var friendly = MapPlatformName (name);
+ if (!platforms.Contains (friendly))
+ platforms.Add (friendly);
+ }
+ return platforms;
+ }
+
+ ///
+ /// Maps Apple platform directory names to friendly names.
+ ///
+ public static string MapPlatformName (string sdkName)
+ {
+ switch (sdkName) {
+ case "iPhoneOS":
+ case "iPhoneSimulator":
+ return "iOS";
+ case "AppleTVOS":
+ case "AppleTVSimulator":
+ return "tvOS";
+ case "WatchOS":
+ case "WatchSimulator":
+ return "watchOS";
+ case "XROS":
+ case "XRSimulator":
+ return "visionOS";
+ case "MacOSX":
+ return "macOS";
+ default:
+ return sdkName;
+ }
+ }
+}
diff --git a/tests/EnvironmentCheckerTests.cs b/tests/EnvironmentCheckerTests.cs
new file mode 100644
index 0000000..a0b1c48
--- /dev/null
+++ b/tests/EnvironmentCheckerTests.cs
@@ -0,0 +1,259 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using Xamarin.MacDev;
+using Xamarin.MacDev.Models;
+
+#nullable enable
+
+namespace tests;
+
+[TestFixture]
+public class EnvironmentCheckerTests {
+
+ ///
+ /// Subclass that overrides external dependencies so Check() can be
+ /// unit-tested without invoking Xcode, xcode-select, or simctl.
+ ///
+ class TestableEnvironmentChecker : EnvironmentChecker {
+
+ public XcodeInfo? XcodeResult { get; set; }
+ public CommandLineToolsInfo CltResult { get; set; } = new CommandLineToolsInfo ();
+ public List RuntimesResult { get; set; } = new List ();
+ public bool LicenseAccepted { get; set; } = true;
+ public List PlatformsResult { get; set; } = new List ();
+ public bool ThrowOnClt { get; set; }
+ public bool ThrowOnRuntimes { get; set; }
+
+ public TestableEnvironmentChecker () : base (ConsoleLogger.Instance) { }
+
+ protected override XcodeInfo? GetBestXcode () => XcodeResult;
+
+ protected override CommandLineToolsInfo CheckCommandLineTools ()
+ {
+ if (ThrowOnClt)
+ throw new InvalidOperationException ("simulated CLT failure");
+ return CltResult;
+ }
+
+ protected override List ListRuntimes ()
+ {
+ if (ThrowOnRuntimes)
+ throw new InvalidOperationException ("simulated runtime failure");
+ return RuntimesResult;
+ }
+
+ public override bool IsXcodeLicenseAccepted () => LicenseAccepted;
+
+ protected override List GetPlatforms (string xcodePath) => PlatformsResult;
+ }
+
+ // ── Constructor ──
+
+ [Test]
+ public void Constructor_ThrowsOnNullLogger ()
+ {
+ Assert.Throws (() => new EnvironmentChecker (null!));
+ }
+
+ // ── Check() aggregation tests ──
+
+ [Test]
+ public void Check_NoXcode_ReturnsMissing ()
+ {
+ var checker = new TestableEnvironmentChecker {
+ XcodeResult = null,
+ CltResult = new CommandLineToolsInfo { IsInstalled = true },
+ };
+ var result = checker.Check ();
+ Assert.That (result.Xcode, Is.Null);
+ Assert.That (result.Status, Is.EqualTo (EnvironmentStatus.Missing));
+ }
+
+ [Test]
+ public void Check_XcodeAndClt_NoRuntimes_ReturnsPartial ()
+ {
+ var checker = new TestableEnvironmentChecker {
+ XcodeResult = new XcodeInfo { Path = "/Applications/Xcode.app", Version = new Version (16, 2) },
+ CltResult = new CommandLineToolsInfo { IsInstalled = true },
+ RuntimesResult = new List (),
+ PlatformsResult = new List { "iOS", "macOS" },
+ };
+ var result = checker.Check ();
+ Assert.That (result.Status, Is.EqualTo (EnvironmentStatus.Partial));
+ Assert.That (result.Xcode, Is.Not.Null);
+ Assert.That (result.CommandLineTools.IsInstalled, Is.True);
+ Assert.That (result.Platforms, Has.Count.EqualTo (2));
+ }
+
+ [Test]
+ public void Check_EverythingPresent_ReturnsOk ()
+ {
+ var checker = new TestableEnvironmentChecker {
+ XcodeResult = new XcodeInfo { Path = "/Applications/Xcode.app", Version = new Version (16, 2) },
+ CltResult = new CommandLineToolsInfo { IsInstalled = true },
+ RuntimesResult = new List {
+ new SimulatorRuntimeInfo { Platform = "iOS", Version = "18.2", IsAvailable = true },
+ },
+ PlatformsResult = new List { "iOS", "macOS" },
+ };
+ var result = checker.Check ();
+ Assert.That (result.Status, Is.EqualTo (EnvironmentStatus.Ok));
+ }
+
+ [Test]
+ public void Check_CltThrows_DoesNotCrash_ReturnsMissing ()
+ {
+ var checker = new TestableEnvironmentChecker {
+ XcodeResult = new XcodeInfo { Path = "/Applications/Xcode.app", Version = new Version (16, 2) },
+ ThrowOnClt = true,
+ RuntimesResult = new List {
+ new SimulatorRuntimeInfo { Platform = "iOS", Version = "18.2", IsAvailable = true },
+ },
+ };
+ var result = checker.Check ();
+ Assert.That (result.CommandLineTools.IsInstalled, Is.False);
+ Assert.That (result.Status, Is.EqualTo (EnvironmentStatus.Missing));
+ }
+
+ [Test]
+ public void Check_RuntimesThrows_DoesNotCrash ()
+ {
+ var checker = new TestableEnvironmentChecker {
+ XcodeResult = new XcodeInfo { Path = "/Applications/Xcode.app", Version = new Version (16, 2) },
+ CltResult = new CommandLineToolsInfo { IsInstalled = true },
+ ThrowOnRuntimes = true,
+ };
+ var result = checker.Check ();
+ Assert.That (result.Runtimes, Is.Empty);
+ Assert.That (result.Status, Is.EqualTo (EnvironmentStatus.Partial));
+ }
+
+ [Test]
+ public void Check_NoClt_ReturnsMissing ()
+ {
+ var checker = new TestableEnvironmentChecker {
+ XcodeResult = new XcodeInfo { Path = "/Applications/Xcode.app", Version = new Version (16, 2) },
+ CltResult = new CommandLineToolsInfo { IsInstalled = false },
+ RuntimesResult = new List {
+ new SimulatorRuntimeInfo { Platform = "iOS", Version = "18.2", IsAvailable = true },
+ },
+ };
+ var result = checker.Check ();
+ Assert.That (result.Status, Is.EqualTo (EnvironmentStatus.Missing));
+ }
+
+ [Test]
+ public void Check_NoXcode_SkipsPlatformsAndLicense ()
+ {
+ var checker = new TestableEnvironmentChecker {
+ XcodeResult = null,
+ PlatformsResult = new List { "iOS" },
+ };
+ var result = checker.Check ();
+ Assert.That (result.Platforms, Is.Empty);
+ }
+
+ // ── Smoke tests (macOS only) ──
+
+ [Test]
+ [Platform ("MacOsX")]
+ public void Check_DoesNotThrow ()
+ {
+ var checker = new EnvironmentChecker (ConsoleLogger.Instance);
+ Assert.DoesNotThrow (() => checker.Check ());
+ }
+
+ [Test]
+ [Platform ("MacOsX")]
+ public void Check_ReturnsValidResult ()
+ {
+ var checker = new EnvironmentChecker (ConsoleLogger.Instance);
+ var result = checker.Check ();
+ Assert.That (result, Is.Not.Null);
+ Assert.That (result.Status, Is.AnyOf (EnvironmentStatus.Ok, EnvironmentStatus.Partial, EnvironmentStatus.Missing));
+ }
+
+ [Test]
+ [Platform ("MacOsX")]
+ public void IsXcodeLicenseAccepted_DoesNotThrow ()
+ {
+ var checker = new EnvironmentChecker (ConsoleLogger.Instance);
+ Assert.DoesNotThrow (() => checker.IsXcodeLicenseAccepted ());
+ }
+
+ [Test]
+ [Platform ("MacOsX")]
+ public void RunFirstLaunch_DoesNotThrow ()
+ {
+ var checker = new EnvironmentChecker (ConsoleLogger.Instance);
+ Assert.DoesNotThrow (() => checker.RunFirstLaunch ());
+ }
+
+ // ── MapDirectoryNamesToPlatforms ──
+
+ [Test]
+ public void MapDirectoryNamesToPlatforms_DeduplicatesIOS ()
+ {
+ var result = EnvironmentChecker.MapDirectoryNamesToPlatforms (
+ new [] { "iPhoneOS", "iPhoneSimulator", "MacOSX" });
+ Assert.That (result, Is.EqualTo (new List { "iOS", "macOS" }));
+ }
+
+ [Test]
+ public void MapDirectoryNamesToPlatforms_AllApplePlatforms ()
+ {
+ var result = EnvironmentChecker.MapDirectoryNamesToPlatforms (
+ new [] { "iPhoneOS", "iPhoneSimulator", "AppleTVOS", "AppleTVSimulator",
+ "WatchOS", "WatchSimulator", "XROS", "XRSimulator", "MacOSX" });
+ Assert.That (result, Is.EqualTo (new List { "iOS", "tvOS", "watchOS", "visionOS", "macOS" }));
+ }
+
+ [Test]
+ public void MapDirectoryNamesToPlatforms_Empty ()
+ {
+ var result = EnvironmentChecker.MapDirectoryNamesToPlatforms (Array.Empty ());
+ Assert.That (result, Is.Empty);
+ }
+
+ [Test]
+ public void MapDirectoryNamesToPlatforms_UnknownPassedThrough ()
+ {
+ var result = EnvironmentChecker.MapDirectoryNamesToPlatforms (
+ new [] { "DriverKit", "MacOSX" });
+ Assert.That (result, Is.EqualTo (new List { "DriverKit", "macOS" }));
+ }
+
+ // ── MapPlatformName ──
+
+ [TestCase ("iPhoneOS", "iOS")]
+ [TestCase ("iPhoneSimulator", "iOS")]
+ [TestCase ("AppleTVOS", "tvOS")]
+ [TestCase ("AppleTVSimulator", "tvOS")]
+ [TestCase ("WatchOS", "watchOS")]
+ [TestCase ("WatchSimulator", "watchOS")]
+ [TestCase ("XROS", "visionOS")]
+ [TestCase ("XRSimulator", "visionOS")]
+ [TestCase ("MacOSX", "macOS")]
+ [TestCase ("UnknownPlatform", "UnknownPlatform")]
+ public void MapPlatformName_MapsCorrectly (string input, string expected)
+ {
+ Assert.That (EnvironmentChecker.MapPlatformName (input), Is.EqualTo (expected));
+ }
+
+ [Test]
+ public void MapPlatformName_ReturnsSameForEmpty ()
+ {
+ Assert.That (EnvironmentChecker.MapPlatformName (""), Is.EqualTo (""));
+ }
+
+ [Test]
+ public void MapPlatformName_IsCaseSensitive ()
+ {
+ Assert.That (EnvironmentChecker.MapPlatformName ("iphoneos"), Is.EqualTo ("iphoneos"));
+ Assert.That (EnvironmentChecker.MapPlatformName ("MACOSX"), Is.EqualTo ("MACOSX"));
+ }
+}