diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 7004c7e6b4..cbd1f89c38 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -38,6 +38,9 @@ import 'xcode_build_settings.dart'; import 'xcodeproj.dart'; import 'xcresult.dart'; +const String kConcurrentRunFailureMessage1 = 'database is locked'; +const String kConcurrentRunFailureMessage2 = 'there are two concurrent builds running'; + class IMobileDevice { IMobileDevice({ required Artifacts artifacts, @@ -354,9 +357,10 @@ Future buildXcodeProject({ buildCommands.add('SCRIPT_OUTPUT_STREAM_FILE=${scriptOutputPipeFile.absolute.path}'); } + final File resultBundleFile = tempDir.childFile(_kResultBundlePath); buildCommands.addAll([ '-resultBundlePath', - tempDir.childFile(_kResultBundlePath).absolute.path, + resultBundleFile.absolute.path, '-resultBundleVersion', _kResultBundleVersion, ]); @@ -378,7 +382,7 @@ Future buildXcodeProject({ final Stopwatch sw = Stopwatch()..start(); initialBuildStatus = globals.logger.startProgress('Running Xcode build...'); - buildResult = await _runBuildWithRetries(buildCommands, app); + buildResult = await _runBuildWithRetries(buildCommands, app, resultBundleFile); // Notifies listener that no more output is coming. scriptOutputPipeFile?.writeAsStringSync('all done'); @@ -508,12 +512,15 @@ Future removeFinderExtendedAttributes(FileSystemEntity projectDirectory, P } } -Future _runBuildWithRetries(List buildCommands, BuildableIOSApp app) async { +Future _runBuildWithRetries(List buildCommands, BuildableIOSApp app, File resultBundleFile) async { int buildRetryDelaySeconds = 1; int remainingTries = 8; RunResult? buildResult; while (remainingTries > 0) { + if (resultBundleFile.existsSync()) { + resultBundleFile.deleteSync(recursive: true); + } remainingTries--; buildRetryDelaySeconds *= 2; @@ -546,8 +553,8 @@ Future _runBuildWithRetries(List buildCommands, BuildableIOS bool _isXcodeConcurrentBuildFailure(RunResult result) { return result.exitCode != 0 && - result.stdout.contains('database is locked') && - result.stdout.contains('there are two concurrent builds running'); + result.stdout.contains(kConcurrentRunFailureMessage1) && + result.stdout.contains(kConcurrentRunFailureMessage2); } Future diagnoseXcodeBuildFailure(XcodeBuildResult result, Usage flutterUsage, Logger logger) async { 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 01cb9ddfde..aaf9581124 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 @@ -5,6 +5,7 @@ import 'package:args/command_runner.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/android/android_sdk.dart'; +import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/os.dart'; @@ -15,6 +16,7 @@ import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/build.dart'; import 'package:flutter_tools/src/commands/build_ios.dart'; import 'package:flutter_tools/src/ios/code_signing.dart'; +import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:test/fake.dart'; @@ -636,6 +638,50 @@ void main() { XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), }); + testUsingContext('Delete xcresult bundle before each xcodebuild command.', () async { + final BuildCommand command = BuildCommand( + androidSdk: FakeAndroidSdk(), + buildSystem: TestBuildSystem.all(BuildResult(success: true)), + fileSystem: MemoryFileSystem.test(), + logger: BufferLogger.test(), + osUtils: FakeOperatingSystemUtils(), + ); + + createMinimalMockProjectFiles(); + + await createTestCommandRunner(command).run(const ['build', 'ios', '--no-pub']); + + expect(testLogger.statusText, contains('Xcode build done.')); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.list([ + xattrCommand, + // Intentionally fail the first xcodebuild command with concurrent run failure message. + setUpFakeXcodeBuildHandler( + exitCode: 1, + stdout: '$kConcurrentRunFailureMessage1 $kConcurrentRunFailureMessage2', + onRun: () { + fileSystem.systemTempDirectory.childFile(_xcBundleFilePath).createSync(); + } + ), + // The second xcodebuild is triggered due to above concurrent run failure message. + setUpFakeXcodeBuildHandler( + onRun: () { + // If the file is not cleaned, throw an error, test failure. + if (fileSystem.systemTempDirectory.childFile(_xcBundleFilePath).existsSync()) { + throwToolExit('xcresult bundle file existed.', exitCode: 2); + } + fileSystem.systemTempDirectory.childFile(_xcBundleFilePath).createSync(); + } + ), + setUpXCResultCommand(stdout: kSampleResultJsonNoIssues), + setUpRsyncCommand(), + ], + ), + Platform: () => macosPlatform, + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), + }); + testUsingContext('Failed to parse xcresult but display missing provisioning profile issue from stdout.', () async { final BuildCommand command = BuildCommand( androidSdk: FakeAndroidSdk(),