Unverified Commit 11460b83 authored by Emmanuel Garcia's avatar Emmanuel Garcia Committed by GitHub

Add flutter build aar (#35217)

`flutter build aar`

This new build command works just like `flutter build apk` or `flutter build appbundle`, but for plugin and module projects.

This PR also refactors how plugins are included in app or module projects. By building the plugins as AARs, the Android Gradle plugin is able to use Jetifier to translate support libraries into AndroidX libraries for all the plugin's native code. Thus, reducing the error rate when using AndroidX in apps.

This change also allows to build modules as AARs, so developers can take these artifacts and distribute them along with the native host app without the need of the Flutter tool. This is a requirement for add to app.

`flutter build aar` generates POM artifacts (XML files) which contain metadata about the native dependencies used by the plugin. This allows Gradle to resolve dependencies at the app level. The result of this new build command is a single build/outputs/repo, the local repository that contains all the generated AARs and POM files.

In a Flutter app project, this local repo is used by the Flutter Gradle plugin to resolve the plugin dependencies. In add to app case, the developer needs to configure the local repo and the dependency manually in `build.gradle`:


repositories {
    maven {
        url "<path-to-flutter-module>build/host/outputs/repo"
    }
}

dependencies {
    implementation("<package-name>:flutter_<build-mode>:1.0@aar") {
       transitive = true
    }
}
parent c8f168fd
......@@ -961,9 +961,14 @@ Future<void> _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);
}
}
// 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<void> 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: <String>['--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: <String>['get'],
);
});
section('Build release AAR');
await inDirectory(projectDir, () async {
await flutter(
'build',
options: <String>['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: <String>['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);
}
});
}
// 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<void> 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: <String>[
'--org', 'io.flutter.devicelab',
'--template', 'plugin',
'hello',
],
);
});
section('Build release AAR');
await inDirectory(projectDir, () async {
await flutter(
'build',
options: <String>['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: <String>[
'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);
}
});
}
// 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<void> 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: <String>[
'--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: <String>['get'],
);
});
section('Build release APK');
await inDirectory(projectDir, () async {
await flutter(
'build',
options: <String>[
'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, <String>[
// 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: <String>[
'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, <String>[
// 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);
}
});
}
// 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<void> 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: <String>['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: <String>[
'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: <String>[
'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: <String>[
'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);
}
});
}
// 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<void> 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: <String>[
'--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: <String>['get'],
);
});
section('Build release APK');
await inDirectory(projectDir, () async {
await flutter(
'build',
options: <String>[
'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, <String>[
// 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: <String>[
'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, <String>[
// 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);
}
});
}
......@@ -42,7 +42,7 @@ Future<void> 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 {
......
......@@ -143,7 +143,7 @@ Future<void> 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 {
......
......@@ -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<void> _extractApk() async {
if (_extracted) {
return;
}
_outputDir = apkFile.parent.createTempSync('apk');
if (Platform.isWindows) {
await eval('7za', <String>['x', apkFile.path], workingDirectory: _outputDir.path);
} else {
await eval('unzip', <String>[apkFile.path], workingDirectory: _outputDir.path);
}
_extracted = true;
}
/// Returns the full path to the [dexdump] tool.
Future<String> _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', <String>['/s/b', 'dexdump.exe'],
workingDirectory: androidHome);
} else {
dexdumps = await eval('find', <String>[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<bool> 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,
<String>[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<void> checkApkContainsClasses(File apk, List<String> 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);
......
......@@ -303,7 +303,7 @@ Future<int> 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<String> eval(
String executable,
List<String> arguments, {
......@@ -311,6 +311,8 @@ Future<String> 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<String> eval(
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('stdout: $line');
if (printStdout) {
print('stdout: $line');
}
output.writeln(line);
}, onDone: () { stdoutDone.complete(); });
process.stderr
.transform<String>(utf8.decoder)
.transform<String>(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<String> 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);
}
}
// 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 `<product-flavor><Build-Type>`. 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)
}
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
}
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
}
// 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<void> 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<void> 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();
}
}
......@@ -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());
......
// 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<AarBuilder>() ?? AarBuilderImpl();
class BuildAarCommand extends BuildSubCommand {
BuildAarCommand({bool verboseHelp = false}) {
addBuildModeFlags(verboseHelp: verboseHelp);
usesFlavorOption();
usesPubOption();
argParser
..addMultiOption('target-platform',
splitCommas: true,
defaultsTo: <String>['android-arm', 'android-arm64'],
allowed: <String>['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 \'<current-directory>android/build\'. ',
);
}
@override
final String name = 'aar';
@override
Future<Map<String, String>> get usageValues async {
final Map<String, String> usage = <String, String>{};
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<String>).join(',');
return usage;
}
@override
Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
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<FlutterCommandResult> runCommand() async {
final BuildInfo buildInfo = getBuildInfo();
final AndroidBuildInfo androidBuildInfo = AndroidBuildInfo(buildInfo,
targetArchs: argResults['target-platform'].map<AndroidArch>(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));
}
}
......@@ -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<Feature> allFeatures = <Feature>[
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
......
......@@ -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();
......
......@@ -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;
......
......@@ -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
......
......@@ -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 ->
......
group '{{androidIdentifier}}'
version '1.0-SNAPSHOT'
version '1.0'
buildscript {
repositories {
......
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
// 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<void>.value());
});
tearDown(() {
tryToDelete(tempDir);
});
Future<BuildAarCommand> runCommandIn(String target, { List<String> arguments }) async {
final BuildAarCommand command = BuildAarCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>[
'aar',
...?arguments,
target,
]);
return command;
}
testUsingContext('indicate that project is a module', () async {
final String projectPath = await createProject(tempDir,
arguments: <String>['--no-pub', '--template=module']);
final BuildAarCommand command = await runCommandIn(projectPath);
expect(await command.usageValues,
containsPair(kCommandBuildAarProjectType, 'module'));
}, overrides: <Type, Generator>{
AarBuilder: () => mockAarBuilder,
}, timeout: allowForCreateFlutterProject);
testUsingContext('indicate that project is a plugin', () async {
final String projectPath = await createProject(tempDir,
arguments: <String>['--no-pub', '--template=plugin', '--project-name=aar_test']);
final BuildAarCommand command = await runCommandIn(projectPath);
expect(await command.usageValues,
containsPair(kCommandBuildAarProjectType, 'plugin'));
}, overrides: <Type, Generator>{
AarBuilder: () => mockAarBuilder,
}, timeout: allowForCreateFlutterProject);
testUsingContext('indicate the target platform', () async {
final String projectPath = await createProject(tempDir,
arguments: <String>['--no-pub', '--template=module']);
final BuildAarCommand command = await runCommandIn(projectPath,
arguments: <String>['--target-platform=android-arm']);
expect(await command.usageValues,
containsPair(kCommandBuildAarTargetPlatform, 'android-arm'));
}, overrides: <Type, Generator>{
AarBuilder: () => mockAarBuilder,
}, timeout: allowForCreateFlutterProject);
});
}
class MockAarBuilder extends Mock implements AarBuilder {}
......@@ -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<bool>(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<bool>(mockFlutterConfig.getValue('enable-build-plugin-as-aar')).thenReturn(true);
expect(featureFlags.isPluginAsAarEnabled, true);
}));
});
}
......
......@@ -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;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment