diff --git a/packages/flutter_tools/lib/src/android/android_builder.dart b/packages/flutter_tools/lib/src/android/android_builder.dart index c06925f2a4f..77bf76a0c2c 100644 --- a/packages/flutter_tools/lib/src/android/android_builder.dart +++ b/packages/flutter_tools/lib/src/android/android_builder.dart @@ -6,19 +6,23 @@ import '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() ?? _AndroidBuilderImpl(); +AndroidBuilder get androidBuilder { + return context.get() ?? 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 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 buildAar({ - @required FlutterProject project, - @required AndroidBuildInfo androidBuildInfo, - @required String target, - @required String outputDir, - }) async {} - - @override - Future buildApk({ - @required FlutterProject project, - @required AndroidBuildInfo androidBuildInfo, - @required String target, - }) async {} - - @override - Future buildAab({ - @required FlutterProject project, - @required AndroidBuildInfo androidBuildInfo, - @required String target, - }) async {} -} diff --git a/packages/flutter_tools/lib/src/android/android_studio.dart b/packages/flutter_tools/lib/src/android/android_studio.dart index 04e347439c1..9ded536263c 100644 --- a/packages/flutter_tools/lib/src/android/android_studio.dart +++ b/packages/flutter_tools/lib/src/android/android_studio.dart @@ -236,7 +236,7 @@ class AndroidStudio implements Comparable { // Read all $HOME/.AndroidStudio*/system/.home files. There may be several // pointing to the same installation, so we grab only the latest one. - if (fs.directory(homeDirPath).existsSync()) { + if (homeDirPath != null && fs.directory(homeDirPath).existsSync()) { for (FileSystemEntity entity in fs.directory(homeDirPath).listSync(followLinks: false)) { if (entity is Directory && entity.basename.startsWith('.AndroidStudio')) { final AndroidStudio studio = AndroidStudio.fromHomeDot(entity); diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index 0dd05ae6b25..dff67bd3533 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -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(); - -/// 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 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 _cachedAppProject = {}; - - /// Gets the [GradleProject] for the [project] if built as an app. - Future getAppProject(FlutterProject project) async { - final String projectPath = project.directory.path; - _cachedAppProject[projectPath] ??= await _readGradleProject(project, isLibrary: false); - return _cachedAppProject[projectPath]; - } - - /// Cached library projects such as plugins or modules. - /// The key is the [FluterProject]'s path, the value is [GradleProject]. - final Map _cachedLibraryProject = {}; - - /// Gets the [GradleProject] for the [project] if built as a library. - Future getLibraryProject(FlutterProject project) async { - final String projectPath = project.directory.path; - _cachedLibraryProject[projectPath] ??= await _readGradleProject(project, isLibrary: true); - return _cachedLibraryProject[projectPath]; - } +/// The directory where the APK artifact is generated. +@visibleForTesting +Directory getApkDirectory(FlutterProject project) { + return project.isModule + ? project.android.buildDirectory + .childDirectory('host') + .childDirectory('outputs') + .childDirectory('apk') + : project.android.buildDirectory + .childDirectory('app') + .childDirectory('outputs') + .childDirectory('apk'); } -final RegExp _assembleTaskPattern = RegExp(r'assemble(\S+)'); - -enum FlutterPluginVersion { - none, - v1, - v2, - managed, +/// The directory where the app bundle artifact is generated. +@visibleForTesting +Directory getBundleDirectory(FlutterProject project) { + return project.isModule + ? project.android.buildDirectory + .childDirectory('host') + .childDirectory('outputs') + .childDirectory('bundle') + : project.android.buildDirectory + .childDirectory('app') + .childDirectory('outputs') + .childDirectory('bundle'); } -// Investigation documented in #13975 suggests the filter should be a subset -// of the impact of -q, but users insist they see the error message sometimes -// anyway. If we can prove it really is impossible, delete the filter. -// This technically matches everything *except* the NDK message, since it's -// passed to a function that filters out all lines that don't match a filter. -final RegExp ndkMessageFilter = RegExp(r'^(?!NDK is missing a ".*" directory' - r'|If you are not using NDK, unset the NDK variable from ANDROID_NDK_HOME or local.properties to remove this warning' - r'|If you are using NDK, verify the ndk.dir is set to a valid NDK directory. It is currently set to .*)'); +/// The directory where the repo is generated. +/// Only applicable to AARs. +@visibleForTesting +Directory getRepoDirectory(Directory buildDirectory) { + return buildDirectory + .childDirectory('outputs') + .childDirectory('repo'); +} -// This regex is intentionally broad. AndroidX errors can manifest in multiple -// different ways and each one depends on the specific code config and -// filesystem paths of the project. Throwing the broadest net possible here to -// catch all known and likely cases. -// -// Example stack traces: -// -// https://github.com/flutter/flutter/issues/27226 "AAPT: error: resource android:attr/fontVariationSettings not found." -// https://github.com/flutter/flutter/issues/27106 "Android resource linking failed|Daemon: AAPT2|error: failed linking references" -// https://github.com/flutter/flutter/issues/27493 "error: cannot find symbol import androidx.annotation.NonNull;" -// https://github.com/flutter/flutter/issues/23995 "error: package android.support.annotation does not exist import android.support.annotation.NonNull;" -final RegExp androidXFailureRegex = RegExp(r'(AAPT|androidx|android\.support)'); +/// Returns the name of Gradle task that starts with [prefix]. +String _taskFor(String prefix, BuildInfo buildInfo) { + final String buildType = camelCase(buildInfo.modeName); + final String productFlavor = buildInfo.flavor ?? ''; + return '$prefix${toTitleCase(productFlavor)}${toTitleCase(buildType)}'; +} -final RegExp androidXPluginWarningRegex = RegExp(r'\*{57}' - r"|WARNING: This version of (\w+) will break your Android build if it or its dependencies aren't compatible with AndroidX." - r'|See https://goo.gl/CP92wY for more information on the problem and how to fix it.' - r'|This warning prints for all Android build failures. The real root cause of the error may be unrelated.'); +/// Returns the task to build an APK. +@visibleForTesting +String getAssembleTaskFor(BuildInfo buildInfo) { + return _taskFor('assemble', buildInfo); +} -FlutterPluginVersion getFlutterPluginVersion(AndroidProject project) { +/// Returns the task to build an AAB. +@visibleForTesting +String getBundleTaskFor(BuildInfo buildInfo) { + return _taskFor('bundle', buildInfo); +} + +/// Returns the task to build an AAR. +@visibleForTesting +String getAarTaskFor(BuildInfo buildInfo) { + return _taskFor('assembleAar', buildInfo); +} + +/// Returns the output APK file names for a given [AndroidBuildInfo]. +/// +/// For example, when [splitPerAbi] is true, multiple APKs are created. +Iterable _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((AndroidArch arch) { + final String abi = getNameForAndroidArch(arch); + return 'app$flavorString-$abi-$buildType.apk'; + }); + } + return ['app$flavorString-$buildType.apk']; +} + +/// Returns true if the current version of the Gradle plugin is supported. +bool _isSupportedVersion(AndroidProject project) { final File plugin = project.hostAppGradleRoot.childFile( fs.path.join('buildSrc', 'src', 'main', 'groovy', 'FlutterPlugin.groovy')); if (plugin.existsSync()) { - final String packageLine = plugin.readAsLinesSync().skip(4).first; - if (packageLine == 'package io.flutter.gradle') { - return FlutterPluginVersion.v2; - } - return FlutterPluginVersion.v1; + return false; } final File appGradle = project.hostAppGradleRoot.childFile( fs.path.join('app', 'build.gradle')); - if (appGradle.existsSync()) { - for (String line in appGradle.readAsLinesSync()) { - if (line.contains(RegExp(r'apply from: .*/flutter.gradle'))) { - return FlutterPluginVersion.managed; - } - if (line.contains("def flutterPluginVersion = 'managed'")) { - return FlutterPluginVersion.managed; - } + if (!appGradle.existsSync()) { + return false; + } + for (String line in appGradle.readAsLinesSync()) { + if (line.contains(RegExp(r'apply from: .*/flutter.gradle')) || + line.contains("def flutterPluginVersion = 'managed'")) { + return true; } } - return FlutterPluginVersion.none; + return false; } /// Returns the apk file created by [buildGradleProject] Future 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 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( - [gradlew, 'dependencies'], + await processUtils.run([ + 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 _readGradleProject( - FlutterProject flutterProject, { - bool isLibrary = false, -}) async { - final String gradlew = await gradleUtils.getExecutable(flutterProject); - - updateLocalProperties(project: flutterProject); - - final FlutterManifest manifest = flutterProject.manifest; - final Directory hostAppGradleRoot = flutterProject.android.hostAppGradleRoot; - - if (manifest.isPlugin) { - assert(isLibrary); - return GradleProject( - ['debug', 'profile', 'release'], - [], // 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( - [gradlew, if (isLibrary) 'properties' else 'app:properties'], - throwOnError: true, - workingDirectory: hostAppGradleRoot.path, - environment: gradleEnv, - ); - final RunResult tasksRunResult = await processUtils.run( - [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( - ['debug', 'profile', 'release'], - [], - fs.path.join(flutterProject.android.hostAppGradleRoot.path, 'app', 'build'), - ); - } - status.stop(); - return project; -} - -/// Handle Gradle error thrown when Gradle needs to download additional -/// Android SDK components (e.g. Platform Tools), and the license -/// for that component has not been accepted. -void throwToolExitIfLicenseNotAccepted(Exception exception) { - const String licenseNotAcceptedMatcher = - r'You have not accepted the license agreements of the following SDK components:' - r'\s*\[(.+)\]'; - final RegExp licenseFailure = RegExp(licenseNotAcceptedMatcher, multiLine: true); - final Match licenseMatch = licenseFailure.firstMatch(exception.toString()); - if (licenseMatch != null) { - final String missingLicenses = licenseMatch.group(1); - final String errorMessage = - '\n\n* Error running Gradle:\n' - 'Unable to download needed Android SDK components, as the following licenses have not been accepted:\n' - '$missingLicenses\n\n' - 'To resolve this, please run the following command in a Terminal:\n' - 'flutter doctor --android-licenses'; - throwToolExit(errorMessage); - } -} - -String _locateGradlewExecutable(Directory directory) { - final File gradle = directory.childFile( - platform.isWindows ? 'gradlew.bat' : 'gradlew', - ); - if (gradle.existsSync()) { - return gradle.absolute.path; - } - return null; -} - -// Gradle crashes for several known reasons when downloading that are not -// actionable by flutter. -const List _kKnownErrorPrefixes = [ - '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 _initializeGradle(FlutterProject project) async { - final Directory android = project.android.hostAppGradleRoot; - final Status status = logger.startProgress('Initializing gradle...', - timeout: timeoutConfiguration.slowOperation); - - - // Update the project if needed. - // TODO(egarciad): https://github.com/flutter/flutter/issues/40460. - migrateToR8(android); - injectGradleWrapperIfNeeded(android); - - final String gradle = _locateGradlewExecutable(android); - if (gradle == null) { - status.stop(); - throwToolExit('Unable to locate gradlew script'); - } - printTrace('Using gradle from $gradle.'); - // Validates the Gradle executable by asking for its version. - // Makes Gradle Wrapper download and install Gradle distribution, if needed. - try { - await processUtils.run( - [gradle, '-v'], - throwOnError: true, - environment: gradleEnv, - ); - } on ProcessException catch (e) { - final String error = e.toString(); - // TODO(jonahwilliams): automatically retry on network errors. - if (_kKnownErrorPrefixes.any((String candidate) => error.contains(candidate))) { - throwToolExit( - '$gradle threw an error while trying to update itself.' - ' Try rerunning to retry the update.\n$e'); - } - // gradlew is missing execute. - if (error.contains('Permission denied')) { - throwToolExit( - '$gradle does not have permission to execute by your user.\n' - 'You should change the ownership of the project directory to your user' - ', or move the project to a directory with execute permissions.\n$error' - ); - } - // No idea what went wrong but we can't do anything about it. - if (error.contains('ProcessException: Process exited abnormally')) { - throwToolExit( - '$gradle exited abnormally. Try rerunning with \'-v\' for more ' - 'infomration, or check the gradlew script above for errors.\n$error'); - } - rethrow; - } finally { - status.stop(); - } - return gradle; -} - -/// Migrates the Android's [directory] to R8. -/// https://developer.android.com/studio/build/shrink-code -@visibleForTesting -void migrateToR8(Directory directory) { - final File gradleProperties = directory.childFile('gradle.properties'); - if (!gradleProperties.existsSync()) { - throwToolExit('Expected file ${gradleProperties.path}.'); - } - final String propertiesContent = gradleProperties.readAsStringSync(); - if (propertiesContent.contains('android.enableR8')) { - printTrace('gradle.properties already sets `android.enableR8`'); - return; - } - printTrace('set `android.enableR8=true` in gradle.properties'); - try { - // Add `android.enableR8=true` to the next line in gradle.properties. - if (propertiesContent.isNotEmpty && !propertiesContent.endsWith('\n')) { - gradleProperties - .writeAsStringSync('\nandroid.enableR8=true\n', mode: FileMode.append); - } else { - gradleProperties - .writeAsStringSync('android.enableR8=true\n', mode: FileMode.append); - } - } on FileSystemException { - throwToolExit( - 'The tool failed to add `android.enableR8=true` to ${gradleProperties.path}. ' - 'Please update the file manually and try this command again.' - ); - } -} - -/// Injects the Gradle wrapper files if any of these files don't exist in [directory]. -void injectGradleWrapperIfNeeded(Directory directory) { - copyDirectorySync( - cache.getArtifactDirectory('gradle_wrapper'), - directory, - shouldCopyFile: (File sourceFile, File destinationFile) { - // Don't override the existing files in the project. - return !destinationFile.existsSync(); - }, - onFileCopied: (File sourceFile, File destinationFile) { - final String modes = sourceFile.statSync().modeString(); - if (modes != null && modes.contains('x')) { - os.makeExecutable(destinationFile); - } - }, - ); - // Add the `gradle-wrapper.properties` file if it doesn't exist. - final File propertiesFile = directory.childFile( - fs.path.join('gradle', 'wrapper', 'gradle-wrapper.properties')); - if (!propertiesFile.existsSync()) { - final String gradleVersion = getGradleVersionForAndroidPlugin(directory); - propertiesFile.writeAsStringSync(''' -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\\://services.gradle.org/distributions/gradle-$gradleVersion-all.zip -''', flush: true, - ); - } -} - -/// Returns true if [targetVersion] is within the range [min] and [max] inclusive. -bool _isWithinVersionRange(String targetVersion, {String min, String max}) { - final Version parsedTargetVersion = Version.parse(targetVersion); - return parsedTargetVersion >= Version.parse(min) && - parsedTargetVersion <= Version.parse(max); -} - -const String defaultGradleVersion = '5.6.2'; - -/// Returns the Gradle version that is required by the given Android Gradle plugin version -/// by picking the largest compatible version from -/// https://developer.android.com/studio/releases/gradle-plugin#updating-gradle -String getGradleVersionFor(String androidPluginVersion) { - if (_isWithinVersionRange(androidPluginVersion, min: '1.0.0', max: '1.1.3')) { - return '2.3'; - } - if (_isWithinVersionRange(androidPluginVersion, min: '1.2.0', max: '1.3.1')) { - return '2.9'; - } - if (_isWithinVersionRange(androidPluginVersion, min: '1.5.0', max: '1.5.0')) { - return '2.2.1'; - } - if (_isWithinVersionRange(androidPluginVersion, min: '2.0.0', max: '2.1.2')) { - return '2.13'; - } - if (_isWithinVersionRange(androidPluginVersion, min: '2.1.3', max: '2.2.3')) { - return '2.14.1'; - } - if (_isWithinVersionRange(androidPluginVersion, min: '2.3.0', max: '2.9.9')) { - return '3.3'; - } - if (_isWithinVersionRange(androidPluginVersion, min: '3.0.0', max: '3.0.9')) { - return '4.1'; - } - if (_isWithinVersionRange(androidPluginVersion, min: '3.1.0', max: '3.1.9')) { - return '4.4'; - } - if (_isWithinVersionRange(androidPluginVersion, min: '3.2.0', max: '3.2.1')) { - return '4.6'; - } - if (_isWithinVersionRange(androidPluginVersion, min: '3.3.0', max: '3.3.2')) { - return '4.10.2'; - } - if (_isWithinVersionRange(androidPluginVersion, min: '3.4.0', max: '3.5.0')) { - return '5.6.2'; - } - throwToolExit('Unsuported Android Plugin version: $androidPluginVersion.'); - return ''; -} - -final RegExp _androidPluginRegExp = RegExp('com\.android\.tools\.build\:gradle\:(\\d+\.\\d+\.\\d+\)'); - -/// Returns the Gradle version that the current Android plugin depends on when found, -/// otherwise it returns a default version. +/// Builds an app. /// -/// The Android plugin version is specified in the [build.gradle] file within -/// the project's Android directory. -String getGradleVersionForAndroidPlugin(Directory directory) { - final File buildFile = directory.childFile('build.gradle'); - if (!buildFile.existsSync()) { - return defaultGradleVersion; - } - final String buildFileContent = buildFile.readAsStringSync(); - final Iterable pluginMatches = _androidPluginRegExp.allMatches(buildFileContent); - - if (pluginMatches.isEmpty) { - return defaultGradleVersion; - } - final String androidPluginVersion = pluginMatches.first.group(1); - return getGradleVersionFor(androidPluginVersion); -} - -/// Overwrite local.properties in the specified Flutter project's Android -/// sub-project, if needed. -/// -/// If [requireAndroidSdk] is true (the default) and no Android SDK is found, -/// this will fail with a [ToolExit]. -void updateLocalProperties({ - @required FlutterProject project, - BuildInfo buildInfo, - bool requireAndroidSdk = true, -}) { - if (requireAndroidSdk) { - _exitIfNoAndroidSdk(); - } - - final File localProperties = project.android.localPropertiesFile; - bool changed = false; - - SettingsFile settings; - if (localProperties.existsSync()) { - settings = SettingsFile.parseFromFile(localProperties); - } else { - settings = SettingsFile(); - changed = true; - } - - void changeIfNecessary(String key, String value) { - if (settings.values[key] != value) { - if (value == null) { - settings.values.remove(key); - } else { - settings.values[key] = value; - } - changed = true; - } - } - - final FlutterManifest manifest = project.manifest; - - if (androidSdk != null) { - changeIfNecessary('sdk.dir', escapePath(androidSdk.directory)); - } - - changeIfNecessary('flutter.sdk', escapePath(Cache.flutterRoot)); - - if (buildInfo != null) { - changeIfNecessary('flutter.buildMode', buildInfo.modeName); - final String buildName = validatedBuildNameForPlatform(TargetPlatform.android_arm, buildInfo.buildName ?? manifest.buildName); - changeIfNecessary('flutter.versionName', buildName); - final String buildNumber = validatedBuildNumberForPlatform(TargetPlatform.android_arm, buildInfo.buildNumber ?? manifest.buildNumber); - changeIfNecessary('flutter.versionCode', buildNumber?.toString()); - } - - if (changed) { - settings.writeContents(localProperties); - } -} - -/// Writes standard Android local properties to the specified [properties] file. -/// -/// Writes the path to the Android SDK, if known. -void writeLocalProperties(File properties) { - final SettingsFile settings = SettingsFile(); - if (androidSdk != null) { - settings.values['sdk.dir'] = escapePath(androidSdk.directory); - } - settings.writeContents(properties); -} - -/// Throws a ToolExit, if the path to the Android SDK is not known. -void _exitIfNoAndroidSdk() { - if (androidSdk == null) { - throwToolExit('Unable to locate Android SDK. Please run `flutter doctor` for more details.'); - } -} - -Future buildGradleProject({ +/// * [project] is typically [FlutterProject.current()]. +/// * [androidBuildInfo] is the build configuration. +/// * [target] is the target dart entrypoint. Typically, `lib/main.dart`. +/// * If [isBuildingBundle] is `true`, then the output artifact is an `*.aab`, +/// otherwise the output artifact is an `*.apk`. +/// * The plugins are built as AARs if [shouldBuildPluginAsAar] is `true`. This isn't set by default +/// because it makes the build slower proportional to the number of plugins. +/// * [retries] is the max number of build retries in case one of the [GradleHandledError] handler +/// returns [GradleBuildStatus.retry] or [GradleBuildStatus.retryWithAarPlugins]. +Future buildGradleApp({ @required FlutterProject project, @required AndroidBuildInfo androidBuildInfo, @required String target, @required bool isBuildingBundle, + @required List localGradleErrors, + bool shouldBuildPluginAsAar = false, + int retries = 1, }) async { - // Update the local.properties file with the build mode, version name and code. - // FlutterPlugin v1 reads local.properties to determine build mode. Plugin v2 - // uses the standard Android way to determine what to build, but we still - // update local.properties, in case we want to use it in the future. - // Version name and number are provided by the pubspec.yaml file - // and can be overwritten with flutter build command. + if (androidSdk == null) { + exitWithNoSdkMessage(); + } + if (!project.android.isUsingGradle) { + _exitWithProjectNotUsingGradleMessage(); + } + if (!_isSupportedVersion(project.android)) { + _exitWithUnsupportedProjectMessage(); + } + + final bool usesAndroidX = isAppUsingAndroidX(project.android.hostAppGradleRoot); + if (usesAndroidX) { + BuildEvent('app-using-android-x').send(); + } else if (!usesAndroidX) { + BuildEvent('app-not-using-android-x').send(); + printStatus('$warningMark Your app isn\'t using AndroidX.', emphasis: true); + printStatus( + 'To avoid potential build failures, you can quickly migrate your app ' + 'by following the steps on https://goo.gl/CP92wY.', + indent: 4, + ); + } // The default Gradle script reads the version name and number // from the local.properties file. updateLocalProperties(project: project, buildInfo: androidBuildInfo.buildInfo); - switch (getFlutterPluginVersion(project.android)) { - case FlutterPluginVersion.none: - // Fall through. Pretend it's v1, and just go for it. - case FlutterPluginVersion.v1: - return _buildGradleProjectV1(project); - case FlutterPluginVersion.managed: - // Fall through. Managed plugin builds the same way as plugin v2. - case FlutterPluginVersion.v2: - return _buildGradleProjectV2(project, androidBuildInfo, target, isBuildingBundle); + if (shouldBuildPluginAsAar) { + // Create a settings.gradle that doesn't import the plugins as subprojects. + createSettingsAarGradle(project.android.hostAppGradleRoot); + await buildPluginsAsAar( + project, + androidBuildInfo, + buildDirectory: project.android.buildDirectory.childDirectory('app'), + ); + } + + final BuildInfo buildInfo = androidBuildInfo.buildInfo; + final String assembleTask = isBuildingBundle + ? getBundleTaskFor(buildInfo) + : getAssembleTaskFor(buildInfo); + + final Status status = logger.startProgress( + 'Running Gradle task \'$assembleTask\'...', + timeout: timeoutConfiguration.slowOperation, + multilineOutput: true, + ); + + final List command = [ + gradleUtils.getExecutable(project), + ]; + if (logger.isVerbose) { + command.add('-Pverbose=true'); + } else { + command.add('-q'); + } + if (artifacts is LocalEngineArtifacts) { + final LocalEngineArtifacts localEngineArtifacts = artifacts; + printTrace('Using local engine: ${localEngineArtifacts.engineOutPath}'); + command.add('-PlocalEngineOut=${localEngineArtifacts.engineOutPath}'); + } + if (target != null) { + command.add('-Ptarget=$target'); + } + assert(buildInfo.trackWidgetCreation != null); + command.add('-Ptrack-widget-creation=${buildInfo.trackWidgetCreation}'); + + if (buildInfo.extraFrontEndOptions != null) { + command.add('-Pextra-front-end-options=${buildInfo.extraFrontEndOptions}'); + } + if (buildInfo.extraGenSnapshotOptions != null) { + command.add('-Pextra-gen-snapshot-options=${buildInfo.extraGenSnapshotOptions}'); + } + if (buildInfo.fileSystemRoots != null && buildInfo.fileSystemRoots.isNotEmpty) { + command.add('-Pfilesystem-roots=${buildInfo.fileSystemRoots.join('|')}'); + } + if (buildInfo.fileSystemScheme != null) { + command.add('-Pfilesystem-scheme=${buildInfo.fileSystemScheme}'); + } + if (androidBuildInfo.splitPerAbi) { + command.add('-Psplit-per-abi=true'); + } + if (androidBuildInfo.shrink) { + command.add('-Pshrink=true'); + } + if (androidBuildInfo.targetArchs.isNotEmpty) { + final String targetPlatforms = androidBuildInfo + .targetArchs + .map(getPlatformNameForAndroidArch).join(','); + command.add('-Ptarget-platform=$targetPlatforms'); + } + if (shouldBuildPluginAsAar) { + // Pass a system flag instead of a project flag, so this flag can be + // read from include_flutter.groovy. + command.add('-Dbuild-plugins-as-aars=true'); + // Don't use settings.gradle from the current project since it includes the plugins as subprojects. + command.add('--settings-file=settings_aar.gradle'); + } + command.add(assembleTask); + + final Stopwatch sw = Stopwatch()..start(); + int exitCode = 1; + GradleHandledError detectedGradleError; + String detectedGradleErrorLine; + try { + exitCode = await processUtils.stream( + command, + workingDirectory: project.android.hostAppGradleRoot.path, + allowReentrantFlutter: true, + environment: gradleEnvironment, + mapFunction: (String line) { + // This message was removed from first-party plugins, + // but older plugin versions still display this message. + if (androidXPluginWarningRegex.hasMatch(line)) { + // Don't pipe. + return null; + } + if (detectedGradleError != null) { + // Pipe stdout/sterr from Gradle. + return line; + } + for (final GradleHandledError gradleError in localGradleErrors) { + if (gradleError.test(line)) { + detectedGradleErrorLine = line; + detectedGradleError = gradleError; + // The first error match wins. + break; + } + } + // Pipe stdout/sterr from Gradle. + return line; + }, + ); + } finally { + status.stop(); + } + + flutterUsage.sendTiming('build', 'gradle', sw.elapsed); + + if (exitCode != 0) { + if (detectedGradleError == null) { + BuildEvent('gradle--unkown-failure').send(); + throwToolExit( + 'Gradle task $assembleTask failed with exit code $exitCode', + exitCode: exitCode, + ); + } else { + final GradleBuildStatus status = await detectedGradleError.handler( + line: detectedGradleErrorLine, + project: project, + usesAndroidX: usesAndroidX, + shouldBuildPluginAsAar: shouldBuildPluginAsAar, + ); + + if (retries >= 1) { + final String successEventLabel = 'gradle--${detectedGradleError.eventLabel}-success'; + switch (status) { + case GradleBuildStatus.retry: + await buildGradleApp( + project: project, + androidBuildInfo: androidBuildInfo, + target: target, + isBuildingBundle: isBuildingBundle, + localGradleErrors: localGradleErrors, + shouldBuildPluginAsAar: shouldBuildPluginAsAar, + retries: retries - 1, + ); + BuildEvent(successEventLabel).send(); + return; + case GradleBuildStatus.retryWithAarPlugins: + await buildGradleApp( + project: project, + androidBuildInfo: androidBuildInfo, + target: target, + isBuildingBundle: isBuildingBundle, + localGradleErrors: localGradleErrors, + shouldBuildPluginAsAar: true, + retries: retries - 1, + ); + BuildEvent(successEventLabel).send(); + return; + case GradleBuildStatus.exit: + // noop. + } + } + BuildEvent('gradle--${detectedGradleError.eventLabel}-failure').send(); + throwToolExit( + 'Gradle task $assembleTask failed with exit code $exitCode', + exitCode: exitCode, + ); + } + } + + if (isBuildingBundle) { + final File bundleFile = findBundleFile(project, buildInfo); + if (bundleFile == null) { + throwToolExit('Gradle build failed to produce an Android bundle package.'); + } + + final String appSize = (buildInfo.mode == BuildMode.debug) + ? '' // Don't display the size when building a debug variant. + : ' (${getSizeAsMB(bundleFile.lengthSync())})'; + + printStatus( + '$successMark Built ${fs.path.relative(bundleFile.path)}$appSize.', + color: TerminalColor.green, + ); + return; + } + // Gradle produced an APK. + final Iterable apkFiles = findApkFiles(project, androidBuildInfo); + if (apkFiles.isEmpty) { + throwToolExit('Gradle build failed to produce an Android package.'); + } + + final Directory apkDirectory = getApkDirectory(project); + // Copy the first APK to app.apk, so `flutter run` can find it. + // TODO(egarciad): Handle multiple APKs. + apkFiles.first.copySync(apkDirectory.childFile('app.apk').path); + printTrace('calculateSha: $apkDirectory/app.apk'); + + final File apkShaFile = apkDirectory.childFile('app.apk.sha1'); + apkShaFile.writeAsStringSync(_calculateSha(apkFiles.first)); + + for (File apkFile in apkFiles) { + final String appSize = (buildInfo.mode == BuildMode.debug) + ? '' // Don't display the size when building a debug variant. + : ' (${getSizeAsMB(apkFile.lengthSync())})'; + printStatus( + '$successMark Built ${fs.path.relative(apkFile.path)}$appSize.', + color: TerminalColor.green, + ); } } +/// Builds AAR and POM files. +/// +/// * [project] is typically [FlutterProject.current()]. +/// * [androidBuildInfo] is the build configuration. +/// * [target] is the target dart entrypoint. Typically, `lib/main.dart`. +/// * [outputDir] is the destination of the artifacts, Future 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 command = [ - 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 buildGradleAar({ command, workingDirectory: project.android.hostAppGradleRoot.path, allowReentrantFlutter: true, - environment: gradleEnv, + environment: gradleEnvironment, ); } finally { status.stop(); @@ -686,41 +533,24 @@ Future 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 _buildGradleProjectV1(FlutterProject project) async { - final String gradlew = await gradleUtils.getExecutable(project); - // Run 'gradlew build'. - final Status status = logger.startProgress( - 'Running \'gradlew build\'...', - timeout: timeoutConfiguration.slowOperation, - multilineOutput: true, + printStatus( + '$successMark Built ${fs.path.relative(repoDirectory.path)}.', + color: TerminalColor.green, ); - final Stopwatch sw = Stopwatch()..start(); - final int exitCode = await processUtils.stream( - [fs.file(gradlew).absolute.path, 'build'], - workingDirectory: project.android.hostAppGradleRoot.path, - allowReentrantFlutter: true, - environment: gradleEnv, - ); - status.stop(); - flutterUsage.sendTiming('build', 'gradle-v1', Duration(milliseconds: sw.elapsedMilliseconds)); - - if (exitCode != 0) { - throwToolExit('Gradle build failed: $exitCode', exitCode: exitCode); - } - - printStatus('Built ${fs.path.relative(project.android.gradleAppOutV1File.path)}.'); } String _hex(List bytes) { @@ -735,251 +565,31 @@ String _calculateSha(File file) { final Stopwatch sw = Stopwatch()..start(); final List 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 ` ' + 'and then move the dart code, assets and pubspec.yaml to the new project.', + ); } -Future _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 command = [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 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 buildPluginsAsAar( FlutterProject flutterProject, AndroidBuildInfo androidBuildInfo, { - String buildDirectory, + Directory buildDirectory, }) async { final File flutterPluginFile = flutterProject.flutterPluginsFile; if (!flutterPluginFile.existsSync()) { @@ -1031,37 +641,41 @@ Future 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 findApkFiles(GradleProject project, AndroidBuildInfo androidBuildInfo) { - final Iterable apkFileNames = project.apkFilesFor(androidBuildInfo); +Iterable findApkFiles( + FlutterProject project, + AndroidBuildInfo androidBuildInfo) +{ + final Iterable apkFileNames = _apkFilesFor(androidBuildInfo); if (apkFileNames.isEmpty) { return const []; } - + final Directory apkDirectory = getApkDirectory(project); return apkFileNames.expand((String apkFileName) { - File apkFile = project.apkDirectory.childFile(apkFileName); + File apkFile = apkDirectory.childFile(apkFileName); if (apkFile.existsSync()) { return [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 [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 [apkFile]; } @@ -1071,22 +685,21 @@ Iterable findApkFiles(GradleProject project, AndroidBuildInfo androidBuild } @visibleForTesting -File findBundleFile(GradleProject project, BuildInfo buildInfo) { +File findBundleFile(FlutterProject project, BuildInfo buildInfo) { final List fileCandidates = [ - 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 get gradleEnv { - final Map env = Map.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 variants = {}; - 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 buildTypes = {}; - final Set productFlavors = {}; - 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 buildTypes; - - /// The product flavors defined in build.gradle. - final List productFlavors; - - /// The build directory. This is typically 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 apkFilesFor(AndroidBuildInfo androidBuildInfo) { - final String buildType = _buildTypeFor(androidBuildInfo.buildInfo); - final String productFlavor = _productFlavorFor(androidBuildInfo.buildInfo); - if (buildType == null || productFlavor == null) { - return const []; - } - - final String flavorString = productFlavor.isEmpty ? '' : '-' + productFlavor; - if (androidBuildInfo.splitPerAbi) { - return androidBuildInfo.targetArchs.map((AndroidArch arch) { - final String abi = getNameForAndroidArch(arch); - return 'app$flavorString-$abi-$buildType.apk'; - }); - } - return ['app$flavorString-$buildType.apk']; - } - - String bundleTaskFor(BuildInfo buildInfo) { - final String buildType = _buildTypeFor(buildInfo); - final String productFlavor = _productFlavorFor(buildInfo); - if (buildType == null || productFlavor == null) { - return null; - } - return 'bundle${toTitleCase(productFlavor)}${toTitleCase(buildType)}'; - } - - String aarTaskFor(BuildInfo buildInfo) { - final String buildType = _buildTypeFor(buildInfo); - final String productFlavor = _productFlavorFor(buildInfo); - if (buildType == null || productFlavor == null) { - return null; - } - return 'assembleAar${toTitleCase(productFlavor)}${toTitleCase(buildType)}'; - } -} diff --git a/packages/flutter_tools/lib/src/android/gradle_errors.dart b/packages/flutter_tools/lib/src/android/gradle_errors.dart new file mode 100644 index 00000000000..bcec1a7b933 --- /dev/null +++ b/packages/flutter_tools/lib/src/android/gradle_errors.dart @@ -0,0 +1,326 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; + +import '../base/process.dart'; +import '../base/terminal.dart'; +import '../globals.dart'; +import '../project.dart'; +import '../reporting/reporting.dart'; +import 'gradle_utils.dart'; + +typedef GradleErrorTest = bool Function(String); + +/// A Gradle error handled by the tool. +class GradleHandledError{ + const GradleHandledError({ + this.test, + this.handler, + this.eventLabel, + }); + + /// The test function. + /// Returns [true] if the current error message should be handled. + final GradleErrorTest test; + + /// The handler function. + final Future 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 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 gradleErrors = [ + licenseNotAcceptedHandler, + networkErrorHandler, + permissionDeniedErrorHandler, + flavorUndefinedHandler, + r8FailureHandler, + androidXFailureHandler, +]; + +// Permission defined error message. +@visibleForTesting +final GradleHandledError permissionDeniedErrorHandler = GradleHandledError( + test: _lineMatcher(const [ + '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 [ + '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 [ + '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 [ + '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( + [ + 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 variants = {}; + 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 productFlavors = {}; + for (final String variant1 in variants) { + for (final String variant2 in variants) { + if (variant2.startsWith(variant1) && variant2 != variant1) { + final String buildType = variant2.substring(variant1.length); + if (variants.contains(buildType)) { + productFlavors.add(variant1); + } + } + } + } + printStatus( + '\n$warningMark Gradle project does not define a task suitable ' + 'for the requested build.' + ); + if (productFlavors.isEmpty) { + printStatus( + 'The android/app/build.gradle file does not define ' + 'any custom product flavors. ' + 'You cannot use the --flavor option.' + ); + } else { + printStatus( + 'The android/app/build.gradle file defines product ' + 'flavors: ${productFlavors.join(', ')} ' + 'You must specify a --flavor option to select one of them.' + ); + } + return GradleBuildStatus.exit; + }, + eventLabel: 'flavor-undefined', +); diff --git a/packages/flutter_tools/lib/src/android/gradle_utils.dart b/packages/flutter_tools/lib/src/android/gradle_utils.dart new file mode 100644 index 00000000000..0b9eaa08db3 --- /dev/null +++ b/packages/flutter_tools/lib/src/android/gradle_utils.dart @@ -0,0 +1,284 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; + +import '../android/android_sdk.dart'; +import '../base/common.dart'; +import '../base/context.dart'; +import '../base/file_system.dart'; +import '../base/os.dart'; +import '../base/platform.dart'; +import '../base/terminal.dart'; +import '../base/utils.dart'; +import '../base/version.dart'; +import '../build_info.dart'; +import '../cache.dart'; +import '../globals.dart'; +import '../project.dart'; +import '../reporting/reporting.dart'; +import 'android_sdk.dart'; +import 'android_studio.dart'; + +/// The environment variables needed to run Gradle. +Map get gradleEnvironment { + final Map environment = Map.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(); + +/// 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 pluginMatches = _androidPluginRegExp.allMatches(buildFileContent); + if (pluginMatches.isEmpty) { + return _defaultGradleVersion; + } + final String androidPluginVersion = pluginMatches.first.group(1); + return getGradleVersionFor(androidPluginVersion); +} + +/// Returns true if [targetVersion] is within the range [min] and [max] inclusive. +bool _isWithinVersionRange( + String targetVersion, { + @required String min, + @required String max, +}) { + assert(min != null); + assert(max != null); + final Version parsedTargetVersion = Version.parse(targetVersion); + return parsedTargetVersion >= Version.parse(min) && + parsedTargetVersion <= Version.parse(max); +} + +/// Returns the Gradle version that is required by the given Android Gradle plugin version +/// by picking the largest compatible version from +/// https://developer.android.com/studio/releases/gradle-plugin#updating-gradle +String getGradleVersionFor(String androidPluginVersion) { + if (_isWithinVersionRange(androidPluginVersion, min: '1.0.0', max: '1.1.3')) { + return '2.3'; + } + if (_isWithinVersionRange(androidPluginVersion, min: '1.2.0', max: '1.3.1')) { + return '2.9'; + } + if (_isWithinVersionRange(androidPluginVersion, min: '1.5.0', max: '1.5.0')) { + return '2.2.1'; + } + if (_isWithinVersionRange(androidPluginVersion, min: '2.0.0', max: '2.1.2')) { + return '2.13'; + } + if (_isWithinVersionRange(androidPluginVersion, min: '2.1.3', max: '2.2.3')) { + return '2.14.1'; + } + if (_isWithinVersionRange(androidPluginVersion, min: '2.3.0', max: '2.9.9')) { + return '3.3'; + } + if (_isWithinVersionRange(androidPluginVersion, min: '3.0.0', max: '3.0.9')) { + return '4.1'; + } + if (_isWithinVersionRange(androidPluginVersion, min: '3.1.0', max: '3.1.9')) { + return '4.4'; + } + if (_isWithinVersionRange(androidPluginVersion, min: '3.2.0', max: '3.2.1')) { + return '4.6'; + } + if (_isWithinVersionRange(androidPluginVersion, min: '3.3.0', max: '3.3.2')) { + return '4.10.2'; + } + if (_isWithinVersionRange(androidPluginVersion, min: '3.4.0', max: '3.5.0')) { + return '5.6.2'; + } + throwToolExit('Unsuported Android Plugin version: $androidPluginVersion.'); + return ''; +} + +/// Overwrite local.properties in the specified Flutter project's Android +/// sub-project, if needed. +/// +/// If [requireAndroidSdk] is true (the default) and no Android SDK is found, +/// this will fail with a [ToolExit]. +void updateLocalProperties({ + @required FlutterProject project, + BuildInfo buildInfo, + bool requireAndroidSdk = true, +}) { + if (requireAndroidSdk && androidSdk == null) { + exitWithNoSdkMessage(); + } + final File localProperties = project.android.localPropertiesFile; + bool changed = false; + + SettingsFile settings; + if (localProperties.existsSync()) { + settings = SettingsFile.parseFromFile(localProperties); + } else { + settings = SettingsFile(); + changed = true; + } + + void changeIfNecessary(String key, String value) { + if (settings.values[key] == value) { + return; + } + if (value == null) { + settings.values.remove(key); + } else { + settings.values[key] = value; + } + changed = true; + } + + if (androidSdk != null) { + changeIfNecessary('sdk.dir', escapePath(androidSdk.directory)); + } + + changeIfNecessary('flutter.sdk', escapePath(Cache.flutterRoot)); + if (buildInfo != null) { + changeIfNecessary('flutter.buildMode', buildInfo.modeName); + final String buildName = validatedBuildNameForPlatform( + TargetPlatform.android_arm, + buildInfo.buildName ?? project.manifest.buildName, + ); + changeIfNecessary('flutter.versionName', buildName); + final String buildNumber = validatedBuildNumberForPlatform( + TargetPlatform.android_arm, + buildInfo.buildNumber ?? project.manifest.buildNumber, + ); + changeIfNecessary('flutter.versionCode', buildNumber?.toString()); + } + + if (changed) { + settings.writeContents(localProperties); + } +} + +/// Writes standard Android local properties to the specified [properties] file. +/// +/// Writes the path to the Android SDK, if known. +void writeLocalProperties(File properties) { + final SettingsFile settings = SettingsFile(); + if (androidSdk != null) { + settings.values['sdk.dir'] = escapePath(androidSdk.directory); + } + settings.writeContents(properties); +} + +void exitWithNoSdkMessage() { + BuildEvent('unsupported-project', eventError: 'android-sdk-not-found').send(); + throwToolExit( + '$warningMark No Android SDK found. ' + 'Try setting the ANDROID_HOME environment variable.' + ); +} diff --git a/packages/flutter_tools/lib/src/base/terminal.dart b/packages/flutter_tools/lib/src/base/terminal.dart index 22ba9aa99c2..09bfd59cd1e 100644 --- a/packages/flutter_tools/lib/src/base/terminal.dart +++ b/packages/flutter_tools/lib/src/base/terminal.dart @@ -23,6 +23,17 @@ enum TerminalColor { AnsiTerminal get terminal { return context?.get() ?? _defaultAnsiTerminal; } + +/// Warning mark to use in stdout or stderr. +String get warningMark { + return terminal.bolden(terminal.color('[!]', TerminalColor.red)); +} + +/// Success mark to use in stdout. +String get successMark { + return terminal.bolden(terminal.color('✓', TerminalColor.green)); +} + final AnsiTerminal _defaultAnsiTerminal = AnsiTerminal(); OutputPreferences get outputPreferences { diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart index c52fb187c05..f18d33ccfe8 100644 --- a/packages/flutter_tools/lib/src/cache.dart +++ b/packages/flutter_tools/lib/src/cache.dart @@ -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'); } diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart index 4d26cb97ab4..808456c2bff 100644 --- a/packages/flutter_tools/lib/src/commands/create.dart +++ b/packages/flutter_tools/lib/src/commands/create.dart @@ -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'; diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index c909973ec1d..e210afd6971 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.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'; diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 9568e5863bc..5bc5c7753ac 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.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 ensureReadyForPlatformSpecificTooling() async { if (isModule && _shouldRegenerateFromTemplate()) { _regenerateLibrary(); diff --git a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart new file mode 100644 index 00000000000..aca791911ad --- /dev/null +++ b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart @@ -0,0 +1,602 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/memory.dart'; + +import 'package:flutter_tools/src/android/gradle_utils.dart'; +import 'package:flutter_tools/src/android/gradle_errors.dart'; +import 'package:flutter_tools/src/base/context.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:flutter_tools/src/reporting/reporting.dart'; +import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; +import 'package:process/process.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/mocks.dart'; + +void main() { + group('gradleErrors', () { + test('list of errors', () { + // If you added a new Gradle error, please update this test. + expect(gradleErrors, + equals([ + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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: { + 'cd43': 'app-not-using-plugins', + }, + )).called(1); + + expect(status, equals(GradleBuildStatus.exit)); + }, overrides: { + 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(); + 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: { + 'cd43': 'app-not-using-androidx', + }, + )).called(1); + + expect(status, equals(GradleBuildStatus.exit)); + }, overrides: { + 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: { + 'cd43': 'using-jetifier', + }, + )).called(1); + + expect(status, equals(GradleBuildStatus.exit)); + }, overrides: { + 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(); + 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: { + 'cd43': 'not-using-jetifier', + }, + )).called(1); + expect(status, equals(GradleBuildStatus.retryWithAarPlugins)); + }, overrides: { + 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(); + 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(); + 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( + [ + '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(); + 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: { + GradleUtils: () => FakeGradleUtils(), + Platform: () => fakePlatform('android'), + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('handler - without flavor', () async { + when(mockProcessManager.run( + [ + '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(); + 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: { + GradleUtils: () => FakeGradleUtils(), + Platform: () => fakePlatform('android'), + ProcessManager: () => mockProcessManager, + }); + }); +} + +class MockUsage extends Mock implements Usage {} + +bool testErrorMessage(String errorMessage, GradleHandledError error) { + return errorMessage + .split('\n') + .any((String line) => error.test(line)); +} + +Platform fakePlatform(String name) { + return FakePlatform + .fromPlatform(const LocalPlatform()) + ..operatingSystem = name; +} + +class FakeGradleUtils extends GradleUtils { + @override + String getExecutable(FlutterProject project) { + return 'gradlew'; + } +} diff --git a/packages/flutter_tools/test/general.shard/android/gradle_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_test.dart index 21ec43e55c7..05711759ab5 100644 --- a/packages/flutter_tools/test/general.shard/android/gradle_test.dart +++ b/packages/flutter_tools/test/general.shard/android/gradle_test.dart @@ -8,79 +8,370 @@ import 'package: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(['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), []); + expect( + getApkDirectory(project).path, + equals(fs.path.join('foo', 'host', 'outputs', 'apk')), + ); + }); + + test('getBundleDirectory in app projects', () { + final FlutterProject project = MockFlutterProject(); + final AndroidProject androidProject = MockAndroidProject(); + when(project.android).thenReturn(androidProject); + when(project.isModule).thenReturn(false); + when(androidProject.buildDirectory).thenReturn(fs.directory('foo')); + + expect( + getBundleDirectory(project).path, + equals(fs.path.join('foo', 'app', 'outputs', 'bundle')), + ); + }); + + test('getBundleDirectory in module projects', () { + final FlutterProject project = MockFlutterProject(); + final AndroidProject androidProject = MockAndroidProject(); + when(project.android).thenReturn(androidProject); + when(project.isModule).thenReturn(true); + when(androidProject.buildDirectory).thenReturn(fs.directory('foo')); + + expect( + getBundleDirectory(project).path, + equals(fs.path.join('foo', 'host', 'outputs', 'bundle')), + ); + }); + + test('getRepoDirectory', () { + expect( + getRepoDirectory(fs.directory('foo')).path, + equals(fs.path.join('foo','outputs', 'repo')), + ); + }); + }); + + group('gradle tasks', () { + test('assemble release', () { + expect( + getAssembleTaskFor(const BuildInfo(BuildMode.release, null)), + equals('assembleRelease'), + ); + expect( + getAssembleTaskFor(const BuildInfo(BuildMode.release, 'flavorFoo')), + equals('assembleFlavorFooRelease'), + ); + }); + + test('assemble debug', () { + expect( + getAssembleTaskFor(const BuildInfo(BuildMode.debug, null)), + equals('assembleDebug'), + ); + expect( + getAssembleTaskFor(const BuildInfo(BuildMode.debug, 'flavorFoo')), + equals('assembleFlavorFooDebug'), + ); + }); + + test('assemble profile', () { + expect( + getAssembleTaskFor(const BuildInfo(BuildMode.profile, null)), + equals('assembleProfile'), + ); + expect( + getAssembleTaskFor(const BuildInfo(BuildMode.profile, 'flavorFoo')), + equals('assembleFlavorFooProfile'), + ); + }); + }); + + group('findBundleFile', () { + testUsingContext('Finds app bundle when flavor contains underscores in release mode', () { + final FlutterProject project = generateFakeAppBundle('foo_barRelease', 'app.aab'); + final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo_bar')); + expect(bundle, isNotNull); + expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barRelease', 'app.aab')); }, overrides: { FileSystem: () => MemoryFileSystem(), ProcessManager: () => FakeProcessManager.any(), }); - test('androidXFailureRegex should match lines with likely AndroidX errors', () { - final List nonMatchingLines = [ - ':app:preBuild UP-TO-DATE', - 'BUILD SUCCESSFUL in 0s', - '', - ]; - final List matchingLines = [ - 'AAPT: error: resource android:attr/fontVariationSettings not found.', - 'AAPT: error: resource android:attr/ttcIndex not found.', - 'error: package android.support.annotation does not exist', - 'import android.support.annotation.NonNull;', - 'import androidx.annotation.NonNull;', - 'Daemon: AAPT2 aapt2-3.2.1-4818971-linux Daemon #0', - ]; - for (String m in nonMatchingLines) { - expect(androidXFailureRegex.hasMatch(m), isFalse); - } - for (String m in matchingLines) { - expect(androidXFailureRegex.hasMatch(m), isTrue); - } + testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in release mode', () { + final FlutterProject project = generateFakeAppBundle('fooRelease', 'app.aab'); + final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo')); + expect(bundle, isNotNull); + expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooRelease', 'app.aab')); + }, overrides: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('Finds app bundle when no flavor is used in release mode', () { + final FlutterProject project = generateFakeAppBundle('release', 'app.aab'); + final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, null)); + expect(bundle, isNotNull); + expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'release', 'app.aab')); + }, overrides: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('Finds app bundle when flavor contains underscores in debug mode', () { + final FlutterProject project = generateFakeAppBundle('foo_barDebug', 'app.aab'); + final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo_bar')); + expect(bundle, isNotNull); + expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barDebug', 'app.aab')); + }, overrides: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in debug mode', () { + final FlutterProject project = generateFakeAppBundle('fooDebug', 'app.aab'); + final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo')); + expect(bundle, isNotNull); + expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooDebug', 'app.aab')); + }, overrides: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('Finds app bundle when no flavor is used in debug mode', () { + final FlutterProject project = generateFakeAppBundle('debug', 'app.aab'); + final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, null)); + expect(bundle, isNotNull); + expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'debug', 'app.aab')); + }, overrides: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('Finds app bundle when flavor contains underscores in profile mode', () { + final FlutterProject project = generateFakeAppBundle('foo_barProfile', 'app.aab'); + final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'foo_bar')); + expect(bundle, isNotNull); + expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barProfile', 'app.aab')); + }, overrides: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in profile mode', () { + final FlutterProject project = generateFakeAppBundle('fooProfile', 'app.aab'); + final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'foo')); + expect(bundle, isNotNull); + expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooProfile', 'app.aab')); + }, overrides: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('Finds app bundle when no flavor is used in profile mode', () { + final FlutterProject project = generateFakeAppBundle('profile', 'app.aab'); + final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, null)); + expect(bundle, isNotNull); + expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'profile', 'app.aab')); + }, overrides: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('Finds app bundle in release mode - Gradle 3.5', () { + final FlutterProject project = generateFakeAppBundle('release', 'app-release.aab'); + final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, null)); + expect(bundle, isNotNull); + expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'release', 'app-release.aab')); + }, overrides: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('Finds app bundle in profile mode - Gradle 3.5', () { + final FlutterProject project = generateFakeAppBundle('profile', 'app-profile.aab'); + final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, null)); + expect(bundle, isNotNull); + expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'profile', 'app-profile.aab')); + }, overrides: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('Finds app bundle in debug mode - Gradle 3.5', () { + final FlutterProject project = generateFakeAppBundle('debug', 'app-debug.aab'); + final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, null)); + expect(bundle, isNotNull); + expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'debug', 'app-debug.aab')); + }, overrides: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('Finds app bundle when flavor contains underscores in release mode - Gradle 3.5', () { + final FlutterProject project = generateFakeAppBundle('foo_barRelease', 'app-foo_bar-release.aab'); + final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo_bar')); + expect(bundle, isNotNull); + expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barRelease', 'app-foo_bar-release.aab')); + }, overrides: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('Finds app bundle when flavor contains underscores in profile mode - Gradle 3.5', () { + final FlutterProject project = generateFakeAppBundle('foo_barProfile', 'app-foo_bar-profile.aab'); + final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'foo_bar')); + expect(bundle, isNotNull); + expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barProfile', 'app-foo_bar-profile.aab')); + }, overrides: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('Finds app bundle when flavor contains underscores in debug mode - Gradle 3.5', () { + final FlutterProject project = generateFakeAppBundle('foo_barDebug', 'app-foo_bar-debug.aab'); + final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo_bar')); + expect(bundle, isNotNull); + expect(bundle.path, fs.path.join('irrelevant','app', 'outputs', 'bundle', 'foo_barDebug', 'app-foo_bar-debug.aab')); + }, overrides: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + }); + }); + + group('findApkFiles', () { + testUsingContext('Finds APK without flavor in release', () { + final FlutterProject project = MockFlutterProject(); + final AndroidProject androidProject = MockAndroidProject(); + + when(project.android).thenReturn(androidProject); + when(project.isModule).thenReturn(false); + when(androidProject.buildDirectory).thenReturn(fs.directory('irrelevant')); + + final Directory apkDirectory = fs.directory(fs.path.join('irrelevant', 'app', 'outputs', 'apk', 'release')); + apkDirectory.createSync(recursive: true); + apkDirectory.childFile('app-release.apk').createSync(); + + final Iterable 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: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('Finds APK with flavor in release mode', () { + final FlutterProject project = MockFlutterProject(); + final AndroidProject androidProject = MockAndroidProject(); + + when(project.android).thenReturn(androidProject); + when(project.isModule).thenReturn(false); + when(androidProject.buildDirectory).thenReturn(fs.directory('irrelevant')); + + final Directory apkDirectory = fs.directory(fs.path.join('irrelevant', 'app', 'outputs', 'apk', 'release')); + apkDirectory.createSync(recursive: true); + apkDirectory.childFile('app-flavor1-release.apk').createSync(); + + final Iterable 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: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('Finds APK with flavor in release mode - AGP v3', () { + final FlutterProject project = MockFlutterProject(); + final AndroidProject androidProject = MockAndroidProject(); + + when(project.android).thenReturn(androidProject); + when(project.isModule).thenReturn(false); + when(androidProject.buildDirectory).thenReturn(fs.directory('irrelevant')); + + final Directory apkDirectory = fs.directory(fs.path.join('irrelevant', 'app', 'outputs', 'apk', 'flavor1', 'release')); + apkDirectory.createSync(recursive: true); + apkDirectory.childFile('app-flavor1-release.apk').createSync(); + + final Iterable 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: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + }); + }); + + group('gradle build', () { + testUsingContext('do not crash if there is no Android SDK', () async { + expect(() { + updateLocalProperties(project: FlutterProject.current()); + }, throwsToolExit( + message: '$warningMark No Android SDK found. Try setting the ANDROID_HOME environment variable.', + )); + }, overrides: { + AndroidSdk: () => null, + }); + + // Regression test for https://github.com/flutter/flutter/issues/34700 + testUsingContext('Does not return nulls in apk list', () { + const AndroidBuildInfo buildInfo = AndroidBuildInfo(BuildInfo.debug); + expect(findApkFiles(FlutterProject.current(), buildInfo), []); + }, overrides: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), }); test('androidXPluginWarningRegex should match lines with the AndroidX plugin warnings', () { @@ -103,355 +394,6 @@ void main() { expect(androidXPluginWarningRegex.hasMatch(m), isTrue); } }); - - test('ndkMessageFilter should only match lines without the error message', () { - final List nonMatchingLines = [ - '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 matchingLines = [ - ':app:preBuild UP-TO-DATE', - 'BUILD SUCCESSFUL in 0s', - '', - 'Something NDK related mentioning ANDROID_NDK_HOME', - ]; - for (String m in nonMatchingLines) { - expect(ndkMessageFilter.hasMatch(m), isFalse); - } - for (String m in matchingLines) { - expect(ndkMessageFilter.hasMatch(m), isTrue); - } - }); - - testUsingContext('Finds app bundle when flavor contains underscores in release mode', () { - final GradleProject gradleProject = generateFakeAppBundle('foo_barRelease', 'app.aab'); - final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, 'foo_bar')); - expect(bundle, isNotNull); - expect(bundle.path, '/foo_barRelease/app.aab'); - }, overrides: { - FileSystem: () => MemoryFileSystem(), - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in release mode', () { - final GradleProject gradleProject = generateFakeAppBundle('fooRelease', 'app.aab'); - final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, 'foo')); - expect(bundle, isNotNull); - expect(bundle.path, '/fooRelease/app.aab'); - }, overrides: { - FileSystem: () => MemoryFileSystem(), - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('Finds app bundle when no flavor is used in release mode', () { - final GradleProject gradleProject = generateFakeAppBundle('release', 'app.aab'); - final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, null)); - expect(bundle, isNotNull); - expect(bundle.path, '/release/app.aab'); - }, overrides: { - FileSystem: () => MemoryFileSystem(), - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('Finds app bundle when flavor contains underscores in debug mode', () { - final GradleProject gradleProject = generateFakeAppBundle('foo_barDebug', 'app.aab'); - final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, 'foo_bar')); - expect(bundle, isNotNull); - expect(bundle.path, '/foo_barDebug/app.aab'); - }, overrides: { - FileSystem: () => MemoryFileSystem(), - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in debug mode', () { - final GradleProject gradleProject = generateFakeAppBundle('fooDebug', 'app.aab'); - final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, 'foo')); - expect(bundle, isNotNull); - expect(bundle.path, '/fooDebug/app.aab'); - }, overrides: { - FileSystem: () => MemoryFileSystem(), - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('Finds app bundle when no flavor is used in debug mode', () { - final GradleProject gradleProject = generateFakeAppBundle('debug', 'app.aab'); - final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, null)); - expect(bundle, isNotNull); - expect(bundle.path, '/debug/app.aab'); - }, overrides: { - FileSystem: () => MemoryFileSystem(), - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('Finds app bundle when flavor contains underscores in profile mode', () { - final GradleProject gradleProject = generateFakeAppBundle('foo_barProfile', 'app.aab'); - final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, 'foo_bar')); - expect(bundle, isNotNull); - expect(bundle.path, '/foo_barProfile/app.aab'); - }, overrides: { - FileSystem: () => MemoryFileSystem(), - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in profile mode', () { - final GradleProject gradleProject = generateFakeAppBundle('fooProfile', 'app.aab'); - final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, 'foo')); - expect(bundle, isNotNull); - expect(bundle.path, '/fooProfile/app.aab'); - }, overrides: { - FileSystem: () => MemoryFileSystem(), - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('Finds app bundle when no flavor is used in profile mode', () { - final GradleProject gradleProject = generateFakeAppBundle('profile', 'app.aab'); - final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, null)); - expect(bundle, isNotNull); - expect(bundle.path, '/profile/app.aab'); - }, overrides: { - FileSystem: () => MemoryFileSystem(), - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('Finds app bundle in release mode - Gradle 3.5', () { - final GradleProject gradleProject = generateFakeAppBundle('release', 'app-release.aab'); - final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, null)); - expect(bundle, isNotNull); - expect(bundle.path, '/release/app-release.aab'); - }, overrides: { - FileSystem: () => MemoryFileSystem(), - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('Finds app bundle in profile mode - Gradle 3.5', () { - final GradleProject gradleProject = generateFakeAppBundle('profile', 'app-profile.aab'); - final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, null)); - expect(bundle, isNotNull); - expect(bundle.path, '/profile/app-profile.aab'); - }, overrides: { - FileSystem: () => MemoryFileSystem(), - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('Finds app bundle in debug mode - Gradle 3.5', () { - final GradleProject gradleProject = generateFakeAppBundle('debug', 'app-debug.aab'); - final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, null)); - expect(bundle, isNotNull); - expect(bundle.path, '/debug/app-debug.aab'); - }, overrides: { - FileSystem: () => MemoryFileSystem(), - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('Finds app bundle when flavor contains underscores in release mode - Gradle 3.5', () { - final GradleProject gradleProject = generateFakeAppBundle('foo_barRelease', 'app-foo_bar-release.aab'); - final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, 'foo_bar')); - expect(bundle, isNotNull); - expect(bundle.path, '/foo_barRelease/app-foo_bar-release.aab'); - }, overrides: { - FileSystem: () => MemoryFileSystem(), - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('Finds app bundle when flavor contains underscores in profile mode - Gradle 3.5', () { - final GradleProject gradleProject = generateFakeAppBundle('foo_barProfile', 'app-foo_bar-profile.aab'); - final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, 'foo_bar')); - expect(bundle, isNotNull); - expect(bundle.path, '/foo_barProfile/app-foo_bar-profile.aab'); - }, overrides: { - FileSystem: () => MemoryFileSystem(), - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('Finds app bundle when flavor contains underscores in debug mode - Gradle 3.5', () { - final GradleProject gradleProject = generateFakeAppBundle('foo_barDebug', 'app-foo_bar-debug.aab'); - final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, 'foo_bar')); - expect(bundle, isNotNull); - expect(bundle.path, '/foo_barDebug/app-foo_bar-debug.aab'); - }, overrides: { - FileSystem: () => MemoryFileSystem(), - ProcessManager: () => FakeProcessManager.any(), - }); - }); - - group('gradle project', () { - GradleProject projectFrom(String properties, String tasks) => GradleProject.fromAppProperties(properties, tasks); - - test('should extract build directory from app properties', () { - final GradleProject project = projectFrom(''' -someProperty: someValue -buildDir: /Users/some/apps/hello/build/app -someOtherProperty: someOtherValue - ''', ''); - expect( - fs.path.normalize(project.apkDirectory.path), - fs.path.normalize('/Users/some/apps/hello/build/app/outputs/apk'), - ); - }); - test('should extract default build variants from app properties', () { - final GradleProject project = projectFrom('buildDir: /Users/some/apps/hello/build/app', ''' -someTask -assemble -assembleAndroidTest -assembleDebug -assembleProfile -assembleRelease -someOtherTask - '''); - expect(project.buildTypes, ['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, ['debug', 'profile', 'release']); - expect(project.productFlavors, ['free', 'paid']); - }); - test('should provide apk file name for default build types', () { - final GradleProject project = GradleProject(['debug', 'profile', 'release'], [], '/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(['debug', 'profile', 'release'], ['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(['debug', 'profile', 'release'], [], '/some/dir'); - expect(project.apkFilesFor( - const AndroidBuildInfo( - BuildInfo.debug, - splitPerAbi: true, - targetArchs: [ - AndroidArch.armeabi_v7a, - AndroidArch.arm64_v8a, - ], - )), - [ - 'app-armeabi-v7a-debug.apk', - 'app-arm64-v8a-debug.apk', - ]); - - expect(project.apkFilesFor( - const AndroidBuildInfo( - BuildInfo.release, - splitPerAbi: true, - targetArchs: [ - AndroidArch.armeabi_v7a, - AndroidArch.arm64_v8a, - ], - )), - [ - 'app-armeabi-v7a-release.apk', - 'app-arm64-v8a-release.apk', - ]); - - expect(project.apkFilesFor( - const AndroidBuildInfo( - BuildInfo(BuildMode.release, 'unknown'), - splitPerAbi: true, - targetArchs: [ - AndroidArch.armeabi_v7a, - AndroidArch.arm64_v8a, - ], - )).isEmpty, isTrue); - }); - test('should provide apks for each ABI and flavored build types', () { - final GradleProject project = GradleProject(['debug', 'profile', 'release'], ['free', 'paid'], '/some/dir'); - expect(project.apkFilesFor( - const AndroidBuildInfo( - BuildInfo(BuildMode.debug, 'free'), - splitPerAbi: true, - targetArchs: [ - AndroidArch.armeabi_v7a, - AndroidArch.arm64_v8a, - ], - )), - [ - '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.armeabi_v7a, - AndroidArch.arm64_v8a, - ], - )), - [ - '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.armeabi_v7a, - AndroidArch.arm64_v8a, - ], - )).isEmpty, isTrue); - }); - test('should provide assemble task name for default build types', () { - final GradleProject project = GradleProject(['debug', 'profile', 'release'], [], '/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(['debug', 'profile', 'release'], ['free', 'paid'], '/some/dir'); - expect(project.assembleTaskFor(const BuildInfo(BuildMode.debug, 'free')), 'assembleFreeDebug'); - expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'paid')), 'assemblePaidRelease'); - expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull); - }); - test('should respect format of the flavored build types', () { - final GradleProject project = GradleProject(['debug'], ['randomFlavor'], '/some/dir'); - expect(project.assembleTaskFor(const BuildInfo(BuildMode.debug, 'randomFlavor')), 'assembleRandomFlavorDebug'); - }); - test('bundle should provide assemble task name for default build types', () { - final GradleProject project = GradleProject(['debug', 'profile', 'release'], [], '/some/dir'); - expect(project.bundleTaskFor(BuildInfo.debug), 'bundleDebug'); - expect(project.bundleTaskFor(BuildInfo.profile), 'bundleProfile'); - expect(project.bundleTaskFor(BuildInfo.release), 'bundleRelease'); - expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull); - }); - test('bundle should provide assemble task name for flavored build types', () { - final GradleProject project = GradleProject(['debug', 'profile', 'release'], ['free', 'paid'], '/some/dir'); - expect(project.bundleTaskFor(const BuildInfo(BuildMode.debug, 'free')), 'bundleFreeDebug'); - expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'paid')), 'bundlePaidRelease'); - expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull); - }); - test('bundle should respect format of the flavored build types', () { - final GradleProject project = GradleProject(['debug'], ['randomFlavor'], '/some/dir'); - expect(project.bundleTaskFor(const BuildInfo(BuildMode.debug, 'randomFlavor')), 'bundleRandomFlavorDebug'); - }); }); group('Config files', () { @@ -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(['debug', 'release'], - const ['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: { - Logger: () => mockLogger, - }); - - testUsingContext('print no flavors', () { - final GradleProject project = GradleProject(['debug', 'release'], - const [], '/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: { - Logger: () => mockLogger, - }); - - testUsingContext('print flavors', () { - final GradleProject project = GradleProject(['debug', 'release'], - const ['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: { - 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 cmd = [ - 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, - ['-v'], - errorMessage, - 1, - ); - when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))) - .thenThrow(exception); - await expectLater(() async { - await checkGradleDependencies(); - }, throwsToolExit(message: errorMessage)); - }, overrides: { - Cache: () => Cache(rootOverride: tempDir), - FileSystem: () => fs, - ProcessManager: () => mockProcessManager, - }); - - testUsingContext('throw toolExit if gradle fails downloading with proxy error', () async { - final List cmd = [ - 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, - ['-v'], - errorMessage, - 1, - ); - when(mockProcessManager.run(cmd, environment: anyNamed('environment'), workingDirectory: null)) - .thenThrow(exception); - await expectLater(() async { - await checkGradleDependencies(); - }, throwsToolExit(message: errorMessage)); - }, overrides: { - Cache: () => Cache(rootOverride: tempDir), - FileSystem: () => fs, - ProcessManager: () => mockProcessManager, - }); - - testUsingContext('throws toolExit if gradle is missing execute permissions. ', () async { - final List cmd = [ - fs.path.join(fs.currentDirectory.path, 'android', gradleBinary), - '-v', - ]; - final ProcessException exception = ProcessException( - gradleBinary, - ['-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: { - Cache: () => Cache(rootOverride: tempDir), - FileSystem: () => fs, - ProcessManager: () => mockProcessManager, - }); - - testUsingContext('throws toolExit if gradle times out waiting for exclusive access to zip', () async { - final List cmd = [ - 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, - ['-v'], - errorMessage, - 1, - ); - when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))) - .thenThrow(exception); - await expectLater(() async { - await checkGradleDependencies(); - }, throwsToolExit(message: errorMessage)); - }, overrides: { - Cache: () => Cache(rootOverride: tempDir), - FileSystem: () => fs, - ProcessManager: () => mockProcessManager, - }); - - testUsingContext('throws toolExit if gradle fails to unzip file', () async { - final List cmd = [ - 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, - ['-v'], - errorMessage, - 1, - ); - when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))) - .thenThrow(exception); - await expectLater(() async { - await checkGradleDependencies(); - }, throwsToolExit(message: errorMessage)); - }, overrides: { - Cache: () => Cache(rootOverride: tempDir), - FileSystem: () => fs, - ProcessManager: () => mockProcessManager, - }); - - testUsingContext('throws toolExit if remote host closes connection', () async { - final List cmd = [ - 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, - ['-v'], - errorMessage, - 1, - ); - when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))) - .thenThrow(exception); - await expectLater(() async { - await checkGradleDependencies(); - }, throwsToolExit(message: errorMessage)); - }, overrides: { - Cache: () => Cache(rootOverride: tempDir), - FileSystem: () => fs, - ProcessManager: () => mockProcessManager, - }); - - testUsingContext('throws toolExit if file opening fails', () async { - final List cmd = [ - 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, - ['-v'], - errorMessage, - 1, - ); - when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))) - .thenThrow(exception); - await expectLater(() async { - await checkGradleDependencies(); - }, throwsToolExit(message: errorMessage)); - }, overrides: { - Cache: () => Cache(rootOverride: tempDir), - FileSystem: () => fs, - ProcessManager: () => mockProcessManager, - }); - - testUsingContext('throws toolExit if the connection is reset', () async { - final List cmd = [ - 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, - ['-v'], - errorMessage, - 1, - ); - when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))) - .thenThrow(exception); - await expectLater(() async { - await checkGradleDependencies(); - }, throwsToolExit(message: errorMessage)); - }, overrides: { - Cache: () => Cache(rootOverride: tempDir), - FileSystem: () => fs, - ProcessManager: () => mockProcessManager, - }); - - testUsingContext('throws toolExit if gradle exits abnormally', () async { - final List cmd = [ - 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, - ['-v'], - errorMessage, - 1, - ); - when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment'))) - .thenThrow(exception); - await expectLater(() async { - await checkGradleDependencies(); - }, throwsToolExit(message: errorMessage)); - }, overrides: { - 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: { - 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: { - 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: { - 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.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( + 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: { + 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.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( + 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: { + 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.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( + 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: { + 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.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( + 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: { + 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.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: [], + ); + + final BufferLogger logger = context.get(); + expect( + logger.statusText, + contains('Built build/app/outputs/apk/release/app-release.apk (0.0MB)'), + ); + + }, overrides: { + 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( ['/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); +FlutterProject generateFakeAppBundle(String directoryName, String fileName) { + final FlutterProject project = MockFlutterProject(); + final AndroidProject androidProject = MockAndroidProject(); - final Directory aabDirectory = gradleProject.bundleDirectory.childDirectory(directoryName); - fs.directory(aabDirectory).createSync(recursive: true); - fs.file(fs.path.join(aabDirectory.path, fileName)).writeAsStringSync('irrelevant'); - return gradleProject; + when(project.isModule).thenReturn(false); + when(project.android).thenReturn(androidProject); + when(androidProject.buildDirectory).thenReturn(fs.directory('irrelevant')); + + final Directory bundleDirectory = getBundleDirectory(project); + bundleDirectory + .childDirectory(directoryName) + ..createSync(recursive: true); + + bundleDirectory + .childDirectory(directoryName) + .childFile(fileName) + .createSync(); + return project; } Platform fakePlatform(String name) { @@ -1716,17 +1553,19 @@ Platform fakePlatform(String name) { class FakeGradleUtils extends GradleUtils { @override - Future getExecutable(FlutterProject project) async { + String getExecutable(FlutterProject project) { return 'gradlew'; } } class MockAndroidSdk extends Mock implements AndroidSdk {} +class MockAndroidProject extends Mock implements AndroidProject {} class MockAndroidStudio extends Mock implements AndroidStudio {} class MockDirectory extends Mock implements Directory {} class MockFile extends Mock implements File {} -class MockGradleProject extends Mock implements GradleProject {} +class MockFlutterProject extends Mock implements FlutterProject {} class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {} class MockProcessManager extends Mock implements ProcessManager {} class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {} class MockitoAndroidSdk extends Mock implements AndroidSdk {} +class MockUsage extends Mock implements Usage {} diff --git a/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart new file mode 100644 index 00000000000..d8b945e0ffc --- /dev/null +++ b/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart @@ -0,0 +1,137 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/android/gradle_utils.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/os.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:process/process.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; + +void main() { + group('injectGradleWrapperIfNeeded', () { + MemoryFileSystem memoryFileSystem; + Directory tempDir; + Directory gradleWrapperDirectory; + + setUp(() { + memoryFileSystem = MemoryFileSystem(); + tempDir = memoryFileSystem.systemTempDirectory.createTempSync('flutter_artifacts_test.'); + gradleWrapperDirectory = memoryFileSystem.directory( + memoryFileSystem.path.join(tempDir.path, 'bin', 'cache', 'artifacts', 'gradle_wrapper')); + gradleWrapperDirectory.createSync(recursive: true); + gradleWrapperDirectory + .childFile('gradlew') + .writeAsStringSync('irrelevant'); + gradleWrapperDirectory + .childDirectory('gradle') + .childDirectory('wrapper') + .createSync(recursive: true); + gradleWrapperDirectory + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.jar') + .writeAsStringSync('irrelevant'); + }); + + testUsingContext('Inject the wrapper when all files are missing', () { + final Directory sampleAppAndroid = fs.directory('/sample-app/android'); + sampleAppAndroid.createSync(recursive: true); + + injectGradleWrapperIfNeeded(sampleAppAndroid); + + expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue); + + expect(sampleAppAndroid + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.jar') + .existsSync(), isTrue); + + expect(sampleAppAndroid + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.properties') + .existsSync(), isTrue); + + expect(sampleAppAndroid + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.properties') + .readAsStringSync(), + 'distributionBase=GRADLE_USER_HOME\n' + 'distributionPath=wrapper/dists\n' + 'zipStoreBase=GRADLE_USER_HOME\n' + 'zipStorePath=wrapper/dists\n' + 'distributionUrl=https\\://services.gradle.org/distributions/gradle-5.6.2-all.zip\n'); + }, overrides: { + 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: { + 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: { + Cache: () => Cache(rootOverride: tempDir), + FileSystem: () => memoryFileSystem, + ProcessManager: () => FakeProcessManager.any(), + OperatingSystemUtils: () => OperatingSystemUtils(), + }); + }); +} \ No newline at end of file diff --git a/packages/flutter_tools/test/general.shard/cache_test.dart b/packages/flutter_tools/test/general.shard/cache_test.dart index be5e4a947a5..dd9e2ac80bb 100644 --- a/packages/flutter_tools/test/general.shard/cache_test.dart +++ b/packages/flutter_tools/test/general.shard/cache_test.dart @@ -11,7 +11,7 @@ import 'package: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.value(ProcessResult(0, 0, '', '')); }); diff --git a/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart b/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart index a93b50e2f3d..38d505d238f 100644 --- a/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart +++ b/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart @@ -8,7 +8,6 @@ import 'package: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: { AndroidSdk: () => mockAndroidSdk, - GradleUtils: () => GradleUtils(), ProcessManager: () => mockProcessManager, FileSystem: () => memoryFileSystem, }); diff --git a/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart b/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart index fd063c1f756..4970b0bd0d5 100644 --- a/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart +++ b/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart @@ -8,7 +8,6 @@ import 'package: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: ['--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: { AndroidSdk: () => mockAndroidSdk, - GradleUtils: () => GradleUtils(), FileSystem: () => memoryFileSystem, ProcessManager: () => mockProcessManager, }); @@ -221,7 +243,6 @@ flutter: overrides: { AndroidSdk: () => mockAndroidSdk, FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), - GradleUtils: () => GradleUtils(), ProcessManager: () => mockProcessManager, }); @@ -252,7 +273,6 @@ flutter: overrides: { 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: { AndroidSdk: () => mockAndroidSdk, - GradleUtils: () => GradleUtils(), FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), ProcessManager: () => mockProcessManager, Usage: () => mockUsage, @@ -344,7 +363,7 @@ flutter: }, throwsToolExit()); final BufferLogger logger = context.get(); - 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: { AndroidSdk: () => mockAndroidSdk, - GradleUtils: () => GradleUtils(), FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), ProcessManager: () => mockProcessManager, Usage: () => mockUsage, @@ -414,7 +432,6 @@ flutter: }, overrides: { AndroidSdk: () => mockAndroidSdk, - GradleUtils: () => GradleUtils(), FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), ProcessManager: () => mockProcessManager, Usage: () => mockUsage, diff --git a/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart b/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart index 7663156ec33..8f79b4a0858 100644 --- a/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart +++ b/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart @@ -8,7 +8,6 @@ import 'package: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: ['--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: { AndroidSdk: () => mockAndroidSdk, - GradleUtils: () => GradleUtils(), ProcessManager: () => mockProcessManager, FileSystem: () => memoryFileSystem, }); @@ -210,7 +227,6 @@ flutter: overrides: { AndroidSdk: () => mockAndroidSdk, FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), - GradleUtils: () => GradleUtils(), ProcessManager: () => mockProcessManager, }); @@ -243,7 +259,6 @@ flutter: overrides: { 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: { AndroidSdk: () => mockAndroidSdk, - GradleUtils: () => GradleUtils(), FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), ProcessManager: () => mockProcessManager, Usage: () => mockUsage, @@ -335,7 +349,7 @@ flutter: }, throwsToolExit()); final BufferLogger logger = context.get(); - 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: { AndroidSdk: () => mockAndroidSdk, - GradleUtils: () => GradleUtils(), FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), ProcessManager: () => mockProcessManager, Usage: () => mockUsage, @@ -388,7 +401,7 @@ flutter: }, throwsToolExit()); final BufferLogger logger = context.get(); - 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: { AndroidSdk: () => mockAndroidSdk, - GradleUtils: () => GradleUtils(), FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir), ProcessManager: () => mockProcessManager, Usage: () => mockUsage, diff --git a/packages/flutter_tools/test/src/android_common.dart b/packages/flutter_tools/test/src/android_common.dart new file mode 100644 index 00000000000..dda0afb75fd --- /dev/null +++ b/packages/flutter_tools/test/src/android_common.dart @@ -0,0 +1,34 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; + +import 'package:flutter_tools/src/android/android_builder.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/project.dart'; + +/// A fake implementation of [AndroidBuilder]. +class FakeAndroidBuilder implements AndroidBuilder { + @override + Future buildAar({ + @required FlutterProject project, + @required AndroidBuildInfo androidBuildInfo, + @required String target, + @required String outputDir, + }) async {} + + @override + Future buildApk({ + @required FlutterProject project, + @required AndroidBuildInfo androidBuildInfo, + @required String target, + }) async {} + + @override + Future buildAab({ + @required FlutterProject project, + @required AndroidBuildInfo androidBuildInfo, + @required String target, + }) async {} +}