diff --git a/dev/bots/test.dart b/dev/bots/test.dart index 48be0bb022eec..4ebcfc32e0c2f 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 0000000000000..be55a15b7ac94 --- /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 0000000000000..70738106ded16 --- /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 0000000000000..bbabfc2e8b11e --- /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 0000000000000..72a1708ad2fcf --- /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 0000000000000..2525bdfa1a6ff --- /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 1b6e8919c56cd..0bdcdb044e2d3 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 ec71e03c7be60..f578754820210 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 0dc28b92e2e98..0c42b2652899b 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 68ef7987abd3b..4cb5ed7fb5793 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 0000000000000..1285c58819078 --- /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 0000000000000..98e3600b90e31 --- /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 52317dc907243..50f40281b29c9 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) - - plugins.each { name, _ -> - def pluginProject = project.rootProject.findProject(":$name") - if (pluginProject != null) { - project.dependencies { - if (project.getConfigurations().findByName("implementation")) { - implementation pluginProject - } else { - compile pluginProject + return readPropertiesIfExist(pluginsFile) + } + + 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] )) + 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 } - pluginProject.android.buildTypes.whenObjectAdded { - def buildMode = buildModeFor(it) - addFlutterJarCompileOnlyDependency(pluginProject, it.name, project.files( flutterJar ?: baseJar[buildMode] )) + 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 + }) } - } else { - project.logger.error("Plugin project :$name not found. Please update settings.gradle.") } } } + /** + * 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 0000000000000..f899531917bff --- /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 0000000000000..e7b4def49cb53 --- /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 0000000000000..f619f775e0f0f --- /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 bf0ad9afbba6c..7d4aa9eb3181f 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,98 @@ 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()); + // Get the `settings.gradle` content variants that should be patched. + final List deprecatedFilesContent = deprecatedFile.readAsStringSync().split(';EOF'); + bool exactMatch = false; + for (String deprecatedFileContent in deprecatedFilesContent) { + if (currentFileContent.trim() == deprecatedFileContent.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. + final String settingsAarContent = fs.file(fs.path.join(flutterRoot, 'packages','flutter_tools', + 'gradle', 'settings_aar.gradle.tmpl')).readAsStringSync(); + 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 +233,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 +306,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 +318,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 +493,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 +624,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 +647,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 +657,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 +698,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 +850,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 +857,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 +897,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 +976,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 1691b57bfc770..a4439809a1547 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 0000000000000..e40f522c7e6cb --- /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 69157a8dac286..c56c77d89eeb1 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 95b526ef908ee..ece08466c5a78 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 ea2c739958592..5a596d3b79a09 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 e08734edae9cc..0a8051b1d155d 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 c6939be730569..7be7efbaf8a7e 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 20d1a386e08cf..c16a39446fbe5 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 0000000000000..019065d1d650c --- /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 b09f73ffc7a60..80f203134d46d 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,154 @@ 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', () { + const String deprecatedFile = ''' +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(deprecatedFile); + + 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(deprecatedFile); + + 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 +642,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 0000000000000..d02d7640508c6 --- /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 94b336b558124..5f56e51d32001 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 c0a6873d1d86f..634acf0377a1b 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; }