Refactor gradle.dart (#43479)
diff --git a/packages/flutter_tools/lib/src/android/android_builder.dart b/packages/flutter_tools/lib/src/android/android_builder.dart
index c06925f..77bf76a 100644
--- a/packages/flutter_tools/lib/src/android/android_builder.dart
+++ b/packages/flutter_tools/lib/src/android/android_builder.dart
@@ -6,19 +6,23 @@
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 @@
/// 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 @@
@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 @@
@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 @@
@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 {}
-}
diff --git a/packages/flutter_tools/lib/src/android/android_studio.dart b/packages/flutter_tools/lib/src/android/android_studio.dart
index 04e3474..9ded536 100644
--- a/packages/flutter_tools/lib/src/android/android_studio.dart
+++ b/packages/flutter_tools/lib/src/android/android_studio.dart
@@ -236,7 +236,7 @@
// 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);
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index 0dd05ae..dff67bd 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -10,16 +10,12 @@
import '../android/android_sdk.dart';
import '../artifacts.dart';
import '../base/common.dart';
-import '../base/context.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
-import '../base/os.dart';
-import '../base/platform.dart';
import '../base/process.dart';
import '../base/terminal.dart';
import '../base/utils.dart';
-import '../base/version.dart';
import '../build_info.dart';
import '../cache.dart';
import '../flutter_manifest.dart';
@@ -27,136 +23,131 @@
import '../project.dart';
import '../reporting/reporting.dart';
import 'android_sdk.dart';
-import 'android_studio.dart';
+import 'gradle_errors.dart';
+import 'gradle_utils.dart';
-/// 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 {
- /// Empty constructor.
- GradleUtils();
-
- String _cachedExecutable;
- /// Gets the Gradle executable path.
- /// This is the `gradlew` or `gradlew.bat` script in the `android/` directory.
- Future<String> getExecutable(FlutterProject project) async {
- _cachedExecutable ??= await _initializeGradle(project);
- return _cachedExecutable;
- }
-
- /// Cached app projects. The key is the [FluterProject]'s path, the value is [GradleProject].
- final Map<String, GradleProject> _cachedAppProject = <String, GradleProject>{};
-
- /// Gets the [GradleProject] for the [project] if built as an app.
- Future<GradleProject> getAppProject(FlutterProject project) async {
- final String projectPath = project.directory.path;
- _cachedAppProject[projectPath] ??= await _readGradleProject(project, isLibrary: false);
- return _cachedAppProject[projectPath];
- }
-
- /// Cached library projects such as plugins or modules.
- /// The key is the [FluterProject]'s path, the value is [GradleProject].
- final Map<String, GradleProject> _cachedLibraryProject = <String, GradleProject>{};
-
- /// Gets the [GradleProject] for the [project] if built as a library.
- Future<GradleProject> getLibraryProject(FlutterProject project) async {
- final String projectPath = project.directory.path;
- _cachedLibraryProject[projectPath] ??= await _readGradleProject(project, isLibrary: true);
- return _cachedLibraryProject[projectPath];
- }
+/// The directory where the APK artifact is generated.
+@visibleForTesting
+Directory getApkDirectory(FlutterProject project) {
+ return project.isModule
+ ? project.android.buildDirectory
+ .childDirectory('host')
+ .childDirectory('outputs')
+ .childDirectory('apk')
+ : project.android.buildDirectory
+ .childDirectory('app')
+ .childDirectory('outputs')
+ .childDirectory('apk');
}
-final RegExp _assembleTaskPattern = RegExp(r'assemble(\S+)');
-
-enum FlutterPluginVersion {
- none,
- v1,
- v2,
- managed,
+/// The directory where the app bundle artifact is generated.
+@visibleForTesting
+Directory getBundleDirectory(FlutterProject project) {
+ return project.isModule
+ ? project.android.buildDirectory
+ .childDirectory('host')
+ .childDirectory('outputs')
+ .childDirectory('bundle')
+ : project.android.buildDirectory
+ .childDirectory('app')
+ .childDirectory('outputs')
+ .childDirectory('bundle');
}
-// Investigation documented in #13975 suggests the filter should be a subset
-// of the impact of -q, but users insist they see the error message sometimes
-// anyway. If we can prove it really is impossible, delete the filter.
-// This technically matches everything *except* the NDK message, since it's
-// passed to a function that filters out all lines that don't match a filter.
-final RegExp ndkMessageFilter = RegExp(r'^(?!NDK is missing a ".*" directory'
- r'|If you are not using NDK, unset the NDK variable from ANDROID_NDK_HOME or local.properties to remove this warning'
- r'|If you are using NDK, verify the ndk.dir is set to a valid NDK directory. It is currently set to .*)');
+/// The directory where the repo is generated.
+/// Only applicable to AARs.
+@visibleForTesting
+Directory getRepoDirectory(Directory buildDirectory) {
+ return buildDirectory
+ .childDirectory('outputs')
+ .childDirectory('repo');
+}
-// 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)');
+/// Returns the name of Gradle task that starts with [prefix].
+String _taskFor(String prefix, BuildInfo buildInfo) {
+ final String buildType = camelCase(buildInfo.modeName);
+ final String productFlavor = buildInfo.flavor ?? '';
+ return '$prefix${toTitleCase(productFlavor)}${toTitleCase(buildType)}';
+}
-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.');
+/// Returns the task to build an APK.
+@visibleForTesting
+String getAssembleTaskFor(BuildInfo buildInfo) {
+ return _taskFor('assemble', buildInfo);
+}
-FlutterPluginVersion getFlutterPluginVersion(AndroidProject project) {
+/// Returns the task to build an AAB.
+@visibleForTesting
+String getBundleTaskFor(BuildInfo buildInfo) {
+ return _taskFor('bundle', buildInfo);
+}
+
+/// Returns the task to build an AAR.
+@visibleForTesting
+String getAarTaskFor(BuildInfo buildInfo) {
+ return _taskFor('assembleAar', buildInfo);
+}
+
+/// Returns the output APK file names for a given [AndroidBuildInfo].
+///
+/// For example, when [splitPerAbi] is true, multiple APKs are created.
+Iterable<String> _apkFilesFor(AndroidBuildInfo androidBuildInfo) {
+ final String buildType = camelCase(androidBuildInfo.buildInfo.modeName);
+ final String productFlavor = androidBuildInfo.buildInfo.flavor ?? '';
+ final String flavorString = productFlavor.isEmpty ? '' : '-$productFlavor';
+ if (androidBuildInfo.splitPerAbi) {
+ return androidBuildInfo.targetArchs.map<String>((AndroidArch arch) {
+ final String abi = getNameForAndroidArch(arch);
+ return 'app$flavorString-$abi-$buildType.apk';
+ });
+ }
+ return <String>['app$flavorString-$buildType.apk'];
+}
+
+/// Returns true if the current version of the Gradle plugin is supported.
+bool _isSupportedVersion(AndroidProject project) {
final File plugin = project.hostAppGradleRoot.childFile(
fs.path.join('buildSrc', 'src', 'main', 'groovy', 'FlutterPlugin.groovy'));
if (plugin.existsSync()) {
- final String packageLine = plugin.readAsLinesSync().skip(4).first;
- if (packageLine == 'package io.flutter.gradle') {
- return FlutterPluginVersion.v2;
- }
- return FlutterPluginVersion.v1;
+ return false;
}
final File appGradle = project.hostAppGradleRoot.childFile(
fs.path.join('app', 'build.gradle'));
- if (appGradle.existsSync()) {
- for (String line in appGradle.readAsLinesSync()) {
- if (line.contains(RegExp(r'apply from: .*/flutter.gradle'))) {
- return FlutterPluginVersion.managed;
- }
- if (line.contains("def flutterPluginVersion = 'managed'")) {
- return FlutterPluginVersion.managed;
- }
+ if (!appGradle.existsSync()) {
+ return false;
+ }
+ for (String line in appGradle.readAsLinesSync()) {
+ if (line.contains(RegExp(r'apply from: .*/flutter.gradle')) ||
+ line.contains("def flutterPluginVersion = 'managed'")) {
+ return true;
}
}
- return FlutterPluginVersion.none;
+ return false;
}
/// Returns the apk file created by [buildGradleProject]
Future<File> getGradleAppOut(AndroidProject androidProject) async {
- switch (getFlutterPluginVersion(androidProject)) {
- case FlutterPluginVersion.none:
- // Fall through. Pretend we're v1, and just go with it.
- case FlutterPluginVersion.v1:
- return androidProject.gradleAppOutV1File;
- case FlutterPluginVersion.managed:
- // Fall through. The managed plugin matches plugin v2 for now.
- case FlutterPluginVersion.v2:
- final GradleProject gradleProject =
- await gradleUtils.getAppProject(FlutterProject.current());
- return fs.file(gradleProject.apkDirectory.childFile('app.apk'));
+ if (!_isSupportedVersion(androidProject)) {
+ _exitWithUnsupportedProjectMessage();
}
- return null;
+ return getApkDirectory(androidProject.parent).childFile('app.apk');
}
/// Runs `gradlew dependencies`, ensuring that dependencies are resolved and
/// potentially downloaded.
Future<void> checkGradleDependencies() async {
- final Status progress = logger.startProgress('Ensuring gradle dependencies are up to date...', timeout: timeoutConfiguration.slowOperation);
+ final Status progress = logger.startProgress(
+ 'Ensuring gradle dependencies are up to date...',
+ timeout: timeoutConfiguration.slowOperation,
+ );
final FlutterProject flutterProject = FlutterProject.current();
- final String gradlew = await gradleUtils.getExecutable(flutterProject);
- await processUtils.run(
- <String>[gradlew, 'dependencies'],
+ await processUtils.run(<String>[
+ gradleUtils.getExecutable(flutterProject),
+ 'dependencies',
+ ],
throwOnError: true,
workingDirectory: flutterProject.android.hostAppGradleRoot.path,
- environment: gradleEnv,
+ environment: gradleEnvironment,
);
androidSdk.reinitialize();
progress.stop();
@@ -200,456 +191,312 @@
}
if (!exactMatch) {
status.cancel();
- printError('*******************************************************************************************');
- printError('Flutter tried to create the file `$newSettingsRelativeFile`, but failed.');
+ printStatus('$warningMark Flutter tried to create the file `$newSettingsRelativeFile`, but failed.');
// Print how to manually update the file.
- printError(fs.file(fs.path.join(flutterRoot, 'packages','flutter_tools',
+ printStatus(fs.file(fs.path.join(flutterRoot, 'packages','flutter_tools',
'gradle', 'manual_migration_settings.gradle.md')).readAsStringSync());
- printError('*******************************************************************************************');
throwToolExit('Please create the file and run this command again.');
}
// Copy the new file.
newSettingsFile.writeAsStringSync(settingsAarContent);
status.stop();
- printStatus('✅ `$newSettingsRelativeFile` created successfully.');
+ printStatus('$successMark `$newSettingsRelativeFile` created successfully.');
}
-// Note: Dependencies are resolved and possibly downloaded as a side-effect
-// of calculating the app properties using Gradle. This may take minutes.
-Future<GradleProject> _readGradleProject(
- FlutterProject flutterProject, {
- bool isLibrary = false,
-}) async {
- final String gradlew = await gradleUtils.getExecutable(flutterProject);
-
- updateLocalProperties(project: flutterProject);
-
- final FlutterManifest manifest = flutterProject.manifest;
- final Directory hostAppGradleRoot = flutterProject.android.hostAppGradleRoot;
-
- if (manifest.isPlugin) {
- assert(isLibrary);
- return GradleProject(
- <String>['debug', 'profile', 'release'],
- <String>[], // Plugins don't have flavors.
- flutterProject.directory.childDirectory('build').path,
- );
- }
- final Status status = logger.startProgress('Resolving dependencies...', timeout: timeoutConfiguration.slowOperation);
- GradleProject project;
- // Get the properties and tasks from Gradle, so we can determinate the `buildDir`,
- // flavors and build types defined in the project. If gradle fails, then check if the failure is due to t
- try {
- final RunResult propertiesRunResult = await processUtils.run(
- <String>[gradlew, if (isLibrary) 'properties' else 'app:properties'],
- throwOnError: true,
- workingDirectory: hostAppGradleRoot.path,
- environment: gradleEnv,
- );
- final RunResult tasksRunResult = await processUtils.run(
- <String>[gradlew, if (isLibrary) 'tasks' else 'app:tasks', '--all', '--console=auto'],
- throwOnError: true,
- workingDirectory: hostAppGradleRoot.path,
- environment: gradleEnv,
- );
- project = GradleProject.fromAppProperties(propertiesRunResult.stdout, tasksRunResult.stdout);
- } catch (exception) {
- if (getFlutterPluginVersion(flutterProject.android) == FlutterPluginVersion.managed) {
- status.cancel();
- // Handle known exceptions.
- throwToolExitIfLicenseNotAccepted(exception);
- // Print a general Gradle error and exit.
- printError('* Error running Gradle:\n$exception\n');
- throwToolExit('Please review your Gradle project setup in the android/ folder.');
- }
- // Fall back to the default
- project = GradleProject(
- <String>['debug', 'profile', 'release'],
- <String>[],
- fs.path.join(flutterProject.android.hostAppGradleRoot.path, 'app', 'build'),
- );
- }
- status.stop();
- return project;
-}
-
-/// 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.
-void throwToolExitIfLicenseNotAccepted(Exception exception) {
- 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);
- final Match licenseMatch = licenseFailure.firstMatch(exception.toString());
- if (licenseMatch != null) {
- final String missingLicenses = licenseMatch.group(1);
- final String errorMessage =
- '\n\n* Error running Gradle:\n'
- 'Unable to download needed Android SDK components, as the following licenses have not been accepted:\n'
- '$missingLicenses\n\n'
- 'To resolve this, please run the following command in a Terminal:\n'
- 'flutter doctor --android-licenses';
- throwToolExit(errorMessage);
- }
-}
-
-String _locateGradlewExecutable(Directory directory) {
- final File gradle = directory.childFile(
- platform.isWindows ? 'gradlew.bat' : 'gradlew',
- );
- if (gradle.existsSync()) {
- return gradle.absolute.path;
- }
- return null;
-}
-
-// Gradle crashes for several known reasons when downloading that are not
-// actionable by flutter.
-const List<String> _kKnownErrorPrefixes = <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',
-];
-
-// Note: Gradle may be bootstrapped and possibly downloaded as a side-effect
-// of validating the Gradle executable. This may take several seconds.
-Future<String> _initializeGradle(FlutterProject project) async {
- final Directory android = project.android.hostAppGradleRoot;
- final Status status = logger.startProgress('Initializing gradle...',
- timeout: timeoutConfiguration.slowOperation);
-
-
- // Update the project if needed.
- // TODO(egarciad): https://github.com/flutter/flutter/issues/40460.
- migrateToR8(android);
- injectGradleWrapperIfNeeded(android);
-
- final String gradle = _locateGradlewExecutable(android);
- if (gradle == null) {
- status.stop();
- throwToolExit('Unable to locate gradlew script');
- }
- printTrace('Using gradle from $gradle.');
- // Validates the Gradle executable by asking for its version.
- // Makes Gradle Wrapper download and install Gradle distribution, if needed.
- try {
- await processUtils.run(
- <String>[gradle, '-v'],
- throwOnError: true,
- environment: gradleEnv,
- );
- } on ProcessException catch (e) {
- final String error = e.toString();
- // TODO(jonahwilliams): automatically retry on network errors.
- if (_kKnownErrorPrefixes.any((String candidate) => error.contains(candidate))) {
- throwToolExit(
- '$gradle threw an error while trying to update itself.'
- ' Try rerunning to retry the update.\n$e');
- }
- // gradlew is missing execute.
- if (error.contains('Permission denied')) {
- throwToolExit(
- '$gradle does not have permission to execute by your user.\n'
- 'You should change the ownership of the project directory to your user'
- ', or move the project to a directory with execute permissions.\n$error'
- );
- }
- // No idea what went wrong but we can't do anything about it.
- if (error.contains('ProcessException: Process exited abnormally')) {
- throwToolExit(
- '$gradle exited abnormally. Try rerunning with \'-v\' for more '
- 'infomration, or check the gradlew script above for errors.\n$error');
- }
- rethrow;
- } finally {
- status.stop();
- }
- return gradle;
-}
-
-/// 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}.');
- }
- 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 {
- // Add `android.enableR8=true` to the next line in gradle.properties.
- if (propertiesContent.isNotEmpty && !propertiesContent.endsWith('\n')) {
- gradleProperties
- .writeAsStringSync('\nandroid.enableR8=true\n', mode: FileMode.append);
- } else {
- 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,
- );
- }
-}
-
-/// Returns true if [targetVersion] is within the range [min] and [max] inclusive.
-bool _isWithinVersionRange(String targetVersion, {String min, String max}) {
- final Version parsedTargetVersion = Version.parse(targetVersion);
- return parsedTargetVersion >= Version.parse(min) &&
- parsedTargetVersion <= Version.parse(max);
-}
-
-const String defaultGradleVersion = '5.6.2';
-
-/// 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 '';
-}
-
-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.
+/// Builds an app.
///
-/// 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);
-}
-
-/// 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) {
- _exitIfNoAndroidSdk();
- }
-
- 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) {
- if (value == null) {
- settings.values.remove(key);
- } else {
- settings.values[key] = value;
- }
- changed = true;
- }
- }
-
- final FlutterManifest manifest = project.manifest;
-
- 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 ?? manifest.buildName);
- changeIfNecessary('flutter.versionName', buildName);
- final String buildNumber = validatedBuildNumberForPlatform(TargetPlatform.android_arm, buildInfo.buildNumber ?? 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);
-}
-
-/// Throws a ToolExit, if the path to the Android SDK is not known.
-void _exitIfNoAndroidSdk() {
- if (androidSdk == null) {
- throwToolExit('Unable to locate Android SDK. Please run `flutter doctor` for more details.');
- }
-}
-
-Future<void> buildGradleProject({
+/// * [project] is typically [FlutterProject.current()].
+/// * [androidBuildInfo] is the build configuration.
+/// * [target] is the target dart entrypoint. Typically, `lib/main.dart`.
+/// * If [isBuildingBundle] is `true`, then the output artifact is an `*.aab`,
+/// otherwise the output artifact is an `*.apk`.
+/// * The plugins are built as AARs if [shouldBuildPluginAsAar] is `true`. This isn't set by default
+/// because it makes the build slower proportional to the number of plugins.
+/// * [retries] is the max number of build retries in case one of the [GradleHandledError] handler
+/// returns [GradleBuildStatus.retry] or [GradleBuildStatus.retryWithAarPlugins].
+Future<void> buildGradleApp({
@required FlutterProject project,
@required AndroidBuildInfo androidBuildInfo,
@required String target,
@required bool isBuildingBundle,
+ @required List<GradleHandledError> localGradleErrors,
+ bool shouldBuildPluginAsAar = false,
+ int retries = 1,
}) async {
- // Update the local.properties file with the build mode, version name and code.
- // FlutterPlugin v1 reads local.properties to determine build mode. Plugin v2
- // uses the standard Android way to determine what to build, but we still
- // update local.properties, in case we want to use it in the future.
- // Version name and number are provided by the pubspec.yaml file
- // and can be overwritten with flutter build command.
+ if (androidSdk == null) {
+ exitWithNoSdkMessage();
+ }
+ if (!project.android.isUsingGradle) {
+ _exitWithProjectNotUsingGradleMessage();
+ }
+ if (!_isSupportedVersion(project.android)) {
+ _exitWithUnsupportedProjectMessage();
+ }
+
+ final bool usesAndroidX = isAppUsingAndroidX(project.android.hostAppGradleRoot);
+ if (usesAndroidX) {
+ BuildEvent('app-using-android-x').send();
+ } else if (!usesAndroidX) {
+ BuildEvent('app-not-using-android-x').send();
+ printStatus('$warningMark Your app isn\'t using AndroidX.', emphasis: true);
+ printStatus(
+ 'To avoid potential build failures, you can quickly migrate your app '
+ 'by following the steps on https://goo.gl/CP92wY.',
+ indent: 4,
+ );
+ }
// The default Gradle script reads the version name and number
// from the local.properties file.
updateLocalProperties(project: project, buildInfo: androidBuildInfo.buildInfo);
- switch (getFlutterPluginVersion(project.android)) {
- case FlutterPluginVersion.none:
- // Fall through. Pretend it's v1, and just go for it.
- case FlutterPluginVersion.v1:
- return _buildGradleProjectV1(project);
- case FlutterPluginVersion.managed:
- // Fall through. Managed plugin builds the same way as plugin v2.
- case FlutterPluginVersion.v2:
- return _buildGradleProjectV2(project, androidBuildInfo, target, isBuildingBundle);
+ if (shouldBuildPluginAsAar) {
+ // Create a settings.gradle that doesn't import the plugins as subprojects.
+ createSettingsAarGradle(project.android.hostAppGradleRoot);
+ await buildPluginsAsAar(
+ project,
+ androidBuildInfo,
+ buildDirectory: project.android.buildDirectory.childDirectory('app'),
+ );
+ }
+
+ final BuildInfo buildInfo = androidBuildInfo.buildInfo;
+ final String assembleTask = isBuildingBundle
+ ? getBundleTaskFor(buildInfo)
+ : getAssembleTaskFor(buildInfo);
+
+ final Status status = logger.startProgress(
+ 'Running Gradle task \'$assembleTask\'...',
+ timeout: timeoutConfiguration.slowOperation,
+ multilineOutput: true,
+ );
+
+ final List<String> command = <String>[
+ gradleUtils.getExecutable(project),
+ ];
+ if (logger.isVerbose) {
+ command.add('-Pverbose=true');
+ } else {
+ command.add('-q');
+ }
+ if (artifacts is LocalEngineArtifacts) {
+ final LocalEngineArtifacts localEngineArtifacts = artifacts;
+ printTrace('Using local engine: ${localEngineArtifacts.engineOutPath}');
+ command.add('-PlocalEngineOut=${localEngineArtifacts.engineOutPath}');
+ }
+ if (target != null) {
+ command.add('-Ptarget=$target');
+ }
+ assert(buildInfo.trackWidgetCreation != null);
+ command.add('-Ptrack-widget-creation=${buildInfo.trackWidgetCreation}');
+
+ if (buildInfo.extraFrontEndOptions != null) {
+ command.add('-Pextra-front-end-options=${buildInfo.extraFrontEndOptions}');
+ }
+ if (buildInfo.extraGenSnapshotOptions != null) {
+ command.add('-Pextra-gen-snapshot-options=${buildInfo.extraGenSnapshotOptions}');
+ }
+ if (buildInfo.fileSystemRoots != null && buildInfo.fileSystemRoots.isNotEmpty) {
+ command.add('-Pfilesystem-roots=${buildInfo.fileSystemRoots.join('|')}');
+ }
+ if (buildInfo.fileSystemScheme != null) {
+ command.add('-Pfilesystem-scheme=${buildInfo.fileSystemScheme}');
+ }
+ if (androidBuildInfo.splitPerAbi) {
+ command.add('-Psplit-per-abi=true');
+ }
+ if (androidBuildInfo.shrink) {
+ command.add('-Pshrink=true');
+ }
+ if (androidBuildInfo.targetArchs.isNotEmpty) {
+ final String targetPlatforms = androidBuildInfo
+ .targetArchs
+ .map(getPlatformNameForAndroidArch).join(',');
+ command.add('-Ptarget-platform=$targetPlatforms');
+ }
+ if (shouldBuildPluginAsAar) {
+ // Pass a system flag instead of a project flag, so this flag can be
+ // read from include_flutter.groovy.
+ command.add('-Dbuild-plugins-as-aars=true');
+ // Don't use settings.gradle from the current project since it includes the plugins as subprojects.
+ command.add('--settings-file=settings_aar.gradle');
+ }
+ command.add(assembleTask);
+
+ final Stopwatch sw = Stopwatch()..start();
+ int exitCode = 1;
+ GradleHandledError detectedGradleError;
+ String detectedGradleErrorLine;
+ try {
+ exitCode = await processUtils.stream(
+ command,
+ workingDirectory: project.android.hostAppGradleRoot.path,
+ allowReentrantFlutter: true,
+ environment: gradleEnvironment,
+ mapFunction: (String line) {
+ // This message was removed from first-party plugins,
+ // but older plugin versions still display this message.
+ if (androidXPluginWarningRegex.hasMatch(line)) {
+ // Don't pipe.
+ return null;
+ }
+ if (detectedGradleError != null) {
+ // Pipe stdout/sterr from Gradle.
+ return line;
+ }
+ for (final GradleHandledError gradleError in localGradleErrors) {
+ if (gradleError.test(line)) {
+ detectedGradleErrorLine = line;
+ detectedGradleError = gradleError;
+ // The first error match wins.
+ break;
+ }
+ }
+ // Pipe stdout/sterr from Gradle.
+ return line;
+ },
+ );
+ } finally {
+ status.stop();
+ }
+
+ flutterUsage.sendTiming('build', 'gradle', sw.elapsed);
+
+ if (exitCode != 0) {
+ if (detectedGradleError == null) {
+ BuildEvent('gradle--unkown-failure').send();
+ throwToolExit(
+ 'Gradle task $assembleTask failed with exit code $exitCode',
+ exitCode: exitCode,
+ );
+ } else {
+ final GradleBuildStatus status = await detectedGradleError.handler(
+ line: detectedGradleErrorLine,
+ project: project,
+ usesAndroidX: usesAndroidX,
+ shouldBuildPluginAsAar: shouldBuildPluginAsAar,
+ );
+
+ if (retries >= 1) {
+ final String successEventLabel = 'gradle--${detectedGradleError.eventLabel}-success';
+ switch (status) {
+ case GradleBuildStatus.retry:
+ await buildGradleApp(
+ project: project,
+ androidBuildInfo: androidBuildInfo,
+ target: target,
+ isBuildingBundle: isBuildingBundle,
+ localGradleErrors: localGradleErrors,
+ shouldBuildPluginAsAar: shouldBuildPluginAsAar,
+ retries: retries - 1,
+ );
+ BuildEvent(successEventLabel).send();
+ return;
+ case GradleBuildStatus.retryWithAarPlugins:
+ await buildGradleApp(
+ project: project,
+ androidBuildInfo: androidBuildInfo,
+ target: target,
+ isBuildingBundle: isBuildingBundle,
+ localGradleErrors: localGradleErrors,
+ shouldBuildPluginAsAar: true,
+ retries: retries - 1,
+ );
+ BuildEvent(successEventLabel).send();
+ return;
+ case GradleBuildStatus.exit:
+ // noop.
+ }
+ }
+ BuildEvent('gradle--${detectedGradleError.eventLabel}-failure').send();
+ throwToolExit(
+ 'Gradle task $assembleTask failed with exit code $exitCode',
+ exitCode: exitCode,
+ );
+ }
+ }
+
+ if (isBuildingBundle) {
+ final File bundleFile = findBundleFile(project, buildInfo);
+ if (bundleFile == null) {
+ throwToolExit('Gradle build failed to produce an Android bundle package.');
+ }
+
+ final String appSize = (buildInfo.mode == BuildMode.debug)
+ ? '' // Don't display the size when building a debug variant.
+ : ' (${getSizeAsMB(bundleFile.lengthSync())})';
+
+ printStatus(
+ '$successMark Built ${fs.path.relative(bundleFile.path)}$appSize.',
+ color: TerminalColor.green,
+ );
+ return;
+ }
+ // Gradle produced an APK.
+ final Iterable<File> apkFiles = findApkFiles(project, androidBuildInfo);
+ if (apkFiles.isEmpty) {
+ throwToolExit('Gradle build failed to produce an Android package.');
+ }
+
+ final Directory apkDirectory = getApkDirectory(project);
+ // Copy the first APK to app.apk, so `flutter run` can find it.
+ // TODO(egarciad): Handle multiple APKs.
+ apkFiles.first.copySync(apkDirectory.childFile('app.apk').path);
+ printTrace('calculateSha: $apkDirectory/app.apk');
+
+ final File apkShaFile = apkDirectory.childFile('app.apk.sha1');
+ apkShaFile.writeAsStringSync(_calculateSha(apkFiles.first));
+
+ for (File apkFile in apkFiles) {
+ final String appSize = (buildInfo.mode == BuildMode.debug)
+ ? '' // Don't display the size when building a debug variant.
+ : ' (${getSizeAsMB(apkFile.lengthSync())})';
+ printStatus(
+ '$successMark Built ${fs.path.relative(apkFile.path)}$appSize.',
+ color: TerminalColor.green,
+ );
}
}
+/// Builds AAR and POM files.
+///
+/// * [project] is typically [FlutterProject.current()].
+/// * [androidBuildInfo] is the build configuration.
+/// * [target] is the target dart entrypoint. Typically, `lib/main.dart`.
+/// * [outputDir] is the destination of the artifacts,
Future<void> buildGradleAar({
@required FlutterProject project,
@required AndroidBuildInfo androidBuildInfo,
@required String target,
- @required String outputDir,
+ @required Directory outputDir,
}) async {
+ if (androidSdk == null) {
+ exitWithNoSdkMessage();
+ }
final FlutterManifest manifest = project.manifest;
-
- GradleProject gradleProject;
- if (manifest.isModule) {
- gradleProject = await gradleUtils.getAppProject(project);
- } else if (manifest.isPlugin) {
- gradleProject = await gradleUtils.getLibraryProject(project);
- } else {
+ if (!manifest.isModule && !manifest.isPlugin) {
throwToolExit('AARs can only be built for plugin or module projects.');
}
- if (outputDir != null && outputDir.isNotEmpty) {
- gradleProject.buildDirectory = outputDir;
- }
-
- final String aarTask = gradleProject.aarTaskFor(androidBuildInfo.buildInfo);
- if (aarTask == null) {
- printUndefinedTask(gradleProject, androidBuildInfo.buildInfo);
- throwToolExit('Gradle build aborted.');
- }
+ final String aarTask = getAarTaskFor(androidBuildInfo.buildInfo);
final Status status = logger.startProgress(
'Running Gradle task \'$aarTask\'...',
timeout: timeoutConfiguration.slowOperation,
multilineOutput: true,
);
- final String gradlew = await gradleUtils.getExecutable(project);
final String flutterRoot = fs.path.absolute(Cache.flutterRoot);
- final String initScript = fs.path.join(flutterRoot, 'packages','flutter_tools', 'gradle', 'aar_init_script.gradle');
+ final String initScript = fs.path.join(
+ flutterRoot,
+ 'packages',
+ 'flutter_tools',
+ 'gradle',
+ 'aar_init_script.gradle',
+ );
final List<String> command = <String>[
- gradlew,
+ gradleUtils.getExecutable(project),
'-I=$initScript',
'-Pflutter-root=$flutterRoot',
- '-Poutput-dir=${gradleProject.buildDirectory}',
+ '-Poutput-dir=${outputDir.path}',
'-Pis-plugin=${manifest.isPlugin}',
];
@@ -676,7 +523,7 @@
command,
workingDirectory: project.android.hostAppGradleRoot.path,
allowReentrantFlutter: true,
- environment: gradleEnv,
+ environment: gradleEnvironment,
);
} finally {
status.stop();
@@ -686,41 +533,24 @@
if (result.exitCode != 0) {
printStatus(result.stdout, wrap: false);
printError(result.stderr, wrap: false);
- throwToolExit('Gradle task $aarTask failed with exit code $exitCode.', exitCode: exitCode);
+ throwToolExit(
+ 'Gradle task $aarTask failed with exit code $exitCode.',
+ exitCode: exitCode,
+ );
}
-
- final Directory repoDirectory = gradleProject.repoDirectory;
+ final Directory repoDirectory = getRepoDirectory(outputDir);
if (!repoDirectory.existsSync()) {
printStatus(result.stdout, wrap: false);
printError(result.stderr, wrap: false);
- throwToolExit('Gradle task $aarTask failed to produce $repoDirectory.', exitCode: exitCode);
+ throwToolExit(
+ 'Gradle task $aarTask failed to produce $repoDirectory.',
+ exitCode: exitCode,
+ );
}
- printStatus('Built ${fs.path.relative(repoDirectory.path)}.', color: TerminalColor.green);
-}
-
-Future<void> _buildGradleProjectV1(FlutterProject project) async {
- final String gradlew = await gradleUtils.getExecutable(project);
- // Run 'gradlew build'.
- final Status status = logger.startProgress(
- 'Running \'gradlew build\'...',
- timeout: timeoutConfiguration.slowOperation,
- multilineOutput: true,
+ printStatus(
+ '$successMark Built ${fs.path.relative(repoDirectory.path)}.',
+ color: TerminalColor.green,
);
- final Stopwatch sw = Stopwatch()..start();
- final int exitCode = await processUtils.stream(
- <String>[fs.file(gradlew).absolute.path, 'build'],
- workingDirectory: project.android.hostAppGradleRoot.path,
- allowReentrantFlutter: true,
- environment: gradleEnv,
- );
- status.stop();
- flutterUsage.sendTiming('build', 'gradle-v1', Duration(milliseconds: sw.elapsedMilliseconds));
-
- if (exitCode != 0) {
- throwToolExit('Gradle build failed: $exitCode', exitCode: exitCode);
- }
-
- printStatus('Built ${fs.path.relative(project.android.gradleAppOutV1File.path)}.');
}
String _hex(List<int> bytes) {
@@ -735,251 +565,31 @@
final Stopwatch sw = Stopwatch()..start();
final List<int> bytes = file.readAsBytesSync();
printTrace('calculateSha: reading file took ${sw.elapsedMilliseconds}us');
- flutterUsage.sendTiming('build', 'apk-sha-read', Duration(milliseconds: sw.elapsedMilliseconds));
+ flutterUsage.sendTiming('build', 'apk-sha-read', sw.elapsed);
sw.reset();
final String sha = _hex(sha1.convert(bytes).bytes);
printTrace('calculateSha: computing sha took ${sw.elapsedMilliseconds}us');
- flutterUsage.sendTiming('build', 'apk-sha-calc', Duration(milliseconds: sw.elapsedMilliseconds));
+ flutterUsage.sendTiming('build', 'apk-sha-calc', sw.elapsed);
return sha;
}
-void printUndefinedTask(GradleProject project, BuildInfo buildInfo) {
- printError('');
- printError('The Gradle project does not define a task suitable for the requested build.');
- if (!project.buildTypes.contains(buildInfo.modeName)) {
- printError('Review the android/app/build.gradle file and ensure it defines a ${buildInfo.modeName} build type.');
- return;
- }
- if (project.productFlavors.isEmpty) {
- printError('The android/app/build.gradle file does not define any custom product flavors.');
- printError('You cannot use the --flavor option.');
- } else {
- printError('The android/app/build.gradle file defines product flavors: ${project.productFlavors.join(', ')}');
- printError('You must specify a --flavor option to select one of them.');
- }
+void _exitWithUnsupportedProjectMessage() {
+ BuildEvent('unsupported-project', eventError: 'gradle-plugin').send();
+ throwToolExit(
+ '$warningMark Your app is using an unsupported Gradle project. '
+ 'To fix this problem, create a new project by running `flutter create -t app <app-directory>` '
+ 'and then move the dart code, assets and pubspec.yaml to the new project.',
+ );
}
-Future<void> _buildGradleProjectV2(
- FlutterProject flutterProject,
- AndroidBuildInfo androidBuildInfo,
- String target,
- bool isBuildingBundle, {
- bool shouldBuildPluginAsAar = false,
-}) async {
- final String gradlew = await gradleUtils.getExecutable(flutterProject);
- final GradleProject gradleProject = await gradleUtils.getAppProject(flutterProject);
-
- if (shouldBuildPluginAsAar) {
- // Create a settings.gradle that doesn't import the plugins as subprojects.
- createSettingsAarGradle(flutterProject.android.hostAppGradleRoot);
- await buildPluginsAsAar(
- flutterProject,
- androidBuildInfo,
- buildDirectory: gradleProject.buildDirectory,
- );
- }
-
- final String exclamationMark = terminal.color('[!]', TerminalColor.red);
- final bool usesAndroidX = isAppUsingAndroidX(flutterProject.android.hostAppGradleRoot);
-
- if (usesAndroidX) {
- BuildEvent('app-using-android-x').send();
- } else if (!usesAndroidX) {
- BuildEvent('app-not-using-android-x').send();
- printStatus('$exclamationMark Your app isn\'t using AndroidX.', emphasis: true);
- printStatus(
- 'To avoid potential build failures, you can quickly migrate your app '
- 'by following the steps on https://goo.gl/CP92wY.',
- indent: 4,
- );
- }
- final BuildInfo buildInfo = androidBuildInfo.buildInfo;
-
- String assembleTask;
-
- if (isBuildingBundle) {
- assembleTask = gradleProject.bundleTaskFor(buildInfo);
- } else {
- assembleTask = gradleProject.assembleTaskFor(buildInfo);
- }
- if (assembleTask == null) {
- printUndefinedTask(gradleProject, buildInfo);
- throwToolExit('Gradle build aborted.');
- }
- final Status status = logger.startProgress(
- 'Running Gradle task \'$assembleTask\'...',
- timeout: timeoutConfiguration.slowOperation,
- multilineOutput: true,
+void _exitWithProjectNotUsingGradleMessage() {
+ BuildEvent('unsupported-project', eventError: 'app-not-using-gradle').send();
+ throwToolExit(
+ '$warningMark 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.'
);
- final List<String> command = <String>[gradlew];
- if (logger.isVerbose) {
- command.add('-Pverbose=true');
- } else {
- command.add('-q');
- }
- if (artifacts is LocalEngineArtifacts) {
- final LocalEngineArtifacts localEngineArtifacts = artifacts;
- printTrace('Using local engine: ${localEngineArtifacts.engineOutPath}');
- command.add('-PlocalEngineOut=${localEngineArtifacts.engineOutPath}');
- }
- if (target != null) {
- command.add('-Ptarget=$target');
- }
- assert(buildInfo.trackWidgetCreation != null);
- command.add('-Ptrack-widget-creation=${buildInfo.trackWidgetCreation}');
- if (buildInfo.extraFrontEndOptions != null) {
- command.add('-Pextra-front-end-options=${buildInfo.extraFrontEndOptions}');
- }
- if (buildInfo.extraGenSnapshotOptions != null) {
- command.add('-Pextra-gen-snapshot-options=${buildInfo.extraGenSnapshotOptions}');
- }
- if (buildInfo.fileSystemRoots != null && buildInfo.fileSystemRoots.isNotEmpty) {
- command.add('-Pfilesystem-roots=${buildInfo.fileSystemRoots.join('|')}');
- }
- if (buildInfo.fileSystemScheme != null) {
- command.add('-Pfilesystem-scheme=${buildInfo.fileSystemScheme}');
- }
- if (androidBuildInfo.splitPerAbi) {
- command.add('-Psplit-per-abi=true');
- }
- if (androidBuildInfo.shrink) {
- command.add('-Pshrink=true');
- }
- if (androidBuildInfo.targetArchs.isNotEmpty) {
- final String targetPlatforms = androidBuildInfo.targetArchs
- .map(getPlatformNameForAndroidArch).join(',');
- command.add('-Ptarget-platform=$targetPlatforms');
- }
- if (shouldBuildPluginAsAar) {
- // Pass a system flag instead of a project flag, so this flag can be
- // read from include_flutter.groovy.
- command.add('-Dbuild-plugins-as-aars=true');
- // Don't use settings.gradle from the current project since it includes the plugins as subprojects.
- command.add('--settings-file=settings_aar.gradle');
- }
- command.add(assembleTask);
- bool potentialAndroidXFailure = false;
- bool potentialR8Failure = false;
- final Stopwatch sw = Stopwatch()..start();
- int exitCode = 1;
- try {
- exitCode = await processUtils.stream(
- command,
- workingDirectory: flutterProject.android.hostAppGradleRoot.path,
- allowReentrantFlutter: true,
- environment: gradleEnv,
- // TODO(mklim): if AndroidX warnings are no longer required, we can remove
- // them from this map function.
- mapFunction: (String line) {
- final bool isAndroidXPluginWarning = androidXPluginWarningRegex.hasMatch(line);
- if (!isAndroidXPluginWarning && androidXFailureRegex.hasMatch(line)) {
- potentialAndroidXFailure = true;
- }
- // R8 errors include references to this package.
- if (!potentialR8Failure && androidBuildInfo.shrink &&
- line.contains('com.android.tools.r8')) {
- potentialR8Failure = true;
- }
- // Always print the full line in verbose mode.
- if (logger.isVerbose) {
- return line;
- } else if (isAndroidXPluginWarning || !ndkMessageFilter.hasMatch(line)) {
- return null;
- }
- return line;
- },
- );
- } finally {
- status.stop();
- }
-
- if (exitCode != 0) {
- if (potentialR8Failure) {
- printStatus('$exclamationMark 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);
- BuildEvent('r8-failure').send();
- } else if (potentialAndroidXFailure) {
- final bool hasPlugins = flutterProject.flutterPluginsFile.existsSync();
- if (!hasPlugins) {
- // If the app doesn't use any plugin, then it's unclear where the incompatibility is coming from.
- BuildEvent('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. ');
- printStatus('Please migrate your app to AndroidX. See https://goo.gl/CP92wY.');
- BuildEvent('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('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('android-x-failure', eventError: 'not-using-jetifier').send();
- // The app is using Androidx, but Jetifier hasn't run yet.
- // Call the current method again, build the plugins as AAR, so Jetifier can translate
- // the dependencies.
- // NOTE: Don't build the plugins as AARs by default since this drastically increases
- // the build time.
- await _buildGradleProjectV2(
- flutterProject,
- androidBuildInfo,
- target,
- isBuildingBundle,
- shouldBuildPluginAsAar: true,
- );
- return;
- }
- }
- throwToolExit('Gradle task $assembleTask failed with exit code $exitCode', exitCode: exitCode);
- }
- flutterUsage.sendTiming('build', 'gradle-v2', Duration(milliseconds: sw.elapsedMilliseconds));
-
- if (!isBuildingBundle) {
- final Iterable<File> apkFiles = findApkFiles(gradleProject, androidBuildInfo);
- if (apkFiles.isEmpty) {
- throwToolExit('Gradle build failed to produce an Android package.');
- }
- // Copy the first APK to app.apk, so `flutter run`, `flutter install`, etc. can find it.
- // TODO(blasten): Handle multiple APKs.
- apkFiles.first.copySync(gradleProject.apkDirectory.childFile('app.apk').path);
-
- printTrace('calculateSha: ${gradleProject.apkDirectory}/app.apk');
- final File apkShaFile = gradleProject.apkDirectory.childFile('app.apk.sha1');
- apkShaFile.writeAsStringSync(_calculateSha(apkFiles.first));
-
- for (File apkFile in apkFiles) {
- String appSize;
- if (buildInfo.mode == BuildMode.debug) {
- appSize = '';
- } else {
- appSize = ' (${getSizeAsMB(apkFile.lengthSync())})';
- }
- printStatus('Built ${fs.path.relative(apkFile.path)}$appSize.',
- color: TerminalColor.green);
- }
- } else {
- final File bundleFile = findBundleFile(gradleProject, buildInfo);
- if (bundleFile == null) {
- throwToolExit('Gradle build failed to produce an Android bundle package.');
- }
-
- String appSize;
- if (buildInfo.mode == BuildMode.debug) {
- appSize = '';
- } else {
- appSize = ' (${getSizeAsMB(bundleFile.lengthSync())})';
- }
- printStatus('Built ${fs.path.relative(bundleFile.path)}$appSize.',
- color: TerminalColor.green);
- }
}
/// Returns [true] if the current app uses AndroidX.
@@ -998,7 +608,7 @@
Future<void> buildPluginsAsAar(
FlutterProject flutterProject,
AndroidBuildInfo androidBuildInfo, {
- String buildDirectory,
+ Directory buildDirectory,
}) async {
final File flutterPluginFile = flutterProject.flutterPluginsFile;
if (!flutterPluginFile.existsSync()) {
@@ -1031,37 +641,41 @@
// Log the entire plugin entry in `.flutter-plugins` since it
// includes the plugin name and the version.
BuildEvent('plugin-aar-failure', eventError: plugin).send();
- throwToolExit('The plugin $pluginName could not be built due to the issue above. ');
+ throwToolExit('The plugin $pluginName could not be built due to the issue above.');
}
}
}
+/// Returns the APK files for a given [FlutterProject] and [AndroidBuildInfo].
@visibleForTesting
-Iterable<File> findApkFiles(GradleProject project, AndroidBuildInfo androidBuildInfo) {
- final Iterable<String> apkFileNames = project.apkFilesFor(androidBuildInfo);
+Iterable<File> findApkFiles(
+ FlutterProject project,
+ AndroidBuildInfo androidBuildInfo)
+{
+ final Iterable<String> apkFileNames = _apkFilesFor(androidBuildInfo);
if (apkFileNames.isEmpty) {
return const <File>[];
}
-
+ final Directory apkDirectory = getApkDirectory(project);
return apkFileNames.expand<File>((String apkFileName) {
- File apkFile = project.apkDirectory.childFile(apkFileName);
+ File apkFile = apkDirectory.childFile(apkFileName);
if (apkFile.existsSync()) {
return <File>[apkFile];
}
final BuildInfo buildInfo = androidBuildInfo.buildInfo;
final String modeName = camelCase(buildInfo.modeName);
- apkFile = project.apkDirectory
- .childDirectory(modeName)
- .childFile(apkFileName);
+ apkFile = apkDirectory
+ .childDirectory(modeName)
+ .childFile(apkFileName);
if (apkFile.existsSync()) {
return <File>[apkFile];
}
if (buildInfo.flavor != null) {
// Android Studio Gradle plugin v3 adds flavor to path.
- apkFile = project.apkDirectory
- .childDirectory(buildInfo.flavor)
- .childDirectory(modeName)
- .childFile(apkFileName);
+ apkFile = apkDirectory
+ .childDirectory(buildInfo.flavor)
+ .childDirectory(modeName)
+ .childFile(apkFileName);
if (apkFile.existsSync()) {
return <File>[apkFile];
}
@@ -1071,22 +685,21 @@
}
@visibleForTesting
-File findBundleFile(GradleProject project, BuildInfo buildInfo) {
+File findBundleFile(FlutterProject project, BuildInfo buildInfo) {
final List<File> fileCandidates = <File>[
- project.bundleDirectory
+ getBundleDirectory(project)
.childDirectory(camelCase(buildInfo.modeName))
.childFile('app.aab'),
- project.bundleDirectory
+ getBundleDirectory(project)
.childDirectory(camelCase(buildInfo.modeName))
.childFile('app-${buildInfo.modeName}.aab'),
];
-
if (buildInfo.flavor != null) {
// The Android Gradle plugin 3.0.0 adds the flavor name to the path.
// For example: In release mode, if the flavor name is `foo_bar`, then
// the directory name is `foo_barRelease`.
fileCandidates.add(
- project.bundleDirectory
+ getBundleDirectory(project)
.childDirectory('${buildInfo.flavor}${camelCase('_' + buildInfo.modeName)}')
.childFile('app.aab'));
@@ -1094,7 +707,7 @@
// For example: In release mode, if the flavor name is `foo_bar`, then
// the file name name is `app-foo_bar-release.aab`.
fileCandidates.add(
- project.bundleDirectory
+ getBundleDirectory(project)
.childDirectory('${buildInfo.flavor}${camelCase('_' + buildInfo.modeName)}')
.childFile('app-${buildInfo.flavor}-${buildInfo.modeName}.aab'));
}
@@ -1105,152 +718,3 @@
}
return null;
}
-
-/// The environment variables needed to run Gradle.
-Map<String, String> get gradleEnv {
- final Map<String, String> env = Map<String, String>.from(platform.environment);
- if (javaPath != null) {
- // Use java bundled with Android Studio.
- env['JAVA_HOME'] = javaPath;
- }
- // Don't log analytics for downstream Flutter commands.
- // e.g. `flutter build bundle`.
- env['FLUTTER_SUPPRESS_ANALYTICS'] = 'true';
- return env;
-}
-
-class GradleProject {
- GradleProject(
- this.buildTypes,
- this.productFlavors,
- this.buildDirectory,
- );
-
- factory GradleProject.fromAppProperties(String properties, String tasks) {
- // Extract build directory.
- final String buildDirectory = properties
- .split('\n')
- .firstWhere((String s) => s.startsWith('buildDir: '))
- .substring('buildDir: '.length)
- .trim();
-
- // Extract build types and product flavors.
- final Set<String> variants = <String>{};
- for (String s in tasks.split('\n')) {
- final Match match = _assembleTaskPattern.matchAsPrefix(s);
- if (match != null) {
- final String variant = match.group(1).toLowerCase();
- if (!variant.endsWith('test')) {
- variants.add(variant);
- }
- }
- }
- final Set<String> buildTypes = <String>{};
- 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)) {
- buildTypes.add(buildType);
- productFlavors.add(variant1);
- }
- }
- }
- }
- if (productFlavors.isEmpty) {
- buildTypes.addAll(variants);
- }
- return GradleProject(
- buildTypes.toList(),
- productFlavors.toList(),
- buildDirectory,
- );
- }
-
- /// The build types such as [release] or [debug].
- final List<String> buildTypes;
-
- /// The product flavors defined in build.gradle.
- final List<String> productFlavors;
-
- /// The build directory. This is typically <project>build/.
- String buildDirectory;
-
- /// The directory where the APK artifact is generated.
- Directory get apkDirectory {
- return fs.directory(fs.path.join(buildDirectory, 'outputs', 'apk'));
- }
-
- /// The directory where the app bundle artifact is generated.
- Directory get bundleDirectory {
- return fs.directory(fs.path.join(buildDirectory, 'outputs', 'bundle'));
- }
-
- /// The directory where the repo is generated.
- /// Only applicable to AARs.
- Directory get repoDirectory {
- return fs.directory(fs.path.join(buildDirectory, 'outputs', 'repo'));
- }
-
- String _buildTypeFor(BuildInfo buildInfo) {
- final String modeName = camelCase(buildInfo.modeName);
- if (buildTypes.contains(modeName.toLowerCase())) {
- return modeName;
- }
- return null;
- }
-
- String _productFlavorFor(BuildInfo buildInfo) {
- if (buildInfo.flavor == null) {
- return productFlavors.isEmpty ? '' : null;
- } else if (productFlavors.contains(buildInfo.flavor)) {
- return buildInfo.flavor;
- }
- return null;
- }
-
- String assembleTaskFor(BuildInfo buildInfo) {
- final String buildType = _buildTypeFor(buildInfo);
- final String productFlavor = _productFlavorFor(buildInfo);
- if (buildType == null || productFlavor == null) {
- return null;
- }
- return 'assemble${toTitleCase(productFlavor)}${toTitleCase(buildType)}';
- }
-
- Iterable<String> apkFilesFor(AndroidBuildInfo androidBuildInfo) {
- final String buildType = _buildTypeFor(androidBuildInfo.buildInfo);
- final String productFlavor = _productFlavorFor(androidBuildInfo.buildInfo);
- if (buildType == null || productFlavor == null) {
- return const <String>[];
- }
-
- final String flavorString = productFlavor.isEmpty ? '' : '-' + productFlavor;
- if (androidBuildInfo.splitPerAbi) {
- return androidBuildInfo.targetArchs.map<String>((AndroidArch arch) {
- final String abi = getNameForAndroidArch(arch);
- return 'app$flavorString-$abi-$buildType.apk';
- });
- }
- return <String>['app$flavorString-$buildType.apk'];
- }
-
- String bundleTaskFor(BuildInfo buildInfo) {
- final String buildType = _buildTypeFor(buildInfo);
- final String productFlavor = _productFlavorFor(buildInfo);
- if (buildType == null || productFlavor == null) {
- return null;
- }
- return 'bundle${toTitleCase(productFlavor)}${toTitleCase(buildType)}';
- }
-
- String aarTaskFor(BuildInfo buildInfo) {
- final String buildType = _buildTypeFor(buildInfo);
- final String productFlavor = _productFlavorFor(buildInfo);
- if (buildType == null || productFlavor == null) {
- return null;
- }
- return 'assembleAar${toTitleCase(productFlavor)}${toTitleCase(buildType)}';
- }
-}
diff --git a/packages/flutter_tools/lib/src/android/gradle_errors.dart b/packages/flutter_tools/lib/src/android/gradle_errors.dart
new file mode 100644
index 0000000..bcec1a7
--- /dev/null
+++ b/packages/flutter_tools/lib/src/android/gradle_errors.dart
@@ -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',
+);
diff --git a/packages/flutter_tools/lib/src/android/gradle_utils.dart b/packages/flutter_tools/lib/src/android/gradle_utils.dart
new file mode 100644
index 0000000..0b9eaa0
--- /dev/null
+++ b/packages/flutter_tools/lib/src/android/gradle_utils.dart
@@ -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.'
+ );
+}
diff --git a/packages/flutter_tools/lib/src/base/terminal.dart b/packages/flutter_tools/lib/src/base/terminal.dart
index 22ba9aa..09bfd59 100644
--- a/packages/flutter_tools/lib/src/base/terminal.dart
+++ b/packages/flutter_tools/lib/src/base/terminal.dart
@@ -23,6 +23,17 @@
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 {
diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart
index c52fb18..f18d33c 100644
--- a/packages/flutter_tools/lib/src/cache.dart
+++ b/packages/flutter_tools/lib/src/cache.dart
@@ -6,7 +6,7 @@
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 @@
'--project-cache-dir', tempDir.path,
'resolveDependencies',
],
- environment: gradleEnv);
+ environment: gradleEnvironment);
if (processResult.exitCode != 0) {
printError('Failed to download the Android dependencies');
}
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart
index 4d26cb9..808456c 100644
--- a/packages/flutter_tools/lib/src/commands/create.dart
+++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -10,7 +10,7 @@
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';
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index c909973..e210afd 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -7,7 +7,7 @@
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';
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index 9568e58..5bc5c77 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -7,7 +7,7 @@
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 @@
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();
diff --git a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart
new file mode 100644
index 0000000..aca7919
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart
@@ -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';
+ }
+}
diff --git a/packages/flutter_tools/test/general.shard/android/gradle_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_test.dart
index 21ec43e..0571175 100644
--- a/packages/flutter_tools/test/general.shard/android/gradle_test.dart
+++ b/packages/flutter_tools/test/general.shard/android/gradle_test.dart
@@ -8,79 +8,370 @@
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/android_studio.dart';
import 'package:flutter_tools/src/android/gradle.dart';
-import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/android/gradle_utils.dart';
+import 'package:flutter_tools/src/android/gradle_errors.dart';
import 'package:flutter_tools/src/artifacts.dart';
+import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
-import 'package:flutter_tools/src/base/os.dart';
+import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/ios/xcodeproj.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';
import '../../src/pubspec_schema.dart';
void main() {
Cache.flutterRoot = getFlutterRoot();
- group('gradle build', () {
- test('do not crash if there is no Android SDK', () async {
- Exception shouldBeToolExit;
- try {
- // We'd like to always set androidSdk to null and test updateLocalProperties. But that's
- // currently impossible as the test is not hermetic. Luckily, our bots don't have Android
- // SDKs yet so androidSdk should be null by default.
- //
- // This test is written to fail if our bots get Android SDKs in the future: shouldBeToolExit
- // will be null and our expectation would fail. That would remind us to make these tests
- // hermetic before adding Android SDKs to the bots.
- updateLocalProperties(project: FlutterProject.current());
- } on Exception catch (e) {
- shouldBeToolExit = e;
- }
- // Ensure that we throw a meaningful ToolExit instead of a general crash.
- expect(shouldBeToolExit, isToolExit);
+
+ group('build artifacts', () {
+ test('getApkDirectory in app projects', () {
+ final FlutterProject project = MockFlutterProject();
+ final AndroidProject androidProject = MockAndroidProject();
+ when(project.android).thenReturn(androidProject);
+ when(project.isModule).thenReturn(false);
+ when(androidProject.buildDirectory).thenReturn(fs.directory('foo'));
+
+ expect(
+ getApkDirectory(project).path,
+ equals(fs.path.join('foo', 'app', 'outputs', 'apk')),
+ );
});
- // Regression test for https://github.com/flutter/flutter/issues/34700
- testUsingContext('Does not return nulls in apk list', () {
- final GradleProject gradleProject = MockGradleProject();
- const AndroidBuildInfo buildInfo = AndroidBuildInfo(BuildInfo.debug);
- when(gradleProject.apkFilesFor(buildInfo)).thenReturn(<String>['not_real']);
- when(gradleProject.apkDirectory).thenReturn(fs.currentDirectory);
+ test('getApkDirectory in module projects', () {
+ final FlutterProject project = MockFlutterProject();
+ final AndroidProject androidProject = MockAndroidProject();
+ when(project.android).thenReturn(androidProject);
+ when(project.isModule).thenReturn(true);
+ when(androidProject.buildDirectory).thenReturn(fs.directory('foo'));
- expect(findApkFiles(gradleProject, buildInfo), <File>[]);
+ expect(
+ getApkDirectory(project).path,
+ equals(fs.path.join('foo', 'host', 'outputs', 'apk')),
+ );
+ });
+
+ test('getBundleDirectory in app projects', () {
+ final FlutterProject project = MockFlutterProject();
+ final AndroidProject androidProject = MockAndroidProject();
+ when(project.android).thenReturn(androidProject);
+ when(project.isModule).thenReturn(false);
+ when(androidProject.buildDirectory).thenReturn(fs.directory('foo'));
+
+ expect(
+ getBundleDirectory(project).path,
+ equals(fs.path.join('foo', 'app', 'outputs', 'bundle')),
+ );
+ });
+
+ test('getBundleDirectory in module projects', () {
+ final FlutterProject project = MockFlutterProject();
+ final AndroidProject androidProject = MockAndroidProject();
+ when(project.android).thenReturn(androidProject);
+ when(project.isModule).thenReturn(true);
+ when(androidProject.buildDirectory).thenReturn(fs.directory('foo'));
+
+ expect(
+ getBundleDirectory(project).path,
+ equals(fs.path.join('foo', 'host', 'outputs', 'bundle')),
+ );
+ });
+
+ test('getRepoDirectory', () {
+ expect(
+ getRepoDirectory(fs.directory('foo')).path,
+ equals(fs.path.join('foo','outputs', 'repo')),
+ );
+ });
+ });
+
+ group('gradle tasks', () {
+ test('assemble release', () {
+ expect(
+ getAssembleTaskFor(const BuildInfo(BuildMode.release, null)),
+ equals('assembleRelease'),
+ );
+ expect(
+ getAssembleTaskFor(const BuildInfo(BuildMode.release, 'flavorFoo')),
+ equals('assembleFlavorFooRelease'),
+ );
+ });
+
+ test('assemble debug', () {
+ expect(
+ getAssembleTaskFor(const BuildInfo(BuildMode.debug, null)),
+ equals('assembleDebug'),
+ );
+ expect(
+ getAssembleTaskFor(const BuildInfo(BuildMode.debug, 'flavorFoo')),
+ equals('assembleFlavorFooDebug'),
+ );
+ });
+
+ test('assemble profile', () {
+ expect(
+ getAssembleTaskFor(const BuildInfo(BuildMode.profile, null)),
+ equals('assembleProfile'),
+ );
+ expect(
+ getAssembleTaskFor(const BuildInfo(BuildMode.profile, 'flavorFoo')),
+ equals('assembleFlavorFooProfile'),
+ );
+ });
+ });
+
+ group('findBundleFile', () {
+ testUsingContext('Finds app bundle when flavor contains underscores in release mode', () {
+ final FlutterProject project = generateFakeAppBundle('foo_barRelease', 'app.aab');
+ final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo_bar'));
+ expect(bundle, isNotNull);
+ expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barRelease', 'app.aab'));
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(),
ProcessManager: () => FakeProcessManager.any(),
});
- test('androidXFailureRegex should match lines with likely AndroidX errors', () {
- final List<String> nonMatchingLines = <String>[
- ':app:preBuild UP-TO-DATE',
- 'BUILD SUCCESSFUL in 0s',
- '',
- ];
- final List<String> matchingLines = <String>[
- 'AAPT: error: resource android:attr/fontVariationSettings not found.',
- 'AAPT: error: resource android:attr/ttcIndex not found.',
- 'error: package android.support.annotation does not exist',
- 'import android.support.annotation.NonNull;',
- 'import androidx.annotation.NonNull;',
- 'Daemon: AAPT2 aapt2-3.2.1-4818971-linux Daemon #0',
- ];
- for (String m in nonMatchingLines) {
- expect(androidXFailureRegex.hasMatch(m), isFalse);
- }
- for (String m in matchingLines) {
- expect(androidXFailureRegex.hasMatch(m), isTrue);
- }
+ testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in release mode', () {
+ final FlutterProject project = generateFakeAppBundle('fooRelease', 'app.aab');
+ final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo'));
+ expect(bundle, isNotNull);
+ expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooRelease', 'app.aab'));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('Finds app bundle when no flavor is used in release mode', () {
+ final FlutterProject project = generateFakeAppBundle('release', 'app.aab');
+ final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, null));
+ expect(bundle, isNotNull);
+ expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'release', 'app.aab'));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('Finds app bundle when flavor contains underscores in debug mode', () {
+ final FlutterProject project = generateFakeAppBundle('foo_barDebug', 'app.aab');
+ final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo_bar'));
+ expect(bundle, isNotNull);
+ expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barDebug', 'app.aab'));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in debug mode', () {
+ final FlutterProject project = generateFakeAppBundle('fooDebug', 'app.aab');
+ final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo'));
+ expect(bundle, isNotNull);
+ expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooDebug', 'app.aab'));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('Finds app bundle when no flavor is used in debug mode', () {
+ final FlutterProject project = generateFakeAppBundle('debug', 'app.aab');
+ final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, null));
+ expect(bundle, isNotNull);
+ expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'debug', 'app.aab'));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('Finds app bundle when flavor contains underscores in profile mode', () {
+ final FlutterProject project = generateFakeAppBundle('foo_barProfile', 'app.aab');
+ final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'foo_bar'));
+ expect(bundle, isNotNull);
+ expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barProfile', 'app.aab'));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in profile mode', () {
+ final FlutterProject project = generateFakeAppBundle('fooProfile', 'app.aab');
+ final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'foo'));
+ expect(bundle, isNotNull);
+ expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooProfile', 'app.aab'));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('Finds app bundle when no flavor is used in profile mode', () {
+ final FlutterProject project = generateFakeAppBundle('profile', 'app.aab');
+ final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, null));
+ expect(bundle, isNotNull);
+ expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'profile', 'app.aab'));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('Finds app bundle in release mode - Gradle 3.5', () {
+ final FlutterProject project = generateFakeAppBundle('release', 'app-release.aab');
+ final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, null));
+ expect(bundle, isNotNull);
+ expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'release', 'app-release.aab'));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('Finds app bundle in profile mode - Gradle 3.5', () {
+ final FlutterProject project = generateFakeAppBundle('profile', 'app-profile.aab');
+ final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, null));
+ expect(bundle, isNotNull);
+ expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'profile', 'app-profile.aab'));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('Finds app bundle in debug mode - Gradle 3.5', () {
+ final FlutterProject project = generateFakeAppBundle('debug', 'app-debug.aab');
+ final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, null));
+ expect(bundle, isNotNull);
+ expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'debug', 'app-debug.aab'));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('Finds app bundle when flavor contains underscores in release mode - Gradle 3.5', () {
+ final FlutterProject project = generateFakeAppBundle('foo_barRelease', 'app-foo_bar-release.aab');
+ final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo_bar'));
+ expect(bundle, isNotNull);
+ expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barRelease', 'app-foo_bar-release.aab'));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('Finds app bundle when flavor contains underscores in profile mode - Gradle 3.5', () {
+ final FlutterProject project = generateFakeAppBundle('foo_barProfile', 'app-foo_bar-profile.aab');
+ final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'foo_bar'));
+ expect(bundle, isNotNull);
+ expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barProfile', 'app-foo_bar-profile.aab'));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('Finds app bundle when flavor contains underscores in debug mode - Gradle 3.5', () {
+ final FlutterProject project = generateFakeAppBundle('foo_barDebug', 'app-foo_bar-debug.aab');
+ final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo_bar'));
+ expect(bundle, isNotNull);
+ expect(bundle.path, fs.path.join('irrelevant','app', 'outputs', 'bundle', 'foo_barDebug', 'app-foo_bar-debug.aab'));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+ });
+
+ group('findApkFiles', () {
+ testUsingContext('Finds APK without flavor in release', () {
+ final FlutterProject project = MockFlutterProject();
+ final AndroidProject androidProject = MockAndroidProject();
+
+ when(project.android).thenReturn(androidProject);
+ when(project.isModule).thenReturn(false);
+ when(androidProject.buildDirectory).thenReturn(fs.directory('irrelevant'));
+
+ final Directory apkDirectory = fs.directory(fs.path.join('irrelevant', 'app', 'outputs', 'apk', 'release'));
+ apkDirectory.createSync(recursive: true);
+ apkDirectory.childFile('app-release.apk').createSync();
+
+ final Iterable<File> apks = findApkFiles(
+ project,
+ const AndroidBuildInfo(BuildInfo(BuildMode.release, '')),
+ );
+ expect(apks.isNotEmpty, isTrue);
+ expect(apks.first.path, equals(fs.path.join('irrelevant', 'app', 'outputs', 'apk', 'release', 'app-release.apk')));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('Finds APK with flavor in release mode', () {
+ final FlutterProject project = MockFlutterProject();
+ final AndroidProject androidProject = MockAndroidProject();
+
+ when(project.android).thenReturn(androidProject);
+ when(project.isModule).thenReturn(false);
+ when(androidProject.buildDirectory).thenReturn(fs.directory('irrelevant'));
+
+ final Directory apkDirectory = fs.directory(fs.path.join('irrelevant', 'app', 'outputs', 'apk', 'release'));
+ apkDirectory.createSync(recursive: true);
+ apkDirectory.childFile('app-flavor1-release.apk').createSync();
+
+ final Iterable<File> apks = findApkFiles(
+ project,
+ const AndroidBuildInfo(BuildInfo(BuildMode.release, 'flavor1')),
+ );
+ expect(apks.isNotEmpty, isTrue);
+ expect(apks.first.path, equals(fs.path.join('irrelevant', 'app', 'outputs', 'apk', 'release', 'app-flavor1-release.apk')));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+
+ testUsingContext('Finds APK with flavor in release mode - AGP v3', () {
+ final FlutterProject project = MockFlutterProject();
+ final AndroidProject androidProject = MockAndroidProject();
+
+ when(project.android).thenReturn(androidProject);
+ when(project.isModule).thenReturn(false);
+ when(androidProject.buildDirectory).thenReturn(fs.directory('irrelevant'));
+
+ final Directory apkDirectory = fs.directory(fs.path.join('irrelevant', 'app', 'outputs', 'apk', 'flavor1', 'release'));
+ apkDirectory.createSync(recursive: true);
+ apkDirectory.childFile('app-flavor1-release.apk').createSync();
+
+ final Iterable<File> apks = findApkFiles(
+ project,
+ const AndroidBuildInfo(BuildInfo(BuildMode.release, 'flavor1')),
+ );
+ expect(apks.isNotEmpty, isTrue);
+ expect(apks.first.path, equals(fs.path.join('irrelevant', 'app', 'outputs', 'apk', 'flavor1', 'release', 'app-flavor1-release.apk')));
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem(),
+ ProcessManager: () => FakeProcessManager.any(),
+ });
+ });
+
+ group('gradle build', () {
+ testUsingContext('do not crash if there is no Android SDK', () async {
+ expect(() {
+ updateLocalProperties(project: FlutterProject.current());
+ }, throwsToolExit(
+ message: '$warningMark No Android SDK found. Try setting the ANDROID_HOME environment variable.',
+ ));
+ }, overrides: <Type, Generator>{
+ AndroidSdk: () => null,
+ });
+
+ // Regression test for https://github.com/flutter/flutter/issues/34700
+ testUsingContext('Does not return nulls in apk list', () {
+ const AndroidBuildInfo buildInfo = AndroidBuildInfo(BuildInfo.debug);
+ expect(findApkFiles(FlutterProject.current(), buildInfo), <File>[]);
+ }, overrides: <Type, Generator>{
+ FileSystem: () => MemoryFileSystem(),
+ ProcessManager: () => FakeProcessManager.any(),
});
test('androidXPluginWarningRegex should match lines with the AndroidX plugin warnings', () {
@@ -103,355 +394,6 @@
expect(androidXPluginWarningRegex.hasMatch(m), isTrue);
}
});
-
- test('ndkMessageFilter should only match lines without the error message', () {
- final List<String> nonMatchingLines = <String>[
- 'NDK is missing a "platforms" directory.',
- 'If you are using NDK, verify the ndk.dir is set to a valid NDK directory. It is currently set to /usr/local/company/home/username/Android/Sdk/ndk-bundle.',
- 'If you are not using NDK, unset the NDK variable from ANDROID_NDK_HOME or local.properties to remove this warning.',
- ];
- final List<String> matchingLines = <String>[
- ':app:preBuild UP-TO-DATE',
- 'BUILD SUCCESSFUL in 0s',
- '',
- 'Something NDK related mentioning ANDROID_NDK_HOME',
- ];
- for (String m in nonMatchingLines) {
- expect(ndkMessageFilter.hasMatch(m), isFalse);
- }
- for (String m in matchingLines) {
- expect(ndkMessageFilter.hasMatch(m), isTrue);
- }
- });
-
- testUsingContext('Finds app bundle when flavor contains underscores in release mode', () {
- final GradleProject gradleProject = generateFakeAppBundle('foo_barRelease', 'app.aab');
- final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, 'foo_bar'));
- expect(bundle, isNotNull);
- expect(bundle.path, '/foo_barRelease/app.aab');
- }, overrides: <Type, Generator>{
- FileSystem: () => MemoryFileSystem(),
- ProcessManager: () => FakeProcessManager.any(),
- });
-
- testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in release mode', () {
- final GradleProject gradleProject = generateFakeAppBundle('fooRelease', 'app.aab');
- final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, 'foo'));
- expect(bundle, isNotNull);
- expect(bundle.path, '/fooRelease/app.aab');
- }, overrides: <Type, Generator>{
- FileSystem: () => MemoryFileSystem(),
- ProcessManager: () => FakeProcessManager.any(),
- });
-
- testUsingContext('Finds app bundle when no flavor is used in release mode', () {
- final GradleProject gradleProject = generateFakeAppBundle('release', 'app.aab');
- final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, null));
- expect(bundle, isNotNull);
- expect(bundle.path, '/release/app.aab');
- }, overrides: <Type, Generator>{
- FileSystem: () => MemoryFileSystem(),
- ProcessManager: () => FakeProcessManager.any(),
- });
-
- testUsingContext('Finds app bundle when flavor contains underscores in debug mode', () {
- final GradleProject gradleProject = generateFakeAppBundle('foo_barDebug', 'app.aab');
- final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, 'foo_bar'));
- expect(bundle, isNotNull);
- expect(bundle.path, '/foo_barDebug/app.aab');
- }, overrides: <Type, Generator>{
- FileSystem: () => MemoryFileSystem(),
- ProcessManager: () => FakeProcessManager.any(),
- });
-
- testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in debug mode', () {
- final GradleProject gradleProject = generateFakeAppBundle('fooDebug', 'app.aab');
- final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, 'foo'));
- expect(bundle, isNotNull);
- expect(bundle.path, '/fooDebug/app.aab');
- }, overrides: <Type, Generator>{
- FileSystem: () => MemoryFileSystem(),
- ProcessManager: () => FakeProcessManager.any(),
- });
-
- testUsingContext('Finds app bundle when no flavor is used in debug mode', () {
- final GradleProject gradleProject = generateFakeAppBundle('debug', 'app.aab');
- final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, null));
- expect(bundle, isNotNull);
- expect(bundle.path, '/debug/app.aab');
- }, overrides: <Type, Generator>{
- FileSystem: () => MemoryFileSystem(),
- ProcessManager: () => FakeProcessManager.any(),
- });
-
- testUsingContext('Finds app bundle when flavor contains underscores in profile mode', () {
- final GradleProject gradleProject = generateFakeAppBundle('foo_barProfile', 'app.aab');
- final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, 'foo_bar'));
- expect(bundle, isNotNull);
- expect(bundle.path, '/foo_barProfile/app.aab');
- }, overrides: <Type, Generator>{
- FileSystem: () => MemoryFileSystem(),
- ProcessManager: () => FakeProcessManager.any(),
- });
-
- testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in profile mode', () {
- final GradleProject gradleProject = generateFakeAppBundle('fooProfile', 'app.aab');
- final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, 'foo'));
- expect(bundle, isNotNull);
- expect(bundle.path, '/fooProfile/app.aab');
- }, overrides: <Type, Generator>{
- FileSystem: () => MemoryFileSystem(),
- ProcessManager: () => FakeProcessManager.any(),
- });
-
- testUsingContext('Finds app bundle when no flavor is used in profile mode', () {
- final GradleProject gradleProject = generateFakeAppBundle('profile', 'app.aab');
- final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, null));
- expect(bundle, isNotNull);
- expect(bundle.path, '/profile/app.aab');
- }, overrides: <Type, Generator>{
- FileSystem: () => MemoryFileSystem(),
- ProcessManager: () => FakeProcessManager.any(),
- });
-
- testUsingContext('Finds app bundle in release mode - Gradle 3.5', () {
- final GradleProject gradleProject = generateFakeAppBundle('release', 'app-release.aab');
- final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, null));
- expect(bundle, isNotNull);
- expect(bundle.path, '/release/app-release.aab');
- }, overrides: <Type, Generator>{
- FileSystem: () => MemoryFileSystem(),
- ProcessManager: () => FakeProcessManager.any(),
- });
-
- testUsingContext('Finds app bundle in profile mode - Gradle 3.5', () {
- final GradleProject gradleProject = generateFakeAppBundle('profile', 'app-profile.aab');
- final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, null));
- expect(bundle, isNotNull);
- expect(bundle.path, '/profile/app-profile.aab');
- }, overrides: <Type, Generator>{
- FileSystem: () => MemoryFileSystem(),
- ProcessManager: () => FakeProcessManager.any(),
- });
-
- testUsingContext('Finds app bundle in debug mode - Gradle 3.5', () {
- final GradleProject gradleProject = generateFakeAppBundle('debug', 'app-debug.aab');
- final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, null));
- expect(bundle, isNotNull);
- expect(bundle.path, '/debug/app-debug.aab');
- }, overrides: <Type, Generator>{
- FileSystem: () => MemoryFileSystem(),
- ProcessManager: () => FakeProcessManager.any(),
- });
-
- testUsingContext('Finds app bundle when flavor contains underscores in release mode - Gradle 3.5', () {
- final GradleProject gradleProject = generateFakeAppBundle('foo_barRelease', 'app-foo_bar-release.aab');
- final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, 'foo_bar'));
- expect(bundle, isNotNull);
- expect(bundle.path, '/foo_barRelease/app-foo_bar-release.aab');
- }, overrides: <Type, Generator>{
- FileSystem: () => MemoryFileSystem(),
- ProcessManager: () => FakeProcessManager.any(),
- });
-
- testUsingContext('Finds app bundle when flavor contains underscores in profile mode - Gradle 3.5', () {
- final GradleProject gradleProject = generateFakeAppBundle('foo_barProfile', 'app-foo_bar-profile.aab');
- final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, 'foo_bar'));
- expect(bundle, isNotNull);
- expect(bundle.path, '/foo_barProfile/app-foo_bar-profile.aab');
- }, overrides: <Type, Generator>{
- FileSystem: () => MemoryFileSystem(),
- ProcessManager: () => FakeProcessManager.any(),
- });
-
- testUsingContext('Finds app bundle when flavor contains underscores in debug mode - Gradle 3.5', () {
- final GradleProject gradleProject = generateFakeAppBundle('foo_barDebug', 'app-foo_bar-debug.aab');
- final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, 'foo_bar'));
- expect(bundle, isNotNull);
- expect(bundle.path, '/foo_barDebug/app-foo_bar-debug.aab');
- }, overrides: <Type, Generator>{
- FileSystem: () => MemoryFileSystem(),
- ProcessManager: () => FakeProcessManager.any(),
- });
- });
-
- group('gradle project', () {
- GradleProject projectFrom(String properties, String tasks) => GradleProject.fromAppProperties(properties, tasks);
-
- test('should extract build directory from app properties', () {
- final GradleProject project = projectFrom('''
-someProperty: someValue
-buildDir: /Users/some/apps/hello/build/app
-someOtherProperty: someOtherValue
- ''', '');
- expect(
- fs.path.normalize(project.apkDirectory.path),
- fs.path.normalize('/Users/some/apps/hello/build/app/outputs/apk'),
- );
- });
- test('should extract default build variants from app properties', () {
- final GradleProject project = projectFrom('buildDir: /Users/some/apps/hello/build/app', '''
-someTask
-assemble
-assembleAndroidTest
-assembleDebug
-assembleProfile
-assembleRelease
-someOtherTask
- ''');
- expect(project.buildTypes, <String>['debug', 'profile', 'release']);
- expect(project.productFlavors, isEmpty);
- });
- test('should extract custom build variants from app properties', () {
- final GradleProject project = projectFrom('buildDir: /Users/some/apps/hello/build/app', '''
-someTask
-assemble
-assembleAndroidTest
-assembleDebug
-assembleFree
-assembleFreeAndroidTest
-assembleFreeDebug
-assembleFreeProfile
-assembleFreeRelease
-assemblePaid
-assemblePaidAndroidTest
-assemblePaidDebug
-assemblePaidProfile
-assemblePaidRelease
-assembleProfile
-assembleRelease
-someOtherTask
- ''');
- expect(project.buildTypes, <String>['debug', 'profile', 'release']);
- expect(project.productFlavors, <String>['free', 'paid']);
- });
- test('should provide apk file name for default build types', () {
- final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], '/some/dir');
- expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo.debug)).first, 'app-debug.apk');
- expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo.profile)).first, 'app-profile.apk');
- expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo.release)).first, 'app-release.apk');
- expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.release, 'unknown'))).isEmpty, isTrue);
- });
- test('should provide apk file name for flavored build types', () {
- final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], '/some/dir');
- expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.debug, 'free'))).first, 'app-free-debug.apk');
- expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.release, 'paid'))).first, 'app-paid-release.apk');
- expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.release, 'unknown'))).isEmpty, isTrue);
- });
- test('should provide apks for default build types and each ABI', () {
- final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], '/some/dir');
- expect(project.apkFilesFor(
- const AndroidBuildInfo(
- BuildInfo.debug,
- splitPerAbi: true,
- targetArchs: <AndroidArch>[
- AndroidArch.armeabi_v7a,
- AndroidArch.arm64_v8a,
- ],
- )),
- <String>[
- 'app-armeabi-v7a-debug.apk',
- 'app-arm64-v8a-debug.apk',
- ]);
-
- expect(project.apkFilesFor(
- const AndroidBuildInfo(
- BuildInfo.release,
- splitPerAbi: true,
- targetArchs: <AndroidArch>[
- AndroidArch.armeabi_v7a,
- AndroidArch.arm64_v8a,
- ],
- )),
- <String>[
- 'app-armeabi-v7a-release.apk',
- 'app-arm64-v8a-release.apk',
- ]);
-
- expect(project.apkFilesFor(
- const AndroidBuildInfo(
- BuildInfo(BuildMode.release, 'unknown'),
- splitPerAbi: true,
- targetArchs: <AndroidArch>[
- AndroidArch.armeabi_v7a,
- AndroidArch.arm64_v8a,
- ],
- )).isEmpty, isTrue);
- });
- test('should provide apks for each ABI and flavored build types', () {
- final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], '/some/dir');
- expect(project.apkFilesFor(
- const AndroidBuildInfo(
- BuildInfo(BuildMode.debug, 'free'),
- splitPerAbi: true,
- targetArchs: <AndroidArch>[
- AndroidArch.armeabi_v7a,
- AndroidArch.arm64_v8a,
- ],
- )),
- <String>[
- 'app-free-armeabi-v7a-debug.apk',
- 'app-free-arm64-v8a-debug.apk',
- ]);
-
- expect(project.apkFilesFor(
- const AndroidBuildInfo(
- BuildInfo(BuildMode.release, 'paid'),
- splitPerAbi: true,
- targetArchs: <AndroidArch>[
- AndroidArch.armeabi_v7a,
- AndroidArch.arm64_v8a,
- ],
- )),
- <String>[
- 'app-paid-armeabi-v7a-release.apk',
- 'app-paid-arm64-v8a-release.apk',
- ]);
-
- expect(project.apkFilesFor(
- const AndroidBuildInfo(
- BuildInfo(BuildMode.release, 'unknown'),
- splitPerAbi: true,
- targetArchs: <AndroidArch>[
- AndroidArch.armeabi_v7a,
- AndroidArch.arm64_v8a,
- ],
- )).isEmpty, isTrue);
- });
- test('should provide assemble task name for default build types', () {
- final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], '/some/dir');
- expect(project.assembleTaskFor(BuildInfo.debug), 'assembleDebug');
- expect(project.assembleTaskFor(BuildInfo.profile), 'assembleProfile');
- expect(project.assembleTaskFor(BuildInfo.release), 'assembleRelease');
- expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
- });
- test('should provide assemble task name for flavored build types', () {
- final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], '/some/dir');
- expect(project.assembleTaskFor(const BuildInfo(BuildMode.debug, 'free')), 'assembleFreeDebug');
- expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'paid')), 'assemblePaidRelease');
- expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
- });
- test('should respect format of the flavored build types', () {
- final GradleProject project = GradleProject(<String>['debug'], <String>['randomFlavor'], '/some/dir');
- expect(project.assembleTaskFor(const BuildInfo(BuildMode.debug, 'randomFlavor')), 'assembleRandomFlavorDebug');
- });
- test('bundle should provide assemble task name for default build types', () {
- final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], '/some/dir');
- expect(project.bundleTaskFor(BuildInfo.debug), 'bundleDebug');
- expect(project.bundleTaskFor(BuildInfo.profile), 'bundleProfile');
- expect(project.bundleTaskFor(BuildInfo.release), 'bundleRelease');
- expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
- });
- test('bundle should provide assemble task name for flavored build types', () {
- final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], '/some/dir');
- expect(project.bundleTaskFor(const BuildInfo(BuildMode.debug, 'free')), 'bundleFreeDebug');
- expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'paid')), 'bundlePaidRelease');
- expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
- });
- test('bundle should respect format of the flavored build types', () {
- final GradleProject project = GradleProject(<String>['debug'], <String>['randomFlavor'], '/some/dir');
- expect(project.bundleTaskFor(const BuildInfo(BuildMode.debug, 'randomFlavor')), 'bundleRandomFlavorDebug');
- });
});
group('Config files', () {
@@ -461,7 +403,6 @@
setUp(() {
mockLogger = BufferLogger();
tempDir = fs.systemTempDirectory.createTempSync('flutter_settings_aar_test.');
-
});
testUsingContext('create settings_aar.gradle when current settings.gradle loads plugins', () {
@@ -547,48 +488,6 @@
});
});
- group('Undefined task', () {
- BufferLogger mockLogger;
-
- setUp(() {
- mockLogger = BufferLogger();
- });
-
- testUsingContext('print undefined build type', () {
- final GradleProject project = GradleProject(<String>['debug', 'release'],
- const <String>['free', 'paid'], '/some/dir');
-
- printUndefinedTask(project, const BuildInfo(BuildMode.profile, 'unknown'));
- expect(mockLogger.errorText, contains('The Gradle project does not define a task suitable for the requested build'));
- expect(mockLogger.errorText, contains('Review the android/app/build.gradle file and ensure it defines a profile build type'));
- }, overrides: <Type, Generator>{
- Logger: () => mockLogger,
- });
-
- testUsingContext('print no flavors', () {
- final GradleProject project = GradleProject(<String>['debug', 'release'],
- const <String>[], '/some/dir');
-
- printUndefinedTask(project, const BuildInfo(BuildMode.debug, 'unknown'));
- expect(mockLogger.errorText, contains('The Gradle project does not define a task suitable for the requested build'));
- expect(mockLogger.errorText, contains('The android/app/build.gradle file does not define any custom product flavors'));
- expect(mockLogger.errorText, contains('You cannot use the --flavor option'));
- }, overrides: <Type, Generator>{
- Logger: () => mockLogger,
- });
-
- testUsingContext('print flavors', () {
- final GradleProject project = GradleProject(<String>['debug', 'release'],
- const <String>['free', 'paid'], '/some/dir');
-
- printUndefinedTask(project, const BuildInfo(BuildMode.debug, 'unknown'));
- expect(mockLogger.errorText, contains('The Gradle project does not define a task suitable for the requested build'));
- expect(mockLogger.errorText, contains('The android/app/build.gradle file defines product flavors: free, paid'));
- }, overrides: <Type, Generator>{
- Logger: () => mockLogger,
- });
- });
-
group('Gradle local.properties', () {
MockLocalEngineArtifacts mockArtifacts;
MockProcessManager mockProcessManager;
@@ -861,477 +760,6 @@
});
});
- group('Gradle failures', () {
- MemoryFileSystem fs;
- Directory tempDir;
- Directory gradleWrapperDirectory;
- MockProcessManager mockProcessManager;
- String gradleBinary;
-
- setUp(() {
- fs = MemoryFileSystem();
- tempDir = fs.systemTempDirectory.createTempSync('flutter_artifacts_test.');
- gradleBinary = platform.isWindows ? 'gradlew.bat' : 'gradlew';
- gradleWrapperDirectory = fs.directory(
- fs.path.join(tempDir.path, 'bin', 'cache', 'artifacts', 'gradle_wrapper'));
- gradleWrapperDirectory.createSync(recursive: true);
- gradleWrapperDirectory
- .childFile(gradleBinary)
- .writeAsStringSync('irrelevant');
- fs.currentDirectory
- .childDirectory('android')
- .createSync();
- fs.currentDirectory
- .childDirectory('android')
- .childFile('gradle.properties')
- .writeAsStringSync('irrelevant');
- gradleWrapperDirectory
- .childDirectory('gradle')
- .childDirectory('wrapper')
- .createSync(recursive: true);
- gradleWrapperDirectory
- .childDirectory('gradle')
- .childDirectory('wrapper')
- .childFile('gradle-wrapper.jar')
- .writeAsStringSync('irrelevant');
-
- mockProcessManager = MockProcessManager();
- });
-
- testUsingContext('throws toolExit if gradle fails while downloading', () async {
- final List<String> cmd = <String>[
- fs.path.join(fs.currentDirectory.path, 'android', gradleBinary),
- '-v',
- ];
- 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)''';
- final ProcessException exception = ProcessException(
- gradleBinary,
- <String>['-v'],
- errorMessage,
- 1,
- );
- when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment')))
- .thenThrow(exception);
- await expectLater(() async {
- await checkGradleDependencies();
- }, throwsToolExit(message: errorMessage));
- }, overrides: <Type, Generator>{
- Cache: () => Cache(rootOverride: tempDir),
- FileSystem: () => fs,
- ProcessManager: () => mockProcessManager,
- });
-
- testUsingContext('throw toolExit if gradle fails downloading with proxy error', () async {
- final List<String> cmd = <String>[
- fs.path.join(fs.currentDirectory.path, 'android', gradleBinary),
- '-v',
- ];
- 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)''';
- final ProcessException exception = ProcessException(
- gradleBinary,
- <String>['-v'],
- errorMessage,
- 1,
- );
- when(mockProcessManager.run(cmd, environment: anyNamed('environment'), workingDirectory: null))
- .thenThrow(exception);
- await expectLater(() async {
- await checkGradleDependencies();
- }, throwsToolExit(message: errorMessage));
- }, overrides: <Type, Generator>{
- Cache: () => Cache(rootOverride: tempDir),
- FileSystem: () => fs,
- ProcessManager: () => mockProcessManager,
- });
-
- testUsingContext('throws toolExit if gradle is missing execute permissions. ', () async {
- final List<String> cmd = <String>[
- fs.path.join(fs.currentDirectory.path, 'android', gradleBinary),
- '-v',
- ];
- final ProcessException exception = ProcessException(
- gradleBinary,
- <String>['-v'],
- 'Permission denied\nCommand: /home/android/gradlew -v',
- 1,
- );
- when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment')))
- .thenThrow(exception);
- await expectLater(() async {
- await checkGradleDependencies();
- }, throwsToolExit(message: 'does not have permission to execute by your user'));
- }, overrides: <Type, Generator>{
- Cache: () => Cache(rootOverride: tempDir),
- FileSystem: () => fs,
- ProcessManager: () => mockProcessManager,
- });
-
- testUsingContext('throws toolExit if gradle times out waiting for exclusive access to zip', () async {
- final List<String> cmd = <String>[
- fs.path.join(fs.currentDirectory.path, 'android', gradleBinary),
- '-v',
- ];
- 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)''';
- final ProcessException exception = ProcessException(
- gradleBinary,
- <String>['-v'],
- errorMessage,
- 1,
- );
- when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment')))
- .thenThrow(exception);
- await expectLater(() async {
- await checkGradleDependencies();
- }, throwsToolExit(message: errorMessage));
- }, overrides: <Type, Generator>{
- Cache: () => Cache(rootOverride: tempDir),
- FileSystem: () => fs,
- ProcessManager: () => mockProcessManager,
- });
-
- testUsingContext('throws toolExit if gradle fails to unzip file', () async {
- final List<String> cmd = <String>[
- fs.path.join(fs.currentDirectory.path, 'android', gradleBinary),
- '-v',
- ];
- const String errorMessage = '''
-Exception in thread "main" java.util.zip.ZipException: error in opening zip 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)''';
- final ProcessException exception = ProcessException(
- gradleBinary,
- <String>['-v'],
- errorMessage,
- 1,
- );
- when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment')))
- .thenThrow(exception);
- await expectLater(() async {
- await checkGradleDependencies();
- }, throwsToolExit(message: errorMessage));
- }, overrides: <Type, Generator>{
- Cache: () => Cache(rootOverride: tempDir),
- FileSystem: () => fs,
- ProcessManager: () => mockProcessManager,
- });
-
- testUsingContext('throws toolExit if remote host closes connection', () async {
- final List<String> cmd = <String>[
- fs.path.join(fs.currentDirectory.path, 'android', gradleBinary),
- '-v',
- ];
- 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)''';
- final ProcessException exception = ProcessException(
- gradleBinary,
- <String>['-v'],
- errorMessage,
- 1,
- );
- when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment')))
- .thenThrow(exception);
- await expectLater(() async {
- await checkGradleDependencies();
- }, throwsToolExit(message: errorMessage));
- }, overrides: <Type, Generator>{
- Cache: () => Cache(rootOverride: tempDir),
- FileSystem: () => fs,
- ProcessManager: () => mockProcessManager,
- });
-
- testUsingContext('throws toolExit if file opening fails', () async {
- final List<String> cmd = <String>[
- fs.path.join(fs.currentDirectory.path, 'android', gradleBinary),
- '-v',
- ];
- 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)''';
- final ProcessException exception = ProcessException(
- gradleBinary,
- <String>['-v'],
- errorMessage,
- 1,
- );
- when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment')))
- .thenThrow(exception);
- await expectLater(() async {
- await checkGradleDependencies();
- }, throwsToolExit(message: errorMessage));
- }, overrides: <Type, Generator>{
- Cache: () => Cache(rootOverride: tempDir),
- FileSystem: () => fs,
- ProcessManager: () => mockProcessManager,
- });
-
- testUsingContext('throws toolExit if the connection is reset', () async {
- final List<String> cmd = <String>[
- fs.path.join(fs.currentDirectory.path, 'android', gradleBinary),
- '-v',
- ];
- 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)''';
- final ProcessException exception = ProcessException(
- gradleBinary,
- <String>['-v'],
- errorMessage,
- 1,
- );
- when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment')))
- .thenThrow(exception);
- await expectLater(() async {
- await checkGradleDependencies();
- }, throwsToolExit(message: errorMessage));
- }, overrides: <Type, Generator>{
- Cache: () => Cache(rootOverride: tempDir),
- FileSystem: () => fs,
- ProcessManager: () => mockProcessManager,
- });
-
- testUsingContext('throws toolExit if gradle exits abnormally', () async {
- final List<String> cmd = <String>[
- fs.path.join(fs.currentDirectory.path, 'android', gradleBinary),
- '-v',
- ];
- const String errorMessage = '''
-ProcessException: Process exited abnormally:
-Exception in thread "main" java.lang.NullPointerException
- at org.gradle.wrapper.BootstrapMainStarter.findLauncherJar(BootstrapMainStarter.java:34)
- at org.gradle.wrapper.BootstrapMainStarter.start(BootstrapMainStarter.java:25)
- at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:129)
- at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
- final ProcessException exception = ProcessException(
- gradleBinary,
- <String>['-v'],
- errorMessage,
- 1,
- );
- when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment')))
- .thenThrow(exception);
- await expectLater(() async {
- await checkGradleDependencies();
- }, throwsToolExit(message: errorMessage));
- }, overrides: <Type, Generator>{
- Cache: () => Cache(rootOverride: tempDir),
- FileSystem: () => fs,
- ProcessManager: () => mockProcessManager,
- });
- });
-
- 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(),
- });
- });
-
group('migrateToR8', () {
MemoryFileSystem memoryFileSystem;
@@ -1532,7 +960,8 @@
plugin1=${plugin1.path}
plugin2=${plugin2.path}
''');
- final Directory buildDirectory = androidDirectory.childDirectory('build');
+ final Directory buildDirectory = androidDirectory
+ .childDirectory('build');
buildDirectory
.childDirectory('outputs')
.childDirectory('repo')
@@ -1541,7 +970,7 @@
await buildPluginsAsAar(
FlutterProject.fromPath(androidDirectory.path),
const AndroidBuildInfo(BuildInfo.release),
- buildDirectory: buildDirectory.path,
+ buildDirectory: buildDirectory,
);
final String flutterRoot = fs.path.absolute(Cache.flutterRoot);
@@ -1584,6 +1013,8 @@
});
group('gradle build', () {
+ final Usage mockUsage = MockUsage();
+
MockAndroidSdk mockAndroidSdk;
MockAndroidStudio mockAndroidStudio;
MockLocalEngineArtifacts mockArtifacts;
@@ -1623,6 +1054,399 @@
.writeAsStringSync('irrelevant');
});
+ testUsingContext('recognizes common errors - tool exit', () async {
+ final Process process = createMockProcess(
+ exitCode: 1,
+ stdout: 'irrelevant\nSome gradle message\nirrelevant',
+ );
+ when(mockProcessManager.start(any,
+ workingDirectory: anyNamed('workingDirectory'),
+ environment: anyNamed('environment')))
+ .thenAnswer((_) => Future<Process>.value(process));
+
+ fs.directory('android')
+ .childFile('build.gradle')
+ .createSync(recursive: true);
+
+ fs.directory('android')
+ .childFile('gradle.properties')
+ .createSync(recursive: true);
+
+ fs.directory('android')
+ .childDirectory('app')
+ .childFile('build.gradle')
+ ..createSync(recursive: true)
+ ..writeAsStringSync('apply from: irrelevant/flutter.gradle');
+
+ bool handlerCalled = false;
+ await expectLater(() async {
+ await buildGradleApp(
+ project: FlutterProject.current(),
+ androidBuildInfo: const AndroidBuildInfo(
+ BuildInfo(
+ BuildMode.release,
+ null,
+ ),
+ ),
+ target: 'lib/main.dart',
+ isBuildingBundle: false,
+ localGradleErrors: <GradleHandledError>[
+ GradleHandledError(
+ test: (String line) {
+ return line.contains('Some gradle message');
+ },
+ handler: ({
+ String line,
+ FlutterProject project,
+ bool usesAndroidX,
+ bool shouldBuildPluginAsAar,
+ }) async {
+ handlerCalled = true;
+ return GradleBuildStatus.exit;
+ },
+ eventLabel: 'random-event-label',
+ ),
+ ],
+ );
+ },
+ throwsToolExit(
+ message: 'Gradle task assembleRelease failed with exit code 1'
+ ));
+
+ expect(handlerCalled, isTrue);
+
+ verify(mockUsage.sendEvent(
+ any,
+ any,
+ label: 'gradle--random-event-label-failure',
+ parameters: anyNamed('parameters'),
+ )).called(1);
+
+ }, overrides: <Type, Generator>{
+ AndroidSdk: () => mockAndroidSdk,
+ Cache: () => cache,
+ Platform: () => android,
+ FileSystem: () => fs,
+ ProcessManager: () => mockProcessManager,
+ Usage: () => mockUsage,
+ });
+
+ testUsingContext('recognizes common errors - retry build', () async {
+ when(mockProcessManager.start(any,
+ workingDirectory: anyNamed('workingDirectory'),
+ environment: anyNamed('environment')))
+ .thenAnswer((_) {
+ final Process process = createMockProcess(
+ exitCode: 1,
+ stdout: 'irrelevant\nSome gradle message\nirrelevant',
+ );
+ return Future<Process>.value(process);
+ });
+
+ fs.directory('android')
+ .childFile('build.gradle')
+ .createSync(recursive: true);
+
+ fs.directory('android')
+ .childFile('gradle.properties')
+ .createSync(recursive: true);
+
+ fs.directory('android')
+ .childDirectory('app')
+ .childFile('build.gradle')
+ ..createSync(recursive: true)
+ ..writeAsStringSync('apply from: irrelevant/flutter.gradle');
+
+ int testFnCalled = 0;
+ await expectLater(() async {
+ await buildGradleApp(
+ project: FlutterProject.current(),
+ androidBuildInfo: const AndroidBuildInfo(
+ BuildInfo(
+ BuildMode.release,
+ null,
+ ),
+ ),
+ target: 'lib/main.dart',
+ isBuildingBundle: false,
+ localGradleErrors: <GradleHandledError>[
+ GradleHandledError(
+ test: (String line) {
+ if (line.contains('Some gradle message')) {
+ testFnCalled++;
+ return true;
+ }
+ return false;
+ },
+ handler: ({
+ String line,
+ FlutterProject project,
+ bool usesAndroidX,
+ bool shouldBuildPluginAsAar,
+ }) async {
+ return GradleBuildStatus.retry;
+ },
+ eventLabel: 'random-event-label',
+ ),
+ ],
+ );
+ }, throwsToolExit(
+ message: 'Gradle task assembleRelease failed with exit code 1'
+ ));
+
+ expect(testFnCalled, equals(2));
+
+ verify(mockUsage.sendEvent(
+ any,
+ any,
+ label: 'gradle--random-event-label-failure',
+ parameters: anyNamed('parameters'),
+ )).called(1);
+
+ }, overrides: <Type, Generator>{
+ AndroidSdk: () => mockAndroidSdk,
+ Cache: () => cache,
+ Platform: () => android,
+ FileSystem: () => fs,
+ ProcessManager: () => mockProcessManager,
+ Usage: () => mockUsage,
+ });
+
+ testUsingContext('logs success event after a sucessful retry', () async {
+ int testFnCalled = 0;
+ when(mockProcessManager.start(any,
+ workingDirectory: anyNamed('workingDirectory'),
+ environment: anyNamed('environment')))
+ .thenAnswer((_) {
+ Process process;
+ if (testFnCalled == 0) {
+ process = createMockProcess(
+ exitCode: 1,
+ stdout: 'irrelevant\nSome gradle message\nirrelevant',
+ );
+ } else {
+ process = createMockProcess(
+ exitCode: 0,
+ stdout: 'irrelevant',
+ );
+ }
+ testFnCalled++;
+ return Future<Process>.value(process);
+ });
+
+ fs.directory('android')
+ .childFile('build.gradle')
+ .createSync(recursive: true);
+
+ fs.directory('android')
+ .childFile('gradle.properties')
+ .createSync(recursive: true);
+
+ fs.directory('android')
+ .childDirectory('app')
+ .childFile('build.gradle')
+ ..createSync(recursive: true)
+ ..writeAsStringSync('apply from: irrelevant/flutter.gradle');
+
+ fs.directory('build')
+ .childDirectory('app')
+ .childDirectory('outputs')
+ .childDirectory('apk')
+ .childDirectory('release')
+ .childFile('app-release.apk')
+ ..createSync(recursive: true);
+
+ await buildGradleApp(
+ project: FlutterProject.current(),
+ androidBuildInfo: const AndroidBuildInfo(
+ BuildInfo(
+ BuildMode.release,
+ null,
+ ),
+ ),
+ target: 'lib/main.dart',
+ isBuildingBundle: false,
+ localGradleErrors: <GradleHandledError>[
+ GradleHandledError(
+ test: (String line) {
+ return line.contains('Some gradle message');
+ },
+ handler: ({
+ String line,
+ FlutterProject project,
+ bool usesAndroidX,
+ bool shouldBuildPluginAsAar,
+ }) async {
+ return GradleBuildStatus.retry;
+ },
+ eventLabel: 'random-event-label',
+ ),
+ ],
+ );
+
+ verify(mockUsage.sendEvent(
+ any,
+ any,
+ label: 'gradle--random-event-label-success',
+ parameters: anyNamed('parameters'),
+ )).called(1);
+
+ }, overrides: <Type, Generator>{
+ AndroidSdk: () => mockAndroidSdk,
+ Cache: () => cache,
+ FileSystem: () => fs,
+ Platform: () => android,
+ ProcessManager: () => mockProcessManager,
+ Usage: () => mockUsage,
+ });
+
+ testUsingContext('recognizes common errors - retry build with AAR plugins', () async {
+ when(mockProcessManager.start(any,
+ workingDirectory: anyNamed('workingDirectory'),
+ environment: anyNamed('environment')))
+ .thenAnswer((_) {
+ final Process process = createMockProcess(
+ exitCode: 1,
+ stdout: 'irrelevant\nSome gradle message\nirrelevant',
+ );
+ return Future<Process>.value(process);
+ });
+
+ fs.directory('android')
+ .childFile('build.gradle')
+ .createSync(recursive: true);
+
+ fs.directory('android')
+ .childFile('gradle.properties')
+ .createSync(recursive: true);
+
+ fs.directory('android')
+ .childDirectory('app')
+ .childFile('build.gradle')
+ ..createSync(recursive: true)
+ ..writeAsStringSync('apply from: irrelevant/flutter.gradle');
+
+ int testFnCalled = 0;
+ bool builtPluginAsAar = false;
+ await expectLater(() async {
+ await buildGradleApp(
+ project: FlutterProject.current(),
+ androidBuildInfo: const AndroidBuildInfo(
+ BuildInfo(
+ BuildMode.release,
+ null,
+ ),
+ ),
+ target: 'lib/main.dart',
+ isBuildingBundle: false,
+ localGradleErrors: <GradleHandledError>[
+ GradleHandledError(
+ test: (String line) {
+ if (line.contains('Some gradle message')) {
+ testFnCalled++;
+ return true;
+ }
+ return false;
+ },
+ handler: ({
+ String line,
+ FlutterProject project,
+ bool usesAndroidX,
+ bool shouldBuildPluginAsAar,
+ }) async {
+ if (testFnCalled == 2) {
+ builtPluginAsAar = shouldBuildPluginAsAar;
+ }
+ return GradleBuildStatus.retryWithAarPlugins;
+ },
+ eventLabel: 'random-event-label',
+ ),
+ ],
+ );
+ }, throwsToolExit(
+ message: 'Gradle task assembleRelease failed with exit code 1'
+ ));
+
+ expect(testFnCalled, equals(2));
+ expect(builtPluginAsAar, isTrue);
+
+ verify(mockUsage.sendEvent(
+ any,
+ any,
+ label: 'gradle--random-event-label-failure',
+ parameters: anyNamed('parameters'),
+ )).called(1);
+
+ }, overrides: <Type, Generator>{
+ AndroidSdk: () => mockAndroidSdk,
+ Cache: () => cache,
+ Platform: () => android,
+ FileSystem: () => fs,
+ ProcessManager: () => mockProcessManager,
+ Usage: () => mockUsage,
+ });
+
+ testUsingContext('indicates that an APK has been built successfully', () async {
+ when(mockProcessManager.start(any,
+ workingDirectory: anyNamed('workingDirectory'),
+ environment: anyNamed('environment')))
+ .thenAnswer((_) {
+ return Future<Process>.value(
+ createMockProcess(
+ exitCode: 0,
+ stdout: '',
+ ));
+ });
+
+ fs.directory('android')
+ .childFile('build.gradle')
+ .createSync(recursive: true);
+
+ fs.directory('android')
+ .childFile('gradle.properties')
+ .createSync(recursive: true);
+
+ fs.directory('android')
+ .childDirectory('app')
+ .childFile('build.gradle')
+ ..createSync(recursive: true)
+ ..writeAsStringSync('apply from: irrelevant/flutter.gradle');
+
+ fs.directory('build')
+ .childDirectory('app')
+ .childDirectory('outputs')
+ .childDirectory('apk')
+ .childDirectory('release')
+ .childFile('app-release.apk')
+ ..createSync(recursive: true);
+
+ await buildGradleApp(
+ project: FlutterProject.current(),
+ androidBuildInfo: const AndroidBuildInfo(
+ BuildInfo(
+ BuildMode.release,
+ null,
+ ),
+ ),
+ target: 'lib/main.dart',
+ isBuildingBundle: false,
+ localGradleErrors: <GradleHandledError>[],
+ );
+
+ final BufferLogger logger = context.get<Logger>();
+ expect(
+ logger.statusText,
+ contains('Built build/app/outputs/apk/release/app-release.apk (0.0MB)'),
+ );
+
+ }, overrides: <Type, Generator>{
+ AndroidSdk: () => mockAndroidSdk,
+ Cache: () => cache,
+ FileSystem: () => fs,
+ Platform: () => android,
+ ProcessManager: () => mockProcessManager,
+ });
+
testUsingContext('build aar uses selected local engine', () async {
when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
platform: TargetPlatform.android_arm, mode: anyNamed('mode'))).thenReturn('engine');
@@ -1650,6 +1474,9 @@
fs.file('path/to/project/.android/gradle.properties')
.writeAsStringSync('irrelevant');
+ fs.file('path/to/project/.android/build.gradle')
+ .createSync(recursive: true);
+
when(mockProcessManager.run(
<String> ['/path/to/project/.android/gradlew', '-v'],
workingDirectory: anyNamed('workingDirectory'),
@@ -1675,7 +1502,7 @@
await buildGradleAar(
androidBuildInfo: const AndroidBuildInfo(BuildInfo(BuildMode.release, null)),
project: FlutterProject.current(),
- outputDir: 'build/',
+ outputDir: fs.directory('build/'),
target: '',
);
@@ -1700,14 +1527,24 @@
}
/// Generates a fake app bundle at the location [directoryName]/[fileName].
-GradleProject generateFakeAppBundle(String directoryName, String fileName) {
- final GradleProject gradleProject = MockGradleProject();
- when(gradleProject.bundleDirectory).thenReturn(fs.currentDirectory);
+FlutterProject generateFakeAppBundle(String directoryName, String fileName) {
+ final FlutterProject project = MockFlutterProject();
+ final AndroidProject androidProject = MockAndroidProject();
- final Directory aabDirectory = gradleProject.bundleDirectory.childDirectory(directoryName);
- fs.directory(aabDirectory).createSync(recursive: true);
- fs.file(fs.path.join(aabDirectory.path, fileName)).writeAsStringSync('irrelevant');
- return gradleProject;
+ when(project.isModule).thenReturn(false);
+ when(project.android).thenReturn(androidProject);
+ when(androidProject.buildDirectory).thenReturn(fs.directory('irrelevant'));
+
+ final Directory bundleDirectory = getBundleDirectory(project);
+ bundleDirectory
+ .childDirectory(directoryName)
+ ..createSync(recursive: true);
+
+ bundleDirectory
+ .childDirectory(directoryName)
+ .childFile(fileName)
+ .createSync();
+ return project;
}
Platform fakePlatform(String name) {
@@ -1716,17 +1553,19 @@
class FakeGradleUtils extends GradleUtils {
@override
- Future<String> getExecutable(FlutterProject project) async {
+ String getExecutable(FlutterProject project) {
return 'gradlew';
}
}
class MockAndroidSdk extends Mock implements AndroidSdk {}
+class MockAndroidProject extends Mock implements AndroidProject {}
class MockAndroidStudio extends Mock implements AndroidStudio {}
class MockDirectory extends Mock implements Directory {}
class MockFile extends Mock implements File {}
-class MockGradleProject extends Mock implements GradleProject {}
+class MockFlutterProject extends Mock implements FlutterProject {}
class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {}
class MockProcessManager extends Mock implements ProcessManager {}
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
class MockitoAndroidSdk extends Mock implements AndroidSdk {}
+class MockUsage extends Mock implements Usage {}
diff --git a/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart
new file mode 100644
index 0000000..d8b945e
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart
@@ -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(),
+ });
+ });
+}
\ No newline at end of file
diff --git a/packages/flutter_tools/test/general.shard/cache_test.dart b/packages/flutter_tools/test/general.shard/cache_test.dart
index be5e4a9..dd9e2ac 100644
--- a/packages/flutter_tools/test/general.shard/cache_test.dart
+++ b/packages/flutter_tools/test/general.shard/cache_test.dart
@@ -11,7 +11,7 @@
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 @@
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, '', ''));
});
diff --git a/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart b/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart
index a93b50e..38d505d 100644
--- a/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart
+++ b/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart
@@ -8,7 +8,6 @@
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: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 @@
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 @@
''');
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 @@
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
- GradleUtils: () => GradleUtils(),
ProcessManager: () => mockProcessManager,
FileSystem: () => memoryFileSystem,
});
diff --git a/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart b/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart
index fd063c1..4970b0b 100644
--- a/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart
+++ b/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart
@@ -8,7 +8,6 @@
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: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 @@
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 @@
''');
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 @@
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 @@
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
- GradleUtils: () => GradleUtils(),
FileSystem: () => memoryFileSystem,
ProcessManager: () => mockProcessManager,
});
@@ -221,7 +243,6 @@
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
- GradleUtils: () => GradleUtils(),
ProcessManager: () => mockProcessManager,
});
@@ -252,7 +273,6 @@
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
- GradleUtils: () => GradleUtils(),
ProcessManager: () => mockProcessManager,
});
@@ -300,13 +320,12 @@
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 @@
}, 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 @@
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
- GradleUtils: () => GradleUtils(),
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => mockProcessManager,
Usage: () => mockUsage,
@@ -414,7 +432,6 @@
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
- GradleUtils: () => GradleUtils(),
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => mockProcessManager,
Usage: () => mockUsage,
diff --git a/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart b/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart
index 7663156..8f79b4a 100644
--- a/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart
+++ b/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart
@@ -8,7 +8,6 @@
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: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 @@
});
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 @@
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 @@
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 @@
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
- GradleUtils: () => GradleUtils(),
ProcessManager: () => mockProcessManager,
FileSystem: () => memoryFileSystem,
});
@@ -210,7 +227,6 @@
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
- GradleUtils: () => GradleUtils(),
ProcessManager: () => mockProcessManager,
});
@@ -243,7 +259,6 @@
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
- GradleUtils: () => GradleUtils(),
ProcessManager: () => mockProcessManager,
});
@@ -291,13 +306,12 @@
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 @@
}, 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 @@
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
- GradleUtils: () => GradleUtils(),
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => mockProcessManager,
Usage: () => mockUsage,
@@ -388,7 +401,7 @@
}, 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 @@
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
- GradleUtils: () => GradleUtils(),
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => mockProcessManager,
Usage: () => mockUsage,
diff --git a/packages/flutter_tools/test/src/android_common.dart b/packages/flutter_tools/test/src/android_common.dart
new file mode 100644
index 0000000..dda0afb
--- /dev/null
+++ b/packages/flutter_tools/test/src/android_common.dart
@@ -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 {}
+}