From 16d408a7a045600d06a6fbca0878924d98ddcea2 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Tue, 24 Sep 2019 16:16:22 -0700 Subject: [PATCH] Reland #40810: Re-enable AAR plugins when an AndroidX failure occurred (#41160) --- dev/bots/test.dart | 1 + .../bin/tasks/gradle_jetifier_test.dart | 5 +- .../gradle/aar_init_script.gradle | 37 ++- packages/flutter_tools/gradle/flutter.gradle | 210 +++++------------- .../flutter_tools/lib/src/android/gradle.dart | 207 ++++++++++++----- packages/flutter_tools/lib/src/features.dart | 3 - .../lib/src/reporting/events.dart | 4 + .../lib/src/reporting/usage.dart | 1 + .../general.shard/android/gradle_test.dart | 166 +++++++++++++- .../test/general.shard/features_test.dart | 15 -- packages/flutter_tools/test/src/testbed.dart | 4 - 11 files changed, 406 insertions(+), 247 deletions(-) diff --git a/dev/bots/test.dart b/dev/bots/test.dart index 1d2cdc8d0d2..e37521359d3 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -1022,6 +1022,7 @@ Future _androidGradleTests(String subShard) async { await _runDevicelabTest('gradle_plugin_fat_apk_test', env: env); await _runDevicelabTest('gradle_r8_test', env: env); await _runDevicelabTest('gradle_non_android_plugin_test', env: env); + await _runDevicelabTest('gradle_jetifier_test', env: env); } if (subShard == 'gradle2') { await _runDevicelabTest('gradle_plugin_bundle_test', env: env); diff --git a/dev/devicelab/bin/tasks/gradle_jetifier_test.dart b/dev/devicelab/bin/tasks/gradle_jetifier_test.dart index bbabfc2e8b1..86ee0ed5b78 100644 --- a/dev/devicelab/bin/tasks/gradle_jetifier_test.dart +++ b/dev/devicelab/bin/tasks/gradle_jetifier_test.dart @@ -64,6 +64,7 @@ Future main() async { options: [ 'apk', '--target-platform', 'android-arm', + '--no-shrink', '--verbose', ], ); @@ -100,7 +101,9 @@ Future main() async { options: [ 'apk', '--target-platform', 'android-arm', - '--debug', '--verbose', + '--debug', + '--no-shrink', + '--verbose', ], ); }); diff --git a/packages/flutter_tools/gradle/aar_init_script.gradle b/packages/flutter_tools/gradle/aar_init_script.gradle index 2439da81c90..4874aa21b11 100644 --- a/packages/flutter_tools/gradle/aar_init_script.gradle +++ b/packages/flutter_tools/gradle/aar_init_script.gradle @@ -7,6 +7,7 @@ // destination of the local repository. // The local repository will contain the AAR and POM files. +import java.nio.file.Paths import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.maven.MavenDeployer @@ -39,16 +40,32 @@ void configureProject(Project project, File outputDir) { } } } - // Check if the project uses the Flutter plugin (defined in flutter.gradle). - Boolean usesFlutterPlugin = project.plugins.find { it.class.name == "FlutterPlugin" } != null - if (!usesFlutterPlugin) { - project.dependencies { - // Some plugins don't include `annotations` and they don't set - // `android.useAndroidX=true` in `gradle.properties`. - compileOnly "androidx.annotation:annotation:+" - compileOnly "com.android.support:support-annotations:+" - // The Flutter plugin already adds `flutter.jar`. - compileOnly project.files("${getFlutterRoot(project)}/bin/cache/artifacts/engine/android-arm-release/flutter.jar") + if (!project.property("is-plugin").toBoolean()) { + return + } + if (project.hasProperty('localEngineOut')) { + // TODO(egarciad): Support local engine. + // This most likely requires refactoring `flutter.gradle`, so the logic can be reused. + throw new GradleException( + "Local engine isn't supported when building the plugins as AAR. " + + "See: https://github.com/flutter/flutter/issues/40866") + } + + // This is a Flutter plugin project. Plugin projects don't apply the Flutter Gradle plugin, + // as a result, add the dependency on the embedding. + project.repositories { + maven { + url "http://download.flutter.io" + } + } + String engineVersion = Paths.get(getFlutterRoot(project), "bin", "internal", "engine.version") + .toFile().text.trim() + project.dependencies { + // Add the embedding dependency. + compileOnly ("io.flutter:flutter_embedding_release:1.0.0-$engineVersion") { + // We only need to expose io.flutter.plugin.* + // No need for the embedding transitive dependencies. + transitive = false } } } diff --git a/packages/flutter_tools/gradle/flutter.gradle b/packages/flutter_tools/gradle/flutter.gradle index c1067bccfad..606d3a3bd2c 100644 --- a/packages/flutter_tools/gradle/flutter.gradle +++ b/packages/flutter_tools/gradle/flutter.gradle @@ -8,6 +8,8 @@ import com.android.builder.model.AndroidProject import com.android.build.OutputFile import java.nio.file.Path import java.nio.file.Paths +import java.util.regex.Matcher +import java.util.regex.Pattern import org.apache.tools.ant.taskdefs.condition.Os import org.gradle.api.DefaultTask import org.gradle.api.GradleException @@ -256,29 +258,65 @@ class FlutterPlugin implements Plugin { */ private void configurePlugins() { if (!buildPluginAsAar()) { - getPluginList().each this.&configurePlugin + getPluginList().each this.&configurePluginProject return } - addPluginTasks() - List tasksToExecute = project.gradle.startParameter.taskNames - Set buildTypes = getBuildTypesForTasks(tasksToExecute) - if (tasksToExecute.contains("clean")) { - // Because the plugins are built during configuration, the task "clean" - // cannot run in conjunction with an assembly task. - if (!buildTypes.empty) { - throw new GradleException("Can't run the clean task along with other assemble tasks") + if (useLocalEngine()) { + throw new GradleException("Local engine isn't supported when building the plugins as AAR") + } + List projects = [project] + // Module projects set the `hostProjects` extra property in `include_flutter.groovy`. + // This is required to set the local repository in each host app project. + if (project.ext.has("hostProjects")) { + projects.addAll(project.ext.get("hostProjects")) + } + // Configure the repository for the plugins. + projects.each { hostProject -> + hostProject.repositories { + maven { + url "${getPluginBuildDir()}/outputs/repo" + } } } - // Build plugins when a task "assembly*" will be called later. - if (!buildTypes.empty) { - // Build the plugin during configuration. - // This is required when Jetifier is enabled, otherwise the implementation dependency - // cannot be added. - buildAarPlugins(buildTypes) + getPluginList().each { pluginName, pluginPath -> + configurePluginAar(pluginName, pluginPath, project) } } - private void configurePlugin(String name, String _) { + private static final Pattern GROUP_PATTERN = ~/group\s+\'(.+)\'/ + private static final Pattern PROJECT_NAME_PATTERN = ~/rootProject\.name\s+=\s+\'(.+)\'/ + + // Adds the plugin AAR dependency to the app project. + private void configurePluginAar(String pluginName, String pluginPath, Project project) { + // Extract the group id from the plugin's build.gradle. + // This is `group ''` + File pluginBuildFile = project.file(Paths.get(pluginPath, "android", "build.gradle")); + if (!pluginBuildFile.exists()) { + throw new GradleException("Plugin $pluginName doesn't have the required file $pluginBuildFile.") + } + + Matcher groupParts = GROUP_PATTERN.matcher(pluginBuildFile.text) + assert groupParts.count == 1 + assert groupParts.hasGroup() + String groupId = groupParts[0][1] + + // Extract the artifact name from the plugin's settings.gradle. + // This is `rootProject.name = ''` + File pluginSettings = project.file(Paths.get(pluginPath, "android", "settings.gradle")); + if (!pluginSettings.exists()) { + throw new GradleException("Plugin $pluginName doesn't have the required file $pluginSettings.") + } + Matcher projectNameParts = PROJECT_NAME_PATTERN.matcher(pluginSettings.text) + assert projectNameParts.count == 1 + assert projectNameParts.hasGroup() + String artifactId = "${projectNameParts[0][1]}_release" + + assert !groupId.empty + project.dependencies.add("api", "$groupId:$artifactId:+") + } + + // Adds the plugin project dependency to the app project . + private void configurePluginProject(String name, String _) { Project pluginProject = project.rootProject.findProject(":$name") if (pluginProject == null) { project.logger.error("Plugin project :$name not found. Please update settings.gradle.") @@ -343,93 +381,6 @@ class FlutterPlugin implements Plugin { return androidPlugins } - private void addPluginTasks() { - Properties plugins = getPluginList() - project.android.buildTypes.each { buildType -> - plugins.each { name, path -> - String buildModeValue = buildType.debuggable ? "debug" : "release" - List taskNameParts = ["build", "plugin", buildModeValue] - taskNameParts.addAll(name.split("_")) - String taskName = toCammelCase(taskNameParts) - // Build types can be extended. For example, a build type can extend the `debug` mode. - // In such cases, prevent creating the same task. - if (project.tasks.findByName(taskName) == null) { - project.tasks.create(name: taskName, type: FlutterPluginTask) { - flutterExecutable this.flutterExecutable - buildMode buildModeValue - verbose isVerbose() - pluginDir project.file(path) - sourceDir project.file(project.flutter.source) - intermediateDir getPluginBuildDir() - } - } - } - } - } - - private void buildAarPlugins(Set buildTypes) { - List projects = [project] - // Module projects set the `hostProjects` extra property in `include_flutter.groovy`. - // This is required to set the local repository in each host app project. - if (project.ext.has("hostProjects")) { - projects.addAll(project.ext.get("hostProjects")) - } - projects.each { hostProject -> - hostProject.repositories { - maven { - url "${getPluginBuildDir()}/outputs/repo" - } - } - } - buildTypes.each { buildType -> - project.tasks.withType(FlutterPluginTask).all { pluginTask -> - String buildMode = buildType.debuggable ? "debug" : "release" - if (pluginTask.buildMode != buildMode) { - return - } - pluginTask.execute() - pluginTask.intermediateDir.eachFileRecurse(FILES) { file -> - if (file.name != "maven-metadata.xml") { - return - } - def mavenMetadata = new XmlParser().parse(file) - String groupId = mavenMetadata.groupId.text() - String artifactId = mavenMetadata.artifactId.text() - - if (!artifactId.endsWith(buildMode)) { - return - } - // Add the plugin dependency based on the Maven metadata. - addApiDependencies(project, buildType.name, "$groupId:$artifactId:+@aar", { - transitive = true - }) - } - } - } - } - - /** - * Returns a set with the build type names that apply to the given list of tasks - * required to configure the plugin dependencies. - */ - private Set getBuildTypesForTasks(List tasksToExecute) { - Set buildTypes = [] - tasksToExecute.each { task -> - project.android.buildTypes.each { buildType -> - if (task == "androidDependencies" || task.endsWith("dependencies")) { - // The tasks to query the dependencies includes all the build types. - buildTypes.add(buildType) - } else if (task.endsWith("assemble")) { - // The `assemble` task includes all the build types. - buildTypes.add(buildType) - } else if (task.endsWith(buildType.name.capitalize())) { - buildTypes.add(buildType) - } - } - } - return buildTypes - } - private static String toCammelCase(List parts) { if (parts.empty) { return "" @@ -927,56 +878,3 @@ class FlutterTask extends BaseFlutterTask { buildBundle() } } - -class FlutterPluginTask extends DefaultTask { - File flutterExecutable - @Optional @Input - Boolean verbose - @Input - String buildMode - @Input - File pluginDir - @Input - File intermediateDir - File sourceDir - - @InputFiles - FileCollection getSourceFiles() { - return project.fileTree( - dir: sourceDir, - exclude: ["android", "ios"], - include: ["pubspec.yaml"] - ) - } - - @OutputDirectory - File getOutputDirectory() { - return intermediateDir - } - - @TaskAction - void build() { - intermediateDir.mkdirs() - project.exec { - executable flutterExecutable.absolutePath - workingDir pluginDir - args "build", "aar" - args "--quiet" - args "--suppress-analytics" - args "--output-dir", "${intermediateDir}" - switch (buildMode) { - case 'release': - args "--release" - break - case 'debug': - args "--debug" - break - default: - assert false - } - if (verbose) { - args "--verbose" - } - } - } -} diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index a0b53c7e843..ed64c50957d 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -22,7 +22,6 @@ import '../base/utils.dart'; import '../base/version.dart'; import '../build_info.dart'; import '../cache.dart'; -import '../features.dart'; import '../flutter_manifest.dart'; import '../globals.dart'; import '../project.dart'; @@ -47,18 +46,25 @@ class GradleUtils { return _cachedExecutable; } - GradleProject _cachedAppProject; - /// Gets the [GradleProject] for the current [FlutterProject] if built as an app. - Future get appProject async { - _cachedAppProject ??= await _readGradleProject(isLibrary: false); - return _cachedAppProject; + /// 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]; } - GradleProject _cachedLibraryProject; - /// Gets the [GradleProject] for the current [FlutterProject] if built as a library. - Future get libraryProject async { - _cachedLibraryProject ??= await _readGradleProject(isLibrary: true); - return _cachedLibraryProject; + /// 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]; } } @@ -133,7 +139,8 @@ Future getGradleAppOut(AndroidProject androidProject) async { case FlutterPluginVersion.managed: // Fall through. The managed plugin matches plugin v2 for now. case FlutterPluginVersion.v2: - final GradleProject gradleProject = await gradleUtils.appProject; + final GradleProject gradleProject = + await gradleUtils.getAppProject(FlutterProject.current()); return fs.file(gradleProject.apkDirectory.childFile('app.apk')); } return null; @@ -209,8 +216,10 @@ void createSettingsAarGradle(Directory androidDirectory) { // Note: Dependencies are resolved and possibly downloaded as a side-effect // of calculating the app properties using Gradle. This may take minutes. -Future _readGradleProject({bool isLibrary = false}) async { - final FlutterProject flutterProject = FlutterProject.current(); +Future _readGradleProject( + FlutterProject flutterProject, { + bool isLibrary = false, +}) async { final String gradlew = await gradleUtils.getExecutable(flutterProject); updateLocalProperties(project: flutterProject); @@ -218,10 +227,6 @@ Future _readGradleProject({bool isLibrary = false}) async { final FlutterManifest manifest = flutterProject.manifest; final Directory hostAppGradleRoot = flutterProject.android.hostAppGradleRoot; - if (featureFlags.isPluginAsAarEnabled && - !manifest.isPlugin && !manifest.isModule) { - createSettingsAarGradle(hostAppGradleRoot); - } if (manifest.isPlugin) { assert(isLibrary); return GradleProject( @@ -581,9 +586,9 @@ Future buildGradleAar({ GradleProject gradleProject; if (manifest.isModule) { - gradleProject = await gradleUtils.appProject; + gradleProject = await gradleUtils.getAppProject(project); } else if (manifest.isPlugin) { - gradleProject = await gradleUtils.libraryProject; + gradleProject = await gradleUtils.getLibraryProject(project); } else { throwToolExit('AARs can only be built for plugin or module projects.'); } @@ -612,13 +617,11 @@ Future buildGradleAar({ '-Pflutter-root=$flutterRoot', '-Poutput-dir=${gradleProject.buildDirectory}', '-Pis-plugin=${manifest.isPlugin}', - '-Dbuild-plugins-as-aars=true', ]; if (target != null && target.isNotEmpty) { command.add('-Ptarget=$target'); } - if (androidBuildInfo.targetArchs.isNotEmpty) { final String targetPlatforms = androidBuildInfo.targetArchs .map(getPlatformNameForAndroidArch).join(','); @@ -633,34 +636,30 @@ Future buildGradleAar({ command.add(aarTask); final Stopwatch sw = Stopwatch()..start(); - int exitCode = 1; - + RunResult result; try { - exitCode = await processUtils.stream( + result = await processUtils.run( command, workingDirectory: project.android.hostAppGradleRoot.path, allowReentrantFlutter: true, environment: gradleEnv, - mapFunction: (String line) { - // Always print the full line in verbose mode. - if (logger.isVerbose) { - return line; - } - return null; - }, ); } finally { status.stop(); } - flutterUsage.sendTiming('build', 'gradle-aar', Duration(milliseconds: sw.elapsedMilliseconds)); + flutterUsage.sendTiming('build', 'gradle-aar', sw.elapsed); - if (exitCode != 0) { - throwToolExit('Gradle task $aarTask failed with exit code $exitCode', exitCode: exitCode); + 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); } final Directory repoDirectory = gradleProject.repoDirectory; if (!repoDirectory.existsSync()) { - throwToolExit('Gradle task $aarTask failed to produce $repoDirectory', exitCode: exitCode); + printStatus(result.stdout, wrap: false); + printError(result.stderr, wrap: false); + throwToolExit('Gradle task $aarTask failed to produce $repoDirectory.', exitCode: exitCode); } printStatus('Built ${fs.path.relative(repoDirectory.path)}.', color: TerminalColor.green); } @@ -730,21 +729,33 @@ Future _buildGradleProjectV2( FlutterProject flutterProject, AndroidBuildInfo androidBuildInfo, String target, - bool isBuildingBundle, -) async { + bool isBuildingBundle, { + bool shouldBuildPluginAsAar = false, +}) async { final String gradlew = await gradleUtils.getExecutable(flutterProject); - final GradleProject project = await gradleUtils.appProject; + 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 BuildInfo buildInfo = androidBuildInfo.buildInfo; String assembleTask; if (isBuildingBundle) { - assembleTask = project.bundleTaskFor(buildInfo); + assembleTask = gradleProject.bundleTaskFor(buildInfo); } else { - assembleTask = project.assembleTaskFor(buildInfo); + assembleTask = gradleProject.assembleTaskFor(buildInfo); } if (assembleTask == null) { - printUndefinedTask(project, buildInfo); + printUndefinedTask(gradleProject, buildInfo); throwToolExit('Gradle build aborted.'); } final Status status = logger.startProgress( @@ -791,13 +802,12 @@ Future _buildGradleProjectV2( .map(getPlatformNameForAndroidArch).join(','); command.add('-Ptarget-platform=$targetPlatforms'); } - if (featureFlags.isPluginAsAarEnabled) { + 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'); - if (!flutterProject.manifest.isModule) { - command.add('--settings-file=settings_aar.gradle'); - } + // 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; @@ -844,24 +854,60 @@ Future _buildGradleProjectV2( printStatus('To learn more, see: https://developer.android.com/studio/build/shrink-code', indent: 4); BuildEvent('r8-failure').send(); } else if (potentialAndroidXFailure) { - printStatus('AndroidX incompatibilities may have caused this build to fail. See https://goo.gl/CP92wY.'); - BuildEvent('android-x-failure').send(); + final bool hasPlugins = flutterProject.flutterPluginsFile.existsSync(); + final bool usesAndroidX = isAppUsingAndroidX(flutterProject.android.hostAppGradleRoot); + 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(project, androidBuildInfo); + 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(project.apkDirectory.childFile('app.apk').path); + apkFiles.first.copySync(gradleProject.apkDirectory.childFile('app.apk').path); - printTrace('calculateSha: ${project.apkDirectory}/app.apk'); - final File apkShaFile = project.apkDirectory.childFile('app.apk.sha1'); + 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) { @@ -875,7 +921,7 @@ Future _buildGradleProjectV2( color: TerminalColor.green); } } else { - final File bundleFile = findBundleFile(project, buildInfo); + final File bundleFile = findBundleFile(gradleProject, buildInfo); if (bundleFile == null) { throwToolExit('Gradle build failed to produce an Android bundle package.'); } @@ -891,6 +937,61 @@ Future _buildGradleProjectV2( } } +/// Returns [true] if the current app uses AndroidX. +// TODO(egarciad): https://github.com/flutter/flutter/issues/40800 +// Remove `FlutterManifest.usesAndroidX` and provide a unified `AndroidProject.usesAndroidX`. +@visibleForTesting +bool isAppUsingAndroidX(Directory androidDirectory) { + final File properties = androidDirectory.childFile('gradle.properties'); + if (!properties.existsSync()) { + return false; + } + return properties.readAsStringSync().contains('android.useAndroidX=true'); +} + +/// Builds the plugins as AARs. +@visibleForTesting +Future buildPluginsAsAar( + FlutterProject flutterProject, + AndroidBuildInfo androidBuildInfo, { + String buildDirectory, +}) async { + final File flutterPluginFile = flutterProject.flutterPluginsFile; + if (!flutterPluginFile.existsSync()) { + return; + } + final List plugins = flutterPluginFile.readAsStringSync().split('\n'); + for (String plugin in plugins) { + final List pluginParts = plugin.split('='); + if (pluginParts.length != 2) { + continue; + } + final Directory pluginDirectory = fs.directory(pluginParts.last); + assert(pluginDirectory.existsSync()); + + final String pluginName = pluginParts.first; + logger.printStatus('Building plugin $pluginName...'); + try { + await buildGradleAar( + project: FlutterProject.fromDirectory(pluginDirectory), + androidBuildInfo: const AndroidBuildInfo( + BuildInfo( + BuildMode.release, // Plugins are built as release. + null, // Plugins don't define flavors. + ), + ), + target: '', + outputDir: buildDirectory, + ); + } on ToolExit { + // 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. '); + } + } +} + @visibleForTesting Iterable findApkFiles(GradleProject project, AndroidBuildInfo androidBuildInfo) { final Iterable apkFileNames = project.apkFilesFor(androidBuildInfo); diff --git a/packages/flutter_tools/lib/src/features.dart b/packages/flutter_tools/lib/src/features.dart index cdb13138af1..170126739e4 100644 --- a/packages/flutter_tools/lib/src/features.dart +++ b/packages/flutter_tools/lib/src/features.dart @@ -36,9 +36,6 @@ class FeatureFlags { /// Whether flutter desktop for Windows is enabled. bool get isWindowsEnabled => _isEnabled(flutterWindowsDesktopFeature); - /// Whether plugins are built as AARs in app projects. - bool get isPluginAsAarEnabled => _isEnabled(flutterBuildPluginAsAarFeature); - // Calculate whether a particular feature is enabled for the current channel. static bool _isEnabled(Feature feature) { final String currentChannel = FlutterVersion.instance.channel; diff --git a/packages/flutter_tools/lib/src/reporting/events.dart b/packages/flutter_tools/lib/src/reporting/events.dart index 8a73f516360..99def6b821b 100644 --- a/packages/flutter_tools/lib/src/reporting/events.dart +++ b/packages/flutter_tools/lib/src/reporting/events.dart @@ -125,6 +125,7 @@ class BuildEvent extends UsageEvent { BuildEvent(String parameter, { this.command, this.settings, + this.eventError, }) : super( 'build' + (FlutterCommand.current == null ? '' : '-${FlutterCommand.current.name}'), @@ -132,6 +133,7 @@ class BuildEvent extends UsageEvent { final String command; final String settings; + final String eventError; @override void send() { @@ -140,6 +142,8 @@ class BuildEvent extends UsageEvent { CustomDimensions.buildEventCommand: command, if (settings != null) CustomDimensions.buildEventSettings: settings, + if (eventError != null) + CustomDimensions.buildEventError: eventError, }); flutterUsage.sendEvent(category, parameter, parameters: parameters); } diff --git a/packages/flutter_tools/lib/src/reporting/usage.dart b/packages/flutter_tools/lib/src/reporting/usage.dart index 1f43c6db626..20ca58905d3 100644 --- a/packages/flutter_tools/lib/src/reporting/usage.dart +++ b/packages/flutter_tools/lib/src/reporting/usage.dart @@ -53,6 +53,7 @@ enum CustomDimensions { commandBuildApkSplitPerAbi, // cd40 commandBuildAppBundleTargetPlatform, // cd41 commandBuildAppBundleBuildMode, // cd42 + buildEventError, // cd43 } String cdKey(CustomDimensions cd) => 'cd${cd.index + 1}'; 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 1c7c447913d..d2ff5d6204f 100644 --- a/packages/flutter_tools/test/general.shard/android/gradle_test.dart +++ b/packages/flutter_tools/test/general.shard/android/gradle_test.dart @@ -25,7 +25,6 @@ import 'package:process/process.dart'; import '../../src/common.dart'; import '../../src/context.dart'; -import '../../src/mocks.dart'; import '../../src/pubspec_schema.dart'; void main() { @@ -1150,6 +1149,153 @@ at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)'''; }); }); + group('isAppUsingAndroidX', () { + FileSystem fs; + + setUp(() { + fs = MemoryFileSystem(); + }); + + testUsingContext('returns true when the project is using AndroidX', () async { + final Directory androidDirectory = fs.systemTempDirectory.createTempSync('android.'); + + androidDirectory + .childFile('gradle.properties') + .writeAsStringSync('android.useAndroidX=true'); + + expect(isAppUsingAndroidX(androidDirectory), isTrue); + + }, overrides: { + FileSystem: () => fs, + }); + + testUsingContext('returns false when the project is not using AndroidX', () async { + final Directory androidDirectory = fs.systemTempDirectory.createTempSync('android.'); + + androidDirectory + .childFile('gradle.properties') + .writeAsStringSync('android.useAndroidX=false'); + + expect(isAppUsingAndroidX(androidDirectory), isFalse); + + }, overrides: { + FileSystem: () => fs, + }); + + testUsingContext('returns false when gradle.properties does not exist', () async { + final Directory androidDirectory = fs.systemTempDirectory.createTempSync('android.'); + + expect(isAppUsingAndroidX(androidDirectory), isFalse); + + }, overrides: { + FileSystem: () => fs, + }); + }); + + group('buildPluginsAsAar', () { + FileSystem fs; + MockProcessManager mockProcessManager; + MockAndroidSdk mockAndroidSdk; + + setUp(() { + fs = MemoryFileSystem(); + + mockProcessManager = MockProcessManager(); + when(mockProcessManager.run( + any, + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'), + )).thenAnswer((_) async => ProcessResult(1, 0, '', '')); + + mockAndroidSdk = MockAndroidSdk(); + when(mockAndroidSdk.directory).thenReturn('irrelevant'); + }); + + testUsingContext('calls gradle', () async { + final Directory androidDirectory = fs.directory('android.'); + androidDirectory.createSync(); + androidDirectory + .childFile('pubspec.yaml') + .writeAsStringSync('name: irrelevant'); + + final Directory plugin1 = fs.directory('plugin1.'); + plugin1 + ..createSync() + ..childFile('pubspec.yaml') + .writeAsStringSync(''' +name: irrelevant +flutter: + plugin: + androidPackage: irrelevant +'''); + final Directory plugin2 = fs.directory('plugin2.'); + plugin2 + ..createSync() + ..childFile('pubspec.yaml') + .writeAsStringSync(''' +name: irrelevant +flutter: + plugin: + androidPackage: irrelevant +'''); + + androidDirectory + .childFile('.flutter-plugins') + .writeAsStringSync(''' +plugin1=${plugin1.path} +plugin2=${plugin2.path} +'''); + final Directory buildDirectory = androidDirectory.childDirectory('build'); + buildDirectory + .childDirectory('outputs') + .childDirectory('repo') + .createSync(recursive: true); + + await buildPluginsAsAar( + FlutterProject.fromPath(androidDirectory.path), + const AndroidBuildInfo(BuildInfo.release), + buildDirectory: buildDirectory.path, + ); + + final String flutterRoot = fs.path.absolute(Cache.flutterRoot); + final String initScript = fs.path.join(flutterRoot, 'packages', + 'flutter_tools', 'gradle', 'aar_init_script.gradle'); + verify(mockProcessManager.run( + [ + 'gradlew', + '-I=$initScript', + '-Pflutter-root=$flutterRoot', + '-Poutput-dir=${buildDirectory.path}', + '-Pis-plugin=true', + '-Ptarget-platform=android-arm,android-arm64', + 'assembleAarRelease', + ], + environment: anyNamed('environment'), + workingDirectory: plugin1.childDirectory('android').path), + ).called(1); + + verify(mockProcessManager.run( + [ + 'gradlew', + '-I=$initScript', + '-Pflutter-root=$flutterRoot', + '-Poutput-dir=${buildDirectory.path}', + '-Pis-plugin=true', + '-Ptarget-platform=android-arm,android-arm64', + 'assembleAarRelease', + ], + environment: anyNamed('environment'), + workingDirectory: plugin2.childDirectory('android').path), + ).called(1); + + }, overrides: { + AndroidSdk: () => mockAndroidSdk, + FileSystem: () => fs, + GradleUtils: () => FakeGradleUtils(), + ProcessManager: () => mockProcessManager, + }); + }); + group('gradle build', () { MockAndroidSdk mockAndroidSdk; MockAndroidStudio mockAndroidStudio; @@ -1230,11 +1376,13 @@ at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)'''; fs.currentDirectory = 'path/to/project'; // Let any process start. Assert after. - when(mockProcessManager.start( + when(mockProcessManager.run( any, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory')) - ).thenAnswer((Invocation invocation) => Future.value(MockProcess())); + ).thenAnswer( + (_) async => ProcessResult(1, 0, '', ''), + ); fs.directory('build/outputs/repo').createSync(recursive: true); await buildGradleAar( @@ -1244,11 +1392,11 @@ at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)'''; target: '', ); - final List actualGradlewCall = verify(mockProcessManager.start( + final List actualGradlewCall = verify(mockProcessManager.run( captureAny, environment: anyNamed('environment'), workingDirectory: anyNamed('workingDirectory')), - ).captured.single; + ).captured.last; expect(actualGradlewCall, contains('/path/to/project/.android/gradlew')); expect(actualGradlewCall, contains('-PlocalEngineOut=out/android_arm')); @@ -1279,6 +1427,14 @@ Platform fakePlatform(String name) { return FakePlatform.fromPlatform(const LocalPlatform())..operatingSystem = name; } +class FakeGradleUtils extends GradleUtils { + @override + Future getExecutable(FlutterProject project) async { + return 'gradlew'; + } +} + +class MockAndroidSdk extends Mock implements AndroidSdk {} class MockAndroidStudio extends Mock implements AndroidStudio {} class MockDirectory extends Mock implements Directory {} class MockFile extends Mock implements File {} diff --git a/packages/flutter_tools/test/general.shard/features_test.dart b/packages/flutter_tools/test/general.shard/features_test.dart index 3594a6e2113..a7c978efd6d 100644 --- a/packages/flutter_tools/test/general.shard/features_test.dart +++ b/packages/flutter_tools/test/general.shard/features_test.dart @@ -432,21 +432,6 @@ void main() { expect(featureFlags.isWindowsEnabled, false); })); - - /// Plugins as AARS - test('plugins built as AARs with config on master', () => testbed.run(() { - when(mockFlutterVerion.channel).thenReturn('master'); - when(mockFlutterConfig.getValue('enable-build-plugin-as-aar')).thenReturn(false); - - expect(featureFlags.isPluginAsAarEnabled, false); - })); - - test('plugins built as AARs with config on dev', () => testbed.run(() { - when(mockFlutterVerion.channel).thenReturn('dev'); - when(mockFlutterConfig.getValue('enable-build-plugin-as-aar')).thenReturn(false); - - expect(featureFlags.isPluginAsAarEnabled, false); - })); }); } diff --git a/packages/flutter_tools/test/src/testbed.dart b/packages/flutter_tools/test/src/testbed.dart index 3529d0ca687..e8eb4ca76a4 100644 --- a/packages/flutter_tools/test/src/testbed.dart +++ b/packages/flutter_tools/test/src/testbed.dart @@ -693,7 +693,6 @@ class TestFeatureFlags implements FeatureFlags { this.isMacOSEnabled = false, this.isWebEnabled = false, this.isWindowsEnabled = false, - this.isPluginAsAarEnabled = false, }); @override @@ -707,7 +706,4 @@ class TestFeatureFlags implements FeatureFlags { @override final bool isWindowsEnabled; - - @override - final bool isPluginAsAarEnabled; }