[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
This commit is contained in:
Chris Yang 2023-05-19 10:18:15 -07:00 committed by GitHub
parent b9e8b0a827
commit 972b447434
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 58 additions and 5 deletions

View File

@ -38,6 +38,9 @@ import 'xcode_build_settings.dart';
import 'xcodeproj.dart'; import 'xcodeproj.dart';
import 'xcresult.dart'; import 'xcresult.dart';
const String kConcurrentRunFailureMessage1 = 'database is locked';
const String kConcurrentRunFailureMessage2 = 'there are two concurrent builds running';
class IMobileDevice { class IMobileDevice {
IMobileDevice({ IMobileDevice({
required Artifacts artifacts, required Artifacts artifacts,
@ -354,9 +357,10 @@ Future<XcodeBuildResult> buildXcodeProject({
buildCommands.add('SCRIPT_OUTPUT_STREAM_FILE=${scriptOutputPipeFile.absolute.path}'); buildCommands.add('SCRIPT_OUTPUT_STREAM_FILE=${scriptOutputPipeFile.absolute.path}');
} }
final File resultBundleFile = tempDir.childFile(_kResultBundlePath);
buildCommands.addAll(<String>[ buildCommands.addAll(<String>[
'-resultBundlePath', '-resultBundlePath',
tempDir.childFile(_kResultBundlePath).absolute.path, resultBundleFile.absolute.path,
'-resultBundleVersion', '-resultBundleVersion',
_kResultBundleVersion, _kResultBundleVersion,
]); ]);
@ -378,7 +382,7 @@ Future<XcodeBuildResult> buildXcodeProject({
final Stopwatch sw = Stopwatch()..start(); final Stopwatch sw = Stopwatch()..start();
initialBuildStatus = globals.logger.startProgress('Running Xcode build...'); 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. // Notifies listener that no more output is coming.
scriptOutputPipeFile?.writeAsStringSync('all done'); scriptOutputPipeFile?.writeAsStringSync('all done');
@ -508,12 +512,15 @@ Future<void> removeFinderExtendedAttributes(FileSystemEntity projectDirectory, P
} }
} }
Future<RunResult?> _runBuildWithRetries(List<String> buildCommands, BuildableIOSApp app) async { Future<RunResult?> _runBuildWithRetries(List<String> buildCommands, BuildableIOSApp app, File resultBundleFile) async {
int buildRetryDelaySeconds = 1; int buildRetryDelaySeconds = 1;
int remainingTries = 8; int remainingTries = 8;
RunResult? buildResult; RunResult? buildResult;
while (remainingTries > 0) { while (remainingTries > 0) {
if (resultBundleFile.existsSync()) {
resultBundleFile.deleteSync(recursive: true);
}
remainingTries--; remainingTries--;
buildRetryDelaySeconds *= 2; buildRetryDelaySeconds *= 2;
@ -546,8 +553,8 @@ Future<RunResult?> _runBuildWithRetries(List<String> buildCommands, BuildableIOS
bool _isXcodeConcurrentBuildFailure(RunResult result) { bool _isXcodeConcurrentBuildFailure(RunResult result) {
return result.exitCode != 0 && return result.exitCode != 0 &&
result.stdout.contains('database is locked') && result.stdout.contains(kConcurrentRunFailureMessage1) &&
result.stdout.contains('there are two concurrent builds running'); result.stdout.contains(kConcurrentRunFailureMessage2);
} }
Future<void> diagnoseXcodeBuildFailure(XcodeBuildResult result, Usage flutterUsage, Logger logger) async { Future<void> diagnoseXcodeBuildFailure(XcodeBuildResult result, Usage flutterUsage, Logger logger) async {

View File

@ -5,6 +5,7 @@
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_sdk.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/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.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.dart';
import 'package:flutter_tools/src/commands/build_ios.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/code_signing.dart';
import 'package:flutter_tools/src/ios/mac.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:test/fake.dart'; import 'package:test/fake.dart';
@ -636,6 +638,50 @@ void main() {
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), 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 <String>['build', 'ios', '--no-pub']);
expect(testLogger.statusText, contains('Xcode build done.'));
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
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 { testUsingContext('Failed to parse xcresult but display missing provisioning profile issue from stdout.', () async {
final BuildCommand command = BuildCommand( final BuildCommand command = BuildCommand(
androidSdk: FakeAndroidSdk(), androidSdk: FakeAndroidSdk(),