diff --git a/dev/devicelab/bin/tasks/gradle_plugin_bundle_test.dart b/dev/devicelab/bin/tasks/gradle_plugin_bundle_test.dart index 69c1854275c..f08e96fcd5a 100644 --- a/dev/devicelab/bin/tasks/gradle_plugin_bundle_test.dart +++ b/dev/devicelab/bin/tasks/gradle_plugin_bundle_test.dart @@ -7,19 +7,26 @@ import 'dart:async'; 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; Future main() async { await task(() async { try { - await runPluginProjectTest((FlutterPluginProject pluginProject) async { + await runProjectTest((FlutterProject project) async { section('App bundle content for task bundleRelease without explicit target platform'); - await pluginProject.runGradleTask('bundleRelease'); + await project.runGradleTask('bundleRelease'); - if (!pluginProject.hasReleaseBundle) - throw TaskResult.failure( - 'Gradle did not produce a release aab file at: ${pluginProject.releaseBundlePath}'); + final String releaseBundle = path.join( + project.rootPath, + 'build', + 'app', + 'outputs', + 'bundle', + 'release', + 'app.aab', + ); - final Iterable bundleFiles = await pluginProject.getFilesInAppBundle(pluginProject.releaseBundlePath); + final Iterable bundleFiles = await getFilesInAppBundle(releaseBundle); checkItContains([ 'base/manifest/AndroidManifest.xml', @@ -31,16 +38,51 @@ Future main() async { ], bundleFiles); }); - await runPluginProjectTest((FlutterPluginProject pluginProject) async { + await runProjectTest((FlutterProject project) async { + section('App bundle content using flavors without explicit target platform'); + // Add a few flavors. + await project.addProductFlavors( ['production', 'staging', 'development']); + // Build the production flavor in release mode. + await project.runGradleTask('bundleProductionRelease'); + + final String bundlePath = path.join( + project.rootPath, + 'build', + 'app', + 'outputs', + 'bundle', + 'productionRelease', + 'app.aab', + ); + + final Iterable bundleFiles = await getFilesInAppBundle(bundlePath); + + checkItContains([ + 'base/manifest/AndroidManifest.xml', + 'base/dex/classes.dex', + 'base/lib/arm64-v8a/libapp.so', + 'base/lib/arm64-v8a/libflutter.so', + 'base/lib/armeabi-v7a/libapp.so', + 'base/lib/armeabi-v7a/libflutter.so', + ], bundleFiles); + }); + + await runProjectTest((FlutterProject project) async { section('App bundle content for task bundleRelease with target platform = android-arm'); - await pluginProject.runGradleTask('bundleRelease', + await project.runGradleTask('bundleRelease', options: ['-Ptarget-platform=android-arm']); - if (!pluginProject.hasReleaseBundle) - throw TaskResult.failure( - 'Gradle did not produce a release aab file at: ${pluginProject.releaseBundlePath}'); + final String releaseBundle = path.join( + project.rootPath, + 'build', + 'app', + 'outputs', + 'bundle', + 'release', + 'app.aab', + ); - final Iterable bundleFiles = await pluginProject.getFilesInAppBundle(pluginProject.releaseBundlePath); + final Iterable bundleFiles = await getFilesInAppBundle(releaseBundle); checkItContains([ 'base/manifest/AndroidManifest.xml', diff --git a/dev/devicelab/bin/tasks/gradle_plugin_fat_apk_test.dart b/dev/devicelab/bin/tasks/gradle_plugin_fat_apk_test.dart index a876528d402..cc3f149418f 100644 --- a/dev/devicelab/bin/tasks/gradle_plugin_fat_apk_test.dart +++ b/dev/devicelab/bin/tasks/gradle_plugin_fat_apk_test.dart @@ -17,11 +17,7 @@ Future main() async { section('APK content for task assembleDebug without explicit target platform'); await pluginProject.runGradleTask('assembleDebug'); - if (!pluginProject.hasDebugApk) - throw TaskResult.failure( - 'Gradle did not produce a debug apk file at: ${pluginProject.debugApkPath}'); - - final Iterable apkFiles = await pluginProject.getFilesInApk(pluginProject.debugApkPath); + final Iterable apkFiles = await getFilesInApk(pluginProject.debugApkPath); checkItContains([ 'AndroidManifest.xml', @@ -48,11 +44,7 @@ Future main() async { section('APK content for task assembleRelease without explicit target platform'); await pluginProject.runGradleTask('assembleRelease'); - if (!pluginProject.hasReleaseApk) - throw TaskResult.failure( - 'Gradle did not produce a release apk file at: ${pluginProject.releaseApkPath}'); - - final Iterable apkFiles = await pluginProject.getFilesInApk(pluginProject.releaseApkPath); + final Iterable apkFiles = await getFilesInApk(pluginProject.releaseApkPath); checkItContains([ 'AndroidManifest.xml', @@ -75,11 +67,7 @@ Future main() async { await pluginProject.runGradleTask('assembleRelease', options: ['-Ptarget-platform=android-arm,android-arm64']); - if (!pluginProject.hasReleaseApk) - throw TaskResult.failure( - 'Gradle did not produce a release apk at: ${pluginProject.releaseApkPath}'); - - final Iterable apkFiles = await pluginProject.getFilesInApk(pluginProject.releaseApkPath); + final Iterable apkFiles = await getFilesInApk(pluginProject.releaseApkPath); checkItContains([ 'AndroidManifest.xml', @@ -103,11 +91,7 @@ Future main() async { await pluginProject.runGradleTask('assembleRelease', options: ['-Ptarget-platform=android-arm,android-arm64', '-Psplit-per-abi=true']); - if (!pluginProject.hasReleaseArmApk) - throw TaskResult.failure( - 'Gradle did not produce a release apk at: ${pluginProject.releaseArmApkPath}'); - - final Iterable armApkFiles = await pluginProject.getFilesInApk(pluginProject.releaseArmApkPath); + final Iterable armApkFiles = await getFilesInApk(pluginProject.releaseArmApkPath); checkItContains([ 'AndroidManifest.xml', @@ -122,11 +106,7 @@ Future main() async { 'assets/flutter_assets/vm_snapshot_data', ], armApkFiles); - if (!pluginProject.hasReleaseArm64Apk) - throw TaskResult.failure( - 'Gradle did not produce a release apk at: ${pluginProject.releaseArm64ApkPath}'); - - final Iterable arm64ApkFiles = await pluginProject.getFilesInApk(pluginProject.releaseArm64ApkPath); + final Iterable arm64ApkFiles = await getFilesInApk(pluginProject.releaseArm64ApkPath); checkItContains([ 'AndroidManifest.xml', diff --git a/dev/devicelab/bin/tasks/gradle_plugin_light_apk_test.dart b/dev/devicelab/bin/tasks/gradle_plugin_light_apk_test.dart index 800a5075c8e..b2961cc94de 100644 --- a/dev/devicelab/bin/tasks/gradle_plugin_light_apk_test.dart +++ b/dev/devicelab/bin/tasks/gradle_plugin_light_apk_test.dart @@ -17,11 +17,61 @@ Future main() async { await pluginProject.runGradleTask('assembleDebug', options: ['-Ptarget-platform=android-arm']); - if (!pluginProject.hasDebugApk) - throw TaskResult.failure( - 'Gradle did not produce a debug apk file at: ${pluginProject.debugApkPath}'); + final Iterable apkFiles = await getFilesInApk(pluginProject.debugApkPath); - final Iterable apkFiles = await pluginProject.getFilesInApk(pluginProject.debugApkPath); + checkItContains([ + 'AndroidManifest.xml', + 'classes.dex', + 'assets/flutter_assets/isolate_snapshot_data', + 'assets/flutter_assets/kernel_blob.bin', + 'assets/flutter_assets/vm_snapshot_data', + 'lib/armeabi-v7a/libflutter.so', + // Debug mode intentionally includes `x86` and `x86_64`. + 'lib/x86/libflutter.so', + 'lib/x86_64/libflutter.so', + ], apkFiles); + + checkItDoesNotContain([ + 'lib/armeabi-v7a/libapp.so', + 'lib/x86/libapp.so', + 'lib/x86_64/libapp.so', + ], apkFiles); + }); + + await runPluginProjectTest((FlutterPluginProject pluginProject) async { + section('APK content for task assembleDebug with target platform = android-x86'); + // This is used by `flutter run` + await pluginProject.runGradleTask('assembleDebug', + options: ['-Ptarget-platform=android-x86']); + + final Iterable apkFiles = await getFilesInApk(pluginProject.debugApkPath); + + checkItContains([ + 'AndroidManifest.xml', + 'classes.dex', + 'assets/flutter_assets/isolate_snapshot_data', + 'assets/flutter_assets/kernel_blob.bin', + 'assets/flutter_assets/vm_snapshot_data', + 'lib/armeabi-v7a/libflutter.so', + // Debug mode intentionally includes `x86` and `x86_64`. + 'lib/x86/libflutter.so', + 'lib/x86_64/libflutter.so', + ], apkFiles); + + checkItDoesNotContain([ + 'lib/armeabi-v7a/libapp.so', + 'lib/x86/libapp.so', + 'lib/x86_64/libapp.so', + ], apkFiles); + }); + + await runPluginProjectTest((FlutterPluginProject pluginProject) async { + section('APK content for task assembleDebug with target platform = android-x64'); + // This is used by `flutter run` + await pluginProject.runGradleTask('assembleDebug', + options: ['-Ptarget-platform=android-x64']); + + final Iterable apkFiles = await getFilesInApk(pluginProject.debugApkPath); checkItContains([ 'AndroidManifest.xml', @@ -47,11 +97,7 @@ Future main() async { await pluginProject.runGradleTask('assembleRelease', options: ['-Ptarget-platform=android-arm']); - if (!pluginProject.hasReleaseApk) - throw TaskResult.failure( - 'Gradle did not produce a release apk file at: ${pluginProject.releaseApkPath}'); - - final Iterable apkFiles = await pluginProject.getFilesInApk(pluginProject.releaseApkPath); + final Iterable apkFiles = await getFilesInApk(pluginProject.releaseApkPath); checkItContains([ 'AndroidManifest.xml', @@ -74,11 +120,7 @@ Future main() async { await pluginProject.runGradleTask('assembleRelease', options: ['-Ptarget-platform=android-arm64']); - if (!pluginProject.hasReleaseApk) - throw TaskResult.failure( - 'Gradle did not produce a release apk file at: ${pluginProject.releaseApkPath}'); - - final Iterable apkFiles = await pluginProject.getFilesInApk(pluginProject.releaseApkPath); + final Iterable apkFiles = await getFilesInApk(pluginProject.releaseApkPath); checkItContains([ 'AndroidManifest.xml', @@ -124,7 +166,7 @@ Future main() async { await runProjectTest((FlutterProject project) async { section('gradlew assembleFreeDebug (product flavor)'); - await project.addProductFlavor('free'); + await project.addProductFlavors(['free']); await project.runGradleTask('assembleFreeDebug'); }); @@ -169,7 +211,7 @@ Future main() async { await runPluginProjectTest((FlutterPluginProject pluginProject) async { section('gradlew assembleDebug on plugin example'); await pluginProject.runGradleTask('assembleDebug'); - if (!pluginProject.hasDebugApk) + if (!File(pluginProject.debugApkPath).existsSync()) throw TaskResult.failure( 'Gradle did not produce an apk file at the expected place'); }); diff --git a/dev/devicelab/lib/framework/apk_utils.dart b/dev/devicelab/lib/framework/apk_utils.dart index 28efcbc54e2..3b178723188 100644 --- a/dev/devicelab/lib/framework/apk_utils.dart +++ b/dev/devicelab/lib/framework/apk_utils.dart @@ -34,6 +34,27 @@ Future runPluginProjectTest(Future testFunction(FlutterPluginProject } } +Future> getFilesInApk(String apk) async { + if (!File(apk).existsSync()) + throw TaskResult.failure( + 'Gradle did not produce an output artifact file at: $apk'); + + final Process unzip = await startProcess( + 'unzip', + ['-v', apk], + isBot: false, // we just want to test the output, not have any debugging info + ); + return unzip.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .map((String line) => line.split(' ').last) + .toList(); +} + +Future> getFilesInAppBundle(String bundle) { + return getFilesInApk(bundle); +} + void checkItContains(Iterable values, Iterable collection) { for (T value in values) { if (!collection.contains(value)) { @@ -95,20 +116,25 @@ android { '''); } - Future addProductFlavor(String name) async { + Future addProductFlavors(Iterable flavors) async { final File buildScript = File( path.join(androidPath, 'app', 'build.gradle'), ); - buildScript.openWrite(mode: FileMode.append).write(''' + final String flavorConfig = flavors.map((String name) { + return ''' +$name { + applicationIdSuffix ".$name" + versionNameSuffix "-$name" +} + '''; + }).join('\n'); + buildScript.openWrite(mode: FileMode.append).write(''' android { flavorDimensions "mode" productFlavors { - $name { - applicationIdSuffix ".$name" - versionNameSuffix "-$name" - } + $flavorConfig } } '''); @@ -160,32 +186,9 @@ class FlutterPluginProject { String get releaseArm64ApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'apk', 'release', 'app-arm64-v8a-release.apk'); String get releaseBundlePath => path.join(examplePath, 'build', 'app', 'outputs', 'bundle', 'release', 'app.aab'); - bool get hasDebugApk => File(debugApkPath).existsSync(); - bool get hasReleaseApk => File(releaseApkPath).existsSync(); - bool get hasReleaseArmApk => File(releaseArmApkPath).existsSync(); - bool get hasReleaseArm64Apk => File(releaseArm64ApkPath).existsSync(); - bool get hasReleaseBundle => File(releaseBundlePath).existsSync(); - Future runGradleTask(String task, {List options}) async { return _runGradleTask(workingDirectory: exampleAndroidPath, task: task, options: options); } - - Future> getFilesInApk(String apk) async { - final Process unzip = await startProcess( - 'unzip', - ['-v', apk], - isBot: false, // we just want to test the output, not have any debugging info - ); - return unzip.stdout - .transform(utf8.decoder) - .transform(const LineSplitter()) - .map((String line) => line.split(' ').last) - .toList(); - } - - Future> getFilesInAppBundle(String bundle) { - return getFilesInApk(bundle); - } } Future _runGradleTask({String workingDirectory, String task, List options}) async { diff --git a/packages/flutter_tools/gradle/flutter.gradle b/packages/flutter_tools/gradle/flutter.gradle index 4619aabb4e4..4dfd2773153 100644 --- a/packages/flutter_tools/gradle/flutter.gradle +++ b/packages/flutter_tools/gradle/flutter.gradle @@ -114,7 +114,6 @@ class FlutterPlugin implements Plugin { String abiValue = PLATFORM_ARCH_MAP[targetArch] project.android { packagingOptions { - pickFirst "lib/${abiValue}/libflutter.so" // Prevent the ELF library from getting corrupted. doNotStrip "*/${abiValue}/libapp.so" } @@ -166,28 +165,16 @@ class FlutterPlugin implements Plugin { // The local engine is built for one of the build type. // However, we use the same engine for each of the build types. project.android.buildTypes.each { - addApiDependencies(project, it, project.files { + addApiDependencies(project, it.name, project.files { flutterJar }) } } else { - // x86/x86_64 native library used for debugging only, for now. - File flutterX86Jar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/flutter-x86.jar") - Task debugX86JarTask = project.tasks.create("${FLUTTER_BUILD_PREFIX}X86Jar", Jar) { - destinationDir flutterX86Jar.parentFile - archiveName flutterX86Jar.name - from("${flutterRoot}/bin/cache/artifacts/engine/android-x86/libflutter.so") { - into 'lib/x86' - } - from("${flutterRoot}/bin/cache/artifacts/engine/android-x64/libflutter.so") { - into 'lib/x86_64' - } - } String basePlatformArch = getBasePlatform(project) // This is meant to include the compiled classes only, however it will include `libflutter.so` as well. Path baseEnginePath = Paths.get(flutterRoot.absolutePath, "bin", "cache", "artifacts", "engine") File debugJar = baseEnginePath.resolve("${basePlatformArch}").resolve("flutter.jar").toFile() - baseJar["debug"] = [debugX86JarTask, debugJar] + baseJar["debug"] = debugJar if (!debugJar.isFile()) { project.exec { executable flutterExecutable.absolutePath @@ -205,13 +192,13 @@ class FlutterPlugin implements Plugin { // added after applying the Flutter plugin. project.android.buildTypes.each { def buildMode = buildModeFor(it) - addApiDependencies(project, it, project.files { + addApiDependencies(project, it.name, project.files { baseJar[buildMode] }) } project.android.buildTypes.whenObjectAdded { def buildMode = buildModeFor(it) - addApiDependencies(project, it, project.files { + addApiDependencies(project, it.name, project.files { baseJar[buildMode] }) } @@ -325,7 +312,7 @@ class FlutterPlugin implements Plugin { provided project.files(flutterJar) } } else { - assert baseJar["debug"].last().isFile() + assert baseJar["debug"].isFile() assert baseJar["profile"].isFile() assert baseJar["release"].isFile() @@ -342,14 +329,14 @@ class FlutterPlugin implements Plugin { } } - private static void addApiDependencies(Project project, buildType, FileCollection files) { + private static void addApiDependencies(Project project, String variantName, FileCollection files) { project.dependencies { String configuration; // `compile` dependencies are now `api` dependencies. if (project.getConfigurations().findByName("api")) { - configuration = buildType.name + "Api"; + configuration = "${variantName}Api"; } else { - configuration = buildType.name + "Compile"; + configuration = "${variantName}Compile"; } add(configuration, files) } @@ -494,10 +481,16 @@ class FlutterPlugin implements Plugin { flutterTasks.add(compileTask) } def libJar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/libs.jar") + def libFlutterPlatforms = targetPlatforms.collect() + // x86/x86_64 native library used for debugging only, for now. + if (flutterBuildMode == 'debug') { + libFlutterPlatforms.add('android-x86') + libFlutterPlatforms.add('android-x64') + } Task packFlutterSnapshotsAndLibsTask = project.tasks.create(name: "packLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", type: Jar) { destinationDir libJar.parentFile archiveName libJar.name - targetPlatforms.each { targetArch -> + libFlutterPlatforms.each { targetArch -> // This check prevents including `libflutter.so` twice, since it's included in the base platform jar. // Unfortunately, the `pickFirst` setting in `packagingOptions` does not work when the project `:flutter` // is included as an implementation dependency, which causes duplicated `libflutter.so`. @@ -527,7 +520,7 @@ class FlutterPlugin implements Plugin { } } // Include the snapshots and libflutter.so in `lib/`. - addApiDependencies(project, buildType, project.files { + addApiDependencies(project, variant.name, project.files { packFlutterSnapshotsAndLibsTask }) diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index c4fe566d12e..78f8307fa99 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -585,8 +585,12 @@ File _findBundleFile(GradleProject project, BuildInfo buildInfo) { if (bundleFile.existsSync()) return bundleFile; if (buildInfo.flavor != null) { + // Android Studio Gradle plugin v3 adds the flavor to the path. For the bundle the folder name is the flavor plus the mode name. - bundleFile = project.bundleDirectory.childDirectory(buildInfo.flavor + modeName).childFile(bundleFileName); + // On linux, filenames are case sensitive. + bundleFile = project.bundleDirectory + .childDirectory(camelCase('${buildInfo.flavor}_$modeName')) + .childFile(bundleFileName); if (bundleFile.existsSync()) return bundleFile; }