mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
331 lines
12 KiB
Dart
331 lines
12 KiB
Dart
// 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',
|
|
);
|