Unverified Commit da90156f authored by Emmanuel Garcia's avatar Emmanuel Garcia Committed by GitHub

Fix plugin java class desugar (#73758)

parent 49667ba2
// Copyright 2014 The Flutter 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:io';
import 'package:flutter_devicelab/framework/apk_utils.dart';
import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/task_result.dart';
import 'package:flutter_devicelab/framework/utils.dart';
Future<void> main() async {
await task(() async {
try {
await runProjectTest((FlutterProject flutterProject) async {
section('APK contains plugin classes');
flutterProject.addPlugin('google_maps_flutter', value: '^1.0.10');
await inDirectory(flutterProject.rootPath, () async {
await flutter('build', options: <String>[
'apk',
'--debug',
'--target-platform=android-arm',
]);
final File apk = File('${flutterProject.rootPath}/build/app/outputs/flutter-apk/app-debug.apk');
if (!apk.existsSync()) {
throw TaskResult.failure('Expected ${apk.path} to exist, but it doesn\'t');
}
// https://github.com/flutter/flutter/issues/72185
await checkApkContainsMethods(apk, <String>[
'io.flutter.plugins.googlemaps.GoogleMapController void onFlutterViewAttached(android.view.View)',
'io.flutter.plugins.googlemaps.GoogleMapController void onFlutterViewDetached()',
]);
});
});
return TaskResult.success(null);
} on TaskResult catch (taskResult) {
return taskResult;
} catch (e) {
return TaskResult.failure(e.toString());
}
});
}
...@@ -115,12 +115,14 @@ String get _androidHome { ...@@ -115,12 +115,14 @@ String get _androidHome {
/// Executes an APK analyzer subcommand. /// Executes an APK analyzer subcommand.
Future<String> _evalApkAnalyzer( Future<String> _evalApkAnalyzer(
List<String> args, { List<String> args, {
bool printStdout = true, bool printStdout = false,
String workingDirectory, String workingDirectory,
}) async { }) async {
final String javaHome = await findJavaHome(); final String javaHome = await findJavaHome();
if (javaHome == null || javaHome.isEmpty) {
final String apkAnalyzer = path throw Exception('No JAVA_HOME set.');
}
final String apkAnalyzer = path
.join(_androidHome, 'cmdline-tools', 'latest', 'bin', Platform.isWindows ? 'apkanalyzer.bat' : 'apkanalyzer'); .join(_androidHome, 'cmdline-tools', 'latest', 'bin', Platform.isWindows ? 'apkanalyzer.bat' : 'apkanalyzer');
if (canRun(apkAnalyzer)) { if (canRun(apkAnalyzer)) {
return eval( return eval(
...@@ -165,6 +167,7 @@ class ApkExtractor { ...@@ -165,6 +167,7 @@ class ApkExtractor {
bool _extracted = false; bool _extracted = false;
Set<String> _classes = const <String>{}; Set<String> _classes = const <String>{};
Set<String> _methods = const <String>{};
Future<void> _extractDex() async { Future<void> _extractDex() async {
if (_extracted) { if (_extracted) {
...@@ -177,22 +180,17 @@ class ApkExtractor { ...@@ -177,22 +180,17 @@ class ApkExtractor {
apkFile.path, apkFile.path,
], ],
); );
final List<String> lines = packages.split('\n');
_classes = Set<String>.from( _classes = Set<String>.from(
packages lines.where((String line) => line.startsWith('C'))
.split('\n') .map<String>((String line) => line.split('\t').last),
.where((String line) => line.startsWith('C'))
.map<String>((String line) => line.split('\t').last),
); );
assert(_classes.isNotEmpty); assert(_classes.isNotEmpty);
_extracted = true; _methods = Set<String>.from(
} lines.where((String line) => line.startsWith('M'))
.map<String>((String line) => line.split('\t').last)
// Removes any temporary directory. );
void dispose() { assert(_methods.isNotEmpty);
if (!_extracted) {
return;
}
_classes = const <String>{};
_extracted = true; _extracted = true;
} }
...@@ -201,6 +199,13 @@ class ApkExtractor { ...@@ -201,6 +199,13 @@ class ApkExtractor {
await _extractDex(); await _extractDex();
return _classes.contains(className); return _classes.contains(className);
} }
/// Returns true if the APK contains a given method.
/// For example: io.flutter.plugins.googlemaps.GoogleMapController void onFlutterViewAttached(android.view.View)
Future<bool> containsMethod(String methodName) async {
await _extractDex();
return _methods.contains(methodName);
}
} }
/// Gets the content of the `AndroidManifest.xml`. /// Gets the content of the `AndroidManifest.xml`.
...@@ -223,7 +228,16 @@ Future<void> checkApkContainsClasses(File apk, List<String> classes) async { ...@@ -223,7 +228,16 @@ Future<void> checkApkContainsClasses(File apk, List<String> classes) async {
throw Exception("APK doesn't contain class `$className`."); throw Exception("APK doesn't contain class `$className`.");
} }
} }
extractor.dispose(); }
/// Checks that the methods are defined in the APK, throws otherwise.
Future<void> checkApkContainsMethods(File apk, List<String> methods) async {
final ApkExtractor extractor = ApkExtractor(apk);
for (final String method in methods) {
if (!(await extractor.containsMethod(method))) {
throw Exception("APK doesn't contain method `$method`.");
}
}
} }
class FlutterProject { class FlutterProject {
...@@ -288,7 +302,7 @@ subprojects { ...@@ -288,7 +302,7 @@ subprojects {
String content = await pubspec.readAsString(); String content = await pubspec.readAsString();
content = content.replaceFirst( content = content.replaceFirst(
'${platformLineSep}dependencies:$platformLineSep', '${platformLineSep}dependencies:$platformLineSep',
'${platformLineSep}dependencies:$platformLineSep $plugin:$value$platformLineSep', '${platformLineSep}dependencies:$platformLineSep $plugin: $value$platformLineSep',
); );
await pubspec.writeAsString(content, flush: true); await pubspec.writeAsString(content, flush: true);
} }
......
...@@ -456,6 +456,8 @@ List<String> flutterCommandArgs(String command, List<String> options) { ...@@ -456,6 +456,8 @@ List<String> flutterCommandArgs(String command, List<String> options) {
]; ];
} }
/// Runs the flutter `command`, and returns the exit code.
/// If `canFail` is `false`, the future completes with an error.
Future<int> flutter(String command, { Future<int> flutter(String command, {
List<String> options = const <String>[], List<String> options = const <String>[],
bool canFail = false, // as in, whether failures are ok. False means that they are fatal. bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
......
...@@ -101,6 +101,19 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -101,6 +101,19 @@ class FlutterPlugin implements Plugin<Project> {
void apply(Project project) { void apply(Project project) {
this.project = project this.project = project
// Configure the Maven repository.
String hostedRepository = System.env.FLUTTER_STORAGE_BASE_URL ?: DEFAULT_MAVEN_HOST
String repository = useLocalEngine()
? project.property('local-engine-repo')
: "$hostedRepository/download.flutter.io"
project.rootProject.allprojects {
repositories {
maven {
url repository
}
}
}
project.extensions.create("flutter", FlutterExtension) project.extensions.create("flutter", FlutterExtension)
this.addFlutterTasks(project) this.addFlutterTasks(project)
...@@ -184,8 +197,7 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -184,8 +197,7 @@ class FlutterPlugin implements Plugin<Project> {
localEngine = engineOut.name localEngine = engineOut.name
localEngineSrcPath = engineOut.parentFile.parent localEngineSrcPath = engineOut.parentFile.parent
} }
project.android.buildTypes.each this.&addFlutterDependencies project.android.buildTypes.all this.&addFlutterDependencies
project.android.buildTypes.whenObjectAdded this.&addFlutterDependencies
} }
/** /**
...@@ -199,21 +211,16 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -199,21 +211,16 @@ class FlutterPlugin implements Plugin<Project> {
if (!supportsBuildMode(flutterBuildMode)) { if (!supportsBuildMode(flutterBuildMode)) {
return return
} }
String hostedRepository = System.env.FLUTTER_STORAGE_BASE_URL ?: DEFAULT_MAVEN_HOST // The embedding is set as an API dependency in a Flutter plugin.
String repository = useLocalEngine() // Therefore, don't make the app project depend on the embedding if there are Flutter
? project.property('local-engine-repo') // plugins.
: "$hostedRepository/download.flutter.io" // This prevents duplicated classes when using custom build types. That is, a custom build
project.rootProject.allprojects { // type like profile is used, and the plugin and app projects have API dependencies on the
repositories { // embedding.
maven { if (!isFlutterAppProject() || getPluginList().size() == 0) {
url repository addApiDependencies(project, buildType.name,
} "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion")
}
} }
// Add the embedding dependency.
addApiDependencies(project, buildType.name,
"io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion")
List<String> platforms = getTargetPlatforms().collect() List<String> platforms = getTargetPlatforms().collect()
// Debug mode includes x86 and x64, which are commonly used in emulators. // Debug mode includes x86 and x64, which are commonly used in emulators.
if (flutterBuildMode == "debug" && !useLocalEngine()) { if (flutterBuildMode == "debug" && !useLocalEngine()) {
...@@ -303,7 +310,7 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -303,7 +310,7 @@ class FlutterPlugin implements Plugin<Project> {
} }
// Add plugin dependency to the app project. // Add plugin dependency to the app project.
project.dependencies { project.dependencies {
implementation pluginProject api pluginProject
} }
Closure addEmbeddingDependencyToPlugin = { buildType -> Closure addEmbeddingDependencyToPlugin = { buildType ->
String flutterBuildMode = buildModeFor(buildType) String flutterBuildMode = buildModeFor(buildType)
...@@ -318,39 +325,23 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -318,39 +325,23 @@ class FlutterPlugin implements Plugin<Project> {
pluginProject.android.buildTypes { pluginProject.android.buildTypes {
"${buildType.name}" {} "${buildType.name}" {}
} }
// The embedding is a compileOnly dependency of a Flutter plugin, // The embedding is API dependency of the plugin, so the AGP is able to desugar
// however prior to Gradle 6.0.0, it must be an API dependency. // default method implementations when the interface is implemented by a plugin.
// //
// Not doing so, causes transitive dependency resolution conflicts. // See https://issuetracker.google.com/139821726, and
// That is, the embedding dependencies resolved in the plugin are // https://github.com/flutter/flutter/issues/72185 for more details.
// different than the ones resolved in the app. addApiDependencies(
if (isGradleVersionGraterOrEqualThan('6.0.0')) { pluginProject,
addCompileOnlyDependency( buildType.name,
pluginProject, "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion"
buildType.name, )
"io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion"
)
} else {
addApiDependencies(
pluginProject,
buildType.name,
"io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion"
)
}
} }
// Wait until the Android plugin loaded. // Wait until the Android plugin loaded.
pluginProject.afterEvaluate { pluginProject.afterEvaluate {
project.android.buildTypes.each addEmbeddingDependencyToPlugin project.android.buildTypes.all addEmbeddingDependencyToPlugin
project.android.buildTypes.whenObjectAdded addEmbeddingDependencyToPlugin
} }
} }
// Returns `true` if the current Gradle version is greater or equal to the given version.
private isGradleVersionGraterOrEqualThan(String version) {
return VersionNumber.parse(project.gradle.gradleVersion)
.compareTo(VersionNumber.parse(version)) >= 0
}
// Returns `true` if the given path contains an `android/build.gradle` file. // Returns `true` if the given path contains an `android/build.gradle` file.
// //
// TODO(egarciad): Fix https://github.com/flutter/flutter/issues/39657. // TODO(egarciad): Fix https://github.com/flutter/flutter/issues/39657.
...@@ -659,6 +650,10 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -659,6 +650,10 @@ class FlutterPlugin implements Plugin<Project> {
return variant.hasProperty("assembleProvider") ? variant.assembleProvider.get() : variant.assemble return variant.hasProperty("assembleProvider") ? variant.assembleProvider.get() : variant.assemble
} }
private boolean isFlutterAppProject() {
return project.android.hasProperty("applicationVariants")
}
private void addFlutterTasks(Project project) { private void addFlutterTasks(Project project) {
if (project.state.failure) { if (project.state.failure) {
return return
...@@ -816,8 +811,7 @@ class FlutterPlugin implements Plugin<Project> { ...@@ -816,8 +811,7 @@ class FlutterPlugin implements Plugin<Project> {
} }
return copyFlutterAssetsTask return copyFlutterAssetsTask
} }
boolean isFlutterAppProject = project.android.hasProperty("applicationVariants") if (isFlutterAppProject()) {
if (isFlutterAppProject) {
project.android.applicationVariants.all { variant -> project.android.applicationVariants.all { variant ->
Task assembleTask = getAssembleTask(variant) Task assembleTask = getAssembleTask(variant)
if (!shouldConfigureFlutterTask(assembleTask)) { if (!shouldConfigureFlutterTask(assembleTask)) {
......
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