Unverified Commit 96482eeb authored by Emmanuel Garcia's avatar Emmanuel Garcia Committed by GitHub

Re-enable AAR plugins when an AndroidX failure occurred (#40810)

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