From 972b447434a67ca7f7d1870c98a20ce6a96290a6 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 19 May 2023 10:18:15 -0700 Subject: [PATCH] [tool] delete xcresult bundle file before each xcode retry. (#127144) xcodebuild command generates a xcresult bundle file on each run, however, it doesn't delete the file generated from previous run and will throw an error if the exact file already exists. In tool, we manually delete the file after each `flutter build` or `flutter run` command. However, there are some internal logic where xcodebuild retries multiple times. This PR deletes the xcresult bundle file at the start of each retry if it exists. Fixes https://github.com/flutter/flutter/issues/127119 --- packages/flutter_tools/lib/src/ios/mac.dart | 17 +++++-- .../hermetic/build_ios_test.dart | 46 +++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) 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(),