Unverified Commit 175b3724 authored by Emmanuel Garcia's avatar Emmanuel Garcia Committed by GitHub

Refactor gradle.dart (#43479)

parent 0028887a
......@@ -6,19 +6,23 @@ import 'dart:async';
import 'package:meta/meta.dart';
import '../base/common.dart';
import '../android/gradle_errors.dart';
import '../base/context.dart';
import '../base/file_system.dart';
import '../build_info.dart';
import '../project.dart';
import 'android_sdk.dart';
import 'gradle.dart';
/// The builder in the current context.
AndroidBuilder get androidBuilder => context.get<AndroidBuilder>() ?? _AndroidBuilderImpl();
AndroidBuilder get androidBuilder {
return context.get<AndroidBuilder>() ?? const _AndroidBuilderImpl();
}
/// Provides the methods to build Android artifacts.
// TODO(egarciad): https://github.com/flutter/flutter/issues/43863
abstract class AndroidBuilder {
const AndroidBuilder();
/// Builds an AAR artifact.
Future<void> buildAar({
@required FlutterProject project,
......@@ -44,7 +48,7 @@ abstract class AndroidBuilder {
/// Default implementation of [AarBuilder].
class _AndroidBuilderImpl extends AndroidBuilder {
_AndroidBuilderImpl();
const _AndroidBuilderImpl();
/// Builds the AAR and POM files for the current Flutter module or plugin.
@override
......@@ -54,27 +58,18 @@ class _AndroidBuilderImpl extends AndroidBuilder {
@required String target,
@required String outputDir,
}) async {
if (!project.android.isUsingGradle) {
throwToolExit(
'The build process for Android has changed, and the current project configuration '
'is no longer valid. Please consult\n\n'
' https://github.com/flutter/flutter/wiki/Upgrading-Flutter-projects-to-build-with-gradle\n\n'
'for details on how to upgrade the project.'
);
}
if (!project.manifest.isModule && !project.manifest.isPlugin) {
throwToolExit('AARs can only be built for plugin or module projects.');
}
// Validate that we can find an Android SDK.
if (androidSdk == null) {
throwToolExit('No Android SDK found. Try setting the `ANDROID_SDK_ROOT` environment variable.');
}
try {
Directory outputDirectory =
fs.directory(outputDir ?? project.android.buildDirectory);
if (project.isModule) {
// Module projects artifacts are located in `build/host`.
outputDirectory = outputDirectory.childDirectory('host');
}
await buildGradleAar(
project: project,
androidBuildInfo: androidBuildInfo,
target: target,
outputDir: outputDir,
outputDir: outputDirectory,
);
} finally {
androidSdk.reinitialize();
......@@ -88,24 +83,13 @@ class _AndroidBuilderImpl extends AndroidBuilder {
@required AndroidBuildInfo androidBuildInfo,
@required String target,
}) async {
if (!project.android.isUsingGradle) {
throwToolExit(
'The build process for Android has changed, and the current project configuration '
'is no longer valid. Please consult\n\n'
' https://github.com/flutter/flutter/wiki/Upgrading-Flutter-projects-to-build-with-gradle\n\n'
'for details on how to upgrade the project.'
);
}
// Validate that we can find an android sdk.
if (androidSdk == null) {
throwToolExit('No Android SDK found. Try setting the ANDROID_SDK_ROOT environment variable.');
}
try {
await buildGradleProject(
await buildGradleApp(
project: project,
androidBuildInfo: androidBuildInfo,
target: target,
isBuildingBundle: false,
localGradleErrors: gradleErrors,
);
} finally {
androidSdk.reinitialize();
......@@ -119,54 +103,16 @@ class _AndroidBuilderImpl extends AndroidBuilder {
@required AndroidBuildInfo androidBuildInfo,
@required String target,
}) async {
if (!project.android.isUsingGradle) {
throwToolExit(
'The build process for Android has changed, and the current project configuration '
'is no longer valid. Please consult\n\n'
'https://github.com/flutter/flutter/wiki/Upgrading-Flutter-projects-to-build-with-gradle\n\n'
'for details on how to upgrade the project.'
);
}
// Validate that we can find an android sdk.
if (androidSdk == null) {
throwToolExit('No Android SDK found. Try setting the ANDROID_HOME environment variable.');
}
try {
await buildGradleProject(
await buildGradleApp(
project: project,
androidBuildInfo: androidBuildInfo,
target: target,
isBuildingBundle: true,
localGradleErrors: gradleErrors,
);
} finally {
androidSdk.reinitialize();
}
}
}
/// A fake implementation of [AndroidBuilder].
@visibleForTesting
class FakeAndroidBuilder implements AndroidBuilder {
@override
Future<void> buildAar({
@required FlutterProject project,
@required AndroidBuildInfo androidBuildInfo,
@required String target,
@required String outputDir,
}) async {}
@override
Future<void> buildApk({
@required FlutterProject project,
@required AndroidBuildInfo androidBuildInfo,
@required String target,
}) async {}
@override
Future<void> buildAab({
@required FlutterProject project,
@required AndroidBuildInfo androidBuildInfo,
@required String target,
}) async {}
}
......@@ -236,7 +236,7 @@ class AndroidStudio implements Comparable<AndroidStudio> {
// Read all $HOME/.AndroidStudio*/system/.home files. There may be several
// pointing to the same installation, so we grab only the latest one.
if (fs.directory(homeDirPath).existsSync()) {
if (homeDirPath != null && fs.directory(homeDirPath).existsSync()) {
for (FileSystemEntity entity in fs.directory(homeDirPath).listSync(followLinks: false)) {
if (entity is Directory && entity.basename.startsWith('.AndroidStudio')) {
final AndroidStudio studio = AndroidStudio.fromHomeDot(entity);
......
......@@ -10,16 +10,12 @@ import 'package:meta/meta.dart';
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 '../globals.dart';
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];
}
/// 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');
}
/// 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>{};
/// 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');
}
/// 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 repo is generated.
/// Only applicable to AARs.
@visibleForTesting
Directory getRepoDirectory(Directory buildDirectory) {
return buildDirectory
.childDirectory('outputs')
.childDirectory('repo');
}
final RegExp _assembleTaskPattern = RegExp(r'assemble(\S+)');
/// 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)}';
}
enum FlutterPluginVersion {
none,
v1,
v2,
managed,
/// Returns the task to build an APK.
@visibleForTesting
String getAssembleTaskFor(BuildInfo buildInfo) {
return _taskFor('assemble', buildInfo);
}
// 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 .*)');
/// Returns the task to build an AAB.
@visibleForTesting
String getBundleTaskFor(BuildInfo buildInfo) {
return _taskFor('bundle', buildInfo);
}
// 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 task to build an AAR.
@visibleForTesting
String getAarTaskFor(BuildInfo buildInfo) {
return _taskFor('assembleAar', buildInfo);
}
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 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'];
}
FlutterPluginVersion getFlutterPluginVersion(AndroidProject project) {
/// 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 @@ void createSettingsAarGradle(Directory androidDirectory) {
}
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,
/// Builds an app.
///
/// * [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 {
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;
if (androidSdk == null) {
exitWithNoSdkMessage();
}
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');
if (!project.android.isUsingGradle) {
_exitWithProjectNotUsingGradleMessage();
}
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();
if (!_isSupportedVersion(project.android)) {
_exitWithUnsupportedProjectMessage();
}
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.'
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);
/// 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,
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'),
);
}
}
/// 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);
}
final BuildInfo buildInfo = androidBuildInfo.buildInfo;
final String assembleTask = isBuildingBundle
? getBundleTaskFor(buildInfo)
: getAssembleTaskFor(buildInfo);
const String defaultGradleVersion = '5.6.2';
final Status status = logger.startProgress(
'Running Gradle task \'$assembleTask\'...',
timeout: timeoutConfiguration.slowOperation,
multilineOutput: true,
);
/// 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';
final List<String> command = <String>[
gradleUtils.getExecutable(project),
];
if (logger.isVerbose) {
command.add('-Pverbose=true');
} else {
command.add('-q');
}
if (_isWithinVersionRange(androidPluginVersion, min: '2.0.0', max: '2.1.2')) {
return '2.13';
if (artifacts is LocalEngineArtifacts) {
final LocalEngineArtifacts localEngineArtifacts = artifacts;
printTrace('Using local engine: ${localEngineArtifacts.engineOutPath}');
command.add('-PlocalEngineOut=${localEngineArtifacts.engineOutPath}');
}
if (_isWithinVersionRange(androidPluginVersion, min: '2.1.3', max: '2.2.3')) {
return '2.14.1';
if (target != null) {
command.add('-Ptarget=$target');
}
if (_isWithinVersionRange(androidPluginVersion, min: '2.3.0', max: '2.9.9')) {
return '3.3';
assert(buildInfo.trackWidgetCreation != null);
command.add('-Ptrack-widget-creation=${buildInfo.trackWidgetCreation}');
if (buildInfo.extraFrontEndOptions != null) {
command.add('-Pextra-front-end-options=${buildInfo.extraFrontEndOptions}');
}
if (_isWithinVersionRange(androidPluginVersion, min: '3.0.0', max: '3.0.9')) {
return '4.1';
if (buildInfo.extraGenSnapshotOptions != null) {
command.add('-Pextra-gen-snapshot-options=${buildInfo.extraGenSnapshotOptions}');
}
if (_isWithinVersionRange(androidPluginVersion, min: '3.1.0', max: '3.1.9')) {
return '4.4';
if (buildInfo.fileSystemRoots != null && buildInfo.fileSystemRoots.isNotEmpty) {
command.add('-Pfilesystem-roots=${buildInfo.fileSystemRoots.join('|')}');
}
if (_isWithinVersionRange(androidPluginVersion, min: '3.2.0', max: '3.2.1')) {
return '4.6';
if (buildInfo.fileSystemScheme != null) {
command.add('-Pfilesystem-scheme=${buildInfo.fileSystemScheme}');
}
if (_isWithinVersionRange(androidPluginVersion, min: '3.3.0', max: '3.3.2')) {
return '4.10.2';
if (androidBuildInfo.splitPerAbi) {
command.add('-Psplit-per-abi=true');
}
if (_isWithinVersionRange(androidPluginVersion, min: '3.4.0', max: '3.5.0')) {
return '5.6.2';
if (androidBuildInfo.shrink) {
command.add('-Pshrink=true');
}
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.
///
/// 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;
if (androidBuildInfo.targetArchs.isNotEmpty) {
final String targetPlatforms = androidBuildInfo
.targetArchs
.map(getPlatformNameForAndroidArch).join(',');
command.add('-Ptarget-platform=$targetPlatforms');
}
final String buildFileContent = buildFile.readAsStringSync();
final Iterable<Match> pluginMatches = _androidPluginRegExp.allMatches(buildFileContent);
if (pluginMatches.isEmpty) {
return defaultGradleVersion;
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');
}
final String androidPluginVersion = pluginMatches.first.group(1);
return getGradleVersionFor(androidPluginVersion);
}
command.add(assembleTask);
/// 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 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();
}
final File localProperties = project.android.localPropertiesFile;
bool changed = false;
flutterUsage.sendTiming('build', 'gradle', sw.elapsed);
SettingsFile settings;
if (localProperties.existsSync()) {
settings = SettingsFile.parseFromFile(localProperties);
} else {
settings = SettingsFile();
changed = true;
}
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,
);
void changeIfNecessary(String key, String value) {
if (settings.values[key] != value) {
if (value == null) {
settings.values.remove(key);
} else {
settings.values[key] = value;
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.
}
}
changed = true;
BuildEvent('gradle--${detectedGradleError.eventLabel}-failure').send();
throwToolExit(
'Gradle task $assembleTask failed with exit code $exitCode',
exitCode: exitCode,
);
}
}
final FlutterManifest manifest = project.manifest;
if (androidSdk != null) {
changeIfNecessary('sdk.dir', escapePath(androidSdk.directory));
}
if (isBuildingBundle) {
final File bundleFile = findBundleFile(project, buildInfo);
if (bundleFile == null) {
throwToolExit('Gradle build failed to produce an Android bundle package.');
}
changeIfNecessary('flutter.sdk', escapePath(Cache.flutterRoot));
final String appSize = (buildInfo.mode == BuildMode.debug)
? '' // Don't display the size when building a debug variant.
: ' (${getSizeAsMB(bundleFile.lengthSync())})';
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());
printStatus(
'$successMark Built ${fs.path.relative(bundleFile.path)}$appSize.',
color: TerminalColor.green,
);
return;
}
if (changed) {
settings.writeContents(localProperties);
// Gradle produced an APK.
final Iterable<File> apkFiles = findApkFiles(project, androidBuildInfo);
if (apkFiles.isEmpty) {
throwToolExit('Gradle build failed to produce an Android package.');
}
}
/// 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.');
}
}
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');
Future<void> buildGradleProject({
@required FlutterProject project,
@required AndroidBuildInfo androidBuildInfo,
@required String target,
@required bool isBuildingBundle,
}) 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.
// The default Gradle script reads the version name and number
// from the local.properties file.
updateLocalProperties(project: project, buildInfo: androidBuildInfo.buildInfo);
final File apkShaFile = apkDirectory.childFile('app.apk.sha1');
apkShaFile.writeAsStringSync(_calculateSha(apkFiles.first));
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);
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 @@ Future<void> buildGradleAar({
command,
workingDirectory: project.android.hostAppGradleRoot.path,
allowReentrantFlutter: true,
environment: gradleEnv,
environment: gradleEnvironment,
);
} finally {
status.stop();
......@@ -686,41 +533,24 @@ Future<void> buildGradleAar({
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,
);
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,
printStatus(
'$successMark Built ${fs.path.relative(repoDirectory.path)}.',
color: TerminalColor.green,
);
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 @@ String _calculateSha(File file) {
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 @@ bool isAppUsingAndroidX(Directory androidDirectory) {
Future<void> buildPluginsAsAar(
FlutterProject flutterProject,
AndroidBuildInfo androidBuildInfo, {
String buildDirectory,
Directory buildDirectory,
}) async {
final File flutterPluginFile = flutterProject.flutterPluginsFile;
if (!flutterPluginFile.existsSync()) {
......@@ -1031,37 +641,41 @@ Future<void> buildPluginsAsAar(
// 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 @@ Iterable<File> findApkFiles(GradleProject project, AndroidBuildInfo androidBuild
}
@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 @@ File findBundleFile(GradleProject project, BuildInfo buildInfo) {
// 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 @@ File findBundleFile(GradleProject project, BuildInfo buildInfo) {
}
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)}';
}
}
// 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',
);
// 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.'
);
}
......@@ -23,6 +23,17 @@ enum TerminalColor {
AnsiTerminal get terminal {
return context?.get<AnsiTerminal>() ?? _defaultAnsiTerminal;
}
/// Warning mark to use in stdout or stderr.
String get warningMark {
return terminal.bolden(terminal.color('[!]', TerminalColor.red));
}
/// Success mark to use in stdout.
String get successMark {
return terminal.bolden(terminal.color('✓', TerminalColor.green));
}
final AnsiTerminal _defaultAnsiTerminal = AnsiTerminal();
OutputPreferences get outputPreferences {
......
......@@ -6,7 +6,7 @@ import 'dart:async';
import 'package:meta/meta.dart';
import 'android/gradle.dart';
import 'android/gradle_utils.dart';
import 'base/common.dart';
import 'base/context.dart';
import 'base/file_system.dart';
......@@ -914,7 +914,7 @@ class AndroidMavenArtifacts extends ArtifactSet {
'--project-cache-dir', tempDir.path,
'resolveDependencies',
],
environment: gradleEnv);
environment: gradleEnvironment);
if (processResult.exitCode != 0) {
printError('Failed to download the Android dependencies');
}
......
......@@ -10,7 +10,7 @@ import 'package:yaml/yaml.dart' as yaml;
import '../android/android.dart' as android;
import '../android/android_sdk.dart' as android_sdk;
import '../android/gradle.dart' as gradle;
import '../android/gradle_utils.dart' as gradle;
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/net.dart';
......
......@@ -7,7 +7,7 @@ import 'dart:async';
import 'android/android_sdk.dart';
import 'android/android_studio.dart';
import 'android/android_workflow.dart';
import 'android/gradle.dart';
import 'android/gradle_utils.dart';
import 'application_package.dart';
import 'artifacts.dart';
import 'asset.dart';
......
......@@ -7,7 +7,7 @@ import 'dart:async';
import 'package:meta/meta.dart';
import 'package:yaml/yaml.dart';
import 'android/gradle.dart' as gradle;
import 'android/gradle_utils.dart' as gradle;
import 'base/common.dart';
import 'base/context.dart';
import 'base/file_system.dart';
......@@ -574,6 +574,11 @@ class AndroidProject {
return _firstMatchInFile(gradleFile, _groupPattern)?.group(1);
}
/// The build directory where the Android artifacts are placed.
Directory get buildDirectory {
return parent.directory.childDirectory('build');
}
Future<void> ensureReadyForPlatformSpecificTooling() async {
if (isModule && _shouldRegenerateFromTemplate()) {
_regenerateLibrary();
......
// 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';
}
}
......@@ -8,449 +8,391 @@ import 'package:file/memory.dart';
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>[]);
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(),
ProcessManager: () => FakeProcessManager.any(),
expect(
getApkDirectory(project).path,
equals(fs.path.join('foo', 'host', 'outputs', 'apk')),
);
});
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);
}
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('androidXPluginWarningRegex should match lines with the AndroidX plugin warnings', () {
final List<String> nonMatchingLines = <String>[
':app:preBuild UP-TO-DATE',
'BUILD SUCCESSFUL in 0s',
'Generic plugin AndroidX text',
'',
];
final List<String> matchingLines = <String>[
'*********************************************************************************************************************************',
"WARNING: This version of image_picker will break your Android build if it or its dependencies aren't compatible with AndroidX.",
'See https://goo.gl/CP92wY for more information on the problem and how to fix it.',
'This warning prints for all Android build failures. The real root cause of the error may be unrelated.',
];
for (String m in nonMatchingLines) {
expect(androidXPluginWarningRegex.hasMatch(m), isFalse);
}
for (String m in matchingLines) {
expect(androidXPluginWarningRegex.hasMatch(m), isTrue);
}
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('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);
}
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 GradleProject gradleProject = generateFakeAppBundle('foo_barRelease', 'app.aab');
final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, 'foo_bar'));
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, '/foo_barRelease/app.aab');
expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', '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'));
final FlutterProject project = generateFakeAppBundle('fooRelease', 'app.aab');
final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo'));
expect(bundle, isNotNull);
expect(bundle.path, '/fooRelease/app.aab');
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 GradleProject gradleProject = generateFakeAppBundle('release', 'app.aab');
final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, null));
final FlutterProject project = generateFakeAppBundle('release', 'app.aab');
final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, null));
expect(bundle, isNotNull);
expect(bundle.path, '/release/app.aab');
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 GradleProject gradleProject = generateFakeAppBundle('foo_barDebug', 'app.aab');
final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, 'foo_bar'));
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, '/foo_barDebug/app.aab');
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 GradleProject gradleProject = generateFakeAppBundle('fooDebug', 'app.aab');
final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, 'foo'));
final FlutterProject project = generateFakeAppBundle('fooDebug', 'app.aab');
final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo'));
expect(bundle, isNotNull);
expect(bundle.path, '/fooDebug/app.aab');
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 GradleProject gradleProject = generateFakeAppBundle('debug', 'app.aab');
final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, null));
final FlutterProject project = generateFakeAppBundle('debug', 'app.aab');
final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, null));
expect(bundle, isNotNull);
expect(bundle.path, '/debug/app.aab');
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 GradleProject gradleProject = generateFakeAppBundle('foo_barProfile', 'app.aab');
final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, 'foo_bar'));
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, '/foo_barProfile/app.aab');
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 GradleProject gradleProject = generateFakeAppBundle('fooProfile', 'app.aab');
final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, 'foo'));
final FlutterProject project = generateFakeAppBundle('fooProfile', 'app.aab');
final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'foo'));
expect(bundle, isNotNull);
expect(bundle.path, '/fooProfile/app.aab');
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 GradleProject gradleProject = generateFakeAppBundle('profile', 'app.aab');
final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, null));
final FlutterProject project = generateFakeAppBundle('profile', 'app.aab');
final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, null));
expect(bundle, isNotNull);
expect(bundle.path, '/profile/app.aab');
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 GradleProject gradleProject = generateFakeAppBundle('release', 'app-release.aab');
final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, null));
final FlutterProject project = generateFakeAppBundle('release', 'app-release.aab');
final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, null));
expect(bundle, isNotNull);
expect(bundle.path, '/release/app-release.aab');
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 GradleProject gradleProject = generateFakeAppBundle('profile', 'app-profile.aab');
final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, null));
final FlutterProject project = generateFakeAppBundle('profile', 'app-profile.aab');
final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, null));
expect(bundle, isNotNull);
expect(bundle.path, '/profile/app-profile.aab');
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 GradleProject gradleProject = generateFakeAppBundle('debug', 'app-debug.aab');
final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, null));
final FlutterProject project = generateFakeAppBundle('debug', 'app-debug.aab');
final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, null));
expect(bundle, isNotNull);
expect(bundle.path, '/debug/app-debug.aab');
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 GradleProject gradleProject = generateFakeAppBundle('foo_barRelease', 'app-foo_bar-release.aab');
final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, 'foo_bar'));
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, '/foo_barRelease/app-foo_bar-release.aab');
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 GradleProject gradleProject = generateFakeAppBundle('foo_barProfile', 'app-foo_bar-profile.aab');
final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, 'foo_bar'));
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, '/foo_barProfile/app-foo_bar-profile.aab');
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 GradleProject gradleProject = generateFakeAppBundle('foo_barDebug', 'app-foo_bar-debug.aab');
final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, 'foo_bar'));
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, '/foo_barDebug/app-foo_bar-debug.aab');
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('gradle project', () {
GradleProject projectFrom(String properties, String tasks) => GradleProject.fromAppProperties(properties, tasks);
group('findApkFiles', () {
testUsingContext('Finds APK without flavor in release', () {
final FlutterProject project = MockFlutterProject();
final AndroidProject androidProject = MockAndroidProject();
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'),
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(),
});
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);
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(),
});
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');
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(),
});
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);
});
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,
});
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);
// 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('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');
test('androidXPluginWarningRegex should match lines with the AndroidX plugin warnings', () {
final List<String> nonMatchingLines = <String>[
':app:preBuild UP-TO-DATE',
'BUILD SUCCESSFUL in 0s',
'Generic plugin AndroidX text',
'',
];
final List<String> matchingLines = <String>[
'*********************************************************************************************************************************',
"WARNING: This version of image_picker will break your Android build if it or its dependencies aren't compatible with AndroidX.",
'See https://goo.gl/CP92wY for more information on the problem and how to fix it.',
'This warning prints for all Android build failures. The real root cause of the error may be unrelated.',
];
for (String m in nonMatchingLines) {
expect(androidXPluginWarningRegex.hasMatch(m), isFalse);
}
for (String m in matchingLines) {
expect(androidXPluginWarningRegex.hasMatch(m), isTrue);
}
});
});
......@@ -461,7 +403,6 @@ someOtherTask
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 @@ include ':app'
});
});
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 @@ flutter:
});
});
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 @@ flutter:
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 @@ plugin2=${plugin2.path}
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 @@ plugin2=${plugin2.path}
});
group('gradle build', () {
final Usage mockUsage = MockUsage();
MockAndroidSdk mockAndroidSdk;
MockAndroidStudio mockAndroidStudio;
MockLocalEngineArtifacts mockArtifacts;
......@@ -1623,6 +1054,399 @@ plugin2=${plugin2.path}
.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 @@ plugin2=${plugin2.path}
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 @@ plugin2=${plugin2.path}
await buildGradleAar(
androidBuildInfo: const AndroidBuildInfo(BuildInfo(BuildMode.release, null)),
project: FlutterProject.current(),
outputDir: 'build/',
outputDir: fs.directory('build/'),
target: '',
);
......@@ -1700,14 +1527,24 @@ plugin2=${plugin2.path}
}
/// 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);
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;
FlutterProject generateFakeAppBundle(String directoryName, String fileName) {
final FlutterProject project = MockFlutterProject();
final AndroidProject androidProject = MockAndroidProject();
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 @@ Platform fakePlatform(String name) {
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 {}
// 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
......@@ -11,7 +11,7 @@ import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import 'package:flutter_tools/src/android/gradle.dart';
import 'package:flutter_tools/src/android/gradle_utils.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/cache.dart';
......@@ -316,7 +316,7 @@ void main() {
expect(args[1], '-b');
expect(args[2].endsWith('resolve_dependencies.gradle'), isTrue);
expect(args[5], 'resolveDependencies');
expect(invocation.namedArguments[#environment], gradleEnv);
expect(invocation.namedArguments[#environment], gradleEnvironment);
return Future<ProcessResult>.value(ProcessResult(0, 0, '', ''));
});
......
......@@ -8,7 +8,6 @@ import 'package:args/command_runner.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_builder.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/gradle.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/cache.dart';
......@@ -17,6 +16,7 @@ import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import '../../src/android_common.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/mocks.dart';
......@@ -120,7 +120,9 @@ void main() {
group('AndroidSdk', () {
testUsingContext('validateSdkWellFormed() not called, sdk reinitialized', () async {
final Directory gradleCacheDir = memoryFileSystem.directory('/flutter_root/bin/cache/artifacts/gradle_wrapper')..createSync(recursive: true);
final Directory gradleCacheDir = memoryFileSystem
.directory('/flutter_root/bin/cache/artifacts/gradle_wrapper')
..createSync(recursive: true);
gradleCacheDir.childFile(platform.isWindows ? 'gradlew.bat' : 'gradlew').createSync();
tempDir.childFile('pubspec.yaml')
......@@ -141,11 +143,31 @@ flutter:
''');
tempDir.childFile('.packages').createSync(recursive: true);
final Directory androidDir = tempDir.childDirectory('android');
androidDir.childFile('build.gradle').createSync(recursive: true);
androidDir.childFile('gradle.properties').createSync(recursive: true);
androidDir.childDirectory('gradle').childDirectory('wrapper').childFile('gradle-wrapper.properties').createSync(recursive: true);
tempDir.childDirectory('build').childDirectory('outputs').childDirectory('repo').createSync(recursive: true);
tempDir.childDirectory('lib').childFile('main.dart').createSync(recursive: true);
androidDir
.childFile('build.gradle')
.createSync(recursive: true);
androidDir
.childDirectory('app')
.childFile('build.gradle')
..createSync(recursive: true)
..writeAsStringSync('apply from: irrelevant/flutter.gradle');
androidDir
.childFile('gradle.properties')
.createSync(recursive: true);
androidDir
.childDirectory('gradle')
.childDirectory('wrapper')
.childFile('gradle-wrapper.properties')
.createSync(recursive: true);
tempDir
.childDirectory('build')
.childDirectory('outputs')
.childDirectory('repo')
.createSync(recursive: true);
tempDir
.childDirectory('lib')
.childFile('main.dart')
.createSync(recursive: true);
await runBuildAarCommand(tempDir.path);
verifyNever(mockAndroidSdk.validateSdkWellFormed());
......@@ -153,7 +175,6 @@ flutter:
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
ProcessManager: () => mockProcessManager,
FileSystem: () => memoryFileSystem,
});
......
......@@ -8,7 +8,6 @@ import 'package:args/command_runner.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_builder.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/gradle.dart';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
......@@ -20,6 +19,7 @@ import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import '../../src/android_common.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/mocks.dart';
......@@ -152,7 +152,10 @@ void main() {
platform.isWindows ? 'gradlew.bat' : 'gradlew');
});
testUsingContext('validateSdkWellFormed() not called, sdk reinitialized', () async {
final Directory gradleCacheDir = memoryFileSystem.directory('/flutter_root/bin/cache/artifacts/gradle_wrapper')..createSync(recursive: true);
final Directory gradleCacheDir = memoryFileSystem
.directory('/flutter_root/bin/cache/artifacts/gradle_wrapper')
..createSync(recursive: true);
gradleCacheDir.childFile(platform.isWindows ? 'gradlew.bat' : 'gradlew').createSync();
tempDir.childFile('pubspec.yaml')
......@@ -170,11 +173,31 @@ flutter:
''');
tempDir.childFile('.packages').createSync(recursive: true);
final Directory androidDir = tempDir.childDirectory('android');
androidDir.childFile('build.gradle').createSync(recursive: true);
androidDir.childFile('gradle.properties').createSync(recursive: true);
androidDir.childDirectory('gradle').childDirectory('wrapper').childFile('gradle-wrapper.properties').createSync(recursive: true);
tempDir.childDirectory('build').childDirectory('outputs').childDirectory('repo').createSync(recursive: true);
tempDir.childDirectory('lib').childFile('main.dart').createSync(recursive: true);
androidDir
.childFile('build.gradle')
.createSync(recursive: true);
androidDir
.childDirectory('app')
.childFile('build.gradle')
..createSync(recursive: true)
..writeAsStringSync('apply from: irrelevant/flutter.gradle');
androidDir
.childFile('gradle.properties')
.createSync(recursive: true);
androidDir
.childDirectory('gradle')
.childDirectory('wrapper')
.childFile('gradle-wrapper.properties')
.createSync(recursive: true);
tempDir
.childDirectory('build')
.childDirectory('outputs')
.childDirectory('repo')
.createSync(recursive: true);
tempDir
.childDirectory('lib')
.childFile('main.dart')
.createSync(recursive: true);
when(mockProcessManager.run(any,
workingDirectory: anyNamed('workingDirectory'),
environment: anyNamed('environment')))
......@@ -182,7 +205,7 @@ flutter:
await expectLater(
runBuildApkCommand(tempDir.path, arguments: <String>['--no-pub', '--flutter-root=/flutter_root']),
throwsToolExit(message: 'Gradle build failed: 1'),
throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'),
);
verifyNever(mockAndroidSdk.validateSdkWellFormed());
......@@ -190,7 +213,6 @@ flutter:
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
FileSystem: () => memoryFileSystem,
ProcessManager: () => mockProcessManager,
});
......@@ -221,7 +243,6 @@ flutter:
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
GradleUtils: () => GradleUtils(),
ProcessManager: () => mockProcessManager,
});
......@@ -252,7 +273,6 @@ flutter:
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
GradleUtils: () => GradleUtils(),
ProcessManager: () => mockProcessManager,
});
......@@ -300,13 +320,12 @@ flutter:
verify(mockUsage.sendEvent(
'build',
'apk',
label: 'r8-failure',
label: 'gradle--r8-failure',
parameters: anyNamed('parameters'),
)).called(1);
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => mockProcessManager,
Usage: () => mockUsage,
......@@ -344,7 +363,7 @@ flutter:
}, throwsToolExit());
final BufferLogger logger = context.get<Logger>();
expect(logger.statusText, contains('[!] Your app isn\'t using AndroidX'));
expect(logger.statusText, contains('Your app isn\'t using AndroidX'));
expect(logger.statusText, contains(
'To avoid potential build failures, you can quickly migrate your app by '
'following the steps on https://goo.gl/CP92wY'
......@@ -359,7 +378,6 @@ flutter:
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => mockProcessManager,
Usage: () => mockUsage,
......@@ -414,7 +432,6 @@ flutter:
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => mockProcessManager,
Usage: () => mockUsage,
......
......@@ -8,7 +8,6 @@ import 'package:args/command_runner.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_builder.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/gradle.dart';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
......@@ -20,6 +19,7 @@ import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import '../../src/android_common.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/mocks.dart';
......@@ -139,7 +139,9 @@ void main() {
});
testUsingContext('validateSdkWellFormed() not called, sdk reinitialized', () async {
final Directory gradleCacheDir = memoryFileSystem.directory('/flutter_root/bin/cache/artifacts/gradle_wrapper')..createSync(recursive: true);
final Directory gradleCacheDir = memoryFileSystem
.directory('/flutter_root/bin/cache/artifacts/gradle_wrapper')
..createSync(recursive: true);
gradleCacheDir.childFile(platform.isWindows ? 'gradlew.bat' : 'gradlew').createSync();
tempDir.childFile('pubspec.yaml')
......@@ -158,10 +160,26 @@ flutter:
tempDir.childFile('.packages').createSync(recursive: true);
final Directory androidDir = tempDir.childDirectory('android');
androidDir.childFile('build.gradle').createSync(recursive: true);
androidDir.childFile('gradle.properties').createSync(recursive: true);
androidDir.childDirectory('gradle').childDirectory('wrapper').childFile('gradle-wrapper.properties').createSync(recursive: true);
tempDir.childDirectory('build').childDirectory('outputs').childDirectory('repo').createSync(recursive: true);
tempDir.childDirectory('lib').childFile('main.dart').createSync(recursive: true);
androidDir
.childDirectory('app')
.childFile('build.gradle')
..createSync(recursive: true)
..writeAsStringSync('apply from: irrelevant/flutter.gradle');
androidDir
.childFile('gradle.properties')
.createSync(recursive: true);
androidDir
.childDirectory('gradle')
.childDirectory('wrapper')
.childFile('gradle-wrapper.properties')
.createSync(recursive: true);
tempDir.childDirectory('build')
.childDirectory('outputs')
.childDirectory('repo')
.createSync(recursive: true);
tempDir.childDirectory('lib')
.childFile('main.dart')
.createSync(recursive: true);
when(mockProcessManager.run(any,
workingDirectory: anyNamed('workingDirectory'),
environment: anyNamed('environment')))
......@@ -169,7 +187,7 @@ flutter:
await expectLater(
runBuildAppBundleCommand(tempDir.path, arguments: <String>['--no-pub', '--flutter-root=/flutter_root']),
throwsToolExit(message: 'Gradle build failed: 1'),
throwsToolExit(message: 'Gradle task bundleRelease failed with exit code 1'),
);
verifyNever(mockAndroidSdk.validateSdkWellFormed());
......@@ -177,7 +195,6 @@ flutter:
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
ProcessManager: () => mockProcessManager,
FileSystem: () => memoryFileSystem,
});
......@@ -210,7 +227,6 @@ flutter:
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
GradleUtils: () => GradleUtils(),
ProcessManager: () => mockProcessManager,
});
......@@ -243,7 +259,6 @@ flutter:
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
GradleUtils: () => GradleUtils(),
ProcessManager: () => mockProcessManager,
});
......@@ -291,13 +306,12 @@ flutter:
verify(mockUsage.sendEvent(
'build',
'appbundle',
label: 'r8-failure',
label: 'gradle--r8-failure',
parameters: anyNamed('parameters'),
)).called(1);
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => mockProcessManager,
Usage: () => mockUsage,
......@@ -335,7 +349,7 @@ flutter:
}, throwsToolExit());
final BufferLogger logger = context.get<Logger>();
expect(logger.statusText, contains('[!] Your app isn\'t using AndroidX'));
expect(logger.statusText, contains('Your app isn\'t using AndroidX'));
expect(logger.statusText, contains(
'To avoid potential build failures, you can quickly migrate your app by '
'following the steps on https://goo.gl/CP92wY'
......@@ -350,7 +364,6 @@ flutter:
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => mockProcessManager,
Usage: () => mockUsage,
......@@ -388,7 +401,7 @@ flutter:
}, throwsToolExit());
final BufferLogger logger = context.get<Logger>();
expect(logger.statusText.contains('[!] Your app isn\'t using AndroidX'), isFalse);
expect(logger.statusText.contains('Your app isn\'t using AndroidX'), isFalse);
expect(
logger.statusText.contains(
'To avoid potential build failures, you can quickly migrate your app by '
......@@ -405,7 +418,6 @@ flutter:
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
GradleUtils: () => GradleUtils(),
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => mockProcessManager,
Usage: () => mockUsage,
......
// 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 {}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment