Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion packages/flutter_tools/lib/src/ios/mac.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'dart:async';

import 'package:meta/meta.dart';
import 'package:process/process.dart';

import '../artifacts.dart';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems a bit fragile (e.g. apple updates this string). Is there any error code you can use instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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';
Expand Down
88 changes: 88 additions & 0 deletions packages/flutter_tools/lib/src/ios/xcresult.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
38 changes: 38 additions & 0 deletions packages/flutter_tools/test/general.shard/ios/mac_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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: [email protected] (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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading