Skip to content

Commit b39604e

Browse files
authored
Check for simulator runtime in flutter doctor (flutter#131795)
Redo of flutter#130728 - code is the same as before. That PR was stuck in Google testing and then I messed up the rebase so started over. ---- Starting in Xcode 15, the simulator is no longer included in Xcode and must be downloaded and installed separately. This adds a validation to `flutter doctor` to warn when the needed simulator runtime is missing. Validation message looks like: ``` [!] Xcode - develop for iOS and macOS (Xcode 15.0) ! iOS 17.0 Simulator not installed; this may be necessary for iOS and macOS development. To download and install the platform, open Xcode, select Xcode > Settings > Platforms, and click the GET button for the required platform. For more information, please visit: https://developer.apple.com/documentation/xcode/installing-additional-simulator-runtimes ``` It may also show an error like this when something goes wrong when checking for the simulator: ``` [!] Xcode - develop for iOS and macOS (Xcode 15.0) � Unable to find the iPhone Simulator SDK. ``` Note: I'm unsure of in the future if the SDK and the simulator runtime will need to match the exact version or just the major. For now, it only checks against the major version. Part 3 of flutter#129558.
1 parent 1cf3907 commit b39604e

File tree

8 files changed

+617
-14
lines changed

8 files changed

+617
-14
lines changed

packages/flutter_tools/lib/src/doctor.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,14 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
138138
if (androidWorkflow!.appliesToHostPlatform)
139139
GroupedValidator(<DoctorValidator>[androidValidator!, androidLicenseValidator!]),
140140
if (globals.iosWorkflow!.appliesToHostPlatform || macOSWorkflow.appliesToHostPlatform)
141-
GroupedValidator(<DoctorValidator>[XcodeValidator(xcode: globals.xcode!, userMessages: userMessages), globals.cocoapodsValidator!]),
141+
GroupedValidator(<DoctorValidator>[
142+
XcodeValidator(
143+
xcode: globals.xcode!,
144+
userMessages: userMessages,
145+
iosSimulatorUtils: globals.iosSimulatorUtils!,
146+
),
147+
globals.cocoapodsValidator!,
148+
]),
142149
if (webWorkflow.appliesToHostPlatform)
143150
ChromeValidator(
144151
chromiumLauncher: ChromiumLauncher(

packages/flutter_tools/lib/src/ios/simulators.dart

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import '../base/io.dart';
1515
import '../base/logger.dart';
1616
import '../base/process.dart';
1717
import '../base/utils.dart';
18+
import '../base/version.dart';
1819
import '../build_info.dart';
1920
import '../convert.dart';
2021
import '../devfs.dart';
@@ -91,6 +92,14 @@ class IOSSimulatorUtils {
9192
);
9293
}).whereType<IOSSimulator>().toList();
9394
}
95+
96+
Future<List<IOSSimulatorRuntime>> getAvailableIOSRuntimes() async {
97+
if (!_xcode.isInstalledAndMeetsVersionCheck) {
98+
return <IOSSimulatorRuntime>[];
99+
}
100+
101+
return _simControl.listAvailableIOSRuntimes();
102+
}
94103
}
95104

96105
/// A wrapper around the `simctl` command line tool.
@@ -293,6 +302,46 @@ class SimControl {
293302
_logger.printError('Unable to take screenshot of $deviceId:\n$exception');
294303
}
295304
}
305+
306+
/// Runs `simctl list runtimes available iOS --json` and returns all available iOS simulator runtimes.
307+
Future<List<IOSSimulatorRuntime>> listAvailableIOSRuntimes() async {
308+
final List<IOSSimulatorRuntime> runtimes = <IOSSimulatorRuntime>[];
309+
final RunResult results = await _processUtils.run(
310+
<String>[
311+
..._xcode.xcrunCommand(),
312+
'simctl',
313+
'list',
314+
'runtimes',
315+
'available',
316+
'iOS',
317+
'--json',
318+
],
319+
);
320+
321+
if (results.exitCode != 0) {
322+
_logger.printError('Error executing simctl: ${results.exitCode}\n${results.stderr}');
323+
return runtimes;
324+
}
325+
326+
try {
327+
final Object? decodeResult = (json.decode(results.stdout) as Map<String, Object?>)['runtimes'];
328+
if (decodeResult is List<Object?>) {
329+
for (final Object? runtimeData in decodeResult) {
330+
if (runtimeData is Map<String, Object?>) {
331+
runtimes.add(IOSSimulatorRuntime.fromJson(runtimeData));
332+
}
333+
}
334+
}
335+
336+
return runtimes;
337+
} on FormatException {
338+
// We failed to parse the simctl output, or it returned junk.
339+
// One known message is "Install Started" isn't valid JSON but is
340+
// returned sometimes.
341+
_logger.printError('simctl returned non-JSON response: ${results.stdout}');
342+
return runtimes;
343+
}
344+
}
296345
}
297346

