| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:meta/meta.dart'; |
| |
| import '../base/error_handling_io.dart'; |
| import '../base/file_system.dart'; |
| import '../base/process.dart'; |
| import '../base/terminal.dart'; |
| import '../globals_null_migrated.dart' as globals; |
| import '../project.dart'; |
| import '../reporting/reporting.dart'; |
| import 'android_studio.dart'; |
| import 'multidex.dart'; |
| |
| typedef GradleErrorTest = bool Function(String); |
| |
| /// A Gradle error handled by the tool. |
| class GradleHandledError { |
| const GradleHandledError({ |
| required this.test, |
| required this.handler, |
| this.eventLabel, |
| }); |
| |
| /// The test function. |
| /// Returns [true] if the current error message should be handled. |
| final GradleErrorTest test; |
| |
| /// The handler function. |
| final Future<GradleBuildStatus> Function({ |
| required String line, |
| required FlutterProject project, |
| required bool usesAndroidX, |
| required bool multidexEnabled, |
| }) handler; |
| |
| /// The [BuildEvent] label is named gradle-[eventLabel]. |
| /// If not empty, the build event is logged along with |
| /// additional metadata such as the attempt number. |
| final String? eventLabel; |
| } |
| |
| /// The status of the Gradle build. |
| enum GradleBuildStatus { |
| /// The tool cannot recover from the failure and should exit. |
| exit, |
| /// The tool can retry the exact same build. |
| retry, |
| } |
| |
| /// Returns a simple test function that evaluates to `true` if at least one of |
| /// `errorMessages` is contained in the error message. |
| GradleErrorTest _lineMatcher(List<String> errorMessages) { |
| return (String line) { |
| return errorMessages.any((String errorMessage) => line.contains(errorMessage)); |
| }; |
| } |
| |
| /// The list of Gradle errors that the tool can handle. |
| /// |
| /// The handlers are executed in the order in which they appear in the list. |
| /// |
| /// Only the first error handler for which the [test] function returns [true] |
| /// is handled. As a result, sort error handlers based on how strict the [test] |
| /// function is to eliminate false positives. |
| final List<GradleHandledError> gradleErrors = <GradleHandledError>[ |
| licenseNotAcceptedHandler, |
| networkErrorHandler, |
| permissionDeniedErrorHandler, |
| flavorUndefinedHandler, |
| r8FailureHandler, |
| minSdkVersion, |
| transformInputIssue, |
| lockFileDepMissing, |
| multidexErrorHandler, |
| incompatibleKotlinVersionHandler, |
| ]; |
| |
| // Multidex error message. |
| @visibleForTesting |
| final GradleHandledError multidexErrorHandler = GradleHandledError( |
| test: _lineMatcher(const <String>[ |
| 'com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives:', |
| 'The number of method references in a .dex file cannot exceed 64K.', |
| ]), |
| handler: ({ |
| required String line, |
| required FlutterProject project, |
| required bool usesAndroidX, |
| required bool multidexEnabled, |
| }) async { |
| globals.printStatus('${globals.logger.terminal.warningMark} App requires Multidex support', emphasis: true); |
| if (multidexEnabled) { |
| globals.printStatus( |
| 'Multidex support is required for your android app to build since the number of methods has exceeded 64k. ' |
| "You may pass the --no-multidex flag to skip Flutter's multidex support to use a manual solution.\n", |
| indent: 4, |
| ); |
| if (!androidManifestHasNameVariable(project.directory)) { |
| globals.printStatus( |
| r'Your `android/app/src/main/AndroidManifest.xml` does not contain `android:name="${applicationName}"` ' |
| 'under the `application` element. This may be due to creating your project with an old version of Flutter. ' |
| 'Add the `android:name="\${applicationName}"` attribute to your AndroidManifest.xml to enable Flutter\'s multidex support:\n', |
| indent: 4, |
| ); |
| globals.printStatus(r''' |
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
| ... |
| <application |
| ... |
| android:name=''', |
| indent: 8, |
| newline: false, |
| color: TerminalColor.grey, |
| ); |
| globals.printStatus(r'"${applicationName}"', color: TerminalColor.green, newline: true); |
| globals.printStatus(r''' |
| ...> |
| ''', |
| indent: 8, |
| color: TerminalColor.grey, |
| ); |
| |
| globals.printStatus( |
| 'You may also roll your own multidex support by following the guide at: https://developer.android.com/studio/build/multidex\n', |
| indent: 4, |
| ); |
| return GradleBuildStatus.exit; |
| } |
| if (!multiDexApplicationExists(project.directory)) { |
| globals.printStatus( |
| 'Flutter tool can add multidex support. The following file will be added by flutter:\n', |
| indent: 4, |
| ); |
| globals.printStatus( |
| 'android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java\n', |
| indent: 8, |
| ); |
| String selection = 'n'; |
| // Default to 'no' if no interactive terminal. |
| try { |
| selection = await globals.terminal.promptForCharInput( |
| <String>['y', 'n'], |
| logger: globals.logger, |
| prompt: 'Do you want to continue with adding multidex support for Android?', |
| defaultChoiceIndex: 0, |
| ); |
| } on StateError catch(e) { |
| globals.printError( |
| e.message, |
| indent: 0, |
| ); |
| } |
| if (selection == 'y') { |
| ensureMultiDexApplicationExists(project.directory); |
| globals.printStatus( |
| 'Multidex enabled. Retrying build.\n', |
| indent: 0, |
| ); |
| return GradleBuildStatus.retry; |
| } |
| } |
| } else { |
| globals.printStatus( |
| 'Flutter multidex handling is disabled. If you wish to let the tool configure multidex, use the --mutidex flag.', |
| indent: 4, |
| ); |
| } |
| return GradleBuildStatus.exit; |
| }, |
| eventLabel: 'multidex-error', |
| ); |
| |
| // Permission defined error message. |
| @visibleForTesting |
| final GradleHandledError permissionDeniedErrorHandler = GradleHandledError( |
| test: _lineMatcher(const <String>[ |
| 'Permission denied', |
| ]), |
| handler: ({ |
| required String line, |
| required FlutterProject project, |
| required bool usesAndroidX, |
| required bool multidexEnabled, |
| }) async { |
| globals.printStatus('${globals.logger.terminal.warningMark} Gradle does not have execution permission.', emphasis: true); |
| globals.printStatus( |
| 'You should change the ownership of the project directory to your user, ' |
| 'or move the project to a directory with execute permissions.', |
| indent: 4, |
| ); |
| return GradleBuildStatus.exit; |
| }, |
| eventLabel: 'permission-denied', |
| ); |
| |
| /// 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 <String>[ |
| 'java.io.FileNotFoundException: https://downloads.gradle.org', |
| 'java.io.IOException: Unable to tunnel through proxy', |
| 'java.lang.RuntimeException: Timeout of', |
| 'java.util.zip.ZipException: error in opening zip file', |
| 'javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake', |
| 'java.net.SocketException: Connection reset', |
| 'java.io.FileNotFoundException', |
| "> Could not get resource 'http", |
| ]), |
| handler: ({ |
| required String line, |
| required FlutterProject project, |
| required bool usesAndroidX, |
| required bool multidexEnabled, |
| }) async { |
| globals.printError( |
| '${globals.logger.terminal.warningMark} Gradle threw an error while downloading artifacts from the network. ' |
| 'Retrying to download...' |
| ); |
| 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', |
| ); |
| |
| // R8 failure. |
| @visibleForTesting |
| final GradleHandledError r8FailureHandler = GradleHandledError( |
| test: _lineMatcher(const <String>[ |
| 'com.android.tools.r8', |
| ]), |
| handler: ({ |
| required String line, |
| required FlutterProject project, |
| required bool usesAndroidX, |
| required bool multidexEnabled, |
| }) async { |
| globals.printStatus('${globals.logger.terminal.warningMark} The shrinker may have failed to optimize the Java bytecode.', emphasis: true); |
| globals.printStatus('To disable the shrinker, pass the `--no-shrink` flag to this command.', indent: 4); |
| globals.printStatus('To learn more, see: https://developer.android.com/studio/build/shrink-code', indent: 4); |
| return GradleBuildStatus.exit; |
| }, |
| eventLabel: 'r8', |
| ); |
| |
| /// Handle Gradle error thrown when Gradle needs to download additional |
| /// Android SDK components (e.g. Platform Tools), and the license |
| /// for that component has not been accepted. |
| @visibleForTesting |
| final GradleHandledError licenseNotAcceptedHandler = GradleHandledError( |
| test: _lineMatcher(const <String>[ |
| 'You have not accepted the license agreements of the following SDK components', |
| ]), |
| handler: ({ |
| required String line, |
| required FlutterProject project, |
| required bool usesAndroidX, |
| required bool multidexEnabled, |
| }) async { |
| const String licenseNotAcceptedMatcher = |
| r'You have not accepted the license agreements of the following SDK components:\s*\[(.+)\]'; |
| |
| final RegExp licenseFailure = RegExp(licenseNotAcceptedMatcher, multiLine: true); |
| assert(licenseFailure != null); |
| final Match? licenseMatch = licenseFailure.firstMatch(line); |
| globals.printStatus( |
| '${globals.logger.terminal.warningMark} Unable to download needed Android SDK components, as the ' |
| 'following licenses have not been accepted:\n' |
| '${licenseMatch?.group(1)}\n\n' |
| 'To resolve this, please run the following command in a Terminal:\n' |
| 'flutter doctor --android-licenses' |
| ); |
| return GradleBuildStatus.exit; |
| }, |
| eventLabel: 'license-not-accepted', |
| ); |
| |
| final RegExp _undefinedTaskPattern = RegExp(r'Task .+ not found in root project.'); |
| |
| final RegExp _assembleTaskPattern = RegExp(r'assemble(\S+)'); |
| |
| /// Handler when a flavor is undefined. |
| @visibleForTesting |
| final GradleHandledError flavorUndefinedHandler = GradleHandledError( |
| test: (String line) { |
| return _undefinedTaskPattern.hasMatch(line); |
| }, |
| handler: ({ |
| required String line, |
| required FlutterProject project, |
| required bool usesAndroidX, |
| required bool multidexEnabled, |
| }) async { |
| final RunResult tasksRunResult = await globals.processUtils.run( |
| <String>[ |
| globals.gradleUtils!.getExecutable(project), |
| 'app:tasks' , |
| '--all', |
| '--console=auto', |
| ], |
| throwOnError: true, |
| workingDirectory: project.android.hostAppGradleRoot.path, |
| environment: <String, String>{ |
| if (javaPath != null) |
| 'JAVA_HOME': javaPath!, |
| }, |
| ); |
| // Extract build types and product flavors. |
| final Set<String> variants = <String>{}; |
| for (final String task in tasksRunResult.stdout.split('\n')) { |
| final Match? match = _assembleTaskPattern.matchAsPrefix(task); |
| if (match != null) { |
| final String variant = match.group(1)!.toLowerCase(); |
| if (!variant.endsWith('test')) { |
| variants.add(variant); |
| } |
| } |
| } |
| final Set<String> productFlavors = <String>{}; |
| for (final String variant1 in variants) { |
| for (final String variant2 in variants) { |
| if (variant2.startsWith(variant1) && variant2 != variant1) { |
| final String buildType = variant2.substring(variant1.length); |
| if (variants.contains(buildType)) { |
| productFlavors.add(variant1); |
| } |
| } |
| } |
| } |
| globals.printStatus( |
| '\n${globals.logger.terminal.warningMark} Gradle project does not define a task suitable ' |
| 'for the requested build.' |
| ); |
| if (productFlavors.isEmpty) { |
| globals.printStatus( |
| 'The android/app/build.gradle file does not define ' |
| 'any custom product flavors. ' |
| 'You cannot use the --flavor option.' |
| ); |
| } else { |
| globals.printStatus( |
| 'The android/app/build.gradle file defines product ' |
| 'flavors: ${productFlavors.join(', ')} ' |
| 'You must specify a --flavor option to select one of them.' |
| ); |
| } |
| return GradleBuildStatus.exit; |
| }, |
| eventLabel: 'flavor-undefined', |
| ); |
| |
| |
| final RegExp _minSdkVersionPattern = RegExp(r'uses-sdk:minSdkVersion ([0-9]+) cannot be smaller than version ([0-9]+) declared in library \[\:(.+)\]'); |
| |
| /// Handler when a plugin requires a higher Android API level. |
| @visibleForTesting |
| final GradleHandledError minSdkVersion = GradleHandledError( |
| test: (String line) { |
| return _minSdkVersionPattern.hasMatch(line); |
| }, |
| handler: ({ |
| required String line, |
| required FlutterProject project, |
| required bool usesAndroidX, |
| required bool multidexEnabled, |
| }) async { |
| final File gradleFile = project.directory |
| .childDirectory('android') |
| .childDirectory('app') |
| .childFile('build.gradle'); |
| |
| final Match? minSdkVersionMatch = _minSdkVersionPattern.firstMatch(line); |
| assert(minSdkVersionMatch?.groupCount == 3); |
| |
| final String bold = globals.logger.terminal.bolden( |
| 'Fix this issue by adding the following to the file ${gradleFile.path}:\n' |
| 'android {\n' |
| ' defaultConfig {\n' |
| ' minSdkVersion ${minSdkVersionMatch?.group(2)}\n' |
| ' }\n' |
| '}\n' |
| ); |
| globals.printStatus( |
| '\n' |
| 'The plugin ${minSdkVersionMatch?.group(3)} requires a higher Android SDK version.\n' |
| '$bold\n' |
| "Note that your app won't be available to users running Android SDKs below ${minSdkVersionMatch?.group(2)}.\n" |
| 'Alternatively, try to find a version of this plugin that supports these lower versions of the Android SDK.' |
| ); |
| return GradleBuildStatus.exit; |
| }, |
| eventLabel: 'plugin-min-sdk', |
| ); |
| |
| /// Handler when https://issuetracker.google.com/issues/141126614 or |
| /// https://github.com/flutter/flutter/issues/58247 is triggered. |
| @visibleForTesting |
| final GradleHandledError transformInputIssue = GradleHandledError( |
| test: (String line) { |
| return line.contains('https://issuetracker.google.com/issues/158753935'); |
| }, |
| handler: ({ |
| required String line, |
| required FlutterProject project, |
| required bool usesAndroidX, |
| required bool multidexEnabled, |
| }) async { |
| final File gradleFile = project.directory |
| .childDirectory('android') |
| .childDirectory('app') |
| .childFile('build.gradle'); |
| final String bold = globals.logger.terminal.bolden( |
| 'Fix this issue by adding the following to the file ${gradleFile.path}:\n' |
| 'android {\n' |
| ' lintOptions {\n' |
| ' checkReleaseBuilds false\n' |
| ' }\n' |
| '}' |
| ); |
| globals.printStatus( |
| '\n' |
| 'This issue appears to be https://github.com/flutter/flutter/issues/58247.\n' |
| '$bold' |
| ); |
| return GradleBuildStatus.exit; |
| }, |
| eventLabel: 'transform-input-issue', |
| ); |
| |
| /// Handler when a dependency is missing in the lockfile. |
| @visibleForTesting |
| final GradleHandledError lockFileDepMissing = GradleHandledError( |
| test: (String line) { |
| return line.contains('which is not part of the dependency lock state'); |
| }, |
| handler: ({ |
| required String line, |
| required FlutterProject project, |
| required bool usesAndroidX, |
| required bool multidexEnabled, |
| }) async { |
| final File gradleFile = project.directory |
| .childDirectory('android') |
| .childFile('build.gradle'); |
| final String bold = globals.logger.terminal.bolden( |
| 'To regenerate the lockfiles run: `./gradlew :generateLockfiles` in ${gradleFile.path}\n' |
| 'To remove dependency locking, remove the `dependencyLocking` from ${gradleFile.path}\n' |
| ); |
| globals.printStatus( |
| '\n' |
| 'You need to update the lockfile, or disable Gradle dependency locking.\n' |
| '$bold' |
| ); |
| return GradleBuildStatus.exit; |
| }, |
| eventLabel: 'lock-dep-issue', |
| ); |
| |
| @visibleForTesting |
| final GradleHandledError incompatibleKotlinVersionHandler = GradleHandledError( |
| test: _lineMatcher(const <String>[ |
| 'Module was compiled with an incompatible version of Kotlin', |
| ]), |
| handler: ({ |
| required String line, |
| required FlutterProject project, |
| required bool usesAndroidX, |
| required bool multidexEnabled, |
| }) async { |
| final File gradleFile = project.directory |
| .childDirectory('android') |
| .childFile('build.gradle'); |
| globals.printStatus( |
| '${globals.logger.terminal.warningMark} Your project requires a newer version of the Kotlin Gradle plugin.', |
| emphasis: true, |
| ); |
| globals.printStatus( |
| 'Find the latest version on https://kotlinlang.org/docs/gradle.html#plugin-and-versions, ' |
| 'then update ${gradleFile.path}:\n' |
| "ext.kotlin_version = '<latest-version>'", |
| indent: 4, |
| ); |
| return GradleBuildStatus.exit; |
| }, |
| eventLabel: 'incompatible-kotlin-version', |
| ); |