From 242a4225a1bfc726360e7d0f26325d5ac4d30891 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Tue, 23 Jul 2019 09:27:42 -0700 Subject: [PATCH] Flutter build aar (#36732) `flutter build aar` This new build command works just like `flutter build apk` or `flutter build appbundle`, but for plugin and module projects. This PR also refactors how plugins are included in app or module projects. By building the plugins as AARs, the Android Gradle plugin is able to use Jetifier to translate support libraries into AndroidX libraries for all the plugin's native code. Thus, reducing the error rate when using AndroidX in apps. This change also allows to build modules as AARs, so developers can take these artifacts and distribute them along with the native host app without the need of the Flutter tool. This is a requirement for add to app. `flutter build aar` generates POM artifacts (XML files) which contain metadata about the native dependencies used by the plugin. This allows Gradle to resolve dependencies at the app level. The result of this new build command is a single build/outputs/repo, the local repository that contains all the generated AARs and POM files. In a Flutter app project, this local repo is used by the Flutter Gradle plugin to resolve the plugin dependencies. In add to app case, the developer needs to configure the local repo and the dependency manually in `build.gradle`: repositories { maven { url "build/host/outputs/repo" } } dependencies { implementation(":flutter_:1.0@aar") { transitive = true } } --- dev/bots/test.dart | 5 + .../bin/tasks/build_aar_module_test.dart | 219 ++++++++++ .../bin/tasks/build_aar_plugin_test.dart | 138 +++++++ .../bin/tasks/gradle_jetifier_test.dart | 138 +++++++ .../tasks/gradle_migrate_settings_test.dart | 180 +++++++++ .../gradle_plugin_dependencies_test.dart | 147 +++++++ dev/devicelab/bin/tasks/module_test.dart | 2 +- dev/devicelab/bin/tasks/module_test_ios.dart | 2 +- dev/devicelab/lib/framework/apk_utils.dart | 87 ++++ dev/devicelab/lib/framework/utils.dart | 19 +- .../gradle/aar_init_script.gradle | 128 ++++++ .../gradle/deprecated_settings.gradle | 31 ++ packages/flutter_tools/gradle/flutter.gradle | 319 +++++++++++---- .../manual_migration_settings.gradle.md | 19 + .../gradle/settings_aar.gradle.tmpl | 1 + .../flutter_tools/lib/src/android/aar.dart | 62 +++ .../flutter_tools/lib/src/android/gradle.dart | 377 +++++++++++++++--- .../flutter_tools/lib/src/commands/build.dart | 2 + .../lib/src/commands/build_aar.dart | 94 +++++ packages/flutter_tools/lib/src/features.dart | 18 + packages/flutter_tools/lib/src/project.dart | 4 - .../lib/src/reporting/usage.dart | 5 +- .../library/Flutter.tmpl/build.gradle.tmpl | 3 + .../library/include_flutter.groovy.copy.tmpl | 33 +- .../android-java.tmpl/build.gradle.tmpl | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 5 + .../general.shard/android/gradle_test.dart | 205 +++++++++- .../commands/build_aar_test.dart | 87 ++++ .../test/general.shard/features_test.dart | 15 + packages/flutter_tools/test/src/testbed.dart | 4 + 30 files changed, 2201 insertions(+), 150 deletions(-) create mode 100644 dev/devicelab/bin/tasks/build_aar_module_test.dart create mode 100644 dev/devicelab/bin/tasks/build_aar_plugin_test.dart create mode 100644 dev/devicelab/bin/tasks/gradle_jetifier_test.dart create mode 100644 dev/devicelab/bin/tasks/gradle_migrate_settings_test.dart create mode 100644 dev/devicelab/bin/tasks/gradle_plugin_dependencies_test.dart create mode 100644 packages/flutter_tools/gradle/aar_init_script.gradle create mode 100644 packages/flutter_tools/gradle/deprecated_settings.gradle create mode 100644 packages/flutter_tools/gradle/manual_migration_settings.gradle.md create mode 100644 packages/flutter_tools/gradle/settings_aar.gradle.tmpl create mode 100644 packages/flutter_tools/lib/src/android/aar.dart create mode 100644 packages/flutter_tools/lib/src/commands/build_aar.dart create mode 100644 packages/flutter_tools/templates/plugin/android.tmpl/gradle/wrapper/gradle-wrapper.properties create mode 100644 packages/flutter_tools/test/general.shard/commands/build_aar_test.dart diff --git a/dev/bots/test.dart b/dev/bots/test.dart index 48be0bb022..4ebcfc32e0 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -961,9 +961,14 @@ Future _androidGradleTests(String subShard) async { if (subShard == 'gradle1') { await _runDevicelabTest('gradle_plugin_light_apk_test', env: env); await _runDevicelabTest('gradle_plugin_fat_apk_test', env: env); + await _runDevicelabTest('gradle_jetifier_test', env: env); + await _runDevicelabTest('gradle_plugin_dependencies_test', env: env); + await _runDevicelabTest('gradle_migrate_settings_test', env: env); } if (subShard == 'gradle2') { await _runDevicelabTest('gradle_plugin_bundle_test', env: env); await _runDevicelabTest('module_test', env: env); + await _runDevicelabTest('build_aar_plugin_test', env: env); + await _runDevicelabTest('build_aar_module_test', env: env); } } diff --git a/dev/devicelab/bin/tasks/build_aar_module_test.dart b/dev/devicelab/bin/tasks/build_aar_module_test.dart new file mode 100644 index 0000000000..be55a15b7a --- /dev/null +++ b/dev/devicelab/bin/tasks/build_aar_module_test.dart @@ -0,0 +1,219 @@ +// Copyright (c) 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 'dart:async'; +import 'dart:io'; + +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; +import 'package:path/path.dart' as path; + +final String gradlew = Platform.isWindows ? 'gradlew.bat' : 'gradlew'; +final String gradlewExecutable = Platform.isWindows ? gradlew : './$gradlew'; + +/// Tests that AARs can be built on module projects. +Future main() async { + await task(() async { + + section('Find Java'); + + final String javaHome = await findJavaHome(); + if (javaHome == null) + return TaskResult.failure('Could not find Java'); + print('\nUsing JAVA_HOME=$javaHome'); + + section('Create module project'); + + final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.'); + final Directory projectDir = Directory(path.join(tempDir.path, 'hello')); + try { + await inDirectory(tempDir, () async { + await flutter( + 'create', + options: ['--org', 'io.flutter.devicelab', '--template', 'module', 'hello'], + ); + }); + + section('Add plugins'); + + final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml')); + String content = pubspec.readAsStringSync(); + content = content.replaceFirst( + '\ndependencies:\n', + '\ndependencies:\n device_info:\n package_info:\n', + ); + pubspec.writeAsStringSync(content, flush: true); + await inDirectory(projectDir, () async { + await flutter( + 'packages', + options: ['get'], + ); + }); + + section('Build release AAR'); + + await inDirectory(projectDir, () async { + await flutter( + 'build', + options: ['aar', '--verbose'], + ); + }); + + final String repoPath = path.join( + projectDir.path, + 'build', + 'host', + 'outputs', + 'repo', + ); + + checkFileExists(path.join( + repoPath, + 'io', + 'flutter', + 'devicelab', + 'hello', + 'flutter_release', + '1.0', + 'flutter_release-1.0.aar', + )); + + checkFileExists(path.join( + repoPath, + 'io', + 'flutter', + 'devicelab', + 'hello', + 'flutter_release', + '1.0', + 'flutter_release-1.0.pom', + )); + + checkFileExists(path.join( + repoPath, + 'io', + 'flutter', + 'plugins', + 'deviceinfo', + 'device_info_release', + '1.0', + 'device_info_release-1.0.aar', + )); + + checkFileExists(path.join( + repoPath, + 'io', + 'flutter', + 'plugins', + 'deviceinfo', + 'device_info_release', + '1.0', + 'device_info_release-1.0.pom', + )); + + checkFileExists(path.join( + repoPath, + 'io', + 'flutter', + 'plugins', + 'packageinfo', + 'package_info_release', + '1.0', + 'package_info_release-1.0.aar', + )); + + checkFileExists(path.join( + repoPath, + 'io', + 'flutter', + 'plugins', + 'packageinfo', + 'package_info_release', + '1.0', + 'package_info_release-1.0.pom', + )); + + section('Build debug AAR'); + + await inDirectory(projectDir, () async { + await flutter( + 'build', + options: ['aar', '--verbose', '--debug'], + ); + }); + + checkFileExists(path.join( + repoPath, + 'io', + 'flutter', + 'devicelab', + 'hello', + 'flutter_release', + '1.0', + 'flutter_release-1.0.aar', + )); + + checkFileExists(path.join( + repoPath, + 'io', + 'flutter', + 'devicelab', + 'hello', + 'flutter_debug', + '1.0', + 'flutter_debug-1.0.pom', + )); + + checkFileExists(path.join( + repoPath, + 'io', + 'flutter', + 'plugins', + 'deviceinfo', + 'device_info_debug', + '1.0', + 'device_info_debug-1.0.aar', + )); + + checkFileExists(path.join( + repoPath, + 'io', + 'flutter', + 'plugins', + 'deviceinfo', + 'device_info_debug', + '1.0', + 'device_info_debug-1.0.pom', + )); + + checkFileExists(path.join( + repoPath, + 'io', + 'flutter', + 'plugins', + 'packageinfo', + 'package_info_debug', + '1.0', + 'package_info_debug-1.0.aar', + )); + + checkFileExists(path.join( + repoPath, + 'io', + 'flutter', + 'plugins', + 'packageinfo', + 'package_info_debug', + '1.0', + 'package_info_debug-1.0.pom', + )); + + return TaskResult.success(null); + } catch (e) { + return TaskResult.failure(e.toString()); + } finally { + rmTree(tempDir); + } + }); +} diff --git a/dev/devicelab/bin/tasks/build_aar_plugin_test.dart b/dev/devicelab/bin/tasks/build_aar_plugin_test.dart new file mode 100644 index 0000000000..70738106de --- /dev/null +++ b/dev/devicelab/bin/tasks/build_aar_plugin_test.dart @@ -0,0 +1,138 @@ +// Copyright (c) 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 'dart:async'; +import 'dart:io'; + +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; +import 'package:path/path.dart' as path; + +final String gradlew = Platform.isWindows ? 'gradlew.bat' : 'gradlew'; +final String gradlewExecutable = Platform.isWindows ? gradlew : './$gradlew'; + +/// Tests that AARs can be built on plugin projects. +Future main() async { + await task(() async { + + section('Find Java'); + + final String javaHome = await findJavaHome(); + if (javaHome == null) + return TaskResult.failure('Could not find Java'); + print('\nUsing JAVA_HOME=$javaHome'); + + section('Create plugin project'); + + final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.'); + final Directory projectDir = Directory(path.join(tempDir.path, 'hello')); + try { + await inDirectory(tempDir, () async { + await flutter( + 'create', + options: [ + '--org', 'io.flutter.devicelab', + '--template', 'plugin', + 'hello', + ], + ); + }); + + section('Build release AAR'); + + await inDirectory(projectDir, () async { + await flutter( + 'build', + options: ['aar', '--verbose'], + ); + }); + + final String repoPath = path.join( + projectDir.path, + 'build', + 'outputs', + 'repo', + ); + + final File releaseAar = File(path.join( + repoPath, + 'io', + 'flutter', + 'devicelab', + 'hello', + 'hello_release', + '1.0', + 'hello_release-1.0.aar', + )); + + if (!exists(releaseAar)) { + return TaskResult.failure('Failed to build the release AAR file.'); + } + + final File releasePom = File(path.join( + repoPath, + 'io', + 'flutter', + 'devicelab', + 'hello', + 'hello_release', + '1.0', + 'hello_release-1.0.pom', + )); + + if (!exists(releasePom)) { + return TaskResult.failure('Failed to build the release POM file.'); + } + + section('Build debug AAR'); + + await inDirectory(projectDir, () async { + await flutter( + 'build', + options: [ + 'aar', + '--verbose', + '--debug', + ], + ); + }); + + final File debugAar = File(path.join( + repoPath, + 'io', + 'flutter', + 'devicelab', + 'hello', + 'hello_debug', + '1.0', + 'hello_debug-1.0.aar', + )); + + if (!exists(debugAar)) { + return TaskResult.failure('Failed to build the debug AAR file.'); + } + + final File debugPom = File(path.join( + repoPath, + 'io', + 'flutter', + 'devicelab', + 'hello', + 'hello_debug', + '1.0', + 'hello_debug-1.0.pom', + )); + + if (!exists(debugPom)) { + return TaskResult.failure('Failed to build the debug POM file.'); + } + + return TaskResult.success(null); + } catch (e) { + return TaskResult.failure(e.toString()); + } finally { + rmTree(tempDir); + } + }); +} diff --git a/dev/devicelab/bin/tasks/gradle_jetifier_test.dart b/dev/devicelab/bin/tasks/gradle_jetifier_test.dart new file mode 100644 index 0000000000..bbabfc2e8b --- /dev/null +++ b/dev/devicelab/bin/tasks/gradle_jetifier_test.dart @@ -0,0 +1,138 @@ +// Copyright (c) 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 'dart:async'; +import 'dart:io'; + +import 'package:flutter_devicelab/framework/apk_utils.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; +import 'package:path/path.dart' as path; + +final String gradlew = Platform.isWindows ? 'gradlew.bat' : 'gradlew'; +final String gradlewExecutable = Platform.isWindows ? gradlew : './$gradlew'; + +/// Tests that Jetifier can translate plugins that use support libraries. +Future main() async { + await task(() async { + + section('Find Java'); + + final String javaHome = await findJavaHome(); + if (javaHome == null) + return TaskResult.failure('Could not find Java'); + print('\nUsing JAVA_HOME=$javaHome'); + + section('Create Flutter AndroidX app project'); + + final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.'); + final Directory projectDir = Directory(path.join(tempDir.path, 'hello')); + try { + await inDirectory(tempDir, () async { + await flutter( + 'create', + options: [ + '--org', 'io.flutter.devicelab', + '--androidx', + 'hello', + ], + ); + }); + + section('Add plugin that uses support libraries'); + + final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml')); + String content = pubspec.readAsStringSync(); + content = content.replaceFirst( + '\ndependencies:\n', + '\ndependencies:\n firebase_auth: 0.7.0\n', + ); + pubspec.writeAsStringSync(content, flush: true); + await inDirectory(projectDir, () async { + await flutter( + 'packages', + options: ['get'], + ); + }); + + section('Build release APK'); + + await inDirectory(projectDir, () async { + await flutter( + 'build', + options: [ + 'apk', + '--target-platform', 'android-arm', + '--verbose', + ], + ); + }); + + final File releaseApk = File(path.join( + projectDir.path, + 'build', + 'app', + 'outputs', + 'apk', + 'release', + 'app-release.apk', + )); + + if (!exists(releaseApk)) { + return TaskResult.failure('Failed to build release APK.'); + } + + checkApkContainsClasses(releaseApk, [ + // The plugin class defined by `firebase_auth`. + 'io.flutter.plugins.firebaseauth.FirebaseAuthPlugin', + // Used by `firebase_auth`. + 'com.google.firebase.FirebaseApp', + // Base class for activities that enables composition of higher level components. + 'androidx.core.app.ComponentActivity', + ]); + + section('Build debug APK'); + + await inDirectory(projectDir, () async { + await flutter( + 'build', + options: [ + 'apk', + '--target-platform', 'android-arm', + '--debug', '--verbose', + ], + ); + }); + + final File debugApk = File(path.join( + projectDir.path, + 'build', + 'app', + 'outputs', + 'apk', + 'debug', + 'app-debug.apk', + )); + + if (!exists(debugApk)) { + return TaskResult.failure('Failed to build debug APK.'); + } + + checkApkContainsClasses(debugApk, [ + // The plugin class defined by `firebase_auth`. + 'io.flutter.plugins.firebaseauth.FirebaseAuthPlugin', + // Used by `firebase_auth`. + 'com.google.firebase.FirebaseApp', + // Base class for activities that enables composition of higher level components. + 'androidx.core.app.ComponentActivity', + ]); + + return TaskResult.success(null); + } catch (e) { + return TaskResult.failure(e.toString()); + } finally { + rmTree(tempDir); + } + }); +} diff --git a/dev/devicelab/bin/tasks/gradle_migrate_settings_test.dart b/dev/devicelab/bin/tasks/gradle_migrate_settings_test.dart new file mode 100644 index 0000000000..72a1708ad2 --- /dev/null +++ b/dev/devicelab/bin/tasks/gradle_migrate_settings_test.dart @@ -0,0 +1,180 @@ +// Copyright (c) 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 'dart:async'; +import 'dart:io'; + +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; +import 'package:path/path.dart' as path; + +final String gradlew = Platform.isWindows ? 'gradlew.bat' : 'gradlew'; +final String gradlewExecutable = Platform.isWindows ? gradlew : './$gradlew'; + +/// Tests that [settings_aar.gradle] is created when possible. +Future main() async { + await task(() async { + + section('Find Java'); + + final String javaHome = await findJavaHome(); + if (javaHome == null) + return TaskResult.failure('Could not find Java'); + print('\nUsing JAVA_HOME=$javaHome'); + + section('Create app project'); + + final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.'); + final Directory projectDir = Directory(path.join(tempDir.path, 'hello')); + try { + await inDirectory(tempDir, () async { + await flutter( + 'create', + options: ['hello'], + ); + }); + + section('Override settings.gradle V1'); + + final String relativeNewSettingsGradle = path.join('android', 'settings_aar.gradle'); + + section('Build APK'); + + String stdout; + await inDirectory(projectDir, () async { + stdout = await evalFlutter( + 'build', + options: [ + 'apk', + '--flavor', 'does-not-exist', + ], + canFail: true, // The flavor doesn't exist. + ); + }); + + const String newFileContent = 'include \':app\''; + + final File settingsGradle = File(path.join(projectDir.path, 'android', 'settings.gradle')); + final File newSettingsGradle = File(path.join(projectDir.path, 'android', 'settings_aar.gradle')); + + if (!newSettingsGradle.existsSync()) { + return TaskResult.failure('Expected file: `${newSettingsGradle.path}`.'); + } + + if (newSettingsGradle.readAsStringSync().trim() != newFileContent) { + return TaskResult.failure('Expected to create `${newSettingsGradle.path}` V1.'); + } + + if (!stdout.contains('Creating `$relativeNewSettingsGradle`') || + !stdout.contains('`$relativeNewSettingsGradle` created successfully')) { + return TaskResult.failure('Expected update message in stdout.'); + } + + section('Override settings.gradle V2'); + + const String deprecatedFileContentV2 = ''' +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withInputStream { stream -> plugins.load(stream) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":\$name" + project(":\$name").projectDir = pluginDirectory +} +'''; + settingsGradle.writeAsStringSync(deprecatedFileContentV2, flush: true); + newSettingsGradle.deleteSync(); + + section('Build APK'); + + await inDirectory(projectDir, () async { + stdout = await evalFlutter( + 'build', + options: [ + 'apk', + '--flavor', 'does-not-exist', + ], + canFail: true, // The flavor doesn't exist. + ); + }); + + if (newSettingsGradle.readAsStringSync().trim() != newFileContent) { + return TaskResult.failure('Expected to create `${newSettingsGradle.path}` V2.'); + } + + if (!stdout.contains('Creating `$relativeNewSettingsGradle`') || + !stdout.contains('`$relativeNewSettingsGradle` created successfully')) { + return TaskResult.failure('Expected update message in stdout.'); + } + + section('Override settings.gradle with custom logic'); + + const String customDeprecatedFileContent = ''' +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withInputStream { stream -> plugins.load(stream) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":\$name" + project(":\$name").projectDir = pluginDirectory +} +// some custom logic +'''; + settingsGradle.writeAsStringSync(customDeprecatedFileContent, flush: true); + newSettingsGradle.deleteSync(); + + section('Build APK'); + + final StringBuffer stderr = StringBuffer(); + await inDirectory(projectDir, () async { + stdout = await evalFlutter( + 'build', + options: [ + 'apk', + '--flavor', 'does-not-exist', + ], + canFail: true, // The flavor doesn't exist. + stderr: stderr, + ); + }); + + if (newSettingsGradle.existsSync()) { + return TaskResult.failure('Unexpected file: `${newSettingsGradle.path}`.'); + } + + if (!stdout.contains('Creating `$relativeNewSettingsGradle`')) { + return TaskResult.failure('Expected update message in stdout.'); + } + + if (stdout.contains('`$relativeNewSettingsGradle` created successfully')) { + return TaskResult.failure('Unexpected message in stdout.'); + } + + if (!stderr.toString().contains('Flutter tried to create the file ' + '`$relativeNewSettingsGradle`, but failed.')) { + return TaskResult.failure('Expected failure message in stdout.'); + } + + return TaskResult.success(null); + } catch (e) { + return TaskResult.failure(e.toString()); + } finally { + rmTree(tempDir); + } + }); +} diff --git a/dev/devicelab/bin/tasks/gradle_plugin_dependencies_test.dart b/dev/devicelab/bin/tasks/gradle_plugin_dependencies_test.dart new file mode 100644 index 0000000000..2525bdfa1a --- /dev/null +++ b/dev/devicelab/bin/tasks/gradle_plugin_dependencies_test.dart @@ -0,0 +1,147 @@ +// Copyright (c) 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 'dart:async'; +import 'dart:io'; + +import 'package:flutter_devicelab/framework/apk_utils.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; +import 'package:path/path.dart' as path; + +final String gradlew = Platform.isWindows ? 'gradlew.bat' : 'gradlew'; +final String gradlewExecutable = Platform.isWindows ? gradlew : './$gradlew'; + +/// Tests that projects can include plugins that have a transtive dependency in common. +/// For more info see: https://github.com/flutter/flutter/issues/27254. +Future main() async { + await task(() async { + + section('Find Java'); + + final String javaHome = await findJavaHome(); + if (javaHome == null) + return TaskResult.failure('Could not find Java'); + print('\nUsing JAVA_HOME=$javaHome'); + + section('Create Flutter AndroidX app project'); + + final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.'); + final Directory projectDir = Directory(path.join(tempDir.path, 'hello')); + try { + await inDirectory(tempDir, () async { + await flutter( + 'create', + options: [ + '--org', 'io.flutter.devicelab', + '--androidx', + 'hello', + ], + ); + }); + + section('Add plugin that have conflicting dependencies'); + + final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml')); + String content = pubspec.readAsStringSync(); + + // `flutter_local_notifications` uses `androidx.core:core:1.0.1` + // `firebase_core` and `firebase_messaging` use `androidx.core:core:1.0.0`. + content = content.replaceFirst( + '\ndependencies:\n', + '\ndependencies:\n flutter_local_notifications: 0.7.1+3\n firebase_core:\n firebase_messaging:\n', + ); + pubspec.writeAsStringSync(content, flush: true); + await inDirectory(projectDir, () async { + await flutter( + 'packages', + options: ['get'], + ); + }); + + section('Build release APK'); + + await inDirectory(projectDir, () async { + await flutter( + 'build', + options: [ + 'apk', + '--target-platform', 'android-arm', + '--verbose', + ], + ); + }); + + final File releaseApk = File(path.join( + projectDir.path, + 'build', + 'app', + 'outputs', + 'apk', + 'release', + 'app-release.apk', + )); + + if (!exists(releaseApk)) { + return TaskResult.failure('Failed to build release APK.'); + } + + checkApkContainsClasses(releaseApk, [ + // Used by `flutter_local_notifications`. + 'com.google.gson.Gson', + // Used by `firebase_core` and `firebase_messaging`. + 'com.google.firebase.FirebaseApp', + // Used by `firebase_core`. + 'com.google.firebase.FirebaseOptions', + // Used by `firebase_messaging`. + 'com.google.firebase.messaging.FirebaseMessaging', + ]); + + section('Build debug APK'); + + await inDirectory(projectDir, () async { + await flutter( + 'build', + options: [ + 'apk', + '--target-platform', 'android-arm', + '--debug', + '--verbose', + ], + ); + }); + + final File debugApk = File(path.join( + projectDir.path, + 'build', + 'app', + 'outputs', + 'apk', + 'debug', + 'app-debug.apk', + )); + + if (!exists(debugApk)) { + return TaskResult.failure('Failed to build debug APK.'); + } + + checkApkContainsClasses(debugApk, [ + // Used by `flutter_local_notifications`. + 'com.google.gson.Gson', + // Used by `firebase_core` and `firebase_messaging`. + 'com.google.firebase.FirebaseApp', + // Used by `firebase_core`. + 'com.google.firebase.FirebaseOptions', + // Used by `firebase_messaging`. + 'com.google.firebase.messaging.FirebaseMessaging', + ]); + + return TaskResult.success(null); + } catch (e) { + return TaskResult.failure(e.toString()); + } finally { + rmTree(tempDir); + } + }); +} diff --git a/dev/devicelab/bin/tasks/module_test.dart b/dev/devicelab/bin/tasks/module_test.dart index 1b6e8919c5..0bdcdb044e 100644 --- a/dev/devicelab/bin/tasks/module_test.dart +++ b/dev/devicelab/bin/tasks/module_test.dart @@ -42,7 +42,7 @@ Future main() async { String content = await pubspec.readAsString(); content = content.replaceFirst( '\ndependencies:\n', - '\ndependencies:\n battery:\n package_info:\n', + '\ndependencies:\n device_info:\n package_info:\n', ); await pubspec.writeAsString(content, flush: true); await inDirectory(projectDir, () async { diff --git a/dev/devicelab/bin/tasks/module_test_ios.dart b/dev/devicelab/bin/tasks/module_test_ios.dart index ec71e03c7b..f578754820 100644 --- a/dev/devicelab/bin/tasks/module_test_ios.dart +++ b/dev/devicelab/bin/tasks/module_test_ios.dart @@ -143,7 +143,7 @@ Future main() async { String content = await pubspec.readAsString(); content = content.replaceFirst( '\ndependencies:\n', - '\ndependencies:\n battery:\n package_info:\n', + '\ndependencies:\n device_info:\n package_info:\n', ); await pubspec.writeAsString(content, flush: true); await inDirectory(projectDir, () async { diff --git a/dev/devicelab/lib/framework/apk_utils.dart b/dev/devicelab/lib/framework/apk_utils.dart index 0dc28b92e2..0c42b26528 100644 --- a/dev/devicelab/lib/framework/apk_utils.dart +++ b/dev/devicelab/lib/framework/apk_utils.dart @@ -83,6 +83,93 @@ bool hasMultipleOccurrences(String text, Pattern pattern) { return text.indexOf(pattern) != text.lastIndexOf(pattern); } +/// Utility class to analyze the content inside an APK using dexdump, +/// which is provided by the Android SDK. +/// https://android.googlesource.com/platform/art/+/master/dexdump/dexdump.cc +class ApkExtractor { + ApkExtractor(this.apkFile); + + /// The APK. + final File apkFile; + + bool _extracted = false; + + Directory _outputDir; + + Future _extractApk() async { + if (_extracted) { + return; + } + _outputDir = apkFile.parent.createTempSync('apk'); + if (Platform.isWindows) { + await eval('7za', ['x', apkFile.path], workingDirectory: _outputDir.path); + } else { + await eval('unzip', [apkFile.path], workingDirectory: _outputDir.path); + } + _extracted = true; + } + + /// Returns the full path to the [dexdump] tool. + Future _findDexDump() async { + final String androidHome = Platform.environment['ANDROID_HOME'] ?? + Platform.environment['ANDROID_SDK_ROOT']; + + if (androidHome == null || androidHome.isEmpty) { + throw Exception('Unset env flag: `ANDROID_HOME` or `ANDROID_SDK_ROOT`.'); + } + String dexdumps; + if (Platform.isWindows) { + dexdumps = await eval('dir', ['/s/b', 'dexdump.exe'], + workingDirectory: androidHome); + } else { + dexdumps = await eval('find', [androidHome, '-name', 'dexdump']); + } + if (dexdumps.isEmpty) { + throw Exception('Couldn\'t find a dexdump executable.'); + } + return dexdumps.split('\n').first; + } + + // Removes any temporary directory. + void dispose() { + if (!_extracted) { + return; + } + rmTree(_outputDir); + _extracted = true; + } + + /// Returns true if the APK contains a given class. + Future containsClass(String className) async { + await _extractApk(); + + final String dexDump = await _findDexDump(); + final String classesDex = path.join(_outputDir.path, 'classes.dex'); + + if (!File(classesDex).existsSync()) { + throw Exception('Couldn\'t find classes.dex in the APK.'); + } + final String classDescriptors = await eval(dexDump, + [classesDex], printStdout: false); + + if (classDescriptors.isEmpty) { + throw Exception('No descriptors found in classes.dex.'); + } + return classDescriptors.contains(className.replaceAll('.', '/')); + } +} + + /// Checks that the classes are contained in the APK, throws otherwise. +Future checkApkContainsClasses(File apk, List classes) async { + final ApkExtractor extractor = ApkExtractor(apk); + for (String className in classes) { + if (!(await extractor.containsClass(className))) { + throw Exception('APK doesn\'t contain class `$className`.'); + } + } + extractor.dispose(); +} + class FlutterProject { FlutterProject(this.parent, this.name); diff --git a/dev/devicelab/lib/framework/utils.dart b/dev/devicelab/lib/framework/utils.dart index 68ef7987ab..4cb5ed7fb5 100644 --- a/dev/devicelab/lib/framework/utils.dart +++ b/dev/devicelab/lib/framework/utils.dart @@ -303,7 +303,7 @@ Future exec( /// Executes a command and returns its standard output as a String. /// -/// For logging purposes, the command's output is also printed out. +/// For logging purposes, the command's output is also printed out by default. Future eval( String executable, List arguments, { @@ -311,6 +311,8 @@ Future eval( bool canFail = false, // as in, whether failures are ok. False means that they are fatal. String workingDirectory, StringBuffer stderr, // if not null, the stderr will be written here + bool printStdout = true, + bool printStderr = true, }) async { final Process process = await startProcess(executable, arguments, environment: environment, workingDirectory: workingDirectory); @@ -321,14 +323,18 @@ Future eval( .transform(utf8.decoder) .transform(const LineSplitter()) .listen((String line) { - print('stdout: $line'); + if (printStdout) { + print('stdout: $line'); + } output.writeln(line); }, onDone: () { stdoutDone.complete(); }); process.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen((String line) { - print('stderr: $line'); + if (printStderr) { + print('stderr: $line'); + } stderr?.writeln(line); }, onDone: () { stderrDone.complete(); }); @@ -619,3 +625,10 @@ void setLocalEngineOptionIfNecessary(List options, [String flavor]) { options.add('--local-engine=${osNames[deviceOperatingSystem]}_$flavor'); } } + +/// Checks that the file exists, otherwise throws a [FileSystemException]. +void checkFileExists(String file) { + if (!exists(File(file))) { + throw FileSystemException('Expected file to exit.', file); + } +} diff --git a/packages/flutter_tools/gradle/aar_init_script.gradle b/packages/flutter_tools/gradle/aar_init_script.gradle new file mode 100644 index 0000000000..1285c58819 --- /dev/null +++ b/packages/flutter_tools/gradle/aar_init_script.gradle @@ -0,0 +1,128 @@ +// 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. +// +// This script is used to initialize the build in a module or plugin project. +// During this phase, the script applies the Maven plugin and configures the +// destination of the local repository. +// The local repository will contain the AAR and POM files. + +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.maven.MavenDeployer +import org.gradle.api.plugins.MavenPlugin +import org.gradle.api.tasks.Upload + +void configureProject(Project project, File outputDir) { + if (!project.hasProperty("android")) { + throw new GradleException("Android property not found.") + } + if (!project.android.hasProperty("libraryVariants")) { + throw new GradleException("Can't generate AAR on a non Android library project."); + } + + project.apply plugin: "maven" + + project.android.libraryVariants.all { variant -> + addAarTask(project, variant) + } + // Snapshot versions include the timestamp in the artifact name. + // Therefore, remove the snapshot part, so new runs of `flutter build aar` overrides existing artifacts. + // This version isn't relevant in Flutter since the pub version is used + // to resolve dependencies. + project.version = project.version.replace("-SNAPSHOT", "") + + project.uploadArchives { + repositories { + mavenDeployer { + repository(url: "file://${outputDir}/outputs/repo") + } + } + } + // Check if the project uses the Flutter plugin (defined in flutter.gradle). + Boolean usesFlutterPlugin = project.plugins.find { it.class.name == "FlutterPlugin" } != null + if (!usesFlutterPlugin) { + // Plugins don't include their dependencies under the assumption that the parent project adds them. + if (project.properties['android.useAndroidX']) { + project.dependencies { + compileOnly "androidx.annotation:annotation:+" + } + } else { + project.dependencies { + compileOnly "com.android.support:support-annotations:+" + } + } + project.dependencies { + // The Flutter plugin already adds `flutter.jar`. + compileOnly project.files("${getFlutterRoot(project)}/bin/cache/artifacts/engine/android-arm-release/flutter.jar") + } + } +} + +String getFlutterRoot(Project project) { + if (!project.hasProperty("flutter-root")) { + throw new GradleException("The `-Pflutter-root` flag must be specified.") + } + return project.property("flutter-root") +} + +void addAarTask(Project project, variant) { + String variantName = variant.name.capitalize() + String taskName = "assembleAar${variantName}" + project.tasks.create(name: taskName) { + // This check is required to be able to configure the archives before `uploadArchives` runs. + if (!project.gradle.startParameter.taskNames.contains(taskName)) { + return + } + // NOTE(blasten): `android.defaultPublishConfig` must equal the variant name to build. + // Where variant name is ``. However, it's too late to configure + // `defaultPublishConfig` at this point. Therefore, the code below ensures that the + // default build config uses the artifacts produced for the specific build variant. + Task bundle = project.tasks.findByName("bundle${variantName}Aar") // gradle:3.2.0 + if (bundle == null) { + bundle = project.tasks.findByName("bundle${variantName}") // gradle:3.1.0 + } + if (bundle == null) { + throw new GradleException("Can't generate AAR for variant ${variantName}."); + } + project.uploadArchives.repositories.mavenDeployer { + pom { + artifactId = "${project.name}_${variant.name.toLowerCase()}" + } + } + // Clear the current archives since the current one is assigned based on + // `android.defaultPublishConfig` which defaults to `release`. + project.configurations["archives"].artifacts.clear() + // Add the artifact that will be published. + project.artifacts.add("archives", bundle) + // Generate the Maven artifacts. + finalizedBy "uploadArchives" + } +} + +projectsEvaluated { + if (rootProject.property("is-plugin").toBoolean()) { + if (rootProject.hasProperty("output-dir")) { + rootProject.buildDir = rootProject.property("output-dir") + } else { + rootProject.buildDir = "../build"; + } + // In plugin projects, the Android library is the root project. + configureProject(rootProject, rootProject.buildDir) + return + } + // In module projects, the Android library project is the `:flutter` subproject. + Project androidLibrarySubproject = rootProject.subprojects.find { it.name == "flutter" } + // In module projects, the `buildDir` is defined in the `:app` subproject. + Project appSubproject = rootProject.subprojects.find { it.name == "app" } + + assert appSubproject != null + assert androidLibrarySubproject != null + + if (appSubproject.hasProperty("output-dir")) { + appSubproject.buildDir = appSubproject.property("output-dir") + } else { + appSubproject.buildDir = "../build/host" + } + configureProject(androidLibrarySubproject, appSubproject.buildDir) +} diff --git a/packages/flutter_tools/gradle/deprecated_settings.gradle b/packages/flutter_tools/gradle/deprecated_settings.gradle new file mode 100644 index 0000000000..98e3600b90 --- /dev/null +++ b/packages/flutter_tools/gradle/deprecated_settings.gradle @@ -0,0 +1,31 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} +;EOF +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withInputStream { stream -> plugins.load(stream) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/packages/flutter_tools/gradle/flutter.gradle b/packages/flutter_tools/gradle/flutter.gradle index 52317dc907..50f40281b2 100644 --- a/packages/flutter_tools/gradle/flutter.gradle +++ b/packages/flutter_tools/gradle/flutter.gradle @@ -1,8 +1,13 @@ -import java.nio.file.Path -import java.nio.file.Paths +// 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 static groovy.io.FileType.FILES import com.android.builder.model.AndroidProject import com.android.build.OutputFile +import java.nio.file.Path +import java.nio.file.Paths import org.apache.tools.ant.taskdefs.condition.Os import org.gradle.api.DefaultTask import org.gradle.api.GradleException @@ -91,7 +96,7 @@ class FlutterPlugin implements Plugin { @Override void apply(Project project) { project.extensions.create("flutter", FlutterExtension) - project.afterEvaluate this.&addFlutterTask + project.afterEvaluate this.&addFlutterTasks // By default, assembling APKs generates fat APKs if multiple platforms are passed. // Configuring split per ABI allows to generate separate APKs for each abi. @@ -203,42 +208,118 @@ class FlutterPlugin implements Plugin { }) } } + } + /** + * Returns the directory where the plugins are built. + */ + private File getPluginBuildDir(Project project) { + // Module projects specify this flag to include plugins in the same repo as the module project. + if (project.ext.has("pluginBuildDir")) { + return project.ext.get("pluginBuildDir") + } + return project.buildDir + } + + private Properties getPluginList(Project project) { File pluginsFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins') - Properties plugins = readPropertiesIfExist(pluginsFile) + return readPropertiesIfExist(pluginsFile) + } - plugins.each { name, _ -> - def pluginProject = project.rootProject.findProject(":$name") - if (pluginProject != null) { - project.dependencies { - if (project.getConfigurations().findByName("implementation")) { - implementation pluginProject - } else { - compile pluginProject + private void addPluginTasks(Project project) { + Properties plugins = getPluginList(project) + 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(project) + pluginDir project.file(path) + sourceDir project.file(project.flutter.source) + intermediateDir getPluginBuildDir(project) } } - pluginProject.afterEvaluate { - pluginProject.android.buildTypes { - profile { - initWith debug - } - } - - pluginProject.android.buildTypes.each { - def buildMode = buildModeFor(it) - addFlutterJarCompileOnlyDependency(pluginProject, it.name, project.files( flutterJar ?: baseJar[buildMode] )) - } - pluginProject.android.buildTypes.whenObjectAdded { - def buildMode = buildModeFor(it) - addFlutterJarCompileOnlyDependency(pluginProject, it.name, project.files( flutterJar ?: baseJar[buildMode] )) - } - } - } else { - project.logger.error("Plugin project :$name not found. Please update settings.gradle.") } } } + private void buildPlugins(Project project, 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(project)}/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(Project project, 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 "" + } + return "${parts[0]}${parts[1..-1].collect { it.capitalize() }.join('')}" + } + private String resolveProperty(Project project, String name, String defaultValue) { if (localProperties == null) { localProperties = readPropertiesIfExist(new File(project.projectDir.parentFile, "local.properties")) @@ -287,6 +368,17 @@ class FlutterPlugin implements Plugin { return project.hasProperty('localEngineOut') } + private static Boolean isVerbose(Project project) { + if (project.hasProperty('verbose')) { + return project.property('verbose').toBoolean() + } + return false + } + + private static Boolean buildPluginAsAar() { + return System.getProperty('build-plugins-as-aars') == 'true' + } + /** * Returns the platform that is used to extract the `libflutter.so` and the .class files. * @@ -304,30 +396,24 @@ class FlutterPlugin implements Plugin { if (project.state.failure) { return } - - project.dependencies { - String configuration; - if (project.getConfigurations().findByName("compileOnly")) { - configuration = "${variantName}CompileOnly"; - } else { - configuration = "${variantName}Provided"; - } - - add(configuration, files) + String configuration; + if (project.getConfigurations().findByName("compileOnly")) { + configuration = "${variantName}CompileOnly"; + } else { + configuration = "${variantName}Provided"; } + project.dependencies.add(configuration, files) } - private static void addApiDependencies(Project project, String variantName, FileCollection files) { - project.dependencies { - String configuration; - // `compile` dependencies are now `api` dependencies. - if (project.getConfigurations().findByName("api")) { - configuration = "${variantName}Api"; - } else { - configuration = "${variantName}Compile"; - } - add(configuration, files) + private static void addApiDependencies(Project project, String variantName, Object dependency, Closure config = null) { + String configuration; + // `compile` dependencies are now `api` dependencies. + if (project.getConfigurations().findByName("api")) { + configuration = "${variantName}Api"; + } else { + configuration = "${variantName}Compile"; } + project.dependencies.add(configuration, dependency, config) } /** @@ -355,14 +441,13 @@ class FlutterPlugin implements Plugin { return "${targetArch}-release" } - private void addFlutterTask(Project project) { + private void addFlutterTasks(Project project) { if (project.state.failure) { return } if (project.flutter.source == null) { throw new GradleException("Must provide Flutter source directory") } - String target = project.flutter.target if (target == null) { target = 'lib/main.dart' @@ -371,10 +456,6 @@ class FlutterPlugin implements Plugin { target = project.property('target') } - Boolean verboseValue = null - if (project.hasProperty('verbose')) { - verboseValue = project.property('verbose').toBoolean() - } String[] fileSystemRootsValue = null if (project.hasProperty('filesystem-roots')) { fileSystemRootsValue = project.property('filesystem-roots').split('\\|') @@ -440,10 +521,9 @@ class FlutterPlugin implements Plugin { } } - def flutterTasks = [] - targetPlatforms.each { targetArch -> + def compileTasks = targetPlatforms.collect { targetArch -> String abiValue = PLATFORM_ARCH_MAP[targetArch] - String taskName = "compile${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}${targetArch.replace('android-', '').capitalize()}" + String taskName = toCammelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name, targetArch.replace('android-', '')]) FlutterTask compileTask = project.tasks.create(name: taskName, type: FlutterTask) { flutterRoot this.flutterRoot flutterExecutable this.flutterExecutable @@ -452,7 +532,7 @@ class FlutterPlugin implements Plugin { localEngineSrcPath this.localEngineSrcPath abi abiValue targetPath target - verbose verboseValue + verbose isVerbose(project) fileSystemRoots fileSystemRootsValue fileSystemScheme fileSystemSchemeValue trackWidgetCreation trackWidgetCreationValue @@ -466,8 +546,8 @@ class FlutterPlugin implements Plugin { extraFrontEndOptions extraFrontEndOptionsValue extraGenSnapshotOptions extraGenSnapshotOptionsValue } - flutterTasks.add(compileTask) } + def libJar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/libs.jar") def libFlutterPlatforms = targetPlatforms.collect() // x86/x86_64 native library used for debugging only, for now. @@ -496,13 +576,13 @@ class FlutterPlugin implements Plugin { include 'lib/**' } } - dependsOn flutterTasks + dependsOn compileTasks // Add the ELF library. - flutterTasks.each { flutterTask -> - from(flutterTask.intermediateDir) { + compileTasks.each { compileTask -> + from(compileTask.intermediateDir) { include '*.so' rename { String filename -> - return "lib/${flutterTask.abi}/lib${filename}" + return "lib/${compileTask.abi}/lib${filename}" } } } @@ -516,7 +596,7 @@ class FlutterPlugin implements Plugin { Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets") Task cleanPackageAssets = project.tasks.findByPath(":flutter:cleanPackage${variant.name.capitalize()}Assets") Task copyFlutterAssetsTask = project.tasks.create(name: "copyFlutterAssets${variant.name.capitalize()}", type: Copy) { - dependsOn flutterTasks + dependsOn compileTasks if (packageAssets && cleanPackageAssets) { dependsOn packageAssets dependsOn cleanPackageAssets @@ -527,7 +607,7 @@ class FlutterPlugin implements Plugin { variant.mergeAssets.mustRunAfter("clean${variant.mergeAssets.name.capitalize()}") into variant.mergeAssets.outputDir } - flutterTasks.each { flutterTask -> + compileTasks.each { flutterTask -> with flutterTask.assets } } @@ -550,12 +630,62 @@ class FlutterPlugin implements Plugin { processResources.dependsOn(copyFlutterAssetsTask) } } - if (project.android.hasProperty("applicationVariants")) { project.android.applicationVariants.all addFlutterDeps } else { project.android.libraryVariants.all addFlutterDeps } + + if (buildPluginAsAar()) { + addPluginTasks(project) + + List tasksToExecute = project.gradle.startParameter.taskNames + Set buildTypes = getBuildTypesForTasks(project, 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") + } + } + // 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. + buildPlugins(project, buildTypes) + } + } else { + getPluginList(project).each { name, _ -> + def pluginProject = project.rootProject.findProject(":$name") + if (pluginProject != null) { + project.dependencies { + if (project.getConfigurations().findByName("implementation")) { + implementation pluginProject + } else { + compile pluginProject + } + } + pluginProject.afterEvaluate { + pluginProject.android.buildTypes { + profile { + initWith debug + } + } + pluginProject.android.buildTypes.each { + def buildMode = buildModeFor(it) + addFlutterJarCompileOnlyDependency(pluginProject, it.name, project.files( flutterJar ?: baseJar[buildMode] )) + } + pluginProject.android.buildTypes.whenObjectAdded { + def buildMode = buildModeFor(it) + addFlutterJarCompileOnlyDependency(pluginProject, it.name, project.files( flutterJar ?: baseJar[buildMode] )) + } + } + } else { + project.logger.error("Plugin project :$name not found. Please update settings.gradle.") + } + } + } } } @@ -784,6 +914,59 @@ class FlutterTask extends BaseFlutterTask { } } +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" + } + } + } +} + gradle.useLogger(new FlutterEventLogger()) class FlutterEventLogger extends BuildAdapter implements TaskExecutionListener { diff --git a/packages/flutter_tools/gradle/manual_migration_settings.gradle.md b/packages/flutter_tools/gradle/manual_migration_settings.gradle.md new file mode 100644 index 0000000000..f899531917 --- /dev/null +++ b/packages/flutter_tools/gradle/manual_migration_settings.gradle.md @@ -0,0 +1,19 @@ +To manually update `settings.gradle`, follow these steps: + + 1. Copy `settings.gradle` as `settings_aar.gradle` + 2. Remove the following code from `settings_aar.gradle`: + + def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + + def plugins = new Properties() + def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') + if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } + } + + plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory + } + diff --git a/packages/flutter_tools/gradle/settings_aar.gradle.tmpl b/packages/flutter_tools/gradle/settings_aar.gradle.tmpl new file mode 100644 index 0000000000..e7b4def49c --- /dev/null +++ b/packages/flutter_tools/gradle/settings_aar.gradle.tmpl @@ -0,0 +1 @@ +include ':app' diff --git a/packages/flutter_tools/lib/src/android/aar.dart b/packages/flutter_tools/lib/src/android/aar.dart new file mode 100644 index 0000000000..f619f775e0 --- /dev/null +++ b/packages/flutter_tools/lib/src/android/aar.dart @@ -0,0 +1,62 @@ +// 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 'dart:async'; + +import 'package:meta/meta.dart'; + +import '../base/common.dart'; +import '../build_info.dart'; +import '../project.dart'; + +import 'android_sdk.dart'; +import 'gradle.dart'; + +/// Provides a method to build a module or plugin as AAR. +abstract class AarBuilder { + /// Builds the AAR artifacts. + Future build({ + @required FlutterProject project, + @required AndroidBuildInfo androidBuildInfo, + @required String target, + @required String outputDir, + }); +} + +/// Default implementation of [AarBuilder]. +class AarBuilderImpl extends AarBuilder { + AarBuilderImpl(); + + /// Builds the AAR and POM files for the current Flutter module or plugin. + @override + Future build({ + @required FlutterProject project, + @required AndroidBuildInfo androidBuildInfo, + @required String target, + @required String outputDir, + }) async { + if (!project.android.isUsingGradle) { + throwToolExit( + 'The build process for Android has changed, and the current project configuration\n' + '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.'); + } + await buildGradleAar( + project: project, + androidBuildInfo: androidBuildInfo, + target: target, + outputDir: outputDir, + ); + androidSdk.reinitialize(); + } +} diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index bf0ad9afbb..edcf064b49 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -17,8 +17,10 @@ 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 '../features.dart'; import '../flutter_manifest.dart'; import '../globals.dart'; import '../project.dart'; @@ -27,10 +29,10 @@ import '../runner/flutter_command.dart'; import 'android_sdk.dart'; import 'android_studio.dart'; -const String gradleVersion = '4.10.2'; final RegExp _assembleTaskPattern = RegExp(r'assemble(\S+)'); -GradleProject _cachedGradleProject; +GradleProject _cachedGradleAppProject; +GradleProject _cachedGradleLibraryProject; String _cachedGradleExecutable; enum FlutterPluginVersion { @@ -102,14 +104,19 @@ Future getGradleAppOut(AndroidProject androidProject) async { case FlutterPluginVersion.managed: // Fall through. The managed plugin matches plugin v2 for now. case FlutterPluginVersion.v2: - return fs.file((await _gradleProject()).apkDirectory.childFile('app.apk')); + return fs.file((await _gradleAppProject()).apkDirectory.childFile('app.apk')); } return null; } -Future _gradleProject() async { - _cachedGradleProject ??= await _readGradleProject(); - return _cachedGradleProject; +Future _gradleAppProject() async { + _cachedGradleAppProject ??= await _readGradleProject(isLibrary: false); + return _cachedGradleAppProject; +} + +Future _gradleLibraryProject() async { + _cachedGradleLibraryProject ??= await _readGradleProject(isLibrary: true); + return _cachedGradleLibraryProject; } /// Runs `gradlew dependencies`, ensuring that dependencies are resolved and @@ -127,32 +134,101 @@ Future checkGradleDependencies() async { progress.stop(); } +/// Tries to create `settings_aar.gradle` in an app project by removing the subprojects +/// from the existing `settings.gradle` file. This operation will fail if the existing +/// `settings.gradle` file has local edits. +void createSettingsAarGradle(Directory androidDirectory) { + final File newSettingsFile = androidDirectory.childFile('settings_aar.gradle'); + if (newSettingsFile.existsSync()) { + return; + } + final File currentSettingsFile = androidDirectory.childFile('settings.gradle'); + if (!currentSettingsFile.existsSync()) { + return; + } + final String currentFileContent = currentSettingsFile.readAsStringSync(); + + final String newSettingsRelativeFile = fs.path.relative(newSettingsFile.path); + final Status status = logger.startProgress('✏️ Creating `$newSettingsRelativeFile`...', + timeout: timeoutConfiguration.fastOperation); + + final String flutterRoot = fs.path.absolute(Cache.flutterRoot); + final File deprecatedFile = fs.file(fs.path.join(flutterRoot, 'packages','flutter_tools', + 'gradle', 'deprecated_settings.gradle')); + assert(deprecatedFile.existsSync()); + final String settingsAarContent = fs.file(fs.path.join(flutterRoot, 'packages','flutter_tools', + 'gradle', 'settings_aar.gradle.tmpl')).readAsStringSync(); + + // Get the `settings.gradle` content variants that should be patched. + final List existingVariants = deprecatedFile.readAsStringSync().split(';EOF'); + existingVariants.add(settingsAarContent); + + bool exactMatch = false; + for (String fileContentVariant in existingVariants) { + if (currentFileContent.trim() == fileContentVariant.trim()) { + exactMatch = true; + break; + } + } + if (!exactMatch) { + status.cancel(); + printError('*******************************************************************************************'); + printError('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', + '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.'); +} + // Note: Dependencies are resolved and possibly downloaded as a side-effect // of calculating the app properties using Gradle. This may take minutes. -Future _readGradleProject() async { +Future _readGradleProject({bool isLibrary = false}) async { final FlutterProject flutterProject = FlutterProject.current(); final String gradle = await _ensureGradle(flutterProject); updateLocalProperties(project: flutterProject); + + 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( + ['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 runCheckedAsync( - [gradle, 'app:properties'], - workingDirectory: flutterProject.android.hostAppGradleRoot.path, + [gradle, isLibrary ? 'properties' : 'app:properties'], + workingDirectory: hostAppGradleRoot.path, environment: _gradleEnv, ); final RunResult tasksRunResult = await runCheckedAsync( - [gradle, 'app:tasks', '--all', '--console=auto'], - workingDirectory: flutterProject.android.hostAppGradleRoot.path, + [gradle, isLibrary ? 'tasks': 'app:tasks', '--all', '--console=auto'], + 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. This will exit if handled. - handleKnownGradleExceptions(exception.toString()); - + // 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.'); @@ -160,23 +236,23 @@ Future _readGradleProject() async { // Fall back to the default project = GradleProject( ['debug', 'profile', 'release'], - [], flutterProject.android.gradleAppOutV1Directory, - flutterProject.android.gradleAppBundleOutV1Directory, + [], + fs.path.join(flutterProject.android.hostAppGradleRoot.path, 'app', 'build') ); } status.stop(); return project; } -void handleKnownGradleExceptions(String exceptionString) { - // 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. - const String matcher = +/// 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(matcher, multiLine: true); - final Match licenseMatch = licenseFailure.firstMatch(exceptionString); + 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 = @@ -233,6 +309,7 @@ void injectGradleWrapper(Directory directory) { _locateGradlewExecutable(directory); 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 @@ -244,6 +321,78 @@ distributionUrl=https\\://services.gradle.org/distributions/gradle-$gradleVersio } } +/// 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 = '4.10.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.1.1'; + } + throwToolExit('Unsuported Android Plugin version: $androidPluginVersion.'); + return ''; +} + +final RegExp _androidPluginRegExp = RegExp('com\.android\.tools\.build\:gradle\:(\\d+\.\\d+\.\\d+\)'); + +/// Returns the Gradle version that the current Android plugin depends on when found, +/// otherwise it returns a default version. +/// +/// The Android plugin version is specified in the [build.gradle] file within +/// the project's Android directory. +String getGradleVersionForAndroidPlugin(Directory directory) { + final File buildFile = directory.childFile('build.gradle'); + if (!buildFile.existsSync()) { + return defaultGradleVersion; + } + 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. /// @@ -347,6 +496,95 @@ Future buildGradleProject({ } } +Future buildGradleAar({ + @required FlutterProject project, + @required AndroidBuildInfo androidBuildInfo, + @required String target, + @required String outputDir, +}) async { + final FlutterManifest manifest = project.manifest; + + GradleProject gradleProject; + if (manifest.isModule) { + gradleProject = await _gradleAppProject(); + } else if (manifest.isPlugin) { + gradleProject = await _gradleLibraryProject(); + } else { + 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 Status status = logger.startProgress( + 'Running Gradle task \'$aarTask\'...', + timeout: timeoutConfiguration.slowOperation, + multilineOutput: true, + ); + + final String gradle = await _ensureGradle(project); + final String gradlePath = fs.file(gradle).absolute.path; + final String flutterRoot = fs.path.absolute(Cache.flutterRoot); + final String initScript = fs.path.join(flutterRoot, 'packages','flutter_tools', 'gradle', 'aar_init_script.gradle'); + final List command = [ + gradlePath, + '-I=$initScript', + '-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(','); + command.add('-Ptarget-platform=$targetPlatforms'); + } + command.add(aarTask); + + final Stopwatch sw = Stopwatch()..start(); + int exitCode = 1; + + try { + exitCode = await runCommandAndStreamOutput( + 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)); + + if (exitCode != 0) { + 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('Built ${fs.path.relative(repoDirectory.path)}.', color: TerminalColor.green); +} + Future _buildGradleProjectV1(FlutterProject project, String gradle) async { // Run 'gradlew build'. final Status status = logger.startProgress( @@ -389,6 +627,22 @@ String _calculateSha(File file) { 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.'); + } +} + Future _buildGradleProjectV2( FlutterProject flutterProject, String gradle, @@ -396,7 +650,7 @@ Future _buildGradleProjectV2( String target, bool isBuildingBundle, ) async { - final GradleProject project = await _gradleProject(); + final GradleProject project = await _gradleAppProject(); final BuildInfo buildInfo = androidBuildInfo.buildInfo; String assembleTask; @@ -406,22 +660,9 @@ Future _buildGradleProjectV2( } else { assembleTask = project.assembleTaskFor(buildInfo); } - if (assembleTask == null) { - 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.'); - } else { - 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.'); - } - throwToolExit('Gradle build aborted.'); - } + printUndefinedTask(project, buildInfo); + throwToolExit('Gradle build aborted.'); } final Status status = logger.startProgress( 'Running Gradle task \'$assembleTask\'...', @@ -460,6 +701,14 @@ Future _buildGradleProjectV2( .map(getPlatformNameForAndroidArch).join(','); command.add('-Ptarget-platform=$targetPlatforms'); } + if (featureFlags.isPluginAsAarEnabled) { + // 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'); + } + } command.add(assembleTask); bool potentialAndroidXFailure = false; final Stopwatch sw = Stopwatch()..start(); @@ -604,7 +853,6 @@ Map get _gradleEnv { // 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'; @@ -612,11 +860,15 @@ Map get _gradleEnv { } class GradleProject { - GradleProject(this.buildTypes, this.productFlavors, this.apkDirectory, this.bundleDirectory); + GradleProject( + this.buildTypes, + this.productFlavors, + this.buildDirectory, + ); factory GradleProject.fromAppProperties(String properties, String tasks) { // Extract build directory. - final String buildDir = properties + final String buildDirectory = properties .split('\n') .firstWhere((String s) => s.startsWith('buildDir: ')) .substring('buildDir: '.length) @@ -648,17 +900,36 @@ class GradleProject { if (productFlavors.isEmpty) buildTypes.addAll(variants); return GradleProject( - buildTypes.toList(), - productFlavors.toList(), - fs.directory(fs.path.join(buildDir, 'outputs', 'apk')), - fs.directory(fs.path.join(buildDir, 'outputs', 'bundle')), - ); + 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; - final Directory apkDirectory; - final Directory bundleDirectory; + + /// 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); @@ -708,6 +979,14 @@ class GradleProject { 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)}'; + } + String bundleFileFor(BuildInfo buildInfo) { // For app bundle all bundle names are called as app.aab. Product flavors // & build types are differentiated as folders, where the aab will be added. diff --git a/packages/flutter_tools/lib/src/commands/build.dart b/packages/flutter_tools/lib/src/commands/build.dart index 1691b57bfc..a4439809a1 100644 --- a/packages/flutter_tools/lib/src/commands/build.dart +++ b/packages/flutter_tools/lib/src/commands/build.dart @@ -9,6 +9,7 @@ import '../commands/build_macos.dart'; import '../commands/build_windows.dart'; import '../runner/flutter_command.dart'; +import 'build_aar.dart'; import 'build_aot.dart'; import 'build_apk.dart'; import 'build_appbundle.dart'; @@ -19,6 +20,7 @@ import 'build_web.dart'; class BuildCommand extends FlutterCommand { BuildCommand({bool verboseHelp = false}) { + addSubcommand(BuildAarCommand(verboseHelp: verboseHelp)); addSubcommand(BuildApkCommand(verboseHelp: verboseHelp)); addSubcommand(BuildAppBundleCommand(verboseHelp: verboseHelp)); addSubcommand(BuildAotCommand()); diff --git a/packages/flutter_tools/lib/src/commands/build_aar.dart b/packages/flutter_tools/lib/src/commands/build_aar.dart new file mode 100644 index 0000000000..e40f522c7e --- /dev/null +++ b/packages/flutter_tools/lib/src/commands/build_aar.dart @@ -0,0 +1,94 @@ +// 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 'dart:async'; + +import '../android/aar.dart'; +import '../base/context.dart'; +import '../base/os.dart'; +import '../build_info.dart'; +import '../project.dart'; +import '../reporting/usage.dart'; +import '../runner/flutter_command.dart' show DevelopmentArtifact, FlutterCommandResult; +import 'build.dart'; + +/// The AAR builder in the current context. +AarBuilder get aarBuilder => context.get() ?? AarBuilderImpl(); + +class BuildAarCommand extends BuildSubCommand { + BuildAarCommand({bool verboseHelp = false}) { + addBuildModeFlags(verboseHelp: verboseHelp); + usesFlavorOption(); + usesPubOption(); + argParser + ..addMultiOption('target-platform', + splitCommas: true, + defaultsTo: ['android-arm', 'android-arm64'], + allowed: ['android-arm', 'android-arm64', 'android-x86', 'android-x64'], + help: 'The target platform for which the project is compiled.', + ) + ..addOption('output-dir', + help: 'The absolute path to the directory where the repository is generated.' + 'By default, this is \'android/build\'. ', + ); + } + + @override + final String name = 'aar'; + + @override + Future> get usageValues async { + final Map usage = {}; + final FlutterProject futterProject = _getProject(); + if (futterProject == null) { + return usage; + } + if (futterProject.manifest.isModule) { + usage[kCommandBuildAarProjectType] = 'module'; + } else if (futterProject.manifest.isPlugin) { + usage[kCommandBuildAarProjectType] = 'plugin'; + } else { + usage[kCommandBuildAarProjectType] = 'app'; + } + usage[kCommandBuildAarTargetPlatform] = + (argResults['target-platform'] as List).join(','); + return usage; + } + + @override + Future> get requiredArtifacts async => const { + DevelopmentArtifact.universal, + DevelopmentArtifact.android, + }; + + @override + final String description = 'Build a repository containing an AAR and a POM file.\n\n' + 'The POM file is used to include the dependencies that the AAR was compiled against.\n\n' + 'To learn more about how to use these artifacts, see ' + 'https://docs.gradle.org/current/userguide/repository_types.html#sub:maven_local'; + + @override + Future runCommand() async { + final BuildInfo buildInfo = getBuildInfo(); + final AndroidBuildInfo androidBuildInfo = AndroidBuildInfo(buildInfo, + targetArchs: argResults['target-platform'].map(getAndroidArchForName)); + + await aarBuilder.build( + project: _getProject(), + target: '', // Not needed because this command only builds Android's code. + androidBuildInfo: androidBuildInfo, + outputDir: argResults['output-dir'], + ); + return null; + } + + /// Returns the [FlutterProject] which is determinated from the remaining command-line + /// argument if any or the current working directory. + FlutterProject _getProject() { + if (argResults.rest.isEmpty) { + return FlutterProject.current(); + } + return FlutterProject.fromPath(findProjectRoot(argResults.rest.first)); + } +} diff --git a/packages/flutter_tools/lib/src/features.dart b/packages/flutter_tools/lib/src/features.dart index 69157a8dac..c56c77d89e 100644 --- a/packages/flutter_tools/lib/src/features.dart +++ b/packages/flutter_tools/lib/src/features.dart @@ -36,6 +36,9 @@ 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; @@ -65,6 +68,7 @@ const List allFeatures = [ flutterLinuxDesktopFeature, flutterMacOSDesktopFeature, flutterWindowsDesktopFeature, + flutterBuildPluginAsAarFeature, ]; /// The [Feature] for flutter web. @@ -115,6 +119,20 @@ const Feature flutterWindowsDesktopFeature = Feature( ), ); +/// The [Feature] for building plugins as AARs in an app project. +const Feature flutterBuildPluginAsAarFeature = Feature( + name: 'Build plugins independently as AARs in app projects', + configSetting: 'enable-build-plugin-as-aar', + master: FeatureChannelSetting( + available: true, + enabledByDefault: true, + ), + dev: FeatureChannelSetting( + available: true, + enabledByDefault: false, + ), +); + /// A [Feature] is a process for conditionally enabling tool features. /// /// All settings are optional, and if not provided will generally default to diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 95b526ef90..ece08466c5 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -510,10 +510,6 @@ class AndroidProject { return fs.directory(fs.path.join(hostAppGradleRoot.path, 'app', 'build', 'outputs', 'apk')); } - Directory get gradleAppBundleOutV1Directory { - return fs.directory(fs.path.join(hostAppGradleRoot.path, 'app', 'build', 'outputs', 'bundle')); - } - /// Whether the current flutter project has an Android sub-project. bool existsSync() { return parent.isModule || _editableHostAppDirectory.existsSync(); diff --git a/packages/flutter_tools/lib/src/reporting/usage.dart b/packages/flutter_tools/lib/src/reporting/usage.dart index ea2c739958..5a596d3b79 100644 --- a/packages/flutter_tools/lib/src/reporting/usage.dart +++ b/packages/flutter_tools/lib/src/reporting/usage.dart @@ -57,13 +57,16 @@ const String kCommandBuildBundleIsModule = 'cd25'; const String kCommandResult = 'cd26'; const String kCommandHasTerminal = 'cd31'; +const String kCommandBuildAarTargetPlatform = 'cd34'; +const String kCommandBuildAarProjectType = 'cd35'; + const String reloadExceptionTargetPlatform = 'cd27'; const String reloadExceptionSdkName = 'cd28'; const String reloadExceptionEmulator = 'cd29'; const String reloadExceptionFullRestart = 'cd30'; const String enabledFlutterFeatures = 'cd32'; -// Next ID: cd34 +// Next ID: cd36 Usage get flutterUsage => Usage.instance; diff --git a/packages/flutter_tools/templates/module/android/library/Flutter.tmpl/build.gradle.tmpl b/packages/flutter_tools/templates/module/android/library/Flutter.tmpl/build.gradle.tmpl index e08734edae..0a8051b1d1 100644 --- a/packages/flutter_tools/templates/module/android/library/Flutter.tmpl/build.gradle.tmpl +++ b/packages/flutter_tools/templates/module/android/library/Flutter.tmpl/build.gradle.tmpl @@ -26,6 +26,9 @@ if (flutterVersionName == null) { apply plugin: 'com.android.library' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +group '{{androidIdentifier}}' +version '1.0' + android { compileSdkVersion 28 diff --git a/packages/flutter_tools/templates/module/android/library/include_flutter.groovy.copy.tmpl b/packages/flutter_tools/templates/module/android/library/include_flutter.groovy.copy.tmpl index c6939be730..7be7efbaf8 100644 --- a/packages/flutter_tools/templates/module/android/library/include_flutter.groovy.copy.tmpl +++ b/packages/flutter_tools/templates/module/android/library/include_flutter.groovy.copy.tmpl @@ -6,24 +6,37 @@ def flutterProjectRoot = new File(scriptFile).parentFile.parentFile gradle.include ':flutter' gradle.project(':flutter').projectDir = new File(flutterProjectRoot, '.android/Flutter') -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot, '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } -} +if (System.getProperty('build-plugins-as-aars') != 'true') { + def plugins = new Properties() + def pluginsFile = new File(flutterProjectRoot, '.flutter-plugins') + if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } + } -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.toPath().resolve(path).resolve('android').toFile() - gradle.include ":$name" - gradle.project(":$name").projectDir = pluginDirectory + plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.toPath().resolve(path).resolve('android').toFile() + gradle.include ":$name" + gradle.project(":$name").projectDir = pluginDirectory + } } - gradle.getGradle().projectsLoaded { g -> g.rootProject.beforeEvaluate { p -> _mainModuleName = binding.variables['mainModuleName'] if (_mainModuleName != null && !_mainModuleName.empty) { p.ext.mainModuleName = _mainModuleName } + def subprojects = [] + def flutterProject + p.subprojects { sp -> + if (sp.name == 'flutter') { + flutterProject = sp + } else { + subprojects.add(sp) + } + } + assert flutterProject != null + flutterProject.ext.hostProjects = subprojects + flutterProject.ext.pluginBuildDir = new File(flutterProjectRoot, 'build/host') } g.rootProject.afterEvaluate { p -> p.subprojects { sp -> diff --git a/packages/flutter_tools/templates/plugin/android-java.tmpl/build.gradle.tmpl b/packages/flutter_tools/templates/plugin/android-java.tmpl/build.gradle.tmpl index 20d1a386e0..c16a39446f 100644 --- a/packages/flutter_tools/templates/plugin/android-java.tmpl/build.gradle.tmpl +++ b/packages/flutter_tools/templates/plugin/android-java.tmpl/build.gradle.tmpl @@ -1,5 +1,5 @@ group '{{androidIdentifier}}' -version '1.0-SNAPSHOT' +version '1.0' buildscript { repositories { diff --git a/packages/flutter_tools/templates/plugin/android.tmpl/gradle/wrapper/gradle-wrapper.properties b/packages/flutter_tools/templates/plugin/android.tmpl/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..019065d1d6 --- /dev/null +++ b/packages/flutter_tools/templates/plugin/android.tmpl/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip 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 b09f73ffc7..d6380f0b3b 100644 --- a/packages/flutter_tools/test/general.shard/android/gradle_test.dart +++ b/packages/flutter_tools/test/general.shard/android/gradle_test.dart @@ -6,7 +6,9 @@ import 'dart:async'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/android/gradle.dart'; +import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; @@ -168,20 +170,20 @@ someOtherTask expect(project.productFlavors, ['free', 'paid']); }); test('should provide apk file name for default build types', () { - final GradleProject project = GradleProject(['debug', 'profile', 'release'], [], fs.directory('/some/dir'),fs.directory('/some/dir')); + 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'], fs.directory('/some/dir'),fs.directory('/some/dir')); + 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'], [], fs.directory('/some/dir'),fs.directory('/some/dir')); + final GradleProject project = GradleProject(['debug', 'profile', 'release'], [], '/some/dir'); expect(project.apkFilesFor( const AndroidBuildInfo( BuildInfo.debug, @@ -224,7 +226,7 @@ someOtherTask ).isEmpty, isTrue); }); test('should provide apks for each ABI and flavored build types', () { - final GradleProject project = GradleProject(['debug', 'profile', 'release'], ['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir')); + final GradleProject project = GradleProject(['debug', 'profile', 'release'], ['free', 'paid'], '/some/dir'); expect(project.apkFilesFor( const AndroidBuildInfo( BuildInfo(BuildMode.debug, 'free'), @@ -267,54 +269,187 @@ someOtherTask ).isEmpty, isTrue); }); test('should provide bundle file name for default build types', () { - final GradleProject project = GradleProject(['debug', 'profile', 'release'], [], fs.directory('/some/dir'),fs.directory('/some/dir')); + final GradleProject project = GradleProject(['debug', 'profile', 'release'], [], '/some/dir'); expect(project.bundleFileFor(BuildInfo.debug), 'app.aab'); expect(project.bundleFileFor(BuildInfo.profile), 'app.aab'); expect(project.bundleFileFor(BuildInfo.release), 'app.aab'); expect(project.bundleFileFor(const BuildInfo(BuildMode.release, 'unknown')), 'app.aab'); }); test('should provide bundle file name for flavored build types', () { - final GradleProject project = GradleProject(['debug', 'profile', 'release'], ['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir')); + final GradleProject project = GradleProject(['debug', 'profile', 'release'], ['free', 'paid'], '/some/dir'); expect(project.bundleFileFor(const BuildInfo(BuildMode.debug, 'free')), 'app.aab'); expect(project.bundleFileFor(const BuildInfo(BuildMode.release, 'paid')), 'app.aab'); expect(project.bundleFileFor(const BuildInfo(BuildMode.release, 'unknown')), 'app.aab'); }); test('should provide assemble task name for default build types', () { - final GradleProject project = GradleProject(['debug', 'profile', 'release'], [], fs.directory('/some/dir'),fs.directory('/some/dir')); + 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'], fs.directory('/some/dir'),fs.directory('/some/dir')); + 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'], fs.directory('/some/dir'),fs.directory('/some/dir')); + 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'], [], fs.directory('/some/dir'),fs.directory('/some/dir')); + 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'], fs.directory('/some/dir'),fs.directory('/some/dir')); + 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'], fs.directory('/some/dir'),fs.directory('/some/dir')); + final GradleProject project = GradleProject(['debug'], ['randomFlavor'], '/some/dir'); expect(project.bundleTaskFor(const BuildInfo(BuildMode.debug, 'randomFlavor')), 'bundleRandomFlavorDebug'); }); }); + group('Config files', () { + BufferLogger mockLogger; + Directory tempDir; + + setUp(() { + mockLogger = BufferLogger(); + tempDir = fs.systemTempDirectory.createTempSync('settings_aar_test.'); + + }); + + testUsingContext('create settings_aar.gradle when current settings.gradle loads plugins', () { + const String currentSettingsGradle = ''' +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":\$name" + project(":\$name").projectDir = pluginDirectory +} +'''; + + const String settingsAarFile = ''' +include ':app' +'''; + + tempDir.childFile('settings.gradle').writeAsStringSync(currentSettingsGradle); + + final String toolGradlePath = fs.path.join( + fs.path.absolute(Cache.flutterRoot), + 'packages', + 'flutter_tools', + 'gradle'); + fs.directory(toolGradlePath).createSync(recursive: true); + fs.file(fs.path.join(toolGradlePath, 'deprecated_settings.gradle')) + .writeAsStringSync(currentSettingsGradle); + + fs.file(fs.path.join(toolGradlePath, 'settings_aar.gradle.tmpl')) + .writeAsStringSync(settingsAarFile); + + createSettingsAarGradle(tempDir); + + expect(mockLogger.statusText, contains('created successfully')); + expect(tempDir.childFile('settings_aar.gradle').existsSync(), isTrue); + + }, overrides: { + FileSystem: () => MemoryFileSystem(), + Logger: () => mockLogger, + }); + + testUsingContext('create settings_aar.gradle when current settings.gradle doesn\'t load plugins', () { + const String currentSettingsGradle = ''' +include ':app' +'''; + + const String settingsAarFile = ''' +include ':app' +'''; + + tempDir.childFile('settings.gradle').writeAsStringSync(currentSettingsGradle); + + final String toolGradlePath = fs.path.join( + fs.path.absolute(Cache.flutterRoot), + 'packages', + 'flutter_tools', + 'gradle'); + fs.directory(toolGradlePath).createSync(recursive: true); + fs.file(fs.path.join(toolGradlePath, 'deprecated_settings.gradle')) + .writeAsStringSync(currentSettingsGradle); + + fs.file(fs.path.join(toolGradlePath, 'settings_aar.gradle.tmpl')) + .writeAsStringSync(settingsAarFile); + + createSettingsAarGradle(tempDir); + + expect(mockLogger.statusText, contains('created successfully')); + expect(tempDir.childFile('settings_aar.gradle').existsSync(), isTrue); + + }, overrides: { + FileSystem: () => MemoryFileSystem(), + Logger: () => mockLogger, + }); + }); + + 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; @@ -540,6 +675,52 @@ flutter: ); }); }); + + group('gradle version', () { + test('should be compatible with the Android plugin version', () { + // Granular versions. + expect(getGradleVersionFor('1.0.0'), '2.3'); + expect(getGradleVersionFor('1.0.1'), '2.3'); + expect(getGradleVersionFor('1.0.2'), '2.3'); + expect(getGradleVersionFor('1.0.4'), '2.3'); + expect(getGradleVersionFor('1.0.8'), '2.3'); + expect(getGradleVersionFor('1.1.0'), '2.3'); + expect(getGradleVersionFor('1.1.2'), '2.3'); + expect(getGradleVersionFor('1.1.2'), '2.3'); + expect(getGradleVersionFor('1.1.3'), '2.3'); + // Version Ranges. + expect(getGradleVersionFor('1.2.0'), '2.9'); + expect(getGradleVersionFor('1.3.1'), '2.9'); + + expect(getGradleVersionFor('1.5.0'), '2.2.1'); + + expect(getGradleVersionFor('2.0.0'), '2.13'); + expect(getGradleVersionFor('2.1.2'), '2.13'); + + expect(getGradleVersionFor('2.1.3'), '2.14.1'); + expect(getGradleVersionFor('2.2.3'), '2.14.1'); + + expect(getGradleVersionFor('2.3.0'), '3.3'); + + expect(getGradleVersionFor('3.0.0'), '4.1'); + + expect(getGradleVersionFor('3.1.0'), '4.4'); + + expect(getGradleVersionFor('3.2.0'), '4.6'); + expect(getGradleVersionFor('3.2.1'), '4.6'); + + expect(getGradleVersionFor('3.3.0'), '4.10.2'); + expect(getGradleVersionFor('3.3.2'), '4.10.2'); + + expect(getGradleVersionFor('3.4.0'), '5.1.1'); + expect(getGradleVersionFor('3.5.0'), '5.1.1'); + }); + + test('throws on unsupported versions', () { + expect(() => getGradleVersionFor('3.6.0'), + throwsA(predicate((Exception e) => e is ToolExit))); + }); + }); } Platform fakePlatform(String name) { 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 new file mode 100644 index 0000000000..d02d764050 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart @@ -0,0 +1,87 @@ +// 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:args/command_runner.dart'; +import 'package:flutter_tools/src/android/aar.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/commands/build_aar.dart'; +import 'package:flutter_tools/src/reporting/usage.dart'; +import 'package:mockito/mockito.dart'; + +import '../../src/common.dart'; +import '../../src/context.dart'; + +void main() { + Cache.disableLocking(); + + group('getUsage', () { + Directory tempDir; + AarBuilder mockAarBuilder; + + setUp(() { + tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.'); + mockAarBuilder = MockAarBuilder(); + when(mockAarBuilder.build( + project: anyNamed('project'), + androidBuildInfo: anyNamed('androidBuildInfo'), + target: anyNamed('target'), + outputDir: anyNamed('outputDir'))).thenAnswer((_) => Future.value()); + }); + + tearDown(() { + tryToDelete(tempDir); + }); + + Future runCommandIn(String target, { List arguments }) async { + final BuildAarCommand command = BuildAarCommand(); + final CommandRunner runner = createTestCommandRunner(command); + await runner.run([ + 'aar', + ...?arguments, + target, + ]); + return command; + } + + testUsingContext('indicate that project is a module', () async { + final String projectPath = await createProject(tempDir, + arguments: ['--no-pub', '--template=module']); + + final BuildAarCommand command = await runCommandIn(projectPath); + expect(await command.usageValues, + containsPair(kCommandBuildAarProjectType, 'module')); + + }, overrides: { + AarBuilder: () => mockAarBuilder, + }, timeout: allowForCreateFlutterProject); + + testUsingContext('indicate that project is a plugin', () async { + final String projectPath = await createProject(tempDir, + arguments: ['--no-pub', '--template=plugin', '--project-name=aar_test']); + + final BuildAarCommand command = await runCommandIn(projectPath); + expect(await command.usageValues, + containsPair(kCommandBuildAarProjectType, 'plugin')); + + }, overrides: { + AarBuilder: () => mockAarBuilder, + }, timeout: allowForCreateFlutterProject); + + testUsingContext('indicate the target platform', () async { + final String projectPath = await createProject(tempDir, + arguments: ['--no-pub', '--template=module']); + + final BuildAarCommand command = await runCommandIn(projectPath, + arguments: ['--target-platform=android-arm']); + expect(await command.usageValues, + containsPair(kCommandBuildAarTargetPlatform, 'android-arm')); + + }, overrides: { + AarBuilder: () => mockAarBuilder, + }, timeout: allowForCreateFlutterProject); + }); +} + +class MockAarBuilder extends Mock implements AarBuilder {} diff --git a/packages/flutter_tools/test/general.shard/features_test.dart b/packages/flutter_tools/test/general.shard/features_test.dart index 94b336b558..5f56e51d32 100644 --- a/packages/flutter_tools/test/general.shard/features_test.dart +++ b/packages/flutter_tools/test/general.shard/features_test.dart @@ -418,6 +418,21 @@ 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(true); + + expect(featureFlags.isPluginAsAarEnabled, true); + })); + + 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(true); + + expect(featureFlags.isPluginAsAarEnabled, true); + })); }); } diff --git a/packages/flutter_tools/test/src/testbed.dart b/packages/flutter_tools/test/src/testbed.dart index c0a6873d1d..634acf0377 100644 --- a/packages/flutter_tools/test/src/testbed.dart +++ b/packages/flutter_tools/test/src/testbed.dart @@ -697,6 +697,7 @@ class TestFeatureFlags implements FeatureFlags { this.isMacOSEnabled = false, this.isWebEnabled = false, this.isWindowsEnabled = false, + this.isPluginAsAarEnabled = false, }); @override @@ -710,4 +711,7 @@ class TestFeatureFlags implements FeatureFlags { @override final bool isWindowsEnabled; + + @override + final bool isPluginAsAarEnabled; }