298347

@@ -624,6 +673,64 @@ class IOSSimulator extends Device {
624673
}
625674
}
626675

676+
class IOSSimulatorRuntime {
677+
IOSSimulatorRuntime._({
678+
this.bundlePath,
679+
this.buildVersion,
680+
this.platform,
681+
this.runtimeRoot,
682+
this.identifier,
683+
this.version,
684+
this.isInternal,
685+
this.isAvailable,
686+
this.name,
687+
});
688+
689+
// Example:
690+
// {
691+
// "bundlePath" : "\/Library\/Developer\/CoreSimulator\/Volumes\/iOS_21A5277g\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS 17.0.simruntime",
692+
// "buildversion" : "21A5277g",
693+
// "platform" : "iOS",
694+
// "runtimeRoot" : "\/Library\/Developer\/CoreSimulator\/Volumes\/iOS_21A5277g\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS 17.0.simruntime\/Contents\/Resources\/RuntimeRoot",
695+
// "identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-17-0",
696+
// "version" : "17.0",
697+
// "isInternal" : false,
698+
// "isAvailable" : true,
699+
// "name" : "iOS 17.0",
700+
// "supportedDeviceTypes" : [
701+
// {
702+
// "bundlePath" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/DeviceTypes\/iPhone 8.simdevicetype",
703+
// "name" : "iPhone 8",
704+
// "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-8",
705+
// "productFamily" : "iPhone"
706+
// }
707+
// ]
708+
// },
709+
factory IOSSimulatorRuntime.fromJson(Map<String, Object?> data) {
710+
return IOSSimulatorRuntime._(
711+
bundlePath: data['bundlePath']?.toString(),
712+
buildVersion: data['buildversion']?.toString(),
713+
platform: data['platform']?.toString(),
714+
runtimeRoot: data['runtimeRoot']?.toString(),
715+
identifier: data['identifier']?.toString(),
716+
version: Version.parse(data['version']?.toString()),
717+
isInternal: data['isInternal'] is bool? ? data['isInternal'] as bool? : null,
718+
isAvailable: data['isAvailable'] is bool? ? data['isAvailable'] as bool? : null,
719+
name: data['name']?.toString(),
720+
);
721+
}
722+
723+
final String? bundlePath;
724+
final String? buildVersion;
725+
final String? platform;
726+
final String? runtimeRoot;
727+
final String? identifier;
728+
final Version? version;
729+
final bool? isInternal;
730+
final bool? isAvailable;
731+
final String? name;
732+
}
733+
627734
/// Launches the device log reader process on the host and parses the syslog.
628735
@visibleForTesting
629736
Future<Process> launchDeviceSystemLogTool(IOSSimulator device) async {

packages/flutter_tools/lib/src/macos/xcode.dart

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ class Xcode {
4848
_fileSystem = fileSystem,
4949
_xcodeProjectInterpreter = xcodeProjectInterpreter,
5050
_processUtils =
51-
ProcessUtils(logger: logger, processManager: processManager);
51+
ProcessUtils(logger: logger, processManager: processManager),
52+
_logger = logger;
5253

5354
/// Create an [Xcode] for testing.
5455
///
@@ -60,16 +61,18 @@ class Xcode {
6061
XcodeProjectInterpreter? xcodeProjectInterpreter,
6162
Platform? platform,
6263
FileSystem? fileSystem,
64+
Logger? logger,
6365
}) {
6466
platform ??= FakePlatform(
6567
operatingSystem: 'macos',
6668
environment: <String, String>{},
6769
);
70+
logger ??= BufferLogger.test();
6871
return Xcode(
6972
platform: platform,
7073
processManager: processManager,
7174
fileSystem: fileSystem ?? MemoryFileSystem.test(),
72-
logger: BufferLogger.test(),
75+
logger: logger,
7376
xcodeProjectInterpreter: xcodeProjectInterpreter ?? XcodeProjectInterpreter.test(processManager: processManager),
7477
);
7578
}
@@ -78,6 +81,7 @@ class Xcode {
7881
final ProcessUtils _processUtils;
7982
final FileSystem _fileSystem;
8083
final XcodeProjectInterpreter _xcodeProjectInterpreter;
84+
final Logger _logger;
8185

8286
bool get isInstalledAndMeetsVersionCheck => _platform.isMacOS && isInstalled && isRequiredVersionSatisfactory;
8387

@@ -198,6 +202,19 @@ class Xcode {
198202
final String appPath = _fileSystem.path.join(selectPath, 'Applications', 'Simulator.app');
199203
return _fileSystem.directory(appPath).existsSync() ? appPath : null;
200204
}
205+
206+
/// Gets the version number of the platform for the selected SDK.
207+
Future<Version?> sdkPlatformVersion(EnvironmentType environmentType) async {
208+
final RunResult runResult = await _processUtils.run(
209+
<String>[...xcrunCommand(), '--sdk', getSDKNameForIOSEnvironmentType(environmentType), '--show-sdk-platform-version'],
210+
);
211+
if (runResult.exitCode != 0) {
212+
_logger.printError('Could not find SDK Platform Version: ${runResult.stderr}');
213+
return null;
214+
}
215+
final String versionString = runResult.stdout.trim();
216+
return Version.parse(versionString);
217+
}
201218
}
202219

203220
EnvironmentType? environmentTypeFromSdkroot(String sdkroot, FileSystem fileSystem) {

packages/flutter_tools/lib/src/macos/xcode_validator.dart

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,32 @@
33
// found in the LICENSE file.
44

55
import '../base/user_messages.dart';
6+
import '../base/version.dart';
7+
import '../build_info.dart';
68
import '../doctor_validator.dart';
9+
import '../ios/simulators.dart';
710
import 'xcode.dart';
811

12+
String _iOSSimulatorMissing(String version) => '''
13+
iOS $version Simulator not installed; this may be necessary for iOS and macOS development.
14+
To download and install the platform, open Xcode, select Xcode > Settings > Platforms,
15+
and click the GET button for the required platform.
16+
17+
For more information, please visit:
18+
https://developer.apple.com/documentation/xcode/installing-additional-simulator-runtimes''';
19+
920
class XcodeValidator extends DoctorValidator {
1021
XcodeValidator({
1122
required Xcode xcode,
23+
required IOSSimulatorUtils iosSimulatorUtils,
1224
required UserMessages userMessages,
13-
}) : _xcode = xcode,
14-
_userMessages = userMessages,
15-
super('Xcode - develop for iOS and macOS');
25+
}) : _xcode = xcode,
26+
_iosSimulatorUtils = iosSimulatorUtils,
27+
_userMessages = userMessages,
28+
super('Xcode - develop for iOS and macOS');
1629

1730
final Xcode _xcode;
31+
final IOSSimulatorUtils _iosSimulatorUtils;
1832
final UserMessages _userMessages;
1933

2034
@override
@@ -57,6 +71,11 @@ class XcodeValidator extends DoctorValidator {
5771
messages.add(ValidationMessage.error(_userMessages.xcodeMissingSimct));
5872
}
5973

74+
final ValidationMessage? missingSimulatorMessage = await _validateSimulatorRuntimeInstalled();
75+
if (missingSimulatorMessage != null) {
76+
xcodeStatus = ValidationType.partial;
77+
messages.add(missingSimulatorMessage);
78+
}
6079
} else {
6180
xcodeStatus = ValidationType.missing;
6281
if (xcodeSelectPath == null || xcodeSelectPath.isEmpty) {
@@ -68,4 +87,45 @@ class XcodeValidator extends DoctorValidator {
6887

6988
return ValidationResult(xcodeStatus, messages, statusInfo: xcodeVersionInfo);
7089
}
90+
91+
/// Validate the Xcode-installed iOS simulator SDK has a corresponding iOS
92+
/// simulator runtime installed.
93+
///
94+
/// Starting with Xcode 15, the iOS simulator runtime is no longer downloaded
95+
/// with Xcode and must be downloaded and installed separately.
96+
/// iOS applications cannot be run without it.
97+
Future<ValidationMessage?> _validateSimulatorRuntimeInstalled() async {
98+
// Skip this validation if Xcode is not installed, Xcode is a version less
99+
// than 15, simctl is not installed, or if the EULA is not signed.
100+
if (!_xcode.isInstalled ||
101+
_xcode.currentVersion == null ||
102+
_xcode.currentVersion!.major < 15 ||
103+
!_xcode.isSimctlInstalled ||
104+
!_xcode.eulaSigned) {
105+
return null;
106+
}
107+
108+
final Version? platformSDKVersion = await _xcode.sdkPlatformVersion(EnvironmentType.simulator);
109+
if (platformSDKVersion == null) {
110+
return const ValidationMessage.error('Unable to find the iPhone Simulator SDK.');
111+
}
112+
113+
final List<IOSSimulatorRuntime> runtimes = await _iosSimulatorUtils.getAvailableIOSRuntimes();
114+
if (runtimes.isEmpty) {
115+
return const ValidationMessage.error('Unable to get list of installed Simulator runtimes.');
116+
}
117+
118+
// Verify there is a simulator runtime installed matching the
119+
// iphonesimulator SDK major version.
120+
try {
121+
runtimes.firstWhere(
122+
(IOSSimulatorRuntime runtime) =>
123+
runtime.version?.major == platformSDKVersion.major,
124+
);
125+
} on StateError {
126+
return ValidationMessage.hint(_iOSSimulatorMissing(platformSDKVersion.toString()));
127+
}
128+
129+
return null;
130+
}
71131
}

0 commit comments

Comments
 (0)