Skip to content

Commit b042e99

Browse files
rmarinhoCopilot
andcommitted
Add environment check APIs
Add EnvironmentChecker that performs a comprehensive check of the Apple development environment by aggregating results from CommandLineTools, XcodeManager, and RuntimeService. Includes Xcode license validation (xcodebuild -license check) and first-launch support (xcodebuild -runFirstLaunch), patterns from ClientTools.Platform iOSSshCommandsExtensions. Also maps platform SDK directory names to friendly names (e.g. iPhoneOS -> iOS, XROS -> visionOS). Includes dependencies from PRs #156, #157, #159 which will merge cleanly when those PRs land first. Closes #148 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 028ff5e commit b042e99

6 files changed

Lines changed: 703 additions & 231 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.IO;
6+
using System.Linq;
7+
8+
using Xamarin.MacDev.Models;
9+
10+
#nullable enable
11+
12+
namespace Xamarin.MacDev {
13+
14+
/// <summary>
15+
/// Performs a comprehensive check of the Apple development environment.
16+
/// Aggregates results from <see cref="CommandLineTools"/>,
17+
/// <see cref="XcodeManager"/>, and <see cref="RuntimeService"/>.
18+
/// Also validates Xcode license acceptance and first-launch state,
19+
/// patterns from ClientTools.Platform iOSSshCommandsExtensions.
20+
/// </summary>
21+
public class EnvironmentChecker {
22+
23+
readonly ICustomLogger log;
24+
25+
public EnvironmentChecker (ICustomLogger log)
26+
{
27+
this.log = log ?? throw new ArgumentNullException (nameof (log));
28+
}
29+
30+
/// <summary>
31+
/// Runs a full environment check and returns the results.
32+
/// </summary>
33+
public EnvironmentCheckResult Check ()
34+
{
35+
var result = new EnvironmentCheckResult ();
36+
37+
// 1. Check Xcode
38+
var xcodeManager = new XcodeManager (log);
39+
var xcode = xcodeManager.GetBest ();
40+
result.Xcode = xcode;
41+
42+
if (xcode is not null) {
43+
log.LogInfo ("Xcode {0} found at '{1}'.", xcode.Version, xcode.Path);
44+
45+
// Check license acceptance (pattern from ClientTools.Platform)
46+
if (IsXcodeLicenseAccepted (xcode.Path))
47+
log.LogInfo ("Xcode license is accepted.");
48+
else
49+
log.LogInfo ("Xcode license may not be accepted. Run 'sudo xcodebuild -license accept'.");
50+
51+
// Collect platform SDKs
52+
result.Platforms = GetPlatforms (xcode.Path);
53+
} else {
54+
log.LogInfo ("No Xcode installation found.");
55+
}
56+
57+
// 2. Check Command Line Tools
58+
var clt = new CommandLineTools (log);
59+
result.CommandLineTools = clt.Check ();
60+
61+
// 3. Check runtimes
62+
var runtimeService = new RuntimeService (log);
63+
result.Runtimes = runtimeService.List (availableOnly: true);
64+
65+
// 4. Derive overall status
66+
result.DeriveStatus ();
67+
68+
log.LogInfo ("Environment check complete. Status: {0}.", result.Status);
69+
return result;
70+
}
71+
72+
/// <summary>
73+
/// Checks whether the Xcode license has been accepted by running
74+
/// <c>xcodebuild -license check</c>.
75+
/// Pattern from ClientTools.Platform iOSSshCommandsExtensions.CheckXcodeLicenseAsync.
76+
/// </summary>
77+
public bool IsXcodeLicenseAccepted (string xcodePath)
78+
{
79+
var xcodebuildPath = Path.Combine (xcodePath, "Contents", "Developer", "usr", "bin", "xcodebuild");
80+
if (!File.Exists (xcodebuildPath))
81+
return false;
82+
83+
try {
84+
var (exitCode, _, _) = ProcessUtils.Exec (xcodebuildPath, "-license", "check");
85+
return exitCode == 0;
86+
} catch (System.ComponentModel.Win32Exception) {
87+
return false;
88+
}
89+
}
90+
91+
/// <summary>
92+
/// Runs <c>xcodebuild -runFirstLaunch</c> to ensure packages are installed.
93+
/// Pattern from ClientTools.Platform iOSSshCommandsExtensions.RunXcodeBuildFirstLaunchAsync.
94+
/// Returns true if the command succeeded.
95+
/// </summary>
96+
public bool RunFirstLaunch (string xcodePath)
97+
{
98+
var xcodebuildPath = Path.Combine (xcodePath, "Contents", "Developer", "usr", "bin", "xcodebuild");
99+
if (!File.Exists (xcodebuildPath)) {
100+
log.LogInfo ("xcodebuild not found at '{0}'.", xcodebuildPath);
101+
return false;
102+
}
103+
104+
try {
105+
log.LogInfo ("Running xcodebuild -runFirstLaunch...");
106+
var (exitCode, _, stderr) = ProcessUtils.Exec (xcodebuildPath, "-runFirstLaunch");
107+
if (exitCode != 0) {
108+
log.LogInfo ("xcodebuild -runFirstLaunch failed (exit {0}): {1}", exitCode, stderr.Trim ());
109+
return false;
110+
}
111+
112+
log.LogInfo ("xcodebuild -runFirstLaunch completed successfully.");
113+
return true;
114+
} catch (System.ComponentModel.Win32Exception ex) {
115+
log.LogInfo ("Could not run xcodebuild: {0}", ex.Message);
116+
return false;
117+
}
118+
}
119+
120+
/// <summary>
121+
/// Gets the list of available platform SDK directories in the Xcode bundle.
122+
/// </summary>
123+
System.Collections.Generic.List<string> GetPlatforms (string xcodePath)
124+
{
125+
var platforms = new System.Collections.Generic.List<string> ();
126+
var platformsDir = Path.Combine (xcodePath, "Contents", "Developer", "Platforms");
127+
128+
if (!Directory.Exists (platformsDir))
129+
return platforms;
130+
131+
try {
132+
foreach (var dir in Directory.GetDirectories (platformsDir, "*.platform")) {
133+
var name = Path.GetFileNameWithoutExtension (dir);
134+
// Convert "iPhoneOS" to "iOS", "AppleTVOS" to "tvOS", etc.
135+
var friendly = MapPlatformName (name);
136+
if (!platforms.Contains (friendly))
137+
platforms.Add (friendly);
138+
}
139+
} catch (UnauthorizedAccessException ex) {
140+
log.LogInfo ("Could not read platforms directory: {0}", ex.Message);
141+
}
142+
143+
return platforms;
144+
}
145+
146+
/// <summary>
147+
/// Maps Apple platform directory names to friendly names.
148+
/// </summary>
149+
public static string MapPlatformName (string sdkName)
150+
{
151+
switch (sdkName) {
152+
case "iPhoneOS":
153+
case "iPhoneSimulator":
154+
return "iOS";
155+
case "AppleTVOS":
156+
case "AppleTVSimulator":
157+
return "tvOS";
158+
case "WatchOS":
159+
case "WatchSimulator":
160+
return "watchOS";
161+
case "XROS":
162+
case "XRSimulator":
163+
return "visionOS";
164+
case "MacOSX":
165+
return "macOS";
166+
default:
167+
return sdkName;
168+
}
169+
}
170+
}
171+
}

Xamarin.MacDev/RuntimeService.cs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
9+
using Xamarin.MacDev.Models;
10+
11+
#nullable enable
12+
13+
namespace Xamarin.MacDev {
14+
15+
/// <summary>
16+
/// Manages simulator runtimes via <c>xcrun simctl</c> and <c>xcodebuild</c>.
17+
/// Lists installed runtimes and supports downloading new platform runtimes.
18+
/// Download approach from ClientTools.Platform: <c>xcodebuild -downloadPlatform iOS</c>.
19+
/// </summary>
20+
public class RuntimeService {
21+
22+
static readonly string XcrunPath = "/usr/bin/xcrun";
23+
static readonly string XcodebuildRelativePath = "Contents/Developer/usr/bin/xcodebuild";
24+
25+
readonly ICustomLogger log;
26+
27+
public RuntimeService (ICustomLogger log)
28+
{
29+
this.log = log ?? throw new ArgumentNullException (nameof (log));
30+
}
31+
32+
/// <summary>
33+
/// Lists installed simulator runtimes. Optionally filters by availability.
34+
/// Uses <c>xcrun simctl list runtimes --json</c>.
35+
/// </summary>
36+
public List<SimulatorRuntimeInfo> List (bool availableOnly = false)
37+
{
38+
var json = RunSimctl ("list", "runtimes", "--json");
39+
if (json is null)
40+
return new List<SimulatorRuntimeInfo> ();
41+
42+
var runtimes = SimctlOutputParser.ParseRuntimes (json);
43+
44+
if (availableOnly)
45+
runtimes.RemoveAll (r => !r.IsAvailable);
46+
47+
log.LogInfo ("Found {0} simulator runtime(s).", runtimes.Count);
48+
return runtimes;
49+
}
50+
51+
/// <summary>
52+
/// Lists runtimes for a specific platform (e.g. "iOS", "tvOS", "watchOS", "visionOS").
53+
/// </summary>
54+
public List<SimulatorRuntimeInfo> ListByPlatform (string platform, bool availableOnly = false)
55+
{
56+
var all = List (availableOnly);
57+
return all.Where (r => string.Equals (r.Platform, platform, StringComparison.OrdinalIgnoreCase)).ToList ();
58+
}
59+
60+
/// <summary>
61+
/// Downloads a platform runtime using <c>xcodebuild -downloadPlatform</c>.
62+
/// Pattern from ClientTools.Platform RemoteSimulatorValidator.
63+
/// </summary>
64+
/// <param name="platform">The platform to download (e.g. "iOS", "tvOS", "watchOS", "visionOS").</param>
65+
/// <param name="xcodePath">The Xcode.app path. If null, looks for xcodebuild in PATH via xcrun.</param>
66+
/// <returns>True if the download command succeeded.</returns>
67+
public bool DownloadPlatform (string platform, string? xcodePath = null)
68+
{
69+
if (string.IsNullOrEmpty (platform))
70+
throw new ArgumentException ("Platform must not be null or empty.", nameof (platform));
71+
72+
var xcodebuildPath = ResolveXcodebuild (xcodePath);
73+
if (xcodebuildPath is null) {
74+
log.LogInfo ("Cannot download platform: xcodebuild not found.");
75+
return false;
76+
}
77+
78+
log.LogInfo ("Downloading {0} platform runtime via xcodebuild...", platform);
79+
80+
try {
81+
var (exitCode, stdout, stderr) = ProcessUtils.Exec (xcodebuildPath, "-downloadPlatform", platform);
82+
if (exitCode != 0) {
83+
log.LogInfo ("xcodebuild -downloadPlatform {0} failed (exit {1}): {2}", platform, exitCode, stderr.Trim ());
84+
return false;
85+
}
86+
87+
log.LogInfo ("Successfully downloaded {0} platform runtime.", platform);
88+
return true;
89+
} catch (System.ComponentModel.Win32Exception ex) {
90+
log.LogInfo ("Could not run xcodebuild: {0}", ex.Message);
91+
return false;
92+
}
93+
}
94+
95+
/// <summary>
96+
/// Resolves the path to xcodebuild. If xcodePath is given, looks inside the Xcode bundle.
97+
/// Otherwise falls back to /usr/bin/xcrun xcodebuild.
98+
/// </summary>
99+
string? ResolveXcodebuild (string? xcodePath)
100+
{
101+
if (!string.IsNullOrEmpty (xcodePath)) {
102+
var path = Path.Combine (xcodePath!, XcodebuildRelativePath);
103+
if (File.Exists (path))
104+
return path;
105+
}
106+
107+
// Fall back to xcrun to find xcodebuild
108+
if (File.Exists (XcrunPath)) {
109+
try {
110+
var (exitCode, stdout, _) = ProcessUtils.Exec (XcrunPath, "--find", "xcodebuild");
111+
if (exitCode == 0) {
112+
var path = stdout.Trim ();
113+
if (File.Exists (path))
114+
return path;
115+
}
116+
} catch (System.ComponentModel.Win32Exception) {
117+
// fall through
118+
}
119+
}
120+
121+
return null;
122+
}
123+
124+
/// <summary>
125+
/// Runs a simctl subcommand and returns stdout, or null on failure.
126+
/// </summary>
127+
string? RunSimctl (params string [] args)
128+
{
129+
if (!File.Exists (XcrunPath)) {
130+
log.LogInfo ("xcrun not found at '{0}'.", XcrunPath);
131+
return null;
132+
}
133+
134+
var fullArgs = new string [args.Length + 1];
135+
fullArgs [0] = "simctl";
136+
Array.Copy (args, 0, fullArgs, 1, args.Length);
137+
138+
try {
139+
var (exitCode, stdout, stderr) = ProcessUtils.Exec (XcrunPath, fullArgs);
140+
if (exitCode != 0) {
141+
log.LogInfo ("simctl {0} failed (exit {1}): {2}", args [0], exitCode, stderr.Trim ());
142+
return null;
143+
}
144+
return stdout;
145+
} catch (System.ComponentModel.Win32Exception ex) {
146+
log.LogInfo ("Could not run xcrun: {0}", ex.Message);
147+
return null;
148+
}
149+
}
150+
}
151+
}

0 commit comments

Comments
 (0)