diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart index eb975fc17e..255e938ef8 100644 --- a/packages/flutter_tools/lib/src/doctor.dart +++ b/packages/flutter_tools/lib/src/doctor.dart @@ -138,7 +138,14 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider { if (androidWorkflow!.appliesToHostPlatform) GroupedValidator([androidValidator!, androidLicenseValidator!]), if (globals.iosWorkflow!.appliesToHostPlatform || macOSWorkflow.appliesToHostPlatform) - GroupedValidator([XcodeValidator(xcode: globals.xcode!, userMessages: userMessages), globals.cocoapodsValidator!]), + GroupedValidator([ + XcodeValidator( + xcode: globals.xcode!, + userMessages: userMessages, + iosSimulatorUtils: globals.iosSimulatorUtils!, + ), + globals.cocoapodsValidator!, + ]), if (webWorkflow.appliesToHostPlatform) ChromeValidator( chromiumLauncher: ChromiumLauncher( diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart index 06211c67a0..49a6bad2b4 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart @@ -15,6 +15,7 @@ import '../base/io.dart'; import '../base/logger.dart'; import '../base/process.dart'; import '../base/utils.dart'; +import '../base/version.dart'; import '../build_info.dart'; import '../convert.dart'; import '../devfs.dart'; @@ -91,6 +92,14 @@ class IOSSimulatorUtils { ); }).whereType().toList(); } + + Future> getAvailableIOSRuntimes() async { + if (!_xcode.isInstalledAndMeetsVersionCheck) { + return []; + } + + return _simControl.listAvailableIOSRuntimes(); + } } /// A wrapper around the `simctl` command line tool. @@ -293,6 +302,46 @@ class SimControl { _logger.printError('Unable to take screenshot of $deviceId:\n$exception'); } } + + /// Runs `simctl list runtimes available iOS --json` and returns all available iOS simulator runtimes. + Future> listAvailableIOSRuntimes() async { + final List runtimes = []; + final RunResult results = await _processUtils.run( + [ + ..._xcode.xcrunCommand(), + 'simctl', + 'list', + 'runtimes', + 'available', + 'iOS', + '--json', + ], + ); + + if (results.exitCode != 0) { + _logger.printError('Error executing simctl: ${results.exitCode}\n${results.stderr}'); + return runtimes; + } + + try { + final Object? decodeResult = (json.decode(results.stdout) as Map)['runtimes']; + if (decodeResult is List) { + for (final Object? runtimeData in decodeResult) { + if (runtimeData is Map) { + runtimes.add(IOSSimulatorRuntime.fromJson(runtimeData)); + } + } + } + + return runtimes; + } on FormatException { + // We failed to parse the simctl output, or it returned junk. + // One known message is "Install Started" isn't valid JSON but is + // returned sometimes. + _logger.printError('simctl returned non-JSON response: ${results.stdout}'); + return runtimes; + } + } } @@ -624,6 +673,64 @@ class IOSSimulator extends Device { } } +class IOSSimulatorRuntime { + IOSSimulatorRuntime._({ + this.bundlePath, + this.buildVersion, + this.platform, + this.runtimeRoot, + this.identifier, + this.version, + this.isInternal, + this.isAvailable, + this.name, + }); + + // Example: + // { + // "bundlePath" : "\/Library\/Developer\/CoreSimulator\/Volumes\/iOS_21A5277g\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS 17.0.simruntime", + // "buildversion" : "21A5277g", + // "platform" : "iOS", + // "runtimeRoot" : "\/Library\/Developer\/CoreSimulator\/Volumes\/iOS_21A5277g\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS 17.0.simruntime\/Contents\/Resources\/RuntimeRoot", + // "identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-17-0", + // "version" : "17.0", + // "isInternal" : false, + // "isAvailable" : true, + // "name" : "iOS 17.0", + // "supportedDeviceTypes" : [ + // { + // "bundlePath" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/DeviceTypes\/iPhone 8.simdevicetype", + // "name" : "iPhone 8", + // "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-8", + // "productFamily" : "iPhone" + // } + // ] + // }, + factory IOSSimulatorRuntime.fromJson(Map data) { + return IOSSimulatorRuntime._( + bundlePath: data['bundlePath']?.toString(), + buildVersion: data['buildversion']?.toString(), + platform: data['platform']?.toString(), + runtimeRoot: data['runtimeRoot']?.toString(), + identifier: data['identifier']?.toString(), + version: Version.parse(data['version']?.toString()), + isInternal: data['isInternal'] is bool? ? data['isInternal'] as bool? : null, + isAvailable: data['isAvailable'] is bool? ? data['isAvailable'] as bool? : null, + name: data['name']?.toString(), + ); + } + + final String? bundlePath; + final String? buildVersion; + final String? platform; + final String? runtimeRoot; + final String? identifier; + final Version? version; + final bool? isInternal; + final bool? isAvailable; + final String? name; +} + /// Launches the device log reader process on the host and parses the syslog. @visibleForTesting Future launchDeviceSystemLogTool(IOSSimulator device) async { diff --git a/packages/flutter_tools/lib/src/macos/xcode.dart b/packages/flutter_tools/lib/src/macos/xcode.dart index ff0b89e7e7..4784013da8 100644 --- a/packages/flutter_tools/lib/src/macos/xcode.dart +++ b/packages/flutter_tools/lib/src/macos/xcode.dart @@ -48,7 +48,8 @@ class Xcode { _fileSystem = fileSystem, _xcodeProjectInterpreter = xcodeProjectInterpreter, _processUtils = - ProcessUtils(logger: logger, processManager: processManager); + ProcessUtils(logger: logger, processManager: processManager), + _logger = logger; /// Create an [Xcode] for testing. /// @@ -60,16 +61,18 @@ class Xcode { XcodeProjectInterpreter? xcodeProjectInterpreter, Platform? platform, FileSystem? fileSystem, + Logger? logger, }) { platform ??= FakePlatform( operatingSystem: 'macos', environment: {}, ); + logger ??= BufferLogger.test(); return Xcode( platform: platform, processManager: processManager, fileSystem: fileSystem ?? MemoryFileSystem.test(), - logger: BufferLogger.test(), + logger: logger, xcodeProjectInterpreter: xcodeProjectInterpreter ?? XcodeProjectInterpreter.test(processManager: processManager), ); } @@ -78,6 +81,7 @@ class Xcode { final ProcessUtils _processUtils; final FileSystem _fileSystem; final XcodeProjectInterpreter _xcodeProjectInterpreter; + final Logger _logger; bool get isInstalledAndMeetsVersionCheck => _platform.isMacOS && isInstalled && isRequiredVersionSatisfactory; @@ -198,6 +202,19 @@ class Xcode { final String appPath = _fileSystem.path.join(selectPath, 'Applications', 'Simulator.app'); return _fileSystem.directory(appPath).existsSync() ? appPath : null; } + + /// Gets the version number of the platform for the selected SDK. + Future sdkPlatformVersion(EnvironmentType environmentType) async { + final RunResult runResult = await _processUtils.run( + [...xcrunCommand(), '--sdk', getSDKNameForIOSEnvironmentType(environmentType), '--show-sdk-platform-version'], + ); + if (runResult.exitCode != 0) { + _logger.printError('Could not find SDK Platform Version: ${runResult.stderr}'); + return null; + } + final String versionString = runResult.stdout.trim(); + return Version.parse(versionString); + } } EnvironmentType? environmentTypeFromSdkroot(String sdkroot, FileSystem fileSystem) { diff --git a/packages/flutter_tools/lib/src/macos/xcode_validator.dart b/packages/flutter_tools/lib/src/macos/xcode_validator.dart index c45351c672..1604876fb9 100644 --- a/packages/flutter_tools/lib/src/macos/xcode_validator.dart +++ b/packages/flutter_tools/lib/src/macos/xcode_validator.dart @@ -3,18 +3,32 @@ // found in the LICENSE file. import '../base/user_messages.dart'; +import '../base/version.dart'; +import '../build_info.dart'; import '../doctor_validator.dart'; +import '../ios/simulators.dart'; import 'xcode.dart'; +String _iOSSimulatorMissing(String version) => ''' +iOS $version 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'''; + class XcodeValidator extends DoctorValidator { XcodeValidator({ required Xcode xcode, + required IOSSimulatorUtils iosSimulatorUtils, required UserMessages userMessages, - }) : _xcode = xcode, - _userMessages = userMessages, - super('Xcode - develop for iOS and macOS'); + }) : _xcode = xcode, + _iosSimulatorUtils = iosSimulatorUtils, + _userMessages = userMessages, + super('Xcode - develop for iOS and macOS'); final Xcode _xcode; + final IOSSimulatorUtils _iosSimulatorUtils; final UserMessages _userMessages; @override @@ -57,6 +71,11 @@ class XcodeValidator extends DoctorValidator { messages.add(ValidationMessage.error(_userMessages.xcodeMissingSimct)); } + final ValidationMessage? missingSimulatorMessage = await _validateSimulatorRuntimeInstalled(); + if (missingSimulatorMessage != null) { + xcodeStatus = ValidationType.partial; + messages.add(missingSimulatorMessage); + } } else { xcodeStatus = ValidationType.missing; if (xcodeSelectPath == null || xcodeSelectPath.isEmpty) { @@ -68,4 +87,45 @@ class XcodeValidator extends DoctorValidator { return ValidationResult(xcodeStatus, messages, statusInfo: xcodeVersionInfo); } + + /// Validate the Xcode-installed iOS simulator SDK has a corresponding iOS + /// simulator runtime installed. + /// + /// Starting with Xcode 15, the iOS simulator runtime is no longer downloaded + /// with Xcode and must be downloaded and installed separately. + /// iOS applications cannot be run without it. + Future _validateSimulatorRuntimeInstalled() async { + // Skip this validation if Xcode is not installed, Xcode is a version less + // than 15, simctl is not installed, or if the EULA is not signed. + if (!_xcode.isInstalled || + _xcode.currentVersion == null || + _xcode.currentVersion!.major < 15 || + !_xcode.isSimctlInstalled || + !_xcode.eulaSigned) { + return null; + } + + final Version? platformSDKVersion = await _xcode.sdkPlatformVersion(EnvironmentType.simulator); + if (platformSDKVersion == null) { + return const ValidationMessage.error('Unable to find the iPhone Simulator SDK.'); + } + + final List runtimes = await _iosSimulatorUtils.getAvailableIOSRuntimes(); + if (runtimes.isEmpty) { + return const ValidationMessage.error('Unable to get list of installed Simulator runtimes.'); + } + + // Verify there is a simulator runtime installed matching the + // iphonesimulator SDK major version. + try { + runtimes.firstWhere( + (IOSSimulatorRuntime runtime) => + runtime.version?.major == platformSDKVersion.major, + ); + } on StateError { + return ValidationMessage.hint(_iOSSimulatorMissing(platformSDKVersion.toString())); + } + + return null; + } } diff --git a/packages/flutter_tools/test/general.shard/ios/simulators_test.dart b/packages/flutter_tools/test/general.shard/ios/simulators_test.dart index e28578305e..f3830d0a09 100644 --- a/packages/flutter_tools/test/general.shard/ios/simulators_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/simulators_test.dart @@ -8,6 +8,7 @@ import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/process.dart'; +import 'package:flutter_tools/src/base/version.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/device.dart'; @@ -771,14 +772,16 @@ Dec 20 17:04:32 md32-11-vm1 Another App[88374]: Ignore this text''' late FakeProcessManager fakeProcessManager; Xcode xcode; late SimControl simControl; + late BufferLogger logger; const String deviceId = 'smart-phone'; const String appId = 'flutterApp'; setUp(() { fakeProcessManager = FakeProcessManager.empty(); xcode = Xcode.test(processManager: FakeProcessManager.any()); + logger = BufferLogger.test(); simControl = SimControl( - logger: BufferLogger.test(), + logger: logger, processManager: fakeProcessManager, xcode: xcode, ); @@ -931,6 +934,159 @@ Dec 20 17:04:32 md32-11-vm1 Another App[88374]: Ignore this text''' expect(await iosSimulator.stopApp(null), isFalse); }); + + testWithoutContext('listAvailableIOSRuntimes succeeds', () async { + const String validRuntimesOutput = ''' +{ + "runtimes" : [ + { + "bundlePath" : "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 15.4.simruntime", + "buildversion" : "19E240", + "platform" : "iOS", + "runtimeRoot" : "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 15.4.simruntime/Contents/Resources/RuntimeRoot", + "identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-15-4", + "version" : "15.4", + "isInternal" : false, + "isAvailable" : true, + "name" : "iOS 15.4", + "supportedDeviceTypes" : [ + { + "bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 6s.simdevicetype", + "name" : "iPhone 6s", + "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-6s", + "productFamily" : "iPhone" + }, + { + "bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 6s Plus.simdevicetype", + "name" : "iPhone 6s Plus", + "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-6s-Plus", + "productFamily" : "iPhone" + } + ] + }, + { + "bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime", + "buildversion" : "20E247", + "platform" : "iOS", + "runtimeRoot" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot", + "identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-16-4", + "version" : "16.4", + "isInternal" : false, + "isAvailable" : true, + "name" : "iOS 16.4", + "supportedDeviceTypes" : [ + { + "bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 8.simdevicetype", + "name" : "iPhone 8", + "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-8", + "productFamily" : "iPhone" + }, + { + "bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 8 Plus.simdevicetype", + "name" : "iPhone 8 Plus", + "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus", + "productFamily" : "iPhone" + } + ] + }, + { + "bundlePath" : "/Library/Developer/CoreSimulator/Volumes/iOS_21A5268h/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.0.simruntime", + "buildversion" : "21A5268h", + "platform" : "iOS", + "runtimeRoot" : "/Library/Developer/CoreSimulator/Volumes/iOS_21A5268h/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.0.simruntime/Contents/Resources/RuntimeRoot", + "identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-17-0", + "version" : "17.0", + "isInternal" : false, + "isAvailable" : true, + "name" : "iOS 17.0", + "supportedDeviceTypes" : [ + { + "bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 8.simdevicetype", + "name" : "iPhone 8", + "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-8", + "productFamily" : "iPhone" + }, + { + "bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 8 Plus.simdevicetype", + "name" : "iPhone 8 Plus", + "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus", + "productFamily" : "iPhone" + } + ] + } + ] +} + +'''; + fakeProcessManager.addCommand(const FakeCommand( + command: [ + 'xcrun', + 'simctl', + 'list', + 'runtimes', + 'available', + 'iOS', + '--json', + ], + stdout: validRuntimesOutput, + )); + + final List runtimes = await simControl.listAvailableIOSRuntimes(); + + final IOSSimulatorRuntime runtime1 = runtimes[0]; + expect(runtime1.bundlePath, '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 15.4.simruntime'); + expect(runtime1.buildVersion, '19E240'); + expect(runtime1.platform, 'iOS'); + expect(runtime1.runtimeRoot, '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 15.4.simruntime/Contents/Resources/RuntimeRoot'); + expect(runtime1.identifier, 'com.apple.CoreSimulator.SimRuntime.iOS-15-4'); + expect(runtime1.version, Version(15, 4, null)); + expect(runtime1.isInternal, false); + expect(runtime1.isAvailable, true); + expect(runtime1.name, 'iOS 15.4'); + + final IOSSimulatorRuntime runtime2 = runtimes[1]; + expect(runtime2.bundlePath, '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime'); + expect(runtime2.buildVersion, '20E247'); + expect(runtime2.platform, 'iOS'); + expect(runtime2.runtimeRoot, '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot'); + expect(runtime2.identifier, 'com.apple.CoreSimulator.SimRuntime.iOS-16-4'); + expect(runtime2.version, Version(16, 4, null)); + expect(runtime2.isInternal, false); + expect(runtime2.isAvailable, true); + expect(runtime2.name, 'iOS 16.4'); + + final IOSSimulatorRuntime runtime3 = runtimes[2]; + expect(runtime3.bundlePath, '/Library/Developer/CoreSimulator/Volumes/iOS_21A5268h/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.0.simruntime'); + expect(runtime3.buildVersion, '21A5268h'); + expect(runtime3.platform, 'iOS'); + expect(runtime3.runtimeRoot, '/Library/Developer/CoreSimulator/Volumes/iOS_21A5268h/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.0.simruntime/Contents/Resources/RuntimeRoot'); + expect(runtime3.identifier, 'com.apple.CoreSimulator.SimRuntime.iOS-17-0'); + expect(runtime3.version, Version(17, 0, null)); + expect(runtime3.isInternal, false); + expect(runtime3.isAvailable, true); + expect(runtime3.name, 'iOS 17.0'); + }); + + testWithoutContext('listAvailableIOSRuntimes handles bad simctl output', () async { + fakeProcessManager.addCommand(const FakeCommand( + command: [ + 'xcrun', + 'simctl', + 'list', + 'runtimes', + 'available', + 'iOS', + '--json', + ], + stdout: 'Install Started', + )); + + final List runtimes = await simControl.listAvailableIOSRuntimes(); + + expect(runtimes, isEmpty); + expect(logger.errorText, contains('simctl returned non-JSON response:')); + expect(fakeProcessManager, hasNoRemainingExpectations); + }); }); group('startApp', () { diff --git a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart index f2b2142090..887712ad2e 100644 --- a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart @@ -99,12 +99,15 @@ void main() { group('macOS', () { late Xcode xcode; + late BufferLogger logger; setUp(() { xcodeProjectInterpreter = FakeXcodeProjectInterpreter(); + logger = BufferLogger.test(); xcode = Xcode.test( processManager: fakeProcessManager, xcodeProjectInterpreter: xcodeProjectInterpreter, + logger: logger, ); }); @@ -277,6 +280,59 @@ void main() { expect(fakeProcessManager, hasNoRemainingExpectations); }); }); + + group('SDK Platform Version', () { + testWithoutContext('--show-sdk-platform-version iphonesimulator', () async { + fakeProcessManager.addCommand(const FakeCommand( + command: ['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'], + stdout: '16.4', + )); + + expect(await xcode.sdkPlatformVersion(EnvironmentType.simulator), Version(16, 4, null)); + expect(fakeProcessManager, hasNoRemainingExpectations); + }); + + testWithoutContext('--show-sdk-platform-version iphonesimulator with leading and trailing new line', () async { + fakeProcessManager.addCommand(const FakeCommand( + command: ['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'], + stdout: '\n16.4\n', + )); + + expect(await xcode.sdkPlatformVersion(EnvironmentType.simulator), Version(16, 4, null)); + expect(fakeProcessManager, hasNoRemainingExpectations); + }); + + testWithoutContext('--show-sdk-platform-version returns version followed by text', () async { + fakeProcessManager.addCommand(const FakeCommand( + command: ['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'], + stdout: '13.2 (a) 12344', + )); + + expect(await xcode.sdkPlatformVersion(EnvironmentType.simulator), Version(13, 2, null, text: '13.2 (a) 12344')); + expect(fakeProcessManager, hasNoRemainingExpectations); + }); + + testWithoutContext('--show-sdk-platform-version returns something unexpected', () async { + fakeProcessManager.addCommand(const FakeCommand( + command: ['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'], + stdout: 'bogus', + )); + + expect(await xcode.sdkPlatformVersion(EnvironmentType.simulator), null); + expect(fakeProcessManager, hasNoRemainingExpectations); + }); + + testWithoutContext('--show-sdk-platform-version fails', () async { + fakeProcessManager.addCommand(const FakeCommand( + command: ['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'], + exitCode: 1, + stderr: 'xcrun: error:', + )); + expect(await xcode.sdkPlatformVersion(EnvironmentType.simulator), null); + expect(fakeProcessManager, hasNoRemainingExpectations); + expect(logger.errorText, contains('Could not find SDK Platform Version')); + }); + }); }); }); diff --git a/packages/flutter_tools/test/general.shard/macos/xcode_validator_test.dart b/packages/flutter_tools/test/general.shard/macos/xcode_validator_test.dart index bb6668e7ac..70a454a57d 100644 --- a/packages/flutter_tools/test/general.shard/macos/xcode_validator_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/xcode_validator_test.dart @@ -5,9 +5,11 @@ import 'package:flutter_tools/src/base/user_messages.dart'; import 'package:flutter_tools/src/base/version.dart'; import 'package:flutter_tools/src/doctor_validator.dart'; +import 'package:flutter_tools/src/ios/simulators.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/macos/xcode.dart'; import 'package:flutter_tools/src/macos/xcode_validator.dart'; +import 'package:test/fake.dart'; import '../../src/common.dart'; import '../../src/fake_process_manager.dart'; @@ -20,7 +22,11 @@ void main() { processManager: processManager, xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager, version: null), ); - final XcodeValidator validator = XcodeValidator(xcode: xcode, userMessages: UserMessages()); + final XcodeValidator validator = XcodeValidator( + xcode: xcode, + userMessages: UserMessages(), + iosSimulatorUtils: FakeIOSSimulatorUtils(), + ); final ValidationResult result = await validator.validate(); expect(result.type, ValidationType.missing); expect(result.statusInfo, isNull); @@ -39,7 +45,11 @@ void main() { processManager: processManager, xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager, version: null), ); - final XcodeValidator validator = XcodeValidator(xcode: xcode, userMessages: UserMessages()); + final XcodeValidator validator = XcodeValidator( + xcode: xcode, + userMessages: UserMessages(), + iosSimulatorUtils: FakeIOSSimulatorUtils(), + ); final ValidationResult result = await validator.validate(); expect(result.type, ValidationType.missing); expect(result.messages.last.type, ValidationMessageType.error); @@ -52,7 +62,11 @@ void main() { processManager: processManager, xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager, version: Version(7, 0, 1)), ); - final XcodeValidator validator = XcodeValidator(xcode: xcode, userMessages: UserMessages()); + final XcodeValidator validator = XcodeValidator( + xcode: xcode, + userMessages: UserMessages(), + iosSimulatorUtils: FakeIOSSimulatorUtils(), + ); final ValidationResult result = await validator.validate(); expect(result.type, ValidationType.partial); expect(result.messages.last.type, ValidationMessageType.error); @@ -65,7 +79,11 @@ void main() { processManager: processManager, xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager, version: Version(12, 4, null)), ); - final XcodeValidator validator = XcodeValidator(xcode: xcode, userMessages: UserMessages()); + final XcodeValidator validator = XcodeValidator( + xcode: xcode, + userMessages: UserMessages(), + iosSimulatorUtils: FakeIOSSimulatorUtils(), + ); final ValidationResult result = await validator.validate(); expect(result.type, ValidationType.partial); expect(result.messages.last.type, ValidationMessageType.hint); @@ -105,11 +123,16 @@ void main() { processManager: processManager, xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager), ); - final XcodeValidator validator = XcodeValidator(xcode: xcode, userMessages: UserMessages()); + final XcodeValidator validator = XcodeValidator( + xcode: xcode, + userMessages: UserMessages(), + iosSimulatorUtils: FakeIOSSimulatorUtils(), + ); final ValidationResult result = await validator.validate(); expect(result.type, ValidationType.partial); expect(result.messages.last.type, ValidationMessageType.error); expect(result.messages.last.message, contains('code end user license agreement not signed')); + expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('Emits partial status when simctl is not installed', () async { @@ -143,11 +166,156 @@ void main() { processManager: processManager, xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager), ); - final XcodeValidator validator = XcodeValidator(xcode: xcode, userMessages: UserMessages()); + final XcodeValidator validator = XcodeValidator( + xcode: xcode, + userMessages: UserMessages(), + iosSimulatorUtils: FakeIOSSimulatorUtils(), + ); final ValidationResult result = await validator.validate(); expect(result.type, ValidationType.partial); expect(result.messages.last.type, ValidationMessageType.error); expect(result.messages.last.message, contains('Xcode requires additional components')); + expect(processManager, hasNoRemainingExpectations); + }); + + testWithoutContext('Emits partial status when unable to find simulator SDK', () async { + final ProcessManager processManager = FakeProcessManager.list([ + const FakeCommand( + command: ['/usr/bin/xcode-select', '--print-path'], + stdout: '/Library/Developer/CommandLineTools', + ), + const FakeCommand( + command: [ + 'which', + 'sysctl', + ], + ), + const FakeCommand( + command: [ + 'sysctl', + 'hw.optional.arm64', + ], + exitCode: 1, + ), + const FakeCommand( + command: ['xcrun', 'clang'], + ), + const FakeCommand( + command: ['xcrun', 'simctl', 'list', 'devices', 'booted'], + ), + const FakeCommand( + command: ['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'], + ), + ]); + final Xcode xcode = Xcode.test( + processManager: processManager, + xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager), + ); + final XcodeValidator validator = XcodeValidator( + xcode: xcode, + userMessages: UserMessages(), + iosSimulatorUtils: FakeIOSSimulatorUtils(), + ); + final ValidationResult result = await validator.validate(); + expect(result.type, ValidationType.partial); + expect(result.messages.last.type, ValidationMessageType.error); + expect(result.messages.last.message, contains('Unable to find the iPhone Simulator SDK')); + expect(processManager, hasNoRemainingExpectations); + }); + + testWithoutContext('Emits partial status when unable to get simulator runtimes', () async { + final ProcessManager processManager = FakeProcessManager.list([ + const FakeCommand( + command: ['/usr/bin/xcode-select', '--print-path'], + stdout: '/Library/Developer/CommandLineTools', + ), + const FakeCommand( + command: [ + 'which', + 'sysctl', + ], + ), + const FakeCommand( + command: [ + 'sysctl', + 'hw.optional.arm64', + ], + exitCode: 1, + ), + const FakeCommand( + command: ['xcrun', 'clang'], + ), + const FakeCommand( + command: ['xcrun', 'simctl', 'list', 'devices', 'booted'], + ), + const FakeCommand( + command: ['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'], + stdout: '17.0' + ), + ]); + final Xcode xcode = Xcode.test( + processManager: processManager, + xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager), + ); + final XcodeValidator validator = XcodeValidator( + xcode: xcode, + userMessages: UserMessages(), + iosSimulatorUtils: FakeIOSSimulatorUtils(), + ); + final ValidationResult result = await validator.validate(); + expect(result.type, ValidationType.partial); + expect(result.messages.last.type, ValidationMessageType.error); + expect(result.messages.last.message, contains('Unable to get list of installed Simulator runtimes')); + expect(processManager, hasNoRemainingExpectations); + }); + + testWithoutContext('Emits partial status with hint when simulator runtimes do not match SDK', () async { + final ProcessManager processManager = FakeProcessManager.list([ + const FakeCommand( + command: ['/usr/bin/xcode-select', '--print-path'], + stdout: '/Library/Developer/CommandLineTools', + ), + const FakeCommand( + command: [ + 'which', + 'sysctl', + ], + ), + const FakeCommand( + command: [ + 'sysctl', + 'hw.optional.arm64', + ], + exitCode: 1, + ), + const FakeCommand( + command: ['xcrun', 'clang'], + ), + const FakeCommand( + command: ['xcrun', 'simctl', 'list', 'devices', 'booted'], + ), + const FakeCommand( + command: ['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'], + stdout: '17.0' + ), + ]); + final Xcode xcode = Xcode.test( + processManager: processManager, + xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager), + ); + final FakeIOSSimulatorUtils simulatorUtils = FakeIOSSimulatorUtils(runtimes: [ + IOSSimulatorRuntime.fromJson({'version': '16.0'}), + ]); + final XcodeValidator validator = XcodeValidator( + xcode: xcode, + userMessages: UserMessages(), + iosSimulatorUtils: simulatorUtils, + ); + final ValidationResult result = await validator.validate(); + expect(result.type, ValidationType.partial); + expect(result.messages.last.type, ValidationMessageType.hint); + expect(result.messages.last.message, contains('iOS 17.0 Simulator not installed')); + expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('Succeeds when all checks pass', () async { @@ -175,12 +343,23 @@ void main() { const FakeCommand( command: ['xcrun', 'simctl', 'list', 'devices', 'booted'], ), + const FakeCommand( + command: ['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-platform-version'], + stdout: '17.0' + ), ]); final Xcode xcode = Xcode.test( processManager: processManager, xcodeProjectInterpreter: XcodeProjectInterpreter.test(processManager: processManager), ); - final XcodeValidator validator = XcodeValidator(xcode: xcode, userMessages: UserMessages()); + final FakeIOSSimulatorUtils simulatorUtils = FakeIOSSimulatorUtils(runtimes: [ + IOSSimulatorRuntime.fromJson({'version': '17.0'}), + ]); + final XcodeValidator validator = XcodeValidator( + xcode: xcode, + userMessages: UserMessages(), + iosSimulatorUtils: simulatorUtils, + ); final ValidationResult result = await validator.validate(); expect(result.type, ValidationType.success); expect(result.messages.length, 2); @@ -189,6 +368,24 @@ void main() { expect(firstMessage.message, 'Xcode at /Library/Developer/CommandLineTools'); expect(result.statusInfo, '1000.0.0'); expect(result.messages[1].message, 'Build 13C100'); + expect(processManager, hasNoRemainingExpectations); }); }); } + +class FakeIOSSimulatorUtils extends Fake implements IOSSimulatorUtils { + FakeIOSSimulatorUtils({ + this.runtimes, + }); + + List? runtimes; + + List get _runtimesList { + return runtimes ?? []; + } + + @override + Future> getAvailableIOSRuntimes() async { + return _runtimesList; + } +} diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart index 0a70b2fd46..7bea7532c3 100644 --- a/packages/flutter_tools/test/src/context.dart +++ b/packages/flutter_tools/test/src/context.dart @@ -319,6 +319,9 @@ class NoopIOSSimulatorUtils implements IOSSimulatorUtils { @override Future> getAttachedDevices() async => []; + + @override + Future> getAvailableIOSRuntimes() async => []; } class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter {