diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 86a4a0546a..6a088c705c 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -4,6 +4,7 @@ import 'dart:async'; +import 'package:meta/meta.dart'; import 'package:process/process.dart'; import '../artifacts.dart'; @@ -41,6 +42,21 @@ import 'xcresult.dart'; const String kConcurrentRunFailureMessage1 = 'database is locked'; const String kConcurrentRunFailureMessage2 = 'there are two concurrent builds running'; +/// User message when missing platform required to use Xcode. +/// +/// Starting with Xcode 15, the simulator is no longer downloaded with Xcode +/// and must be downloaded and installed separately. +@visibleForTesting +String missingPlatformInstructions(String simulatorVersion) => ''' +════════════════════════════════════════════════════════════════════════════════ +$simulatorVersion is not installed. 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 IMobileDevice { IMobileDevice({ required Artifacts artifacts, @@ -700,6 +716,11 @@ _XCResultIssueHandlingResult _handleXCResultIssue({required XCResultIssue issue, return _XCResultIssueHandlingResult(requiresProvisioningProfile: true, hasProvisioningProfileIssue: true); } else if (message.toLowerCase().contains('provisioning profile')) { return _XCResultIssueHandlingResult(requiresProvisioningProfile: false, hasProvisioningProfileIssue: true); + } else if (message.toLowerCase().contains('ineligible destinations')) { + final String? missingPlatform = _parseMissingPlatform(message); + if (missingPlatform != null) { + return _XCResultIssueHandlingResult(requiresProvisioningProfile: false, hasProvisioningProfileIssue: false, missingPlatform: missingPlatform); + } } return _XCResultIssueHandlingResult(requiresProvisioningProfile: false, hasProvisioningProfileIssue: false); } @@ -709,6 +730,7 @@ bool _handleIssues(XCResult? xcResult, Logger logger, XcodeBuildExecution? xcode bool requiresProvisioningProfile = false; bool hasProvisioningProfileIssue = false; bool issueDetected = false; + String? missingPlatform; if (xcResult != null && xcResult.parseSuccess) { for (final XCResultIssue issue in xcResult.issues) { @@ -719,6 +741,7 @@ bool _handleIssues(XCResult? xcResult, Logger logger, XcodeBuildExecution? xcode if (handlingResult.requiresProvisioningProfile) { requiresProvisioningProfile = true; } + missingPlatform = handlingResult.missingPlatform; issueDetected = true; } } else if (xcResult != null) { @@ -738,6 +761,8 @@ bool _handleIssues(XCResult? xcResult, Logger logger, XcodeBuildExecution? xcode logger.printError(' open ios/Runner.xcworkspace'); logger.printError(''); logger.printError("Also try selecting 'Product > Build' to fix the problem."); + } else if (missingPlatform != null) { + logger.printError(missingPlatformInstructions(missingPlatform), emphasis: true); } return issueDetected; @@ -773,18 +798,41 @@ void _parseIssueInStdout(XcodeBuildExecution xcodeBuildExecution, Logger logger, && (result.stdout?.contains('requires a provisioning profile. Select a provisioning profile in the Signing & Capabilities editor') ?? false)) { logger.printError(noProvisioningProfileInstruction, emphasis: true); } + + if (stderr != null && stderr.contains('Ineligible destinations')) { + final String? version = _parseMissingPlatform(stderr); + if (version != null) { + logger.printError(missingPlatformInstructions(version), emphasis: true); + } + } +} + +String? _parseMissingPlatform(String message) { + final RegExp pattern = RegExp(r'error:(.*?) is not installed\. To use with Xcode, first download and install the platform'); + final RegExpMatch? match = pattern.firstMatch(message); + if (match != null) { + final String? version = match.group(1); + return version; + } + return null; } // The result of [_handleXCResultIssue]. class _XCResultIssueHandlingResult { - _XCResultIssueHandlingResult({required this.requiresProvisioningProfile, required this.hasProvisioningProfileIssue}); + _XCResultIssueHandlingResult({ + required this.requiresProvisioningProfile, + required this.hasProvisioningProfileIssue, + this.missingPlatform, + }); // An issue indicates that user didn't provide the provisioning profile. final bool requiresProvisioningProfile; // An issue indicates that there is a provisioning profile issue. final bool hasProvisioningProfileIssue; + + final String? missingPlatform; } const String _kResultBundlePath = 'temporary_xcresult_bundle'; diff --git a/packages/flutter_tools/lib/src/ios/xcresult.dart b/packages/flutter_tools/lib/src/ios/xcresult.dart index 5bb520e0a5..1329d6b3bd 100644 --- a/packages/flutter_tools/lib/src/ios/xcresult.dart +++ b/packages/flutter_tools/lib/src/ios/xcresult.dart @@ -104,6 +104,13 @@ class XCResult { issueDiscarder: issueDiscarders, )); } + + final Object? actionsMap = resultJson['actions']; + if (actionsMap is Map) { + final List actionIssues = _parseActionIssues(actionsMap, issueDiscarders: issueDiscarders); + issues.addAll(actionIssues); + } + return XCResult._(issues: issues); } @@ -383,3 +390,84 @@ List _parseIssuesFromIssueSummariesJson({ } return issues; } + +List _parseActionIssues( + Map actionsMap, { + required List issueDiscarders, +}) { + // Example of json: + // { + // "actions" : { + // "_values" : [ + // { + // "actionResult" : { + // "_type" : { + // "_name" : "ActionResult" + // }, + // "issues" : { + // "_type" : { + // "_name" : "ResultIssueSummaries" + // }, + // "testFailureSummaries" : { + // "_type" : { + // "_name" : "Array" + // }, + // "_values" : [ + // { + // "_type" : { + // "_name" : "TestFailureIssueSummary", + // "_supertype" : { + // "_name" : "IssueSummary" + // } + // }, + // "issueType" : { + // "_type" : { + // "_name" : "String" + // }, + // "_value" : "Uncategorized" + // }, + // "message" : { + // "_type" : { + // "_name" : "String" + // }, + // "_value" : "Unable to find a destination matching the provided destination specifier:\n\t\t{ id:1234D567-890C-1DA2-34E5-F6789A0123C4 }\n\n\tIneligible destinations for the \"Runner\" scheme:\n\t\t{ platform:iOS, id:dvtdevice-DVTiPhonePlaceholder-iphoneos:placeholder, name:Any iOS Device, error:iOS 17.0 is not installed. To use with Xcode, first download and install the platform }" + // } + // } + // ] + // } + // } + // } + // } + // ] + // } + // } + final List issues = []; + final Object? actionsValues = actionsMap['_values']; + if (actionsValues is! List) { + return issues; + } + + for (final Object? actionValue in actionsValues) { + if (actionValue is!Map) { + continue; + } + final Object? actionResult = actionValue['actionResult']; + if (actionResult is! Map) { + continue; + } + final Object? actionResultIssues = actionResult['issues']; + if (actionResultIssues is! Map) { + continue; + } + final Object? testFailureSummaries = actionResultIssues['testFailureSummaries']; + if (testFailureSummaries is Map) { + issues.addAll(_parseIssuesFromIssueSummariesJson( + type: XCResultIssueType.error, + issueSummariesJson: testFailureSummaries, + issueDiscarder: issueDiscarders, + )); + } + } + + return issues; + } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart index c1f2dc3b47..a369604574 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart @@ -638,6 +638,37 @@ void main() { XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), }); + testUsingContext('Extra error message for missing simulator platform in xcresult bundle.', () async { + final BuildCommand command = BuildCommand( + androidSdk: FakeAndroidSdk(), + buildSystem: TestBuildSystem.all(BuildResult(success: true)), + fileSystem: MemoryFileSystem.test(), + logger: BufferLogger.test(), + osUtils: FakeOperatingSystemUtils(), + ); + + createMinimalMockProjectFiles(); + + await expectLater( + createTestCommandRunner(command).run(const ['build', 'ios', '--no-pub']), + throwsToolExit(), + ); + + expect(testLogger.errorText, contains(missingPlatformInstructions('iOS 17.0'))); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.list([ + xattrCommand, + setUpFakeXcodeBuildHandler(exitCode: 1, onRun: () { + fileSystem.systemTempDirectory.childDirectory(_xcBundleDirectoryPath).createSync(); + }), + setUpXCResultCommand(stdout: kSampleResultJsonWithActionIssues), + setUpRsyncCommand(), + ]), + Platform: () => macosPlatform, + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), + }); + testUsingContext('Delete xcresult bundle before each xcodebuild command.', () async { final BuildCommand command = BuildCommand( androidSdk: FakeAndroidSdk(), diff --git a/packages/flutter_tools/test/general.shard/ios/mac_test.dart b/packages/flutter_tools/test/general.shard/ios/mac_test.dart index e7fef592be..303d261273 100644 --- a/packages/flutter_tools/test/general.shard/ios/mac_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/mac_test.dart @@ -245,6 +245,44 @@ Error launching application on iPhone.''', ); }); + testWithoutContext('fallback to stdout: Ineligible destinations', () async { + final Map buildSettingsWithDevTeam = { + 'PRODUCT_BUNDLE_IDENTIFIER': 'test.app', + 'DEVELOPMENT_TEAM': 'a team', + }; + final XcodeBuildResult buildResult = XcodeBuildResult( + success: false, + stderr: ''' +Launching lib/main.dart on iPhone in debug mode... +Signing iOS app for device deployment using developer identity: "iPhone Developer: test@flutter.io (1122334455)" +Running Xcode build... 1.3s +Failed to build iOS app +Error output from Xcode build: +↳ + xcodebuild: error: Unable to find a destination matching the provided destination specifier: + { id:1234D567-890C-1DA2-34E5-F6789A0123C4 } + + Ineligible destinations for the "Runner" scheme: + { platform:iOS, id:dvtdevice-DVTiPhonePlaceholder-iphoneos:placeholder, name:Any iOS Device, error:iOS 17.0 is not installed. To use with Xcode, first download and install the platform } + +Could not build the precompiled application for the device. + +Error launching application on iPhone.''', + xcodeBuildExecution: XcodeBuildExecution( + buildCommands: ['xcrun', 'xcodebuild', 'blah'], + appDirectory: '/blah/blah', + environmentType: EnvironmentType.physical, + buildSettings: buildSettingsWithDevTeam, + ), + ); + + await diagnoseXcodeBuildFailure(buildResult, testUsage, logger); + expect( + logger.errorText, + contains(missingPlatformInstructions('iOS 17.0')), + ); + }); + testWithoutContext('No development team shows message', () async { final XcodeBuildResult buildResult = XcodeBuildResult( success: false, diff --git a/packages/flutter_tools/test/general.shard/ios/xcresult_test.dart b/packages/flutter_tools/test/general.shard/ios/xcresult_test.dart index 72b78681c8..19f6944b08 100644 --- a/packages/flutter_tools/test/general.shard/ios/xcresult_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/xcresult_test.dart @@ -204,6 +204,19 @@ void main() { expect(result.parsingErrorMessage, isNull); }); + testWithoutContext( + 'correctly parse sample result json with action issues.', () async { + final XCResultGenerator generator = setupGenerator(resultJson: kSampleResultJsonWithActionIssues); + final XCResultIssueDiscarder discarder = XCResultIssueDiscarder(typeMatcher: XCResultIssueType.warning); + final XCResult result = await generator.generate(issueDiscarders: [discarder]); + expect(result.issues.length, 1); + expect(result.issues.first.type, XCResultIssueType.error); + expect(result.issues.first.subType, 'Uncategorized'); + expect(result.issues.first.message, contains('Unable to find a destination matching the provided destination specifier')); + expect(result.parseSuccess, isTrue); + expect(result.parsingErrorMessage, isNull); + }); + testWithoutContext( 'error: `xcresulttool get` process fail should return an `XCResult` with stderr as `parsingErrorMessage`.', () async { diff --git a/packages/flutter_tools/test/general.shard/ios/xcresult_test_data.dart b/packages/flutter_tools/test/general.shard/ios/xcresult_test_data.dart index 645afd1e87..5845262c21 100644 --- a/packages/flutter_tools/test/general.shard/ios/xcresult_test_data.dart +++ b/packages/flutter_tools/test/general.shard/ios/xcresult_test_data.dart @@ -378,3 +378,252 @@ const String kSampleResultJsonWithProvisionIssue = r''' } } '''; + + +/// An example xcresult bundle json that contains action issues. +const String kSampleResultJsonWithActionIssues = r''' +{ + "_type" : { + "_name" : "ActionsInvocationRecord" + }, + "actions" : { + "_type" : { + "_name" : "Array" + }, + "_values" : [ + { + "_type" : { + "_name" : "ActionRecord" + }, + "actionResult" : { + "_type" : { + "_name" : "ActionResult" + }, + "coverage" : { + "_type" : { + "_name" : "CodeCoverageInfo" + } + }, + "issues" : { + "_type" : { + "_name" : "ResultIssueSummaries" + }, + "testFailureSummaries" : { + "_type" : { + "_name" : "Array" + }, + "_values" : [ + { + "_type" : { + "_name" : "TestFailureIssueSummary", + "_supertype" : { + "_name" : "IssueSummary" + } + }, + "issueType" : { + "_type" : { + "_name" : "String" + }, + "_value" : "Uncategorized" + }, + "message" : { + "_type" : { + "_name" : "String" + }, + "_value" : "Unable to find a destination matching the provided destination specifier:\n\t\t{ id:1234D567-890C-1DA2-34E5-F6789A0123C4 }\n\n\tIneligible destinations for the \"Runner\" scheme:\n\t\t{ platform:iOS, id:dvtdevice-DVTiPhonePlaceholder-iphoneos:placeholder, name:Any iOS Device, error:iOS 17.0 is not installed. To use with Xcode, first download and install the platform }" + } + } + ] + } + }, + "logRef" : { + "_type" : { + "_name" : "Reference" + }, + "id" : { + "_type" : { + "_name" : "String" + }, + "_value" : "0~5X-qvql8_ppq0bj9taBMeZd4L2lXQagy1twsFRWwc06r42obpBZfP87uKnGO98mp5CUz1Ppr1knHiTMH9tOuwQ==" + }, + "targetType" : { + "_type" : { + "_name" : "TypeDefinition" + }, + "name" : { + "_type" : { + "_name" : "String" + }, + "_value" : "ActivityLogSection" + } + } + }, + "metrics" : { + "_type" : { + "_name" : "ResultMetrics" + } + }, + "resultName" : { + "_type" : { + "_name" : "String" + }, + "_value" : "All Tests" + }, + "status" : { + "_type" : { + "_name" : "String" + }, + "_value" : "failedToStart" + }, + "testsRef" : { + "_type" : { + "_name" : "Reference" + }, + "id" : { + "_type" : { + "_name" : "String" + }, + "_value" : "0~Dmuz8-g6YRb8HPVbTUXJD21oy3r5jxIGi-njd2Lc43yR5JlJf7D78HtNn2BsrF5iw1uYMnsuJ9xFDV7ZAmwhGg==" + }, + "targetType" : { + "_type" : { + "_name" : "TypeDefinition" + }, + "name" : { + "_type" : { + "_name" : "String" + }, + "_value" : "ActionTestPlanRunSummaries" + } + } + } + }, + "buildResult" : { + "_type" : { + "_name" : "ActionResult" + }, + "coverage" : { + "_type" : { + "_name" : "CodeCoverageInfo" + } + }, + "issues" : { + "_type" : { + "_name" : "ResultIssueSummaries" + } + }, + "metrics" : { + "_type" : { + "_name" : "ResultMetrics" + } + }, + "resultName" : { + "_type" : { + "_name" : "String" + }, + "_value" : "Build Succeeded" + }, + "status" : { + "_type" : { + "_name" : "String" + }, + "_value" : "succeeded" + } + }, + "endedTime" : { + "_type" : { + "_name" : "Date" + }, + "_value" : "2023-07-10T12:52:22.592-0500" + }, + "runDestination" : { + "_type" : { + "_name" : "ActionRunDestinationRecord" + }, + "localComputerRecord" : { + "_type" : { + "_name" : "ActionDeviceRecord" + }, + "platformRecord" : { + "_type" : { + "_name" : "ActionPlatformRecord" + } + } + }, + "targetDeviceRecord" : { + "_type" : { + "_name" : "ActionDeviceRecord" + }, + "platformRecord" : { + "_type" : { + "_name" : "ActionPlatformRecord" + } + } + }, + "targetSDKRecord" : { + "_type" : { + "_name" : "ActionSDKRecord" + } + } + }, + "schemeCommandName" : { + "_type" : { + "_name" : "String" + }, + "_value" : "Test" + }, + "schemeTaskName" : { + "_type" : { + "_name" : "String" + }, + "_value" : "BuildAndAction" + }, + "startedTime" : { + "_type" : { + "_name" : "Date" + }, + "_value" : "2023-07-10T12:52:22.592-0500" + }, + "title" : { + "_type" : { + "_name" : "String" + }, + "_value" : "RunnerTests.xctest" + } + } + ] + }, + "issues" : { + "_type" : { + "_name" : "ResultIssueSummaries" + } + }, + "metadataRef" : { + "_type" : { + "_name" : "Reference" + }, + "id" : { + "_type" : { + "_name" : "String" + }, + "_value" : "0~pY0GqmiVE6Q3qlWdLJDp_PnrsUKsJ7KKM1zKGnvEZOWGdBeGNArjjU62kgF2UBFdQLdRmf5SGpImQfJB6e7vDQ==" + }, + "targetType" : { + "_type" : { + "_name" : "TypeDefinition" + }, + "name" : { + "_type" : { + "_name" : "String" + }, + "_value" : "ActionsInvocationMetadata" + } + } + }, + "metrics" : { + "_type" : { + "_name" : "ResultMetrics" + } + } +} +''';