|
5 | 5 | using System.Collections.Generic; |
6 | 6 | using System.IO; |
7 | 7 | using System.Linq; |
8 | | - |
9 | 8 | using Xamarin.MacDev.Models; |
10 | 9 |
|
11 | 10 | #nullable enable |
12 | 11 |
|
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"; |
| 12 | +namespace Xamarin.MacDev; |
24 | 13 |
|
25 | | - readonly ICustomLogger log; |
| 14 | +/// <summary> |
| 15 | +/// Manages simulator runtimes via <c>xcrun simctl</c> and <c>xcrun xcodebuild</c>. |
| 16 | +/// </summary> |
| 17 | +public class RuntimeService { |
26 | 18 |
|
27 | | - public RuntimeService (ICustomLogger log) |
28 | | - { |
29 | | - this.log = log ?? throw new ArgumentNullException (nameof (log)); |
30 | | - } |
| 19 | + static readonly string XcrunPath = "/usr/bin/xcrun"; |
31 | 20 |
|
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> (); |
| 21 | + readonly ICustomLogger log; |
41 | 22 |
|
42 | | - var runtimes = SimctlOutputParser.ParseRuntimes (json); |
| 23 | + public RuntimeService (ICustomLogger log) |
| 24 | + { |
| 25 | + this.log = log ?? throw new ArgumentNullException (nameof (log)); |
| 26 | + } |
43 | 27 |
|
44 | | - if (availableOnly) |
45 | | - runtimes.RemoveAll (r => !r.IsAvailable); |
| 28 | + /// <summary> |
| 29 | + /// Lists installed simulator runtimes. Optionally filters by availability. |
| 30 | + /// </summary> |
| 31 | + public List<SimulatorRuntimeInfo> List (bool availableOnly = false) |
| 32 | + { |
| 33 | + var json = RunSimctl ("list", "runtimes", "--json"); |
| 34 | + if (json is null) |
| 35 | + return new List<SimulatorRuntimeInfo> (); |
46 | 36 |
|
47 | | - log.LogInfo ("Found {0} simulator runtime(s).", runtimes.Count); |
48 | | - return runtimes; |
49 | | - } |
| 37 | + var runtimes = SimctlOutputParser.ParseRuntimes (json); |
50 | 38 |
|
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 | | - } |
| 39 | + if (availableOnly) |
| 40 | + runtimes.RemoveAll (r => !r.IsAvailable); |
59 | 41 |
|
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 | | - } |
| 42 | + log.LogInfo ("Found {0} simulator runtime(s).", runtimes.Count); |
| 43 | + return runtimes; |
| 44 | + } |
77 | 45 |
|
78 | | - log.LogInfo ("Downloading {0} platform runtime via xcodebuild...", platform); |
| 46 | + /// <summary> |
| 47 | + /// Lists runtimes for a specific platform (e.g. "iOS", "tvOS", "watchOS", "visionOS"). |
| 48 | + /// </summary> |
| 49 | + public List<SimulatorRuntimeInfo> ListByPlatform (string platform, bool availableOnly = false) |
| 50 | + { |
| 51 | + if (string.IsNullOrEmpty (platform)) |
| 52 | + throw new ArgumentException ("Platform must not be null or empty.", nameof (platform)); |
79 | 53 |
|
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 | | - } |
| 54 | + var all = List (availableOnly); |
| 55 | + return all.Where (r => string.Equals (r.Platform, platform, StringComparison.OrdinalIgnoreCase)).ToList (); |
| 56 | + } |
86 | 57 |
|
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); |
| 58 | + /// <summary> |
| 59 | + /// Downloads a platform runtime using <c>xcrun xcodebuild -downloadPlatform</c>. |
| 60 | + /// </summary> |
| 61 | + /// <param name="platform">The platform to download (e.g. "iOS", "tvOS", "watchOS", "visionOS").</param> |
| 62 | + /// <param name="version">Optional specific version to download (e.g. "17.5").</param> |
| 63 | + /// <returns>True if the download command succeeded.</returns> |
| 64 | + public bool DownloadPlatform (string platform, string? version = null) |
| 65 | + { |
| 66 | + if (string.IsNullOrEmpty (platform)) |
| 67 | + throw new ArgumentException ("Platform must not be null or empty.", nameof (platform)); |
| 68 | + |
| 69 | + log.LogInfo ("Downloading {0} platform runtime via xcodebuild...", platform); |
| 70 | + |
| 71 | + try { |
| 72 | + var args = string.IsNullOrEmpty (version) |
| 73 | + ? new [] { "xcodebuild", "-downloadPlatform", platform } |
| 74 | + : new [] { "xcodebuild", "-downloadPlatform", platform, "-buildVersion", version! }; |
| 75 | + |
| 76 | + var (exitCode, _, stderr) = ProcessUtils.Exec (XcrunPath, args); |
| 77 | + if (exitCode != 0) { |
| 78 | + log.LogInfo ("xcodebuild -downloadPlatform {0} failed (exit {1}): {2}", platform, exitCode, stderr.Trim ()); |
91 | 79 | return false; |
92 | 80 | } |
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 | 81 |
|
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 | | - } |
| 82 | + log.LogInfo ("Successfully downloaded {0} platform runtime.", platform); |
| 83 | + return true; |
| 84 | + } catch (System.ComponentModel.Win32Exception ex) { |
| 85 | + log.LogInfo ("Could not run xcodebuild: {0}", ex.Message); |
| 86 | + return false; |
| 87 | + } catch (InvalidOperationException ex) { |
| 88 | + log.LogInfo ("Could not run xcodebuild: {0}", ex.Message); |
| 89 | + return false; |
| 90 | + } |
| 91 | + } |
120 | 92 |
|
| 93 | + /// <summary> |
| 94 | + /// Runs a simctl subcommand and returns stdout, or null on failure. |
| 95 | + /// </summary> |
| 96 | + string? RunSimctl (params string [] args) |
| 97 | + { |
| 98 | + if (!File.Exists (XcrunPath)) { |
| 99 | + log.LogInfo ("xcrun not found at '{0}'.", XcrunPath); |
121 | 100 | return null; |
122 | 101 | } |
123 | 102 |
|
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 | | - } |
| 103 | + var fullArgs = new string [args.Length + 1]; |
| 104 | + fullArgs [0] = "simctl"; |
| 105 | + Array.Copy (args, 0, fullArgs, 1, args.Length); |
133 | 106 |
|
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); |
| 107 | + try { |
| 108 | + var (exitCode, stdout, stderr) = ProcessUtils.Exec (XcrunPath, fullArgs); |
| 109 | + if (exitCode != 0) { |
| 110 | + log.LogInfo ("simctl {0} failed (exit {1}): {2}", args [0], exitCode, stderr.Trim ()); |
147 | 111 | return null; |
148 | 112 | } |
| 113 | + return stdout; |
| 114 | + } catch (System.ComponentModel.Win32Exception ex) { |
| 115 | + log.LogInfo ("Could not run xcrun: {0}", ex.Message); |
| 116 | + return null; |
| 117 | + } catch (InvalidOperationException ex) { |
| 118 | + log.LogInfo ("Could not run xcrun: {0}", ex.Message); |
| 119 | + return null; |
149 | 120 | } |
150 | 121 | } |
151 | 122 | } |
0 commit comments