diff --git a/packages/flutter_tools/lib/src/android/gradle_errors.dart b/packages/flutter_tools/lib/src/android/gradle_errors.dart index cade7c22ce..dc9749ef7b 100644 --- a/packages/flutter_tools/lib/src/android/gradle_errors.dart +++ b/packages/flutter_tools/lib/src/android/gradle_errors.dart @@ -81,6 +81,7 @@ final List gradleErrors = [ jvm11RequiredHandler, outdatedGradleHandler, sslExceptionHandler, + zipExceptionHandler, ]; const String _boxTitle = 'Flutter Fix'; @@ -206,12 +207,6 @@ final GradleHandledError permissionDeniedErrorHandler = GradleHandledError( /// Gradle crashes for several known reasons when downloading that are not /// actionable by Flutter. -/// -/// The Gradle cache directory must be deleted, otherwise it may attempt to -/// re-use the bad zip file. -/// -/// See also: -/// * https://docs.gradle.org/current/userguide/directory_layout.html#dir:gradle_user_home @visibleForTesting final GradleHandledError networkErrorHandler = GradleHandledError( test: _lineMatcher(const [ @@ -234,20 +229,73 @@ final GradleHandledError networkErrorHandler = GradleHandledError( '${globals.logger.terminal.warningMark} ' 'Gradle threw an error while downloading artifacts from the network.' ); - try { - final String? homeDir = globals.platform.environment['HOME']; - if (homeDir != null) { - final Directory directory = globals.fs.directory(globals.fs.path.join(homeDir, '.gradle')); - ErrorHandlingFileSystem.deleteIfExists(directory, recursive: true); - } - } on FileSystemException catch (err) { - globals.printTrace('Failed to delete Gradle cache: $err'); - } return GradleBuildStatus.retry; }, eventLabel: 'network', ); +/// Handles corrupted jar or other types of zip files. +/// +/// If a terminal is attached, this handler prompts the user if they would like to +/// delete the $HOME/.gradle directory prior to retrying the build. +/// +/// If this handler runs on a bot (e.g. a CI bot), the $HOME/.gradle is automatically deleted. +/// +/// See also: +/// * https://github.com/flutter/flutter/issues/51195 +/// * https://github.com/flutter/flutter/issues/89959 +/// * https://docs.gradle.org/current/userguide/directory_layout.html#dir:gradle_user_home +@visibleForTesting +final GradleHandledError zipExceptionHandler = GradleHandledError( + test: _lineMatcher(const [ + 'java.util.zip.ZipException: error in opening zip file', + ]), + handler: ({ + required String line, + required FlutterProject project, + required bool usesAndroidX, + required bool multidexEnabled, + }) async { + globals.printError( + '${globals.logger.terminal.warningMark} ' + 'Your .gradle directory under the home directory might be corrupted.' + ); + bool shouldDeleteUserGradle = await globals.botDetector.isRunningOnBot; + if (!shouldDeleteUserGradle && globals.terminal.stdinHasTerminal) { + try { + final String selection = await globals.terminal.promptForCharInput( + ['y', 'n'], + logger: globals.logger, + prompt: 'Do you want to delete the .gradle directory under the home directory?', + defaultChoiceIndex: 0, + ); + shouldDeleteUserGradle = selection == 'y'; + } on StateError catch(e) { + globals.printError( + e.message, + indent: 0, + ); + } + } + if (shouldDeleteUserGradle) { + final String? homeDir = globals.platform.environment['HOME']; + if (homeDir == null) { + globals.logger.printStatus("Could not delete .gradle directory because there isn't a HOME env variable"); + return GradleBuildStatus.retry; + } + final Directory userGradle = globals.fs.directory(globals.fs.path.join(homeDir, '.gradle')); + globals.logger.printStatus('Deleting ${userGradle.path}'); + try { + ErrorHandlingFileSystem.deleteIfExists(userGradle, recursive: true); + } on FileSystemException catch (err) { + globals.printTrace('Failed to delete Gradle cache: $err'); + } + } + return GradleBuildStatus.retry; + }, + eventLabel: 'zip-exception', +); + // R8 failure. @visibleForTesting final GradleHandledError r8FailureHandler = GradleHandledError( 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 583ae5c676..9c207301de 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 @@ -8,6 +8,7 @@ import 'package:file/memory.dart'; import 'package:file_testing/file_testing.dart'; import 'package:flutter_tools/src/android/gradle_errors.dart'; import 'package:flutter_tools/src/android/gradle_utils.dart'; +import 'package:flutter_tools/src/base/bot_detector.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; @@ -17,6 +18,7 @@ import 'package:flutter_tools/src/project.dart'; import '../../src/common.dart'; import '../../src/context.dart'; +import '../../src/fakes.dart'; void main() { group('gradleErrors', () { @@ -38,53 +40,13 @@ void main() { jvm11RequiredHandler, outdatedGradleHandler, sslExceptionHandler, + zipExceptionHandler, ]) ); }); }); group('network errors', () { - testUsingContext('retries and deletes zip if gradle fails to unzip', () async { - globals.fs.file('foo/.gradle/fizz.zip').createSync(recursive: true); - const String errorMessage = r''' -Exception in thread "main" java.util.zip.ZipException: error in opening zip file -at java.util.zip.ZipFile.open(Native Method) -at java.util.zip.ZipFile.(ZipFile.java:225) -at java.util.zip.ZipFile.(ZipFile.java:155) -at java.util.zip.ZipFile.(ZipFile.java:169) -at org.gradle.wrapper.Install.unzip(Install.java:214) -at org.gradle.wrapper.Install.access$600(Install.java:27) -at org.gradle.wrapper.Install$1.call(Install.java:74) -at org.gradle.wrapper.Install$1.call(Install.java:48) -at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:65) -at org.gradle.wrapper.Install.createDist(Install.java:48) -at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:128) -at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61) -[!] Gradle threw an error while trying to update itself. Retrying the update... -Exception in thread "main" java.util.zip.ZipException: error in opening zip file -at java.util.zip.ZipFile.open(Native Method) -at java.util.zip.ZipFile.(ZipFile.java:225) -at java.util.zip.ZipFile.(ZipFile.java:155) -at java.util.zip.ZipFile.(ZipFile.java:169) -at org.gradle.wrapper.Install.unzip(Install.java:214) -at org.gradle.wrapper.Install.access$600(Install.java:27) -at org.gradle.wrapper.Install$1.call(Install.java:74) -at org.gradle.wrapper.Install$1.call(Install.java:48) -at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:65) -at org.gradle.wrapper.Install.createDist(Install.java:48) -at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:128) -at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61) -'''; - - expect(formatTestErrorMessage(errorMessage, networkErrorHandler), isTrue); - expect(await networkErrorHandler.handler(), equals(GradleBuildStatus.retry)); - expect(globals.fs.file('foo/.gradle/fizz.zip'), isNot(exists)); - }, overrides: { - FileSystem: () => MemoryFileSystem.test(), - ProcessManager: () => FakeProcessManager.any(), - Platform: () => FakePlatform(environment: {'HOME': 'foo/'}), - }); - testUsingContext('retries if gradle fails while downloading', () async { const String errorMessage = r''' Exception in thread "main" java.io.FileNotFoundException: https://downloads.gradle.org/distributions/gradle-4.1.1-all.zip @@ -1122,6 +1084,121 @@ at java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:108)''' }); }); + group('Zip exception', () { + testWithoutContext('pattern', () { + expect( + zipExceptionHandler.test(r''' +Exception in thread "main" java.util.zip.ZipException: error in opening zip file +at java.util.zip.ZipFile.open(Native Method) +at java.util.zip.ZipFile.(ZipFile.java:225) +at java.util.zip.ZipFile.(ZipFile.java:155) +at java.util.zip.ZipFile.(ZipFile.java:169) +at org.gradle.wrapper.Install.unzip(Install.java:214) +at org.gradle.wrapper.Install.access$600(Install.java:27) +at org.gradle.wrapper.Install$1.call(Install.java:74) +at org.gradle.wrapper.Install$1.call(Install.java:48) +at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:65) +at org.gradle.wrapper.Install.createDist(Install.java:48) +at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:128) +at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''' + ), + isTrue, + ); + }); + + testUsingContext('suggestion', () async { + globals.fs.file('foo/.gradle/fizz.zip').createSync(recursive: true); + + final GradleBuildStatus result = await zipExceptionHandler.handler(); + + expect(result, equals(GradleBuildStatus.retry)); + expect(globals.fs.file('foo/.gradle/fizz.zip'), exists); + expect( + testLogger.errorText, + contains( + '[!] Your .gradle directory under the home directory might be corrupted.\n' + ) + ); + expect(testLogger.statusText, ''); + }, overrides: { + Platform: () => FakePlatform(environment: {'HOME': 'foo/'}), + FileSystem: () => MemoryFileSystem.test(), + ProcessManager: () => FakeProcessManager.empty(), + BotDetector: () => const FakeBotDetector(false), + }); + + testUsingContext('suggestion if running as bot', () async { + globals.fs.file('foo/.gradle/fizz.zip').createSync(recursive: true); + + final GradleBuildStatus result = await zipExceptionHandler.handler(); + + expect(result, equals(GradleBuildStatus.retry)); + expect(globals.fs.file('foo/.gradle/fizz.zip'), isNot(exists)); + + expect( + testLogger.errorText, + contains( + '[!] Your .gradle directory under the home directory might be corrupted.\n' + ) + ); + expect( + testLogger.statusText, + contains('Deleting foo/.gradle\n'), + ); + }, overrides: { + Platform: () => FakePlatform(environment: {'HOME': 'foo/'}), + FileSystem: () => MemoryFileSystem.test(), + ProcessManager: () => FakeProcessManager.empty(), + BotDetector: () => const FakeBotDetector(true), + }); + + testUsingContext('suggestion if stdin has terminal and user entered y', () async { + globals.fs.file('foo/.gradle/fizz.zip').createSync(recursive: true); + + final GradleBuildStatus result = await zipExceptionHandler.handler(); + + expect(result, equals(GradleBuildStatus.retry)); + expect(globals.fs.file('foo/.gradle/fizz.zip'), isNot(exists)); + expect( + testLogger.errorText, + contains( + '[!] Your .gradle directory under the home directory might be corrupted.\n' + ) + ); + expect( + testLogger.statusText, + contains('Deleting foo/.gradle\n'), + ); + }, overrides: { + Platform: () => FakePlatform(environment: {'HOME': 'foo/'}), + FileSystem: () => MemoryFileSystem.test(), + ProcessManager: () => FakeProcessManager.empty(), + AnsiTerminal: () => _TestPromptTerminal('y'), + BotDetector: () => const FakeBotDetector(false), + }); + + testUsingContext('suggestion if stdin has terminal and user entered n', () async { + globals.fs.file('foo/.gradle/fizz.zip').createSync(recursive: true); + + final GradleBuildStatus result = await zipExceptionHandler.handler(); + + expect(result, equals(GradleBuildStatus.retry)); + expect(globals.fs.file('foo/.gradle/fizz.zip'), exists); + expect( + testLogger.errorText, + contains( + '[!] Your .gradle directory under the home directory might be corrupted.\n' + ) + ); + expect(testLogger.statusText, ''); + }, overrides: { + Platform: () => FakePlatform(environment: {'HOME': 'foo/'}), + FileSystem: () => MemoryFileSystem.test(), + ProcessManager: () => FakeProcessManager.empty(), + AnsiTerminal: () => _TestPromptTerminal('n'), + BotDetector: () => const FakeBotDetector(false), + }); + }); } bool formatTestErrorMessage(String errorMessage, GradleHandledError error) { @@ -1162,4 +1239,7 @@ class _TestPromptTerminal extends AnsiTerminal { }) { return Future.value(promptResult); } + + @override + bool get stdinHasTerminal => true; }