| // 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/process.dart'; |
| import '../base/terminal.dart'; |
| import '../globals.dart' as globals; |
| import '../project.dart'; |
| import '../reporting/reporting.dart'; |
| import 'gradle_utils.dart'; |
| |
| typedef GradleErrorTest = bool Function(String); |
| |
| /// A Gradle error handled by the tool. |
| class GradleHandledError { |
| const GradleHandledError({ |
| this.test, |
| 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({ |
| String line, |
| FlutterProject project, |
| bool usesAndroidX, |
| bool shouldBuildPluginAsAar, |
| }) 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, |
| /// The tool can build the plugins as AAR and retry the build. |
| retryWithAarPlugins, |
| } |
| |
| /// Returns a simple test function that evaluates to [true] if |
| /// [errorMessage] 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, |
| androidXFailureHandler, |
| ]; |
| |
| // Permission defined error message. |
| @visibleForTesting |
| final GradleHandledError permissionDeniedErrorHandler = GradleHandledError( |
| test: _lineMatcher(const <String>[ |
| 'Permission denied', |
| ]), |
| handler: ({ |
| String line, |
| FlutterProject project, |
| bool usesAndroidX, |
| bool shouldBuildPluginAsAar, |
| }) async { |
| globals.printStatus('$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. |
| @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', |
| 'Gateway Time-out' |
| ]), |
| handler: ({ |
| String line, |
| FlutterProject project, |
| bool usesAndroidX, |
| bool shouldBuildPluginAsAar, |
| }) async { |
| globals.printError( |
| '$warningMark Gradle threw an error while downloading artifacts from the network. ' |
| 'Retrying to download...' |
| ); |
| return GradleBuildStatus.retry; |
| }, |
| eventLabel: 'network', |
| ); |
| |
| // R8 failure. |
| @visibleForTesting |
| final GradleHandledError r8FailureHandler = GradleHandledError( |
| test: _lineMatcher(const <String>[ |
| 'com.android.tools.r8', |
| ]), |
| handler: ({ |
| String line, |
| FlutterProject project, |
| bool usesAndroidX, |
| bool shouldBuildPluginAsAar, |
| }) async { |
| globals.printStatus('$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', |
| ); |
| |
| // AndroidX failure. |
| // |
| // This regex is intentionally broad. AndroidX errors can manifest in multiple |
| // different ways and each one depends on the specific code config and |
| // filesystem paths of the project. Throwing the broadest net possible here to |
| // catch all known and likely cases. |
| // |
| // Example stack traces: |
| // https://github.com/flutter/flutter/issues/27226 "AAPT: error: resource android:attr/fontVariationSettings not found." |
| // https://github.com/flutter/flutter/issues/27106 "Android resource linking failed|Daemon: AAPT2|error: failed linking references" |
| // https://github.com/flutter/flutter/issues/27493 "error: cannot find symbol import androidx.annotation.NonNull;" |
| // https://github.com/flutter/flutter/issues/23995 "error: package android.support.annotation does not exist import android.support.annotation.NonNull;" |
| final RegExp _androidXFailureRegex = RegExp(r'(AAPT|androidx|android\.support)'); |
| |
| final RegExp androidXPluginWarningRegex = RegExp(r'\*{57}' |
| r"|WARNING: This version of (\w+) will break your Android build if it or its dependencies aren't compatible with AndroidX." |
| r'|See https://goo.gl/CP92wY for more information on the problem and how to fix it.' |
| r'|This warning prints for all Android build failures. The real root cause of the error may be unrelated.'); |
| |
| @visibleForTesting |
| final GradleHandledError androidXFailureHandler = GradleHandledError( |
| test: (String line) { |
| return !androidXPluginWarningRegex.hasMatch(line) && |
| _androidXFailureRegex.hasMatch(line); |
| }, |
| handler: ({ |
| String line, |
| FlutterProject project, |
| bool usesAndroidX, |
| bool shouldBuildPluginAsAar, |
| }) async { |
| final bool hasPlugins = project.flutterPluginsFile.existsSync(); |
| if (!hasPlugins) { |
| // If the app doesn't use any plugin, then it's unclear where |
| // the incompatibility is coming from. |
| BuildEvent( |
| 'gradle-android-x-failure', |
| eventError: 'app-not-using-plugins', |
| flutterUsage: globals.flutterUsage, |
| ).send(); |
| } |
| if (hasPlugins && !usesAndroidX) { |
| // If the app isn't using AndroidX, then the app is likely using |
| // a plugin already migrated to AndroidX. |
| globals.printStatus( |
| 'AndroidX incompatibilities may have caused this build to fail. ' |
| 'Please migrate your app to AndroidX. See https://goo.gl/CP92wY .' |
| ); |
| BuildEvent( |
| 'gradle-android-x-failure', |
| eventError: 'app-not-using-androidx', |
| flutterUsage: globals.flutterUsage, |
| ).send(); |
| } |
| if (hasPlugins && usesAndroidX && shouldBuildPluginAsAar) { |
| // This is a dependency conflict instead of an AndroidX failure since |
| // by this point the app is using AndroidX, the plugins are built as |
| // AARs, Jetifier translated Support libraries for AndroidX equivalents. |
| BuildEvent( |
| 'gradle-android-x-failure', |
| eventError: 'using-jetifier', |
| flutterUsage: globals.flutterUsage, |
| ).send(); |
| } |
| if (hasPlugins && usesAndroidX && !shouldBuildPluginAsAar) { |
| globals.printStatus( |
| 'The built failed likely due to AndroidX incompatibilities in a plugin. ' |
| 'The tool is about to try using Jetfier to solve the incompatibility.' |
| ); |
| BuildEvent( |
| 'gradle-android-x-failure', |
| eventError: 'not-using-jetifier', |
| flutterUsage: globals.flutterUsage, |
| ).send(); |
| return GradleBuildStatus.retryWithAarPlugins; |
| } |
| return GradleBuildStatus.exit; |
| }, |
| eventLabel: 'android-x', |
| ); |
| |
| /// 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: ({ |
| String line, |
| FlutterProject project, |
| bool usesAndroidX, |
| bool shouldBuildPluginAsAar, |
| }) 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( |
| '$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: ({ |
| String line, |
| FlutterProject project, |
| bool usesAndroidX, |
| bool shouldBuildPluginAsAar, |
| }) async { |
| final RunResult tasksRunResult = await processUtils.run( |
| <String>[ |
| gradleUtils.getExecutable(project), |
| 'app:tasks' , |
| '--all', |
| '--console=auto', |
| ], |
| throwOnError: true, |
| workingDirectory: project.android.hostAppGradleRoot.path, |
| environment: gradleEnvironment, |
| ); |
| // 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$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', |
| ); |