Refactor gradle.dart (#43479)

This commit is contained in:
Emmanuel Garcia 2019-10-31 13:19:15 -07:00 committed by GitHub
parent 0028887a69
commit 175b37247d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 2688 additions and 1990 deletions

View File

@ -6,19 +6,23 @@ import 'dart:async';
import 'package:meta/meta.dart';
import '../base/common.dart';
import '../android/gradle_errors.dart';
import '../base/context.dart';
import '../base/file_system.dart';
import '../build_info.dart';
import '../project.dart';
import 'android_sdk.dart';
import 'gradle.dart';
/// The builder in the current context.
AndroidBuilder get androidBuilder => context.get<AndroidBuilder>() ?? _AndroidBuilderImpl();
AndroidBuilder get androidBuilder {
return context.get<AndroidBuilder>() ?? const _AndroidBuilderImpl();
}
/// Provides the methods to build Android artifacts.
// TODO(egarciad): https://github.com/flutter/flutter/issues/43863
abstract class AndroidBuilder {
const AndroidBuilder();
/// Builds an AAR artifact.
Future<void> buildAar({
@required FlutterProject project,
@ -44,7 +48,7 @@ abstract class AndroidBuilder {
/// Default implementation of [AarBuilder].
class _AndroidBuilderImpl extends AndroidBuilder {
_AndroidBuilderImpl();
const _AndroidBuilderImpl();
/// Builds the AAR and POM files for the current Flutter module or plugin.
@override
@ -54,27 +58,18 @@ class _AndroidBuilderImpl extends AndroidBuilder {
@required String target,
@required String outputDir,
}) async {
if (!project.android.isUsingGradle) {
throwToolExit(
'The build process for Android has changed, and the current project configuration '
'is no longer valid. Please consult\n\n'
' https://github.com/flutter/flutter/wiki/Upgrading-Flutter-projects-to-build-with-gradle\n\n'
'for details on how to upgrade the project.'
);
}
if (!project.manifest.isModule && !project.manifest.isPlugin) {
throwToolExit('AARs can only be built for plugin or module projects.');
}
// Validate that we can find an Android SDK.
if (androidSdk == null) {
throwToolExit('No Android SDK found. Try setting the `ANDROID_SDK_ROOT` environment variable.');
}
try {
Directory outputDirectory =
fs.directory(outputDir ?? project.android.buildDirectory);
if (project.isModule) {
// Module projects artifacts are located in `build/host`.
outputDirectory = outputDirectory.childDirectory('host');
}
await buildGradleAar(
project: project,
androidBuildInfo: androidBuildInfo,
target: target,
outputDir: outputDir,
outputDir: outputDirectory,
);
} finally {
androidSdk.reinitialize();
@ -88,24 +83,13 @@ class _AndroidBuilderImpl extends AndroidBuilder {
@required AndroidBuildInfo androidBuildInfo,
@required String target,
}) async {
if (!project.android.isUsingGradle) {
throwToolExit(
'The build process for Android has changed, and the current project configuration '
'is no longer valid. Please consult\n\n'
' https://github.com/flutter/flutter/wiki/Upgrading-Flutter-projects-to-build-with-gradle\n\n'
'for details on how to upgrade the project.'
);
}
// Validate that we can find an android sdk.
if (androidSdk == null) {
throwToolExit('No Android SDK found. Try setting the ANDROID_SDK_ROOT environment variable.');
}
try {
await buildGradleProject(
await buildGradleApp(
project: project,
androidBuildInfo: androidBuildInfo,
target: target,
isBuildingBundle: false,
localGradleErrors: gradleErrors,
);
} finally {
androidSdk.reinitialize();
@ -119,54 +103,16 @@ class _AndroidBuilderImpl extends AndroidBuilder {
@required AndroidBuildInfo androidBuildInfo,
@required String target,
}) async {
if (!project.android.isUsingGradle) {
throwToolExit(
'The build process for Android has changed, and the current project configuration '
'is no longer valid. Please consult\n\n'
'https://github.com/flutter/flutter/wiki/Upgrading-Flutter-projects-to-build-with-gradle\n\n'
'for details on how to upgrade the project.'
);
}
// Validate that we can find an android sdk.
if (androidSdk == null) {
throwToolExit('No Android SDK found. Try setting the ANDROID_HOME environment variable.');
}
try {
await buildGradleProject(
await buildGradleApp(
project: project,
androidBuildInfo: androidBuildInfo,
target: target,
isBuildingBundle: true,
localGradleErrors: gradleErrors,
);
} finally {
androidSdk.reinitialize();
}
}
}
/// A fake implementation of [AndroidBuilder].
@visibleForTesting
class FakeAndroidBuilder implements AndroidBuilder {
@override
Future<void> buildAar({
@required FlutterProject project,
@required AndroidBuildInfo androidBuildInfo,
@required String target,
@required String outputDir,
}) async {}
@override
Future<void> buildApk({
@required FlutterProject project,
@required AndroidBuildInfo androidBuildInfo,
@required String target,
}) async {}
@override
Future<void> buildAab({
@required FlutterProject project,
@required AndroidBuildInfo androidBuildInfo,
@required String target,
}) async {}
}

View File

@ -236,7 +236,7 @@ class AndroidStudio implements Comparable<AndroidStudio> {
// Read all $HOME/.AndroidStudio*/system/.home files. There may be several
// pointing to the same installation, so we grab only the latest one.
if (fs.directory(homeDirPath).existsSync()) {
if (homeDirPath != null && fs.directory(homeDirPath).existsSync()) {
for (FileSystemEntity entity in fs.directory(homeDirPath).listSync(followLinks: false)) {
if (entity is Directory && entity.basename.startsWith('.AndroidStudio')) {
final AndroidStudio studio = AndroidStudio.fromHomeDot(entity);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,326 @@
// Copyright 2019 The Chromium 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';
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 {
printStatus('$warningMark Gradle does not have permission to execute by your user.', emphasis: true);
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',
]),
handler: ({
String line,
FlutterProject project,
bool usesAndroidX,
bool shouldBuildPluginAsAar,
}) async {
printError(
'$warningMark Gradle threw an error while trying to update itself. '
'Retrying the update...'
);
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 {
printStatus('$warningMark The shrinker may have failed to optimize the Java bytecode.', emphasis: true);
printStatus('To disable the shrinker, pass the `--no-shrink` flag to this command.', indent: 4);
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',
).send();
}
if (hasPlugins && !usesAndroidX) {
// If the app isn't using AndroidX, then the app is likely using
// a plugin already migrated to AndroidX.
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',
).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',
).send();
}
if (hasPlugins && usesAndroidX && !shouldBuildPluginAsAar) {
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',
).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:'
r'\s*\[(.+)\]';
final RegExp licenseFailure = RegExp(licenseNotAcceptedMatcher, multiLine: true);
assert(licenseFailure != null);
final Match licenseMatch = licenseFailure.firstMatch(line);
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 (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);
}
}
}
}
printStatus(
'\n$warningMark Gradle project does not define a task suitable '
'for the requested build.'
);
if (productFlavors.isEmpty) {
printStatus(
'The android/app/build.gradle file does not define '
'any custom product flavors. '
'You cannot use the --flavor option.'
);
} else {
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',
);

View File

@ -0,0 +1,284 @@
// Copyright 2019 The Chromium 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 '../android/android_sdk.dart';
import '../base/common.dart';
import '../base/context.dart';
import '../base/file_system.dart';
import '../base/os.dart';
import '../base/platform.dart';
import '../base/terminal.dart';
import '../base/utils.dart';
import '../base/version.dart';
import '../build_info.dart';
import '../cache.dart';
import '../globals.dart';
import '../project.dart';
import '../reporting/reporting.dart';
import 'android_sdk.dart';
import 'android_studio.dart';
/// The environment variables needed to run Gradle.
Map<String, String> get gradleEnvironment {
final Map<String, String> environment = Map<String, String>.from(platform.environment);
if (javaPath != null) {
// Use java bundled with Android Studio.
environment['JAVA_HOME'] = javaPath;
}
// Don't log analytics for downstream Flutter commands.
// e.g. `flutter build bundle`.
environment['FLUTTER_SUPPRESS_ANALYTICS'] = 'true';
return environment;
}
/// Gradle utils in the current [AppContext].
GradleUtils get gradleUtils => context.get<GradleUtils>();
/// Provides utilities to run a Gradle task,
/// such as finding the Gradle executable or constructing a Gradle project.
class GradleUtils {
/// Gets the Gradle executable path and prepares the Gradle project.
/// This is the `gradlew` or `gradlew.bat` script in the `android/` directory.
String getExecutable(FlutterProject project) {
final Directory androidDir = project.android.hostAppGradleRoot;
// Update the project if needed.
// TODO(egarciad): https://github.com/flutter/flutter/issues/40460
migrateToR8(androidDir);
injectGradleWrapperIfNeeded(androidDir);
final File gradle = androidDir.childFile(
platform.isWindows ? 'gradlew.bat' : 'gradlew',
);
if (gradle.existsSync()) {
printTrace('Using gradle from ${gradle.absolute.path}.');
return gradle.absolute.path;
}
throwToolExit(
'Unable to locate gradlew script. Please check that ${gradle.path} '
'exists or that ${gradle.dirname} can be read.'
);
return null;
}
}
/// Migrates the Android's [directory] to R8.
/// https://developer.android.com/studio/build/shrink-code
@visibleForTesting
void migrateToR8(Directory directory) {
final File gradleProperties = directory.childFile('gradle.properties');
if (!gradleProperties.existsSync()) {
throwToolExit(
'Expected file ${gradleProperties.path}. '
'Please ensure that this file exists or that ${gradleProperties.dirname} can be read.'
);
}
final String propertiesContent = gradleProperties.readAsStringSync();
if (propertiesContent.contains('android.enableR8')) {
printTrace('gradle.properties already sets `android.enableR8`');
return;
}
printTrace('set `android.enableR8=true` in gradle.properties');
try {
if (propertiesContent.isNotEmpty && !propertiesContent.endsWith('\n')) {
// Add a new line if the file doesn't end with a new line.
gradleProperties.writeAsStringSync('\n', mode: FileMode.append);
}
gradleProperties.writeAsStringSync('android.enableR8=true\n', mode: FileMode.append);
} on FileSystemException {
throwToolExit(
'The tool failed to add `android.enableR8=true` to ${gradleProperties.path}. '
'Please update the file manually and try this command again.'
);
}
}
/// Injects the Gradle wrapper files if any of these files don't exist in [directory].
void injectGradleWrapperIfNeeded(Directory directory) {
copyDirectorySync(
cache.getArtifactDirectory('gradle_wrapper'),
directory,
shouldCopyFile: (File sourceFile, File destinationFile) {
// Don't override the existing files in the project.
return !destinationFile.existsSync();
},
onFileCopied: (File sourceFile, File destinationFile) {
final String modes = sourceFile.statSync().modeString();
if (modes != null && modes.contains('x')) {
os.makeExecutable(destinationFile);
}
},
);
// Add the `gradle-wrapper.properties` file if it doesn't exist.
final File propertiesFile = directory.childFile(
fs.path.join('gradle', 'wrapper', 'gradle-wrapper.properties'));
if (!propertiesFile.existsSync()) {
final String gradleVersion = getGradleVersionForAndroidPlugin(directory);
propertiesFile.writeAsStringSync('''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\\://services.gradle.org/distributions/gradle-$gradleVersion-all.zip
''', flush: true,
);
}
}
const String _defaultGradleVersion = '5.6.2';
final RegExp _androidPluginRegExp = RegExp('com\.android\.tools\.build\:gradle\:(\\d+\.\\d+\.\\d+\)');
/// Returns the Gradle version that the current Android plugin depends on when found,
/// otherwise it returns a default version.
///
/// The Android plugin version is specified in the [build.gradle] file within
/// the project's Android directory.
String getGradleVersionForAndroidPlugin(Directory directory) {
final File buildFile = directory.childFile('build.gradle');
if (!buildFile.existsSync()) {
return _defaultGradleVersion;
}
final String buildFileContent = buildFile.readAsStringSync();
final Iterable<Match> pluginMatches = _androidPluginRegExp.allMatches(buildFileContent);
if (pluginMatches.isEmpty) {
return _defaultGradleVersion;
}
final String androidPluginVersion = pluginMatches.first.group(1);
return getGradleVersionFor(androidPluginVersion);
}
/// Returns true if [targetVersion] is within the range [min] and [max] inclusive.
bool _isWithinVersionRange(
String targetVersion, {
@required String min,
@required String max,
}) {
assert(min != null);
assert(max != null);
final Version parsedTargetVersion = Version.parse(targetVersion);
return parsedTargetVersion >= Version.parse(min) &&
parsedTargetVersion <= Version.parse(max);
}
/// Returns the Gradle version that is required by the given Android Gradle plugin version
/// by picking the largest compatible version from
/// https://developer.android.com/studio/releases/gradle-plugin#updating-gradle
String getGradleVersionFor(String androidPluginVersion) {
if (_isWithinVersionRange(androidPluginVersion, min: '1.0.0', max: '1.1.3')) {
return '2.3';
}
if (_isWithinVersionRange(androidPluginVersion, min: '1.2.0', max: '1.3.1')) {
return '2.9';
}
if (_isWithinVersionRange(androidPluginVersion, min: '1.5.0', max: '1.5.0')) {
return '2.2.1';
}
if (_isWithinVersionRange(androidPluginVersion, min: '2.0.0', max: '2.1.2')) {
return '2.13';
}
if (_isWithinVersionRange(androidPluginVersion, min: '2.1.3', max: '2.2.3')) {
return '2.14.1';
}
if (_isWithinVersionRange(androidPluginVersion, min: '2.3.0', max: '2.9.9')) {
return '3.3';
}
if (_isWithinVersionRange(androidPluginVersion, min: '3.0.0', max: '3.0.9')) {
return '4.1';
}
if (_isWithinVersionRange(androidPluginVersion, min: '3.1.0', max: '3.1.9')) {
return '4.4';
}
if (_isWithinVersionRange(androidPluginVersion, min: '3.2.0', max: '3.2.1')) {
return '4.6';
}
if (_isWithinVersionRange(androidPluginVersion, min: '3.3.0', max: '3.3.2')) {
return '4.10.2';
}
if (_isWithinVersionRange(androidPluginVersion, min: '3.4.0', max: '3.5.0')) {
return '5.6.2';
}
throwToolExit('Unsuported Android Plugin version: $androidPluginVersion.');
return '';
}
/// Overwrite local.properties in the specified Flutter project's Android
/// sub-project, if needed.
///
/// If [requireAndroidSdk] is true (the default) and no Android SDK is found,
/// this will fail with a [ToolExit].
void updateLocalProperties({
@required FlutterProject project,
BuildInfo buildInfo,
bool requireAndroidSdk = true,
}) {
if (requireAndroidSdk && androidSdk == null) {
exitWithNoSdkMessage();
}
final File localProperties = project.android.localPropertiesFile;
bool changed = false;
SettingsFile settings;
if (localProperties.existsSync()) {
settings = SettingsFile.parseFromFile(localProperties);
} else {
settings = SettingsFile();
changed = true;
}
void changeIfNecessary(String key, String value) {
if (settings.values[key] == value) {
return;
}
if (value == null) {
settings.values.remove(key);
} else {
settings.values[key] = value;
}
changed = true;
}
if (androidSdk != null) {
changeIfNecessary('sdk.dir', escapePath(androidSdk.directory));
}
changeIfNecessary('flutter.sdk', escapePath(Cache.flutterRoot));
if (buildInfo != null) {
changeIfNecessary('flutter.buildMode', buildInfo.modeName);
final String buildName = validatedBuildNameForPlatform(
TargetPlatform.android_arm,
buildInfo.buildName ?? project.manifest.buildName,
);
changeIfNecessary('flutter.versionName', buildName);
final String buildNumber = validatedBuildNumberForPlatform(
TargetPlatform.android_arm,
buildInfo.buildNumber ?? project.manifest.buildNumber,
);
changeIfNecessary('flutter.versionCode', buildNumber?.toString());
}
if (changed) {
settings.writeContents(localProperties);
}
}
/// Writes standard Android local properties to the specified [properties] file.
///
/// Writes the path to the Android SDK, if known.
void writeLocalProperties(File properties) {
final SettingsFile settings = SettingsFile();
if (androidSdk != null) {
settings.values['sdk.dir'] = escapePath(androidSdk.directory);
}
settings.writeContents(properties);
}
void exitWithNoSdkMessage() {
BuildEvent('unsupported-project', eventError: 'android-sdk-not-found').send();
throwToolExit(
'$warningMark No Android SDK found. '
'Try setting the ANDROID_HOME environment variable.'
);
}

View File

@ -23,6 +23,17 @@ enum TerminalColor {
AnsiTerminal get terminal {
return context?.get<AnsiTerminal>() ?? _defaultAnsiTerminal;
}
/// Warning mark to use in stdout or stderr.
String get warningMark {
return terminal.bolden(terminal.color('[!]', TerminalColor.red));
}
/// Success mark to use in stdout.
String get successMark {
return terminal.bolden(terminal.color('', TerminalColor.green));
}
final AnsiTerminal _defaultAnsiTerminal = AnsiTerminal();
OutputPreferences get outputPreferences {

View File

@ -6,7 +6,7 @@ import 'dart:async';
import 'package:meta/meta.dart';
import 'android/gradle.dart';
import 'android/gradle_utils.dart';
import 'base/common.dart';
import 'base/context.dart';
import 'base/file_system.dart';
@ -914,7 +914,7 @@ class AndroidMavenArtifacts extends ArtifactSet {
'--project-cache-dir', tempDir.path,
'resolveDependencies',
],
environment: gradleEnv);
environment: gradleEnvironment);
if (processResult.exitCode != 0) {
printError('Failed to download the Android dependencies');
}

View File

@ -10,7 +10,7 @@ import 'package:yaml/yaml.dart' as yaml;
import '../android/android.dart' as android;
import '../android/android_sdk.dart' as android_sdk;
import '../android/gradle.dart' as gradle;
import '../android/gradle_utils.dart' as gradle;
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/net.dart';

View File

@ -7,7 +7,7 @@ import 'dart:async';
import 'android/android_sdk.dart';
import 'android/android_studio.dart';
import 'android/android_workflow.dart';
import 'android/gradle.dart';
import 'android/gradle_utils.dart';
import 'application_package.dart';
import 'artifacts.dart';
import 'asset.dart';

View File

@ -7,7 +7,7 @@ import 'dart:async';
import 'package:meta/meta.dart';
import 'package:yaml/yaml.dart';
import 'android/gradle.dart' as gradle;
import 'android/gradle_utils.dart' as gradle;
import 'base/common.dart';
import 'base/context.dart';
import 'base/file_system.dart';
@ -574,6 +574,11 @@ class AndroidProject {
return _firstMatchInFile(gradleFile, _groupPattern)?.group(1);
}
/// The build directory where the Android artifacts are placed.
Directory get buildDirectory {
return parent.directory.childDirectory('build');
}
Future<void> ensureReadyForPlatformSpecificTooling() async {
if (isModule && _shouldRegenerateFromTemplate()) {
_regenerateLibrary();

View File

@ -0,0 +1,602 @@
// Copyright 2019 The Chromium 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:file/memory.dart';
import 'package:flutter_tools/src/android/gradle_utils.dart';
import 'package:flutter_tools/src/android/gradle_errors.dart';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/mocks.dart';
void main() {
group('gradleErrors', () {
test('list of errors', () {
// If you added a new Gradle error, please update this test.
expect(gradleErrors,
equals(<GradleHandledError>[
licenseNotAcceptedHandler,
networkErrorHandler,
permissionDeniedErrorHandler,
flavorUndefinedHandler,
r8FailureHandler,
androidXFailureHandler,
])
);
});
});
group('network errors', () {
testUsingContext('throws toolExit if gradle fails while downloading', () async {
const String errorMessage = '''
Exception in thread "main" java.io.FileNotFoundException: https://downloads.gradle.org/distributions/gradle-4.1.1-all.zip
at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1872)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1474)
at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:254)
at org.gradle.wrapper.Download.downloadInternal(Download.java:58)
at org.gradle.wrapper.Download.download(Download.java:44)
at org.gradle.wrapper.Install\$1.call(Install.java:61)
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(testErrorMessage(errorMessage, networkErrorHandler), isTrue);
expect(await networkErrorHandler.handler(), equals(GradleBuildStatus.retry));
final BufferLogger logger = context.get<Logger>();
expect(logger.errorText,
contains(
'Gradle threw an error while trying to update itself. '
'Retrying the update...'
)
);
});
testUsingContext('throw toolExit if gradle fails downloading with proxy error', () async {
const String errorMessage = '''
Exception in thread "main" java.io.IOException: Unable to tunnel through proxy. Proxy returns "HTTP/1.1 400 Bad Request"
at sun.net.www.protocol.http.HttpURLConnection.doTunneling(HttpURLConnection.java:2124)
at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:183)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1546)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1474)
at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:254)
at org.gradle.wrapper.Download.downloadInternal(Download.java:58)
at org.gradle.wrapper.Download.download(Download.java:44)
at org.gradle.wrapper.Install\$1.call(Install.java:61)
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(testErrorMessage(errorMessage, networkErrorHandler), isTrue);
expect(await networkErrorHandler.handler(), equals(GradleBuildStatus.retry));
final BufferLogger logger = context.get<Logger>();
expect(logger.errorText,
contains(
'Gradle threw an error while trying to update itself. '
'Retrying the update...'
)
);
});
testUsingContext('throws toolExit if gradle times out waiting for exclusive access to zip', () async {
const String errorMessage = '''
Exception in thread "main" java.lang.RuntimeException: Timeout of 120000 reached waiting for exclusive access to file: /User/documents/gradle-5.6.2-all.zip
at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:61)
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(testErrorMessage(errorMessage, networkErrorHandler), isTrue);
expect(await networkErrorHandler.handler(), equals(GradleBuildStatus.retry));
final BufferLogger logger = context.get<Logger>();
expect(logger.errorText,
contains(
'Gradle threw an error while trying to update itself. '
'Retrying the update...'
)
);
});
testUsingContext('throws toolExit if remote host closes connection', () async {
const String errorMessage = '''
Downloading https://services.gradle.org/distributions/gradle-5.6.2-all.zip
Exception in thread "main" javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake
at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:994)
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1395)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1379)
at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:559)
at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
at sun.net.www.protocol.http.HttpURLConnection.followRedirect0(HttpURLConnection.java:2729)
at sun.net.www.protocol.http.HttpURLConnection.followRedirect(HttpURLConnection.java:2641)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1824)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1492)
at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:263)
at org.gradle.wrapper.Download.downloadInternal(Download.java:58)
at org.gradle.wrapper.Download.download(Download.java:44)
at org.gradle.wrapper.Install\$1.call(Install.java:61)
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(testErrorMessage(errorMessage, networkErrorHandler), isTrue);
expect(await networkErrorHandler.handler(), equals(GradleBuildStatus.retry));
final BufferLogger logger = context.get<Logger>();
expect(logger.errorText,
contains(
'Gradle threw an error while trying to update itself. '
'Retrying the update...'
)
);
});
testUsingContext('throws toolExit if file opening fails', () async {
const String errorMessage = r'''
Downloading https://services.gradle.org/distributions/gradle-3.5.0-all.zip
Exception in thread "main" java.io.FileNotFoundException: https://downloads.gradle-dn.com/distributions/gradle-3.5.0-all.zip
at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1890)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1492)
at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:263)
at org.gradle.wrapper.Download.downloadInternal(Download.java:58)
at org.gradle.wrapper.Download.download(Download.java:44)
at org.gradle.wrapper.Install$1.call(Install.java:61)
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(testErrorMessage(errorMessage, networkErrorHandler), isTrue);
expect(await networkErrorHandler.handler(), equals(GradleBuildStatus.retry));
final BufferLogger logger = context.get<Logger>();
expect(logger.errorText,
contains(
'Gradle threw an error while trying to update itself. '
'Retrying the update...'
)
);
});
testUsingContext('throws toolExit if the connection is reset', () async {
const String errorMessage = '''
Downloading https://services.gradle.org/distributions/gradle-5.6.2-all.zip
Exception in thread "main" java.net.SocketException: Connection reset
at java.net.SocketInputStream.read(SocketInputStream.java:210)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.security.ssl.InputRecord.readFully(InputRecord.java:465)
at sun.security.ssl.InputRecord.readV3Record(InputRecord.java:593)
at sun.security.ssl.InputRecord.read(InputRecord.java:532)
at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:975)
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1395)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1379)
at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:559)
at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1564)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1492)
at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:263)
at org.gradle.wrapper.Download.downloadInternal(Download.java:58)
at org.gradle.wrapper.Download.download(Download.java:44)
at org.gradle.wrapper.Install\$1.call(Install.java:61)
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(testErrorMessage(errorMessage, networkErrorHandler), isTrue);
expect(await networkErrorHandler.handler(), equals(GradleBuildStatus.retry));
final BufferLogger logger = context.get<Logger>();
expect(logger.errorText,
contains(
'Gradle threw an error while trying to update itself. '
'Retrying the update...'
)
);
});
});
group('permission errors', () {
testUsingContext('throws toolExit if gradle is missing execute permissions', () async {
const String errorMessage = '''
Permission denied
Command: /home/android/gradlew assembleRelease
''';
expect(testErrorMessage(errorMessage, permissionDeniedErrorHandler), isTrue);
expect(await permissionDeniedErrorHandler.handler(), equals(GradleBuildStatus.exit));
final BufferLogger logger = context.get<Logger>();
expect(
logger.statusText,
contains('Gradle does not have permission to execute by your user.'),
);
expect(
logger.statusText,
contains(
'You should change the ownership of the project directory to your user, '
'or move the project to a directory with execute permissions.'
)
);
});
});
group('AndroidX', () {
final Usage mockUsage = MockUsage();
test('pattern', () {
expect(androidXFailureHandler.test(
'AAPT: error: resource android:attr/fontVariationSettings not found.'
), isTrue);
expect(androidXFailureHandler.test(
'AAPT: error: resource android:attr/ttcIndex not found.'
), isTrue);
expect(androidXFailureHandler.test(
'error: package android.support.annotation does not exist'
), isTrue);
expect(androidXFailureHandler.test(
'import android.support.annotation.NonNull;'
), isTrue);
expect(androidXFailureHandler.test(
'import androidx.annotation.NonNull;'
), isTrue);
expect(androidXFailureHandler.test(
'Daemon: AAPT2 aapt2-3.2.1-4818971-linux Daemon #0'
), isTrue);
});
testUsingContext('handler - no plugins', () async {
final GradleBuildStatus status = await androidXFailureHandler
.handler(line: '', project: FlutterProject.current());
verify(mockUsage.sendEvent(
any,
any,
label: 'gradle--android-x-failure',
parameters: <String, String>{
'cd43': 'app-not-using-plugins',
},
)).called(1);
expect(status, equals(GradleBuildStatus.exit));
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(),
ProcessManager: () => MockProcessManager(),
Usage: () => mockUsage,
});
testUsingContext('handler - plugins and no AndroidX', () async {
fs.file('.flutter-plugins').createSync(recursive: true);
final GradleBuildStatus status = await androidXFailureHandler
.handler(
line: '',
project: FlutterProject.current(),
usesAndroidX: false,
);
final BufferLogger logger = context.get<Logger>();
expect(logger.statusText,
contains(
'AndroidX incompatibilities may have caused this build to fail. '
'Please migrate your app to AndroidX. See https://goo.gl/CP92wY.'
)
);
verify(mockUsage.sendEvent(
any,
any,
label: 'gradle--android-x-failure',
parameters: <String, String>{
'cd43': 'app-not-using-androidx',
},
)).called(1);
expect(status, equals(GradleBuildStatus.exit));
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(),
ProcessManager: () => MockProcessManager(),
Usage: () => mockUsage,
});
testUsingContext('handler - plugins, AndroidX, and AAR', () async {
fs.file('.flutter-plugins').createSync(recursive: true);
final GradleBuildStatus status = await androidXFailureHandler.handler(
line: '',
project: FlutterProject.current(),
usesAndroidX: true,
shouldBuildPluginAsAar: true,
);
verify(mockUsage.sendEvent(
any,
any,
label: 'gradle--android-x-failure',
parameters: <String, String>{
'cd43': 'using-jetifier',
},
)).called(1);
expect(status, equals(GradleBuildStatus.exit));
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(),
ProcessManager: () => MockProcessManager(),
Usage: () => mockUsage,
});
testUsingContext('handler - plugins, AndroidX, and no AAR', () async {
fs.file('.flutter-plugins').createSync(recursive: true);
final GradleBuildStatus status = await androidXFailureHandler.handler(
line: '',
project: FlutterProject.current(),
usesAndroidX: true,
shouldBuildPluginAsAar: false,
);
final BufferLogger logger = context.get<Logger>();
expect(logger.statusText,
contains(
'The built failed likely due to AndroidX incompatibilities in a plugin. '
'The tool is about to try using Jetfier to solve the incompatibility.'
)
);
verify(mockUsage.sendEvent(
any,
any,
label: 'gradle--android-x-failure',
parameters: <String, String>{
'cd43': 'not-using-jetifier',
},
)).called(1);
expect(status, equals(GradleBuildStatus.retryWithAarPlugins));
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(),
ProcessManager: () => MockProcessManager(),
Usage: () => mockUsage,
});
});
group('permission errors', () {
testUsingContext('pattern', () async {
const String errorMessage = '''
Permission denied
Command: /home/android/gradlew assembleRelease
''';
expect(testErrorMessage(errorMessage, permissionDeniedErrorHandler), isTrue);
});
testUsingContext('handler', () async {
expect(await permissionDeniedErrorHandler.handler(), equals(GradleBuildStatus.exit));
final BufferLogger logger = context.get<Logger>();
expect(
logger.statusText,
contains('Gradle does not have permission to execute by your user.'),
);
expect(
logger.statusText,
contains(
'You should change the ownership of the project directory to your user, '
'or move the project to a directory with execute permissions.'
)
);
});
});
group('license not accepted', () {
test('pattern', () {
expect(
licenseNotAcceptedHandler.test(
'You have not accepted the license agreements of the following SDK components'
),
isTrue,
);
});
testUsingContext('handler', () async {
await licenseNotAcceptedHandler.handler(
line: 'You have not accepted the license agreements of the following SDK components: [foo, bar]',
project: FlutterProject.current(),
);
final BufferLogger logger = context.get<Logger>();
expect(
logger.statusText,
contains(
'Unable to download needed Android SDK components, as the '
'following licenses have not been accepted:\n'
'foo, bar\n\n'
'To resolve this, please run the following command in a Terminal:\n'
'flutter doctor --android-licenses'
)
);
});
});
group('flavor undefined', () {
MockProcessManager mockProcessManager;
setUp(() {
mockProcessManager = MockProcessManager();
});
test('pattern', () {
expect(
flavorUndefinedHandler.test(
'Task assembleFooRelease not found in root project.'
),
isTrue,
);
expect(
flavorUndefinedHandler.test(
'Task assembleBarRelease not found in root project.'
),
isTrue,
);
expect(
flavorUndefinedHandler.test(
'Task assembleBar not found in root project.'
),
isTrue,
);
expect(
flavorUndefinedHandler.test(
'Task assembleBar_foo not found in root project.'
),
isTrue,
);
});
testUsingContext('handler - with flavor', () async {
when(mockProcessManager.run(
<String>[
'gradlew',
'app:tasks' ,
'--all',
'--console=auto',
],
workingDirectory: anyNamed('workingDirectory'),
environment: anyNamed('environment'),
)).thenAnswer((_) async {
return ProcessResult(
1,
0,
'''
assembleRelease
assembleFlavor1
assembleFlavor1Release
assembleFlavor_2
assembleFlavor_2Release
assembleDebug
assembleProfile
assembles
assembleFooTest
''',
'',
);
});
await flavorUndefinedHandler.handler(
project: FlutterProject.current(),
);
final BufferLogger logger = context.get<Logger>();
expect(
logger.statusText,
contains(
'Gradle project does not define a task suitable '
'for the requested build.'
)
);
expect(
logger.statusText,
contains(
'The android/app/build.gradle file defines product '
'flavors: flavor1, flavor_2 '
'You must specify a --flavor option to select one of them.'
)
);
}, overrides: <Type, Generator>{
GradleUtils: () => FakeGradleUtils(),
Platform: () => fakePlatform('android'),
ProcessManager: () => mockProcessManager,
});
testUsingContext('handler - without flavor', () async {
when(mockProcessManager.run(
<String>[
'gradlew',
'app:tasks' ,
'--all',
'--console=auto',
],
workingDirectory: anyNamed('workingDirectory'),
environment: anyNamed('environment'),
)).thenAnswer((_) async {
return ProcessResult(
1,
0,
'''
assembleRelease
assembleDebug
assembleProfile
''',
'',
);
});
await flavorUndefinedHandler.handler(
project: FlutterProject.current(),
);
final BufferLogger logger = context.get<Logger>();
expect(
logger.statusText,
contains(
'Gradle project does not define a task suitable '
'for the requested build.'
)
);
expect(
logger.statusText,
contains(
'The android/app/build.gradle file does not define any custom product flavors. '
'You cannot use the --flavor option.'
)
);
}, overrides: <Type, Generator>{
GradleUtils: () => FakeGradleUtils(),
Platform: () => fakePlatform('android'),
ProcessManager: () => mockProcessManager,
});
});
}
class MockUsage extends Mock implements Usage {}
bool testErrorMessage(String errorMessage, GradleHandledError error) {
return errorMessage
.split('\n')
.any((String line) => error.test(line));
}
Platform fakePlatform(String name) {
return FakePlatform
.fromPlatform(const LocalPlatform())
..operatingSystem = name;
}
class FakeGradleUtils extends GradleUtils {
@override
String getExecutable(FlutterProject project) {
return 'gradlew';
}
}

View File

@ -0,0 +1,137 @@
// Copyright 2019 The Chromium 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:file/memory.dart';
import 'package:flutter_tools/src/android/gradle_utils.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:process/process.dart';
import '../../src/common.dart';
import '../../src/context.dart';
void main() {
group('injectGradleWrapperIfNeeded', () {
MemoryFileSystem memoryFileSystem;
Directory tempDir;
Directory gradleWrapperDirectory;
setUp(() {
memoryFileSystem = MemoryFileSystem();
tempDir = memoryFileSystem.systemTempDirectory.createTempSync('flutter_artifacts_test.');
gradleWrapperDirectory = memoryFileSystem.directory(
memoryFileSystem.path.join(tempDir.path, 'bin', 'cache', 'artifacts', 'gradle_wrapper'));
gradleWrapperDirectory.createSync(recursive: true);
gradleWrapperDirectory
.childFile('gradlew')
.writeAsStringSync('irrelevant');
gradleWrapperDirectory
.childDirectory('gradle')
.childDirectory('wrapper')
.createSync(recursive: true);
gradleWrapperDirectory
.childDirectory('gradle')
.childDirectory('wrapper')
.childFile('gradle-wrapper.jar')
.writeAsStringSync('irrelevant');
});
testUsingContext('Inject the wrapper when all files are missing', () {
final Directory sampleAppAndroid = fs.directory('/sample-app/android');
sampleAppAndroid.createSync(recursive: true);
injectGradleWrapperIfNeeded(sampleAppAndroid);
expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue);
expect(sampleAppAndroid
.childDirectory('gradle')
.childDirectory('wrapper')
.childFile('gradle-wrapper.jar')
.existsSync(), isTrue);
expect(sampleAppAndroid
.childDirectory('gradle')
.childDirectory('wrapper')
.childFile('gradle-wrapper.properties')
.existsSync(), isTrue);
expect(sampleAppAndroid
.childDirectory('gradle')
.childDirectory('wrapper')
.childFile('gradle-wrapper.properties')
.readAsStringSync(),
'distributionBase=GRADLE_USER_HOME\n'
'distributionPath=wrapper/dists\n'
'zipStoreBase=GRADLE_USER_HOME\n'
'zipStorePath=wrapper/dists\n'
'distributionUrl=https\\://services.gradle.org/distributions/gradle-5.6.2-all.zip\n');
}, overrides: <Type, Generator>{
Cache: () => Cache(rootOverride: tempDir),
FileSystem: () => memoryFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Inject the wrapper when some files are missing', () {
final Directory sampleAppAndroid = fs.directory('/sample-app/android');
sampleAppAndroid.createSync(recursive: true);
// There's an existing gradlew
sampleAppAndroid.childFile('gradlew').writeAsStringSync('existing gradlew');
injectGradleWrapperIfNeeded(sampleAppAndroid);
expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue);
expect(sampleAppAndroid.childFile('gradlew').readAsStringSync(),
equals('existing gradlew'));
expect(sampleAppAndroid
.childDirectory('gradle')
.childDirectory('wrapper')
.childFile('gradle-wrapper.jar')
.existsSync(), isTrue);
expect(sampleAppAndroid
.childDirectory('gradle')
.childDirectory('wrapper')
.childFile('gradle-wrapper.properties')
.existsSync(), isTrue);
expect(sampleAppAndroid
.childDirectory('gradle')
.childDirectory('wrapper')
.childFile('gradle-wrapper.properties')
.readAsStringSync(),
'distributionBase=GRADLE_USER_HOME\n'
'distributionPath=wrapper/dists\n'
'zipStoreBase=GRADLE_USER_HOME\n'
'zipStorePath=wrapper/dists\n'
'distributionUrl=https\\://services.gradle.org/distributions/gradle-5.6.2-all.zip\n');
}, overrides: <Type, Generator>{
Cache: () => Cache(rootOverride: tempDir),
FileSystem: () => memoryFileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Gives executable permission to gradle', () {
final Directory sampleAppAndroid = fs.directory('/sample-app/android');
sampleAppAndroid.createSync(recursive: true);
// Make gradlew in the wrapper executable.
os.makeExecutable(gradleWrapperDirectory.childFile('gradlew'));
injectGradleWrapperIfNeeded(sampleAppAndroid);
final File gradlew = sampleAppAndroid.childFile('gradlew');
expect(gradlew.existsSync(), isTrue);
expect(gradlew.statSync().modeString().contains('x'), isTrue);
}, overrides: <Type, Generator>{
Cache: () => Cache(rootOverride: tempDir),
FileSystem: () => memoryFileSystem,
ProcessManager: () => FakeProcessManager.any(),
OperatingSystemUtils: () => OperatingSystemUtils(),
});
});
}

View File

@ -11,7 +11,7 @@ import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import 'package:flutter_tools/src/android/gradle.dart';
import 'package:flutter_tools/src/android/gradle_utils.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/cache.dart';
@ -316,7 +316,7 @@ void main() {
expect(args[1], '-b');
expect(args[2].endsWith('resolve_dependencies.gradle'), isTrue);
expect(args[5], 'resolveDependencies');
expect(invocation.namedArguments[#environment], gradleEnv);
expect(invocation.namedArguments[#environment], gradleEnvironment);
return Future<ProcessResult>.value(ProcessResult(0, 0, '', ''));
});

View File

@ -8,7 +8,6 @@ import 'package:args/command_runner.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_builder.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/gradle.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/cache.dart';
@ -17,6 +16,7 @@ import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import '../../src/android_common.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/mocks.dart';
@ -120,7 +120,9 @@ void main() {
group('AndroidSdk', () {
testUsingContext('validateSdkWellFormed() not called, sdk reinitialized', () async {
final Directory gradleCacheDir = memoryFileSystem.directory('/flutter_root/bin/cache/artifacts/gradle_wrapper')..createSync(recursive: true);
final Directory gradleCacheDir = memoryFileSystem
.directory('/flutter_root/bin/cache/artifacts/gradle_wrapper')
..createSync(recursive: true);
gradleCacheDir.childFile(platform.isWindows ? 'gradlew.bat' : 'gradlew').createSync();
tempDir.childFile('pubspec.yaml')
@ -141,11 +143,31 @@ flutter:
''');
tempDir.childFile('.packages').createSync(recursive: true);
final Directory androidDir = tempDir.childDirectory('android');
androidDir.childFile('build.gradle').createSync(recursive: true);
androidDir.childFile('gradle.properties').createSync(recursive: true);
androidDir.childDirectory('gradle').childDirectory('wrapper').childFile('gradle-wrapper.properties').createSync(recursive: true);
tempDir.childDirectory('build').childDirectory('outputs').childDirectory('repo').createSync(recursive: true);
tempDir.childDirectory('lib').childFile('main.dart').createSync(recursive: true);
androidDir
.childFile('build.gradle')
.createSync(recursive: true);
androidDir
.childDirectory('app')
.childFile('build.gradle')
..createSync(recursive: true)
..writeAsStringSync('apply from: irrelevant/flutter.gradle');
androidDir
.childFile('gradle.properties')
.createSync(recursive: true);
androidDir
.childDirectory('gradle')
.childDirectory('wrapper')
.childFile('gradle-wrapper.properties')
.createSync(recursive: true);
tempDir
.childDirectory('build')
.childDirectory('outputs')
.childDirectory('repo')
.createSync(recursive: true);
tempDir
.childDirectory('lib')
.childFile('main.dart')
.createSync(recursive: true);
await runBuildAarCommand(tempDir.path);
verifyNever(mockAndroidSdk.validateSdkWellFormed());
@ -153,7 +175,6 @@ flutter:
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
ProcessManager: () => mockProcessManager,
FileSystem: () => memoryFileSystem,
});

View File

@ -8,7 +8,6 @@ import 'package:args/command_runner.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_builder.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/gradle.dart';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
@ -20,6 +19,7 @@ import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import '../../src/android_common.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/mocks.dart';
@ -152,7 +152,10 @@ void main() {
platform.isWindows ? 'gradlew.bat' : 'gradlew');
});
testUsingContext('validateSdkWellFormed() not called, sdk reinitialized', () async {
final Directory gradleCacheDir = memoryFileSystem.directory('/flutter_root/bin/cache/artifacts/gradle_wrapper')..createSync(recursive: true);
final Directory gradleCacheDir = memoryFileSystem
.directory('/flutter_root/bin/cache/artifacts/gradle_wrapper')
..createSync(recursive: true);
gradleCacheDir.childFile(platform.isWindows ? 'gradlew.bat' : 'gradlew').createSync();
tempDir.childFile('pubspec.yaml')
@ -170,11 +173,31 @@ flutter:
''');
tempDir.childFile('.packages').createSync(recursive: true);
final Directory androidDir = tempDir.childDirectory('android');
androidDir.childFile('build.gradle').createSync(recursive: true);
androidDir.childFile('gradle.properties').createSync(recursive: true);
androidDir.childDirectory('gradle').childDirectory('wrapper').childFile('gradle-wrapper.properties').createSync(recursive: true);
tempDir.childDirectory('build').childDirectory('outputs').childDirectory('repo').createSync(recursive: true);
tempDir.childDirectory('lib').childFile('main.dart').createSync(recursive: true);
androidDir
.childFile('build.gradle')
.createSync(recursive: true);
androidDir
.childDirectory('app')
.childFile('build.gradle')
..createSync(recursive: true)
..writeAsStringSync('apply from: irrelevant/flutter.gradle');
androidDir
.childFile('gradle.properties')
.createSync(recursive: true);
androidDir
.childDirectory('gradle')
.childDirectory('wrapper')
.childFile('gradle-wrapper.properties')
.createSync(recursive: true);
tempDir
.childDirectory('build')
.childDirectory('outputs')
.childDirectory('repo')
.createSync(recursive: true);
tempDir
.childDirectory('lib')
.childFile('main.dart')
.createSync(recursive: true);
when(mockProcessManager.run(any,
workingDirectory: anyNamed('workingDirectory'),
environment: anyNamed('environment')))
@ -182,7 +205,7 @@ flutter:
await expectLater(
runBuildApkCommand(tempDir.path, arguments: <String>['--no-pub', '--flutter-root=/flutter_root']),
throwsToolExit(message: 'Gradle build failed: 1'),
throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'),
);
verifyNever(mockAndroidSdk.validateSdkWellFormed());
@ -190,7 +213,6 @@ flutter:
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
FileSystem: () => memoryFileSystem,
ProcessManager: () => mockProcessManager,
});
@ -221,7 +243,6 @@ flutter:
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
GradleUtils: () => GradleUtils(),
ProcessManager: () => mockProcessManager,
});
@ -252,7 +273,6 @@ flutter:
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
GradleUtils: () => GradleUtils(),
ProcessManager: () => mockProcessManager,
});
@ -300,13 +320,12 @@ flutter:
verify(mockUsage.sendEvent(
'build',
'apk',
label: 'r8-failure',
label: 'gradle--r8-failure',
parameters: anyNamed('parameters'),
)).called(1);
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => mockProcessManager,
Usage: () => mockUsage,
@ -344,7 +363,7 @@ flutter:
}, throwsToolExit());
final BufferLogger logger = context.get<Logger>();
expect(logger.statusText, contains('[!] Your app isn\'t using AndroidX'));
expect(logger.statusText, contains('Your app isn\'t using AndroidX'));
expect(logger.statusText, contains(
'To avoid potential build failures, you can quickly migrate your app by '
'following the steps on https://goo.gl/CP92wY'
@ -359,7 +378,6 @@ flutter:
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => mockProcessManager,
Usage: () => mockUsage,
@ -414,7 +432,6 @@ flutter:
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => mockProcessManager,
Usage: () => mockUsage,

View File

@ -8,7 +8,6 @@ import 'package:args/command_runner.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_builder.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/gradle.dart';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
@ -20,6 +19,7 @@ import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import '../../src/android_common.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/mocks.dart';
@ -139,7 +139,9 @@ void main() {
});
testUsingContext('validateSdkWellFormed() not called, sdk reinitialized', () async {
final Directory gradleCacheDir = memoryFileSystem.directory('/flutter_root/bin/cache/artifacts/gradle_wrapper')..createSync(recursive: true);
final Directory gradleCacheDir = memoryFileSystem
.directory('/flutter_root/bin/cache/artifacts/gradle_wrapper')
..createSync(recursive: true);
gradleCacheDir.childFile(platform.isWindows ? 'gradlew.bat' : 'gradlew').createSync();
tempDir.childFile('pubspec.yaml')
@ -158,10 +160,26 @@ flutter:
tempDir.childFile('.packages').createSync(recursive: true);
final Directory androidDir = tempDir.childDirectory('android');
androidDir.childFile('build.gradle').createSync(recursive: true);
androidDir.childFile('gradle.properties').createSync(recursive: true);
androidDir.childDirectory('gradle').childDirectory('wrapper').childFile('gradle-wrapper.properties').createSync(recursive: true);
tempDir.childDirectory('build').childDirectory('outputs').childDirectory('repo').createSync(recursive: true);
tempDir.childDirectory('lib').childFile('main.dart').createSync(recursive: true);
androidDir
.childDirectory('app')
.childFile('build.gradle')
..createSync(recursive: true)
..writeAsStringSync('apply from: irrelevant/flutter.gradle');
androidDir
.childFile('gradle.properties')
.createSync(recursive: true);
androidDir
.childDirectory('gradle')
.childDirectory('wrapper')
.childFile('gradle-wrapper.properties')
.createSync(recursive: true);
tempDir.childDirectory('build')
.childDirectory('outputs')
.childDirectory('repo')
.createSync(recursive: true);
tempDir.childDirectory('lib')
.childFile('main.dart')
.createSync(recursive: true);
when(mockProcessManager.run(any,
workingDirectory: anyNamed('workingDirectory'),
environment: anyNamed('environment')))
@ -169,7 +187,7 @@ flutter:
await expectLater(
runBuildAppBundleCommand(tempDir.path, arguments: <String>['--no-pub', '--flutter-root=/flutter_root']),
throwsToolExit(message: 'Gradle build failed: 1'),
throwsToolExit(message: 'Gradle task bundleRelease failed with exit code 1'),
);
verifyNever(mockAndroidSdk.validateSdkWellFormed());
@ -177,7 +195,6 @@ flutter:
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
ProcessManager: () => mockProcessManager,
FileSystem: () => memoryFileSystem,
});
@ -210,7 +227,6 @@ flutter:
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
GradleUtils: () => GradleUtils(),
ProcessManager: () => mockProcessManager,
});
@ -243,7 +259,6 @@ flutter:
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
GradleUtils: () => GradleUtils(),
ProcessManager: () => mockProcessManager,
});
@ -291,13 +306,12 @@ flutter:
verify(mockUsage.sendEvent(
'build',
'appbundle',
label: 'r8-failure',
label: 'gradle--r8-failure',
parameters: anyNamed('parameters'),
)).called(1);
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => mockProcessManager,
Usage: () => mockUsage,
@ -335,7 +349,7 @@ flutter:
}, throwsToolExit());
final BufferLogger logger = context.get<Logger>();
expect(logger.statusText, contains('[!] Your app isn\'t using AndroidX'));
expect(logger.statusText, contains('Your app isn\'t using AndroidX'));
expect(logger.statusText, contains(
'To avoid potential build failures, you can quickly migrate your app by '
'following the steps on https://goo.gl/CP92wY'
@ -350,7 +364,6 @@ flutter:
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => mockProcessManager,
Usage: () => mockUsage,
@ -388,7 +401,7 @@ flutter:
}, throwsToolExit());
final BufferLogger logger = context.get<Logger>();
expect(logger.statusText.contains('[!] Your app isn\'t using AndroidX'), isFalse);
expect(logger.statusText.contains('Your app isn\'t using AndroidX'), isFalse);
expect(
logger.statusText.contains(
'To avoid potential build failures, you can quickly migrate your app by '
@ -405,7 +418,6 @@ flutter:
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => mockProcessManager,
Usage: () => mockUsage,

View File

@ -0,0 +1,34 @@
// Copyright 2019 The Chromium 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 'package:flutter_tools/src/android/android_builder.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/project.dart';
/// A fake implementation of [AndroidBuilder].
class FakeAndroidBuilder implements AndroidBuilder {
@override
Future<void> buildAar({
@required FlutterProject project,
@required AndroidBuildInfo androidBuildInfo,
@required String target,
@required String outputDir,
}) async {}
@override
Future<void> buildApk({
@required FlutterProject project,
@required AndroidBuildInfo androidBuildInfo,
@required String target,
}) async {}
@override
Future<void> buildAab({
@required FlutterProject project,
@required AndroidBuildInfo androidBuildInfo,
@required String target,
}) async {}
}