Unverified Commit 604010e9 authored by Victoria Ashworth's avatar Victoria Ashworth Committed by GitHub

Print pretty error when xcodebuild fails due to missing simulator (#130286)

Starting in Xcode 15, the simulator is no longer included in Xcode and must be downloaded and installed separately.

If you try to run flutter and the simulator is missing, you'll get an error like
```
xcodebuild: error: Unable to find a destination matching the provided destination specifier:
               		{ id:B1234A5C-67B8-901D-B2CB-FE34F56BDE78 }

               	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 }
```

Print a pretty error to make it easier for developers to know what to do.

Part 2 of https://github.com/flutter/flutter/issues/129558.
parent 60c1f376
......@@ -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';
......
......@@ -104,6 +104,13 @@ class XCResult {
issueDiscarder: issueDiscarders,
));
}
final Object? actionsMap = resultJson['actions'];
if (actionsMap is Map<String, Object?>) {
final List<XCResultIssue> actionIssues = _parseActionIssues(actionsMap, issueDiscarders: issueDiscarders);
issues.addAll(actionIssues);
}
return XCResult._(issues: issues);
}
......@@ -383,3 +390,84 @@ List<XCResultIssue> _parseIssuesFromIssueSummariesJson({
}
return issues;
}
List<XCResultIssue> _parseActionIssues(
Map<String, Object?> actionsMap, {
required List<XCResultIssueDiscarder> 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<XCResultIssue> issues = <XCResultIssue>[];
final Object? actionsValues = actionsMap['_values'];
if (actionsValues is! List<Object?>) {
return issues;
}
for (final Object? actionValue in actionsValues) {
if (actionValue is!Map<String, Object?>) {
continue;
}
final Object? actionResult = actionValue['actionResult'];
if (actionResult is! Map<String, Object?>) {
continue;
}
final Object? actionResultIssues = actionResult['issues'];
if (actionResultIssues is! Map<String, Object?>) {
continue;
}
final Object? testFailureSummaries = actionResultIssues['testFailureSummaries'];
if (testFailureSummaries is Map<String, Object?>) {
issues.addAll(_parseIssuesFromIssueSummariesJson(
type: XCResultIssueType.error,
issueSummariesJson: testFailureSummaries,
issueDiscarder: issueDiscarders,
));
}
}
return issues;
}
......@@ -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 <String>['build', 'ios', '--no-pub']),
throwsToolExit(),
);
expect(testLogger.errorText, contains(missingPlatformInstructions('iOS 17.0')));
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
xattrCommand,
setUpFakeXcodeBuildHandler(exitCode: 1, onRun: () {
fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync();
}),
setUpXCResultCommand(stdout: kSampleResultJsonWithActionIssues),
setUpRsyncCommand(),
]),
Platform: () => macosPlatform,
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
});
testUsingContext('Delete xcresult bundle before each xcodebuild command.', () async {
final BuildCommand command = BuildCommand(
androidSdk: FakeAndroidSdk(),
......
......@@ -245,6 +245,44 @@ Error launching application on iPhone.''',
);
});
testWithoutContext('fallback to stdout: Ineligible destinations', () async {
final Map<String, String> buildSettingsWithDevTeam = <String, String>{
'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: <String>['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,
......
......@@ -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: <XCResultIssueDiscarder>[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 {
......
......@@ -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"
}
}
}
''';
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment