diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index d210b764224..876a036b6a7 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:math'; + import 'package:crypto/crypto.dart'; import 'package:meta/meta.dart'; import 'package:process/process.dart'; @@ -106,6 +108,9 @@ Iterable _apkFilesFor(AndroidBuildInfo androidBuildInfo) { return ['app$flavorString-$buildType.apk']; } +// The maximum time to wait before the tool retries a Gradle build. +const Duration kMaxRetryTime = Duration(seconds: 10); + /// An implementation of the [AndroidBuilder] that delegates to gradle. class AndroidGradleBuilder implements AndroidBuilder { AndroidGradleBuilder({ @@ -212,7 +217,7 @@ class AndroidGradleBuilder implements AndroidBuilder { /// * [target] is the target dart entry point. Typically, `lib/main.dart`. /// * If [isBuildingBundle] is `true`, then the output artifact is an `*.aab`, /// otherwise the output artifact is an `*.apk`. - /// * [retries] is the max number of build retries in case one of the [GradleHandledError] handler + /// * [maxRetries] If not `null`, this is the max number of build retries in case a retry is triggered. Future buildGradleApp({ required FlutterProject project, required AndroidBuildInfo androidBuildInfo, @@ -221,7 +226,8 @@ class AndroidGradleBuilder implements AndroidBuilder { required List localGradleErrors, bool validateDeferredComponents = true, bool deferredComponentsEnabled = false, - int retries = 1, + int retry = 0, + @visibleForTesting int? maxRetries, }) async { assert(project != null); assert(androidBuildInfo != null); @@ -401,38 +407,44 @@ class AndroidGradleBuilder implements AndroidBuilder { 'Gradle task $assembleTask failed with exit code $exitCode', exitCode: exitCode, ); - } else { - final GradleBuildStatus status = await detectedGradleError!.handler( - line: detectedGradleErrorLine!, - project: project, - usesAndroidX: usesAndroidX, - multidexEnabled: androidBuildInfo.multidexEnabled, - ); - - if (retries >= 1) { - final String successEventLabel = 'gradle-${detectedGradleError!.eventLabel}-success'; - switch (status) { - case GradleBuildStatus.retry: - await buildGradleApp( - project: project, - androidBuildInfo: androidBuildInfo, - target: target, - isBuildingBundle: isBuildingBundle, - localGradleErrors: localGradleErrors, - retries: retries - 1, - ); - BuildEvent(successEventLabel, type: 'gradle', flutterUsage: _usage).send(); - return; - case GradleBuildStatus.exit: - // noop. - } - } - BuildEvent('gradle-${detectedGradleError?.eventLabel}-failure', type: 'gradle', flutterUsage: _usage).send(); - throwToolExit( - 'Gradle task $assembleTask failed with exit code $exitCode', - exitCode: exitCode, - ); } + final GradleBuildStatus status = await detectedGradleError!.handler( + line: detectedGradleErrorLine!, + project: project, + usesAndroidX: usesAndroidX, + multidexEnabled: androidBuildInfo.multidexEnabled, + ); + + if (maxRetries == null || retry < maxRetries) { + switch (status) { + case GradleBuildStatus.retry: + // Use binary exponential backoff before retriggering the build. + // The expected wait times are: 100ms, 200ms, 400ms, and so on... + final int waitTime = min(pow(2, retry).toInt() * 100, kMaxRetryTime.inMicroseconds); + retry += 1; + _logger.printStatus('Retrying Gradle Build: #$retry, wait time: ${waitTime}ms'); + await Future.delayed(Duration(milliseconds: waitTime)); + await buildGradleApp( + project: project, + androidBuildInfo: androidBuildInfo, + target: target, + isBuildingBundle: isBuildingBundle, + localGradleErrors: localGradleErrors, + retry: retry, + maxRetries: maxRetries, + ); + final String successEventLabel = 'gradle-${detectedGradleError!.eventLabel}-success'; + BuildEvent(successEventLabel, type: 'gradle', flutterUsage: _usage).send(); + return; + case GradleBuildStatus.exit: + // Continue and throw tool exit. + } + } + BuildEvent('gradle-${detectedGradleError?.eventLabel}-failure', type: 'gradle', flutterUsage: _usage).send(); + throwToolExit( + 'Gradle task $assembleTask failed with exit code $exitCode', + exitCode: exitCode, + ); } if (isBuildingBundle) { diff --git a/packages/flutter_tools/lib/src/android/gradle_errors.dart b/packages/flutter_tools/lib/src/android/gradle_errors.dart index 4aeac61f699..f8d5436ca3b 100644 --- a/packages/flutter_tools/lib/src/android/gradle_errors.dart +++ b/packages/flutter_tools/lib/src/android/gradle_errors.dart @@ -226,8 +226,8 @@ final GradleHandledError networkErrorHandler = GradleHandledError( required bool multidexEnabled, }) async { globals.printError( - '${globals.logger.terminal.warningMark} Gradle threw an error while downloading artifacts from the network. ' - 'Retrying to download...' + '${globals.logger.terminal.warningMark} ' + 'Gradle threw an error while downloading artifacts from the network.' ); try { final String? homeDir = globals.platform.environment['HOME']; diff --git a/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart b/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart index c18caf1bd5f..5448acf9ba2 100644 --- a/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart +++ b/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart @@ -138,7 +138,8 @@ void main() { gradleUtils: FakeGradleUtils(), platform: FakePlatform(), ); - processManager.addCommand(const FakeCommand( + + const FakeCommand fakeCmd = FakeCommand( command: [ 'gradlew', '-q', @@ -151,23 +152,15 @@ void main() { 'assembleRelease', ], exitCode: 1, - stderr: '\nSome gradle message\n' - )); - processManager.addCommand(const FakeCommand( - command: [ - 'gradlew', - '-q', - '-Ptarget-platform=android-arm,android-arm64,android-x64', - '-Ptarget=lib/main.dart', - '-Pbase-application-name=io.flutter.app.FlutterApplication', - '-Pdart-obfuscation=false', - '-Ptrack-widget-creation=false', - '-Ptree-shake-icons=false', - 'assembleRelease', - ], - exitCode: 1, - stderr: '\nSome gradle message\n' - )); + stderr: '\nSome gradle message\n', + ); + + processManager.addCommand(fakeCmd); + + const int maxRetries = 2; + for (int i = 0; i < maxRetries; i++) { + processManager.addCommand(fakeCmd); + } fileSystem.directory('android') .childFile('build.gradle') @@ -186,6 +179,7 @@ void main() { int testFnCalled = 0; await expectLater(() async { await builder.buildGradleApp( + maxRetries: maxRetries, project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), androidBuildInfo: const AndroidBuildInfo( BuildInfo( @@ -221,7 +215,10 @@ void main() { message: 'Gradle task assembleRelease failed with exit code 1' )); - expect(testFnCalled, equals(2)); + expect(logger.statusText, contains('Retrying Gradle Build: #1, wait time: 100ms')); + expect(logger.statusText, contains('Retrying Gradle Build: #2, wait time: 200ms')); + + expect(testFnCalled, equals(maxRetries + 1)); expect(testUsage.events, contains( const TestUsageEvent( 'build', diff --git a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart index b251ffd61a9..f30f3e0a0e5 100644 --- a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart +++ b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart @@ -102,8 +102,7 @@ at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)'''; expect(testLogger.errorText, contains( - 'Gradle threw an error while downloading artifacts from the network. ' - 'Retrying to download...' + 'Gradle threw an error while downloading artifacts from the network.' ) ); }, overrides: { @@ -133,8 +132,7 @@ at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)'''; expect(testLogger.errorText, contains( - 'Gradle threw an error while downloading artifacts from the network. ' - 'Retrying to download...' + 'Gradle threw an error while downloading artifacts from the network.' ) ); }, overrides: { @@ -155,8 +153,7 @@ Exception in thread "main" java.lang.RuntimeException: Timeout of 120000 reached expect(testLogger.errorText, contains( - 'Gradle threw an error while downloading artifacts from the network. ' - 'Retrying to download...' + 'Gradle threw an error while downloading artifacts from the network.' ) ); }, overrides: { @@ -193,8 +190,7 @@ Exception in thread "main" javax.net.ssl.SSLHandshakeException: Remote host clos expect(testLogger.errorText, contains( - 'Gradle threw an error while downloading artifacts from the network. ' - 'Retrying to download...' + 'Gradle threw an error while downloading artifacts from the network.' ) ); }, overrides: { @@ -223,8 +219,7 @@ Exception in thread "main" java.io.FileNotFoundException: https://downloads.grad expect(testLogger.errorText, contains( - 'Gradle threw an error while downloading artifacts from the network. ' - 'Retrying to download...' + 'Gradle threw an error while downloading artifacts from the network.' ) ); }, overrides: { @@ -264,8 +259,7 @@ Exception in thread "main" java.net.SocketException: Connection reset expect(testLogger.errorText, contains( - 'Gradle threw an error while downloading artifacts from the network. ' - 'Retrying to download...' + 'Gradle threw an error while downloading artifacts from the network.' ) ); }, overrides: { @@ -292,8 +286,7 @@ A problem occurred configuring root project 'android'. expect(testLogger.errorText, contains( - 'Gradle threw an error while downloading artifacts from the network. ' - 'Retrying to download...' + 'Gradle threw an error while downloading artifacts from the network.' ) ); }, overrides: { @@ -324,8 +317,7 @@ A problem occurred configuring root project 'android'. expect(testLogger.errorText, contains( - 'Gradle threw an error while downloading artifacts from the network. ' - 'Retrying to download...' + 'Gradle threw an error while downloading artifacts from the network.' ) ); }, overrides: {