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 87a524de7159f..e7499901c53f3 100644 --- a/dev/devicelab/bin/tasks/gradle_plugin_fat_apk_test.dart +++ b/dev/devicelab/bin/tasks/gradle_plugin_fat_apk_test.dart @@ -154,15 +154,12 @@ Future main() async { section('AGP cxx build artifacts'); final String defaultPath = path.join(project.rootPath, 'android', 'app', '.cxx'); - final String modifiedPath = path.join(project.rootPath, 'build', '.cxx'); if (Directory(defaultPath).existsSync()) { throw TaskResult.failure('Producing unexpected build artifacts in $defaultPath'); } - if (!Directory(modifiedPath).existsSync()) { - throw TaskResult.failure( - 'Not producing external native build output directory in $modifiedPath', - ); + if (Directory(modifiedPath).existsSync()) { + throw TaskResult.failure('Producing unexpected build artifacts in $modifiedPath'); } }); diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt index 7d09171f31e33..425932e18ccd6 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt @@ -299,6 +299,7 @@ class FlutterPlugin : Plugin { FlutterPluginUtils.addTaskForKGPVersion(projectToAddTasksTo) if (FlutterPluginUtils.isFlutterAppProject(projectToAddTasksTo)) { FlutterPluginUtils.addTaskForPrintBuildVariants(projectToAddTasksTo) + FlutterPluginUtils.addTaskForPrintNdkVersion(projectToAddTasksTo) FlutterPluginUtils.addTasksForOutputsAppLinkSettings(projectToAddTasksTo) } diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt index 893e338af33af..2a488ff5f7669 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt @@ -39,6 +39,17 @@ object FlutterPluginUtils { internal const val PROP_LOCAL_ENGINE_BUILD_MODE = "local-engine-build-mode" internal const val PROP_TARGET_PLATFORM = "target-platform" internal const val PROP_DISABLE_ABI_FILTERING = "disable-abi-filtering" + internal const val PROP_SDK_MANAGER_PATH = "flutter.sdkManagerPath" + internal const val PROP_ANDROID_SDK_ROOT = "flutter.androidSdkRoot" + internal const val PROP_INSTALLED_NDK_VERSIONS = "flutter.installedNdkVersions" + internal const val TASK_PRINT_NDK_VERSION = "printNdkVersion" + internal const val NDK_VERSION_OUTPUT_PREFIX = "NdkVersion: " + + private data class ToolNdkProvisioningProperties( + val androidSdkRoot: String, + val installedNdkVersions: Set, + val sdkManagerPath: String? + ) /** * The URL for documentation for general information on migration to built-in Kotlin. @@ -500,6 +511,10 @@ object FlutterPluginUtils { internal fun getAndroidApplicationExtension(project: Project): ApplicationExtension = project.extensions.getByType(ApplicationExtension::class.java) + internal fun getConfiguredNdkVersion(project: Project): String? = + project.extensions.findByType(ApplicationExtension::class.java)?.ndkVersion + ?: getLegacyAndroidExtension(project).ndkVersion + /** * Expected format of getAndroidExtension(project).compileSdkVersion is a string of the form * `android-` followed by either the numeric version, e.g. `android-35`, or a preview version, @@ -703,9 +718,8 @@ object FlutterPluginUtils { // TODO(gmackall): We can remove this elvis when our minimum AGP is >= 8.2. // This value (ndkVersion) is nullable on AGP versions below that. // See https://developer.android.com/reference/tools/gradle-api/8.1/com/android/build/api/dsl/CommonExtension#ndkVersion(). - @Suppress("USELESS_ELVIS") val projectNdkVersion: String = - getLegacyAndroidExtension(project).ndkVersion ?: ndkVersionIfUnspecified + getConfiguredNdkVersion(project) ?: ndkVersionIfUnspecified var maxPluginNdkVersion = projectNdkVersion var numProcessedPlugins = pluginList.size val pluginsWithHigherSdkVersion = mutableListOf() @@ -734,9 +748,8 @@ object FlutterPluginUtils { // TODO(gmackall): We can remove this elvis when our minimum AGP is >= 8.2. // This value (ndkVersion) is nullable on AGP versions below that. // See https://developer.android.com/reference/tools/gradle-api/8.1/com/android/build/api/dsl/CommonExtension#ndkVersion(). - @Suppress("USELESS_ELVIS") val pluginNdkVersion: String = - getLegacyAndroidExtension(pluginProject).ndkVersion ?: ndkVersionIfUnspecified + getConfiguredNdkVersion(pluginProject) ?: ndkVersionIfUnspecified maxPluginNdkVersion = VersionUtils.mostRecentSemanticVersion( pluginNdkVersion, @@ -787,6 +800,10 @@ object FlutterPluginUtils { gradleProject: Project, flutterSdkRootPath: String ) { + if (isFlutterAppProject(gradleProject) && isInvokingMetadataNdkVersionTask(gradleProject)) { + return + } + // If the project is already configuring a native build, we don't need to do anything. val gradleProjectAndroidExtension = getLegacyAndroidExtension(gradleProject) val forcingNotRequired: Boolean = @@ -795,7 +812,103 @@ object FlutterPluginUtils { return } - // Otherwise, point to an empty CMakeLists.txt, and ignore associated warnings. + val toolNdkProvisioningProperties = getToolNdkProvisioningProperties(gradleProject) + if (toolNdkProvisioningProperties != null) { + val configuredNdkVersion = getConfiguredNdkVersion(gradleProject) + if ( + !configuredNdkVersion.isNullOrBlank() && + toolNdkProvisioningProperties.installedNdkVersions.contains(configuredNdkVersion) + ) { + return + } + if (toolNdkProvisioningProperties.sdkManagerPath == null) { + configureSyntheticExternalNativeBuildFallback( + gradleProject = gradleProject, + flutterSdkRootPath = flutterSdkRootPath + ) + return + } + gradleProject.afterEvaluate { + val handledByToolProvisioning = maybeHandleToolNdkProvisioning( + gradleProject = gradleProject, + toolNdkProvisioningProperties = toolNdkProvisioningProperties + ) + if (!handledByToolProvisioning) { + configureSyntheticExternalNativeBuildFallback( + gradleProject = gradleProject, + flutterSdkRootPath = flutterSdkRootPath + ) + } + } + return + } + + configureSyntheticExternalNativeBuildFallback( + gradleProject = gradleProject, + flutterSdkRootPath = flutterSdkRootPath + ) + } + + private fun getToolNdkProvisioningProperties(project: Project): ToolNdkProvisioningProperties? { + val androidSdkRoot = project.findProperty(PROP_ANDROID_SDK_ROOT)?.toString() ?: return null + val installedNdkVersions = + project.findProperty(PROP_INSTALLED_NDK_VERSIONS) + ?.toString() + ?.split(",") + ?.map(String::trim) + ?.filter(String::isNotEmpty) + ?.toSet() ?: return null + val sdkManagerPath = project.findProperty(PROP_SDK_MANAGER_PATH)?.toString() + return ToolNdkProvisioningProperties( + androidSdkRoot = androidSdkRoot, + installedNdkVersions = installedNdkVersions, + sdkManagerPath = sdkManagerPath + ) + } + + private fun maybeHandleToolNdkProvisioning( + gradleProject: Project, + toolNdkProvisioningProperties: ToolNdkProvisioningProperties + ): Boolean { + val configuredNdkVersion = getConfiguredNdkVersion(gradleProject) + if (configuredNdkVersion.isNullOrBlank()) { + return false + } + + if (toolNdkProvisioningProperties.installedNdkVersions.contains(configuredNdkVersion)) { + return true + } + + val sdkManagerPath = toolNdkProvisioningProperties.sdkManagerPath ?: return false + gradleProject.exec { + commandLine( + listOf( + sdkManagerPath, + "--sdk_root=${toolNdkProvisioningProperties.androidSdkRoot}", + "--install", + "ndk;$configuredNdkVersion" + ) + ) + }.assertNormalExitValue() + + val installedNdkMarker = + File( + File(File(toolNdkProvisioningProperties.androidSdkRoot, "ndk"), configuredNdkVersion), + "source.properties" + ) + if (!installedNdkMarker.exists()) { + throw GradleException( + "Android sdkmanager did not install NDK $configuredNdkVersion into ${toolNdkProvisioningProperties.androidSdkRoot}." + ) + } + return true + } + + private fun configureSyntheticExternalNativeBuildFallback( + gradleProject: Project, + flutterSdkRootPath: String + ) { + val gradleProjectAndroidExtension = getLegacyAndroidExtension(gradleProject) gradleProjectAndroidExtension.externalNativeBuild.cmake.path( "$flutterSdkRootPath/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt" ) @@ -829,6 +942,13 @@ object FlutterPluginUtils { } } + @JvmStatic + @JvmName("isInvokingMetadataNdkVersionTask") + internal fun isInvokingMetadataNdkVersionTask(project: Project): Boolean = + project.gradle.startParameter.taskNames.any { taskName -> + taskName == TASK_PRINT_NDK_VERSION || taskName.endsWith(":$TASK_PRINT_NDK_VERSION") + } + @JvmStatic @JvmName("isFlutterAppProject") internal fun isFlutterAppProject(project: Project): Boolean = @@ -961,6 +1081,27 @@ object FlutterPluginUtils { } } + // Add a task that can be called on Flutter projects that prints the effective ndkVersion + // configured for the Android app. + // + // This task prints the version in this format: + // + // NdkVersion: 28.2.13676358 + // + // Format of the output of this task is kept for diagnostics and targeted testing. + @JvmStatic + @JvmName("addTaskForPrintNdkVersion") + internal fun addTaskForPrintNdkVersion(project: Project) { + val androidExtension = getAndroidApplicationExtension(project) + project.tasks.register(TASK_PRINT_NDK_VERSION) { + description = "Prints out the configured ndkVersion for this Android project" + doLast { + val configuredNdkVersion = androidExtension.ndkVersion + println("$NDK_VERSION_OUTPUT_PREFIX$configuredNdkVersion") + } + } + } + // TODO(gmackall): Migrate to AGPs variant api. // https://github.com/flutter/flutter/issues/166550 @Suppress("DEPRECATION") diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt index c1cbdceb41c8f..bdc6f0fc81c7e 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt @@ -122,6 +122,7 @@ class FlutterPluginTest { verify { project.tasks.register("generateLockfiles", any()) } verify { project.tasks.register("javaVersion", any()) } verify { project.tasks.register("printBuildVariants", any()) } + verify { project.tasks.register("printNdkVersion", any()) } } @Test diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt index 28bc9c00d806a..b13100079905c 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt @@ -34,6 +34,8 @@ import org.gradle.api.logging.Logger import org.gradle.api.plugins.PluginManager import org.gradle.internal.impldep.junit.framework.TestCase.assertFalse import org.gradle.internal.impldep.junit.framework.TestCase.assertTrue +import org.gradle.process.ExecResult +import org.gradle.process.ExecSpec import org.jetbrains.kotlin.gradle.plugin.extraProperties import org.junit.jupiter.api.Nested import org.junit.jupiter.api.assertThrows @@ -597,10 +599,12 @@ class FlutterPluginUtilsTest { every { project.afterEvaluate(any>()) } returns Unit every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35" every { project.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "26.3.11579264" + every { project.extensions.findByType(ApplicationExtension::class.java) } returns null every { project.rootProject.findProject(":${cameraDependency["name"]}") } returns cameraPluginProject every { cameraPluginProject.afterEvaluate(capture(cameraPluginProjectActionSlot)) } returns Unit every { cameraPluginProject.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35" every { cameraPluginProject.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "26.3.11579264" + every { cameraPluginProject.extensions.findByType(ApplicationExtension::class.java) } returns null FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(project, listOf(cameraDependency)) @@ -638,12 +642,14 @@ class FlutterPluginUtilsTest { every { project.afterEvaluate(any>()) } returns Unit every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-33" every { project.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "24.3.11579264" + every { project.extensions.findByType(ApplicationExtension::class.java) } returns null every { project.rootProject.findProject(":${cameraDependency["name"]}") } returns cameraPluginProject every { project.rootProject.findProject(":${flutterPluginAndroidLifecycleDependency["name"]}") } returns flutterPluginAndroidLifecycleDependencyPluginProject every { cameraPluginProject.afterEvaluate(capture(cameraPluginProjectActionSlot)) } returns Unit every { cameraPluginProject.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35" every { cameraPluginProject.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "26.3.11579264" + every { cameraPluginProject.extensions.findByType(ApplicationExtension::class.java) } returns null every { flutterPluginAndroidLifecycleDependencyPluginProject.afterEvaluate( capture( @@ -665,6 +671,11 @@ class FlutterPluginUtilsTest { )!! .ndkVersion } returns "25.3.11579264" + every { + flutterPluginAndroidLifecycleDependencyPluginProject.extensions.findByType( + ApplicationExtension::class.java + ) + } returns null val dependencyList: List> = listOf(cameraDependency, flutterPluginAndroidLifecycleDependency) @@ -747,6 +758,70 @@ class FlutterPluginUtilsTest { } } + @Test + fun `detectLowCompileSdkVersionOrNdkVersion reads ndkVersion from ApplicationExtension when available`( + @TempDir tempDir: Path + ) { + val buildGradleFile = + tempDir + .resolve("app") + .createDirectory() + .resolve("build.gradle") + .toFile() + buildGradleFile.createNewFile() + val projectDir = tempDir.resolve("app").toFile() + + val project = mockk() + val mockLogger = mockk() + val projectBaseExtension = mockk() + val projectApplicationExtension = mockk() + val cameraPluginProject = mockk() + val cameraBaseExtension = mockk() + val cameraApplicationExtension = mockk() + val projectActionSlot = slot>() + val cameraPluginProjectActionSlot = slot>() + + every { project.logger } returns mockLogger + every { mockLogger.error(any()) } returns Unit + every { project.projectDir } returns projectDir + every { project.afterEvaluate(any>()) } returns Unit + every { project.extensions.findByType(BaseExtension::class.java) } returns projectBaseExtension + every { project.extensions.findByType(ApplicationExtension::class.java) } returns projectApplicationExtension + every { projectBaseExtension.compileSdkVersion } returns "android-35" + every { projectBaseExtension.ndkVersion } answers { + throw AssertionError("legacy project ndkVersion should not be read when ApplicationExtension is available") + } + every { projectApplicationExtension.ndkVersion } returns "24.3.11579264" + every { project.rootProject.findProject(":${cameraDependency["name"]}") } returns cameraPluginProject + + every { cameraPluginProject.afterEvaluate(capture(cameraPluginProjectActionSlot)) } returns Unit + every { cameraPluginProject.extensions.findByType(BaseExtension::class.java) } returns cameraBaseExtension + every { cameraPluginProject.extensions.findByType(ApplicationExtension::class.java) } returns cameraApplicationExtension + every { cameraBaseExtension.compileSdkVersion } returns "android-35" + every { cameraBaseExtension.ndkVersion } answers { + throw AssertionError("legacy plugin ndkVersion should not be read when ApplicationExtension is available") + } + every { cameraApplicationExtension.ndkVersion } returns "26.3.11579264" + + FlutterPluginUtils.detectLowCompileSdkVersionOrNdkVersion(project, listOf(cameraDependency)) + + verify { project.afterEvaluate(capture(projectActionSlot)) } + projectActionSlot.captured.execute(project) + verify { cameraPluginProject.afterEvaluate(capture(cameraPluginProjectActionSlot)) } + cameraPluginProjectActionSlot.captured.execute(cameraPluginProject) + + verify { + mockLogger.error( + "Your project is configured with Android NDK 24.3.11579264, but the following plugin(s) depend on a different Android NDK version:" + ) + } + verify { + mockLogger.error( + "- ${cameraDependency["name"]} requires Android NDK 26.3.11579264" + ) + } + } + @Test fun `detectLowCompileSdkVersionOrNdkVersion throws IllegalArgumentException when plugin has no name`() { val project = mockk() @@ -754,6 +829,7 @@ class FlutterPluginUtilsTest { every { project.afterEvaluate(any>()) } returns Unit every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35" every { project.extensions.findByType(BaseExtension::class.java)!!.ndkVersion } returns "26.3.11579264" + every { project.extensions.findByType(ApplicationExtension::class.java) } returns null val pluginWithoutName: MutableMap = cameraDependency.toMutableMap() pluginWithoutName.remove("name") @@ -1616,6 +1692,7 @@ class FlutterPluginUtilsTest { val project = mockk() val mockCmakeOptions = mockk() val mockDefaultConfig = mockk() + every { project.extensions.findByType(ApplicationExtension::class.java) } returns null every { project.extensions .findByType(BaseExtension::class.java)!! @@ -1634,6 +1711,244 @@ class FlutterPluginUtilsTest { verify { mockDefaultConfig wasNot called } } + @Test + fun `forceNdkDownload installs a missing ndk when tool properties are provided`( + @TempDir tempDir: Path + ) { + val project = mockk() + val projectActionSlot = slot>() + val execActionSlot = slot>() + val mockExecSpec = mockk() + val mockExecResult = mockk() + val mockCmakeOptions = mockk() + val mockDefaultConfig = mockk() + val mockBaseExtension = mockk() + every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension + every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions + every { mockBaseExtension.defaultConfig } returns mockDefaultConfig + every { mockBaseExtension.ndkVersion } returns "29.0.13846066" + every { mockCmakeOptions.path } returns null + every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns "/sdkmanager" + every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns tempDir.toString() + every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns "" + every { project.gradle.startParameter.taskNames } returns emptyList() + every { project.extensions.findByType(ApplicationExtension::class.java) } returns null + every { project.afterEvaluate(capture(projectActionSlot)) } returns Unit + every { project.exec(capture(execActionSlot)) } answers { + File(tempDir.toFile(), "ndk/29.0.13846066/source.properties").apply { + parentFile.mkdirs() + createNewFile() + } + mockExecResult + } + every { mockExecResult.assertNormalExitValue() } returns mockExecResult + every { mockExecSpec.commandLine(any>()) } returns mockExecSpec + + FlutterPluginUtils.forceNdkDownload(project, "/base/path") + + verify { project.afterEvaluate(capture(projectActionSlot)) } + projectActionSlot.captured.execute(project) + + execActionSlot.captured.execute(mockExecSpec) + verify(exactly = 1) { project.exec(any>()) } + verify { + mockExecSpec.commandLine( + listOf( + "/sdkmanager", + "--sdk_root=${tempDir}", + "--install", + "ndk;29.0.13846066" + ) + ) + } + verify(exactly = 0) { mockCmakeOptions.path(any()) } + verify { mockDefaultConfig wasNot called } + } + + @Test + fun `forceNdkDownload skips sdkmanager install when the requested ndk is already installed`() { + val project = mockk() + val mockCmakeOptions = mockk() + val mockDefaultConfig = mockk() + val mockBaseExtension = mockk() + every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension + every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions + every { mockBaseExtension.defaultConfig } returns mockDefaultConfig + every { mockBaseExtension.ndkVersion } returns "29.0.13846066" + every { mockCmakeOptions.path } returns null + every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns "/sdkmanager" + every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns "/sdk/root" + every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns "29.0.13846066" + every { project.gradle.startParameter.taskNames } returns emptyList() + every { project.extensions.findByType(ApplicationExtension::class.java) } returns null + + FlutterPluginUtils.forceNdkDownload(project, "/base/path") + + verify(exactly = 0) { project.afterEvaluate(any>()) } + verify(exactly = 0) { project.exec(any>()) } + verify(exactly = 0) { mockCmakeOptions.path(any()) } + verify { mockDefaultConfig wasNot called } + } + + @Test + fun `forceNdkDownload skips fallback when sdkmanager is unavailable but the requested ndk is already installed`() { + val project = mockk() + val mockCmakeOptions = mockk() + val mockDefaultConfig = mockk() + val mockBaseExtension = mockk() + every { project.extensions.findByType(ApplicationExtension::class.java) } returns null + every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension + every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions + every { mockBaseExtension.defaultConfig } returns mockDefaultConfig + every { mockBaseExtension.ndkVersion } returns "29.0.13846066" + every { mockCmakeOptions.path } returns null + every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns null + every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns "/sdk/root" + every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns "29.0.13846066" + every { project.gradle.startParameter.taskNames } returns emptyList() + + FlutterPluginUtils.forceNdkDownload(project, "/base/path") + + verify(exactly = 0) { project.afterEvaluate(any>()) } + verify(exactly = 0) { project.exec(any>()) } + verify(exactly = 0) { mockCmakeOptions.path(any()) } + verify { mockDefaultConfig wasNot called } + } + + @Test + fun `forceNdkDownload reads ndkVersion from ApplicationExtension when legacy extension does not expose it`() { + val project = mockk() + val mockCmakeOptions = mockk() + val mockDefaultConfig = mockk() + val mockBaseExtension = mockk() + val mockApplicationExtension = mockk() + every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension + every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockApplicationExtension + every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions + every { mockBaseExtension.defaultConfig } returns mockDefaultConfig + every { mockBaseExtension.ndkVersion } answers { + throw AssertionError("legacy ndkVersion should not be read when ApplicationExtension is available") + } + every { mockApplicationExtension.ndkVersion } returns "29.0.13846066" + every { mockCmakeOptions.path } returns null + every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns "/sdkmanager" + every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns "/sdk/root" + every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns "29.0.13846066" + every { project.gradle.startParameter.taskNames } returns emptyList() + + FlutterPluginUtils.forceNdkDownload(project, "/base/path") + + verify(exactly = 0) { project.afterEvaluate(any>()) } + verify(exactly = 0) { project.exec(any>()) } + verify(exactly = 0) { mockCmakeOptions.path(any()) } + verify { mockDefaultConfig wasNot called } + } + + @Test + fun `forceNdkDownload throws when sdkmanager install does not produce the requested ndk`( + @TempDir tempDir: Path + ) { + val project = mockk() + val projectActionSlot = slot>() + val mockExecResult = mockk() + val mockCmakeOptions = mockk() + val mockDefaultConfig = mockk() + val mockBaseExtension = mockk() + every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension + every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions + every { mockBaseExtension.defaultConfig } returns mockDefaultConfig + every { mockBaseExtension.ndkVersion } returns "29.0.13846066" + every { mockCmakeOptions.path } returns null + every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns "/sdkmanager" + every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns tempDir.toString() + every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns "" + every { project.gradle.startParameter.taskNames } returns emptyList() + every { project.extensions.findByType(ApplicationExtension::class.java) } returns null + every { project.afterEvaluate(capture(projectActionSlot)) } returns Unit + every { project.exec(any>()) } returns mockExecResult + every { mockExecResult.assertNormalExitValue() } returns mockExecResult + + FlutterPluginUtils.forceNdkDownload(project, "/base/path") + + verify { project.afterEvaluate(capture(projectActionSlot)) } + assertThrows { + projectActionSlot.captured.execute(project) + } + + verify(exactly = 0) { mockCmakeOptions.path(any()) } + verify { mockDefaultConfig wasNot called } + } + + @Test + fun `forceNdkDownload skips when invoking the ndk metadata task`() { + val project = mockk() + val mockCmakeOptions = mockk() + val mockDefaultConfig = mockk() + val mockBaseExtension = mockk() + every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension + every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions + every { mockBaseExtension.defaultConfig } returns mockDefaultConfig + every { mockCmakeOptions.path } returns null + every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns null + every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns null + every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns null + every { project.gradle.startParameter.taskNames } returns listOf(FlutterPluginUtils.TASK_PRINT_NDK_VERSION) + every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockk(relaxed = true) + + FlutterPluginUtils.forceNdkDownload(project, "/base/path") + + verify(exactly = 0) { mockCmakeOptions.path(any()) } + verify { mockDefaultConfig wasNot called } + } + + @Test + fun `forceNdkDownload falls back when tool properties are present but sdkmanager is unavailable`() { + val project = mockk() + val mockCmakeOptions = mockk() + val mockDefaultConfig = mockk() + val mockDirectoryProperty = mockk() + val mockDirectory = mockk() + val mockBaseExtension = mockk() + every { project.extensions.findByType(ApplicationExtension::class.java) } returns null + every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension + every { mockBaseExtension.externalNativeBuild.cmake } returns mockCmakeOptions + every { mockBaseExtension.defaultConfig } returns mockDefaultConfig + every { mockBaseExtension.ndkVersion } returns "29.0.13846066" + every { mockCmakeOptions.path } returns null + every { mockCmakeOptions.path(any()) } returns Unit + every { mockCmakeOptions.buildStagingDirectory(any()) } returns Unit + every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns null + every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns "/sdk/root" + every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns "" + every { project.gradle.startParameter.taskNames } returns emptyList() + every { project.layout.buildDirectory } returns mockDirectoryProperty + every { mockDirectoryProperty.dir(any()) } returns mockDirectoryProperty + every { mockDirectoryProperty.get() } returns mockDirectory + every { mockDirectory.asFile.path } returns "/randomapp/build/app/" + val basePath = "/base/path" + + val mockBuildType = mockk() + every { mockBaseExtension.buildTypes.iterator() } returns mutableListOf(mockBuildType).iterator() + every { mockBuildType.name } returns "Debug" + every { mockBuildType.externalNativeBuild.cmake.arguments(any(), any(), any()) } returns Unit + + FlutterPluginUtils.forceNdkDownload(project, basePath) + + verify(exactly = 0) { project.afterEvaluate(any>()) } + verify(exactly = 0) { project.exec(any>()) } + verify(exactly = 1) { + mockCmakeOptions.path("$basePath/packages/flutter_tools/gradle/src/main/scripts/CMakeLists.txt") + } + verify(exactly = 1) { mockCmakeOptions.buildStagingDirectory(any()) } + verify(exactly = 1) { + mockBuildType.externalNativeBuild.cmake.arguments( + "-Wno-dev", + "--no-warn-unused-cli", + "-DCMAKE_BUILD_TYPE=Debug" + ) + } + } + @Test fun `forceNdkDownload sets externalNativeBuild properties`() { val project = mockk() @@ -1641,6 +1956,10 @@ class FlutterPluginUtilsTest { val mockDefaultConfig = mockk() val mockDirectoryProperty = mockk() val mockDirectory = mockk() + every { project.extensions.findByType(ApplicationExtension::class.java) } returns null + every { project.findProperty(FlutterPluginUtils.PROP_SDK_MANAGER_PATH) } returns null + every { project.findProperty(FlutterPluginUtils.PROP_ANDROID_SDK_ROOT) } returns null + every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns null every { project.extensions .findByType(BaseExtension::class.java)!! @@ -1684,6 +2003,29 @@ class FlutterPluginUtilsTest { } } + @Test + fun `addTaskForPrintNdkVersion adds task for printing ndk version`() { + val project = mockk() + val androidExtension = mockk() + every { androidExtension.ndkVersion } returns "29.0.13846066" + every { project.extensions.getByType(ApplicationExtension::class.java) } returns androidExtension + every { project.tasks.register(any(), any>()) } returns mockk() + val captureSlot = slot>() + + FlutterPluginUtils.addTaskForPrintNdkVersion(project) + + verify { project.tasks.register("printNdkVersion", capture(captureSlot)) } + val mockTask = mockk() + every { mockTask.description = any() } returns Unit + every { mockTask.doLast(any>()) } returns mockk() + + captureSlot.captured.execute(mockTask) + + verify { + mockTask.description = "Prints out the configured ndkVersion for this Android project" + } + } + // isFlutterAppProject skipped as it is a wrapper for a single getter that we would have to mock // addFlutterDependencies diff --git a/packages/flutter_tools/lib/src/android/android_sdk.dart b/packages/flutter_tools/lib/src/android/android_sdk.dart index 91df3abafb9da..225fecd0150f2 100644 --- a/packages/flutter_tools/lib/src/android/android_sdk.dart +++ b/packages/flutter_tools/lib/src/android/android_sdk.dart @@ -353,64 +353,69 @@ class AndroidSdk { /// 5. Look for the default install location inside the Android SDK: /// [directory]/ndk/\/. If multiple versions exist, use the /// newest. - String? getNdkBinaryPath(String binaryName, {Platform? platform, Config? config}) { + Iterable getNdkDirectoriesInResolutionOrder({Platform? platform, Config? config}) { platform ??= globals.platform; config ??= globals.config; - Directory? findAndroidNdkHomeDir() { - String? androidNdkHomeDir; - if (config!.containsKey('android-ndk')) { - androidNdkHomeDir = config.getValue('android-ndk') as String?; - } else if (platform!.environment.containsKey(kAndroidNdkHome)) { - androidNdkHomeDir = platform.environment[kAndroidNdkHome]; - } else if (platform.environment.containsKey(kAndroidNdkPath)) { - androidNdkHomeDir = platform.environment[kAndroidNdkPath]; - } else if (platform.environment.containsKey(kAndroidNdkRoot)) { - androidNdkHomeDir = platform.environment[kAndroidNdkRoot]; - } - if (androidNdkHomeDir != null) { - return directory.fileSystem.directory(androidNdkHomeDir); - } - // Look for the default install location of the NDK inside the Android - // SDK when installed through `sdkmanager` or Android studio. - final Directory ndk = directory.childDirectory('ndk'); - if (!ndk.existsSync()) { - return null; - } - final List ndkVersions = - ndk - .listSync() - .map((FileSystemEntity entity) { - try { - return Version.parse(entity.basename); - } on Exception { - return null; - } - }) - .whereType() - .toList() - // Use latest NDK first. - ..sort((Version a, Version b) => -a.compareTo(b)); - if (ndkVersions.isEmpty) { - return null; - } - return ndk.childDirectory(ndkVersions.first.toString()); + final ndkDirectories = []; + String? androidNdkHomeDir; + if (config.containsKey('android-ndk')) { + androidNdkHomeDir = config.getValue('android-ndk') as String?; + } else if (platform.environment.containsKey(kAndroidNdkHome)) { + androidNdkHomeDir = platform.environment[kAndroidNdkHome]; + } else if (platform.environment.containsKey(kAndroidNdkPath)) { + androidNdkHomeDir = platform.environment[kAndroidNdkPath]; + } else if (platform.environment.containsKey(kAndroidNdkRoot)) { + androidNdkHomeDir = platform.environment[kAndroidNdkRoot]; + } + if (androidNdkHomeDir != null) { + ndkDirectories.add(directory.fileSystem.directory(androidNdkHomeDir)); } - final Directory? androidNdkHomeDir = findAndroidNdkHomeDir(); - if (androidNdkHomeDir == null) { - return null; + // Look for the default install location of the NDK inside the Android + // SDK when installed through `sdkmanager` or Android studio. + final Directory ndk = directory.childDirectory('ndk'); + if (!ndk.existsSync()) { + return ndkDirectories; } - final File executable = androidNdkHomeDir - .childDirectory('toolchains') - .childDirectory('llvm') - .childDirectory('prebuilt') - .childDirectory(_llvmHostDirectoryName[platform.operatingSystem]!) - .childDirectory('bin') - .childFile(binaryName); - if (executable.existsSync()) { - // LLVM missing in this NDK version. - return executable.path; + final List ndkVersions = + ndk + .listSync() + .map((FileSystemEntity entity) { + try { + return Version.parse(entity.basename); + } on Exception { + return null; + } + }) + .whereType() + .toList() + // Use latest NDK first. + ..sort((Version a, Version b) => -a.compareTo(b)); + for (final ndkVersion in ndkVersions) { + ndkDirectories.add(ndk.childDirectory(ndkVersion.toString())); + } + return ndkDirectories; + } + + String? getNdkBinaryPath(String binaryName, {Platform? platform, Config? config}) { + platform ??= globals.platform; + config ??= globals.config; + for (final Directory androidNdkHomeDir in getNdkDirectoriesInResolutionOrder( + platform: platform, + config: config, + )) { + final File executable = androidNdkHomeDir + .childDirectory('toolchains') + .childDirectory('llvm') + .childDirectory('prebuilt') + .childDirectory(_llvmHostDirectoryName[platform.operatingSystem]!) + .childDirectory('bin') + .childFile(binaryName); + if (executable.existsSync()) { + // LLVM missing in this NDK version. + return executable.path; + } } return null; } @@ -554,6 +559,41 @@ class AndroidSdk { String toString() => 'AndroidSdk: $directory'; } +extension AndroidSdkNdkHelpers on AndroidSdk { + /// Returns whether the Android SDK already contains the requested NDK version. + bool hasNdkVersion(String version) { + final Directory ndkDirectory = directory.childDirectory('ndk').childDirectory(version); + return ndkDirectory.childFile('source.properties').existsSync(); + } + + /// Installs a specific Android SDK component with sdkmanager. + Future installSdkComponent( + String component, { + Java? java, + ProcessUtils? processUtils, + }) async { + processUtils ??= globals.processUtils; + final String? executable = sdkManagerPath; + if (executable == null || !globals.processManager.canRun(executable)) { + throwToolExit( + 'Android sdkmanager not found. Update to the latest Android SDK and ensure that ' + 'the cmdline-tools are installed to resolve this.', + ); + } + return processUtils.run([ + executable, + '--sdk_root=${directory.path}', + '--install', + component, + ], environment: java?.environment); + } + + /// Installs the requested NDK version via sdkmanager. + Future installNdkVersion(String version, {Java? java, ProcessUtils? processUtils}) { + return installSdkComponent('ndk;$version', java: java, processUtils: processUtils); + } +} + class AndroidSdkVersion implements Comparable { AndroidSdkVersion._( this.sdk, { diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index 068732f6efa19..f012a57493a1e 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -30,6 +30,7 @@ import '../flutter_manifest.dart'; import '../globals.dart' as globals; import '../project.dart'; import 'android_builder.dart'; +import 'android_sdk.dart'; import 'android_studio.dart'; import 'gradle_errors.dart'; import 'gradle_utils.dart'; @@ -55,6 +56,9 @@ import 'migrations/top_level_gradle_build_file_migration.dart'; final _kBuildVariantRegex = RegExp('^BuildVariant: (?<$_kBuildVariantRegexGroupName>.*)\$'); const _kBuildVariantRegexGroupName = 'variant'; const _kBuildVariantTaskName = 'printBuildVariants'; +const _kSdkManagerPathProperty = 'flutter.sdkManagerPath'; +const _kAndroidSdkRootProperty = 'flutter.androidSdkRoot'; +const _kInstalledNdkVersionsProperty = 'flutter.installedNdkVersions'; @visibleForTesting const failedToStripDebugSymbolsErrorMessage = r''' Release app bundle failed to strip debug symbols from native libraries. @@ -165,6 +169,7 @@ class AndroidGradleBuilder implements AndroidBuilder { required GradleUtils gradleUtils, required Platform platform, required AndroidStudio? androidStudio, + AndroidSdk? androidSdk, }) : _java = java, _logger = logger, _fileSystem = fileSystem, @@ -172,6 +177,7 @@ class AndroidGradleBuilder implements AndroidBuilder { _analytics = analytics, _gradleUtils = gradleUtils, _androidStudio = androidStudio, + _androidSdk = androidSdk, _fileSystemUtils = FileSystemUtils(fileSystem: fileSystem, platform: platform), _processUtils = ProcessUtils(logger: logger, processManager: processManager); @@ -184,6 +190,7 @@ class AndroidGradleBuilder implements AndroidBuilder { final GradleUtils _gradleUtils; final FileSystemUtils _fileSystemUtils; final AndroidStudio? _androidStudio; + final AndroidSdk? _androidSdk; /// Builds the AAR and POM files for the current Flutter module or plugin. @override @@ -273,6 +280,7 @@ class AndroidGradleBuilder implements AndroidBuilder { required FlutterProject project, required List localGradleErrors, required String gradleExecutablePath, + bool printOutput = true, int retry = 0, VoidCallback? preRunTask, VoidCallback? postRunTask, @@ -335,7 +343,7 @@ class AndroidGradleBuilder implements AndroidBuilder { } } // Pipe stdout/stderr from Gradle. - return line; + return printOutput ? line : null; } final Status status = _logger.startProgress("Running Gradle task '$taskName'..."); @@ -395,6 +403,7 @@ class AndroidGradleBuilder implements AndroidBuilder { postRunTask: postRunTask, localGradleErrors: localGradleErrors, gradleExecutablePath: gradleExecutablePath, + printOutput: printOutput, retry: retry, project: project, maxRetries: maxRetries, @@ -568,6 +577,8 @@ class AndroidGradleBuilder implements AndroidBuilder { if (androidBuildInfo.splitPerAbi) { options.add('-Psplit-per-abi=true'); } + + options.addAll(_getAndroidNdkProvisioningProperties()); late Stopwatch sw; final int exitCode = await _runGradleTask( assembleTask, @@ -673,19 +684,19 @@ class AndroidGradleBuilder implements AndroidBuilder { String aabPath, Iterable targetArchs, ) async { - if (globals.androidSdk == null) { + if (_androidSdk == null) { _logger.printTrace( 'Failed to find android sdk when checking final appbundle for debug symbols.', ); return false; } - if (!globals.androidSdk!.cmdlineToolsAvailable) { + if (!_androidSdk.cmdlineToolsAvailable) { _logger.printTrace( 'Failed to find cmdline-tools when checking final appbundle for debug symbols.', ); return false; } - final String? apkAnalyzerPath = globals.androidSdk!.getCmdlineToolsPath(apkAnalyzerBinaryName); + final String? apkAnalyzerPath = _androidSdk.getCmdlineToolsPath(apkAnalyzerBinaryName); if (apkAnalyzerPath == null) { _logger.printTrace( 'Failed to find apkanalyzer when checking final appbundle for debug symbols.', @@ -821,6 +832,7 @@ class AndroidGradleBuilder implements AndroidBuilder { command.add('-Ptarget=$target'); } command.addAll(androidBuildInfo.buildInfo.toGradleConfig()); + command.addAll(_getAndroidNdkProvisioningProperties()); if (buildInfo.dartObfuscation && buildInfo.mode != BuildMode.release) { _logger.printStatus( 'Dart obfuscation is not supported in ${buildInfo.mode.uppercaseFriendlyName}' @@ -995,6 +1007,26 @@ class AndroidGradleBuilder implements AndroidBuilder { } return outputPath; } + + List _getAndroidNdkProvisioningProperties() { + final AndroidSdk? androidSdk = _androidSdk; + if (androidSdk == null || !androidSdk.directory.existsSync()) { + return const []; + } + + final properties = [ + '-P$_kAndroidSdkRootProperty=${androidSdk.directory.path}', + '-P$_kInstalledNdkVersionsProperty=${_getInstalledNdkVersionsForGradle(androidSdk).join(',')}', + ]; + + final String? sdkManagerPath = androidSdk.sdkManagerPath; + if (sdkManagerPath != null && + androidSdk.cmdlineToolsAvailable && + androidSdk.licensesAvailable) { + properties.add('-P$_kSdkManagerPathProperty=$sdkManagerPath'); + } + return properties; + } } /// Prints how to consume the AAR from a host app. @@ -1364,3 +1396,23 @@ String _getTargetPlatformByLocalEnginePath(String engineOutPath) { } return result; } + +List _getInstalledNdkVersionsForGradle(AndroidSdk androidSdk) { + final Directory ndkDir = androidSdk.directory.childDirectory('ndk'); + if (!ndkDir.existsSync()) { + return const []; + } + + final List installedNdkVersions = + ndkDir + .listSync() + .whereType() + .map((Directory dir) => dir.basename) + .where( + (String version) => + ndkDir.childDirectory(version).childFile('source.properties').existsSync(), + ) + .toList() + ..sort(); + return installedNdkVersions; +} diff --git a/packages/flutter_tools/lib/src/android/gradle_errors.dart b/packages/flutter_tools/lib/src/android/gradle_errors.dart index 594e09261c448..432662cb0fa93 100644 --- a/packages/flutter_tools/lib/src/android/gradle_errors.dart +++ b/packages/flutter_tools/lib/src/android/gradle_errors.dart @@ -655,6 +655,9 @@ final missingNdkSourcePropertiesFile = GradleHandledError( ${globals.logger.terminal.warningMark} This is likely due to a malformed download of the NDK. This can be fixed by deleting the local NDK copy at: $path and allowing the Android Gradle Plugin to automatically re-download it. + + If this keeps happening after a clean retry, Flutter's tool-side Android NDK provisioning + may have failed or been skipped before Gradle fell back to AGP's automatic download path. ''', title: _boxTitle); return GradleBuildStatus.exit; }, diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index d17067a260427..f6fa5041ba807 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -102,6 +102,7 @@ Future runInContext(FutureOr Function() runner, {Map? gradleUtils: globals.gradleUtils!, platform: globals.platform, androidStudio: globals.androidStudio, + androidSdk: globals.androidSdk, ), AndroidLicenseValidator: () => AndroidLicenseValidator( platform: globals.platform, diff --git a/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart b/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart index f8d9fbf8f1ce4..f543ced6a7b01 100644 --- a/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart +++ b/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart @@ -20,12 +20,14 @@ import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/base/user_messages.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/project.dart'; import 'package:test/fake.dart'; import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; import '../../src/context.dart'; +import '../../src/context.dart' as test_context; import '../../src/fake_process_manager.dart'; import '../../src/fakes.dart'; @@ -59,8 +61,42 @@ void main() { ); }); + String sdkPath() => fileSystem.directory('android-sdk').absolute.path; + String missingSdkPath() => fileSystem.directory('nonexistent-android-sdk').absolute.path; + String sdkManagerPath() => fileSystem.path.join( + sdkPath(), + 'cmdline-tools', + 'latest', + 'bin', + globals.platform.isWindows ? 'sdkmanager.bat' : 'sdkmanager', + ); + String sdkLicensesPath() => fileSystem.path.join(sdkPath(), 'licenses'); + String ndkPath(String version) => fileSystem.path.join(sdkPath(), 'ndk', version); + String apkAnalyzerPath() => + fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin', apkAnalyzerBinaryName); + + void testUsingContext( + String description, + dynamic Function() body, { + Map overrides = const {}, + }) { + test_context.testUsingContext( + description, + body, + overrides: { + AndroidSdk: () => AndroidSdk( + fileSystem.directory(missingSdkPath()), + java: FakeJava(), + fileSystem: fileSystem, + ), + ProcessManager: () => processManager, + ...overrides, + }, + ); + } + testUsingContext( - 'Can immediately tool exit on recognized exit code/stderr', + 'build apk passes sdkmanager path, sdk root, and validated installed ndk versions to gradle', () async { final builder = AndroidGradleBuilder( java: FakeJava(), @@ -72,9 +108,10 @@ void main() { gradleUtils: FakeGradleUtils(), platform: FakePlatform(), androidStudio: FakeAndroidStudio(), + androidSdk: globals.androidSdk, ); processManager.addCommand( - const FakeCommand( + FakeCommand( command: [ 'gradlew', '-q', @@ -84,20 +121,27 @@ void main() { '-Pdart-obfuscation=false', '-Ptrack-widget-creation=false', '-Ptree-shake-icons=false', - 'assembleRelease', + '-Pflutter.androidSdkRoot=${sdkPath()}', + '-Pflutter.installedNdkVersions=29.0.13846066', + '-Pflutter.sdkManagerPath=${sdkManagerPath()}', + 'assembleDevRelease', ], - exitCode: 1, - stderr: '\nSome gradle message\n', ), ); - fileSystem.directory('android').childFile('build.gradle').createSync(recursive: true); - + fileSystem.file('android/gradlew').createSync(recursive: true); fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); - + fileSystem.file('android/build.gradle').createSync(recursive: true); fileSystem.directory('android').childDirectory('app').childFile('build.gradle') ..createSync(recursive: true) ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); + fileSystem + .directory('build') + .childDirectory('app') + .childDirectory('outputs') + .childDirectory('flutter-apk') + .childFile('app-dev-release.apk') + .createSync(recursive: true); final FlutterProject project = FlutterProject.fromDirectoryTest( fileSystem.currentDirectory, @@ -106,72 +150,53 @@ void main() { ..createSync(recursive: true) ..writeAsStringSync(minimalV2EmbeddingManifest); - var handlerCalled = false; - await expectLater(() async { - await builder.buildGradleApp( - project: project, - androidBuildInfo: const AndroidBuildInfo( - BuildInfo( - BuildMode.release, - null, - treeShakeIcons: false, - packageConfigPath: '.dart_tool/package_config.json', - ), - ), - target: 'lib/main.dart', - isBuildingBundle: false, - configOnly: false, - localGradleErrors: [ - GradleHandledError( - test: (String line) { - return line.contains('Some gradle message'); - }, - handler: ({String? line, FlutterProject? project, bool? usesAndroidX}) async { - handlerCalled = true; - return GradleBuildStatus.exit; - }, - eventLabel: 'random-event-label', - ), - ], - ); - }, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1')); - - expect(handlerCalled, isTrue); - - expect( - fakeAnalytics.sentEvents, - containsAll([ - Event.flutterBuildInfo( - label: 'app-not-using-android-x', - buildType: 'gradle', - settings: 'androidGradlePluginVersion: null', - ), - Event.flutterBuildInfo( - label: 'gradle-random-event-label-failure', - buildType: 'gradle', - settings: 'androidGradlePluginVersion: null', + await builder.buildGradleApp( + project: project, + androidBuildInfo: const AndroidBuildInfo( + BuildInfo( + BuildMode.release, + 'dev', + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', ), - ]), - ); - - expect( - analyticsTimingEventExists( - sentEvents: fakeAnalytics.sentEvents, - workflow: 'build', - variableName: 'gradle', ), - true, + target: 'lib/main.dart', + isBuildingBundle: false, + configOnly: false, + localGradleErrors: const [], ); + + expect(processManager, hasNoRemainingExpectations); + }, + overrides: { + AndroidSdk: () { + fileSystem.directory(sdkPath()).createSync(recursive: true); + fileSystem.directory(sdkLicensesPath()).createSync(recursive: true); + fileSystem + .directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin')) + .childFile(globals.platform.isWindows ? 'sdkmanager.bat' : 'sdkmanager') + .createSync(recursive: true); + fileSystem + .directory(ndkPath('29.0.13846066')) + .childFile('source.properties') + .createSync(recursive: true); + fileSystem.directory(ndkPath('29.0.13846066-bad')).createSync(recursive: true); + return AndroidSdk( + fileSystem.directory(sdkPath()), + java: FakeJava(), + fileSystem: fileSystem, + ); + }, + AndroidStudio: () => FakeAndroidStudio(), }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, ); testUsingContext( - 'Verbose mode for APKs includes Gradle stacktrace and sets debug log level', + 'build apk keeps skip dependency checks on the main gradle invocation when ndk provisioning is unavailable', () async { final builder = AndroidGradleBuilder( java: FakeJava(), - logger: BufferLogger.test(verbose: true), + logger: logger, processManager: processManager, fileSystem: fileSystem, artifacts: Artifacts.test(), @@ -179,39 +204,39 @@ void main() { gradleUtils: FakeGradleUtils(), platform: FakePlatform(), androidStudio: FakeAndroidStudio(), + androidSdk: globals.androidSdk, ); processManager.addCommand( - const FakeCommand( + FakeCommand( command: [ 'gradlew', - '--full-stacktrace', - '--info', - '-Pverbose=true', + '-q', + '-PskipDependencyChecks=true', '-Ptarget-platform=android-arm,android-arm64,android-x64', '-Ptarget=lib/main.dart', '-Pbase-application-name=android.app.Application', '-Pdart-obfuscation=false', '-Ptrack-widget-creation=false', '-Ptree-shake-icons=false', - 'assembleRelease', + '-Pflutter.androidSdkRoot=${sdkPath()}', + '-Pflutter.installedNdkVersions=', + 'assembleDevRelease', ], ), ); - fileSystem.directory('android').childFile('build.gradle').createSync(recursive: true); - + fileSystem.file('android/gradlew').createSync(recursive: true); fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); - + fileSystem.file('android/build.gradle').createSync(recursive: true); fileSystem.directory('android').childDirectory('app').childFile('build.gradle') ..createSync(recursive: true) ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); - fileSystem .directory('build') .childDirectory('app') .childDirectory('outputs') .childDirectory('flutter-apk') - .childFile('app-release.apk') + .childFile('app-dev-release.apk') .createSync(recursive: true); final FlutterProject project = FlutterProject.fromDirectoryTest( @@ -226,23 +251,39 @@ void main() { androidBuildInfo: const AndroidBuildInfo( BuildInfo( BuildMode.release, - null, + 'dev', treeShakeIcons: false, packageConfigPath: '.dart_tool/package_config.json', + androidSkipBuildDependencyValidation: true, ), ), target: 'lib/main.dart', isBuildingBundle: false, configOnly: false, - localGradleErrors: [], + localGradleErrors: const [], ); + expect(processManager, hasNoRemainingExpectations); }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, + overrides: { + AndroidSdk: () { + fileSystem.directory(sdkPath()).createSync(recursive: true); + fileSystem + .directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin')) + .childFile(globals.platform.isWindows ? 'sdkmanager.bat' : 'sdkmanager') + .createSync(recursive: true); + return AndroidSdk( + fileSystem.directory(sdkPath()), + java: FakeJava(), + fileSystem: fileSystem, + ); + }, + AndroidStudio: () => FakeAndroidStudio(), + }, ); testUsingContext( - 'Can retry build on recognized exit code/stderr', + 'build apk passes an empty installed ndk version list when no valid ndks are installed', () async { final builder = AndroidGradleBuilder( java: FakeJava(), @@ -254,9 +295,98 @@ void main() { gradleUtils: FakeGradleUtils(), platform: FakePlatform(), androidStudio: FakeAndroidStudio(), + androidSdk: globals.androidSdk, + ); + processManager.addCommand( + FakeCommand( + command: [ + 'gradlew', + '-q', + '-Ptarget-platform=android-arm,android-arm64,android-x64', + '-Ptarget=lib/main.dart', + '-Pbase-application-name=android.app.Application', + '-Pdart-obfuscation=false', + '-Ptrack-widget-creation=false', + '-Ptree-shake-icons=false', + '-Pflutter.androidSdkRoot=${sdkPath()}', + '-Pflutter.installedNdkVersions=', + '-Pflutter.sdkManagerPath=${sdkManagerPath()}', + 'assembleDevRelease', + ], + ), + ); + + fileSystem.file('android/gradlew').createSync(recursive: true); + fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); + fileSystem.file('android/build.gradle').createSync(recursive: true); + fileSystem.directory('android').childDirectory('app').childFile('build.gradle') + ..createSync(recursive: true) + ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); + fileSystem + .directory('build') + .childDirectory('app') + .childDirectory('outputs') + .childDirectory('flutter-apk') + .childFile('app-dev-release.apk') + .createSync(recursive: true); + + final FlutterProject project = FlutterProject.fromDirectoryTest( + fileSystem.currentDirectory, ); + project.android.appManifestFile + ..createSync(recursive: true) + ..writeAsStringSync(minimalV2EmbeddingManifest); + + await builder.buildGradleApp( + project: project, + androidBuildInfo: const AndroidBuildInfo( + BuildInfo( + BuildMode.release, + 'dev', + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', + ), + ), + target: 'lib/main.dart', + isBuildingBundle: false, + configOnly: false, + localGradleErrors: const [], + ); + + expect(processManager, hasNoRemainingExpectations); + }, + overrides: { + AndroidSdk: () { + fileSystem.directory(sdkPath()).createSync(recursive: true); + fileSystem.directory(sdkLicensesPath()).createSync(recursive: true); + fileSystem + .directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin')) + .childFile(globals.platform.isWindows ? 'sdkmanager.bat' : 'sdkmanager') + .createSync(recursive: true); + return AndroidSdk( + fileSystem.directory(sdkPath()), + java: FakeJava(), + fileSystem: fileSystem, + ); + }, + AndroidStudio: () => FakeAndroidStudio(), + }, + ); - const fakeCmd = FakeCommand( + testUsingContext('Can immediately tool exit on recognized exit code/stderr', () async { + final builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.test(), + analytics: fakeAnalytics, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); + processManager.addCommand( + const FakeCommand( command: [ 'gradlew', '-q', @@ -270,89 +400,86 @@ void main() { ], exitCode: 1, stderr: '\nSome gradle message\n', - ); - - processManager.addCommand(fakeCmd); - - const maxRetries = 2; - for (var i = 0; i < maxRetries; i++) { - processManager.addCommand(fakeCmd); - } + ), + ); - fileSystem.directory('android').childFile('build.gradle').createSync(recursive: true); + fileSystem.directory('android').childFile('build.gradle').createSync(recursive: true); - fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); + fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); - fileSystem.directory('android').childDirectory('app').childFile('build.gradle') - ..createSync(recursive: true) - ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); + fileSystem.directory('android').childDirectory('app').childFile('build.gradle') + ..createSync(recursive: true) + ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); - final FlutterProject project = FlutterProject.fromDirectoryTest( - fileSystem.currentDirectory, - ); - project.android.appManifestFile - ..createSync(recursive: true) - ..writeAsStringSync(minimalV2EmbeddingManifest); + final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + project.android.appManifestFile + ..createSync(recursive: true) + ..writeAsStringSync(minimalV2EmbeddingManifest); - var testFnCalled = 0; - await expectLater(() async { - await builder.buildGradleApp( - maxRetries: maxRetries, - project: project, - androidBuildInfo: const AndroidBuildInfo( - BuildInfo( - BuildMode.release, - null, - treeShakeIcons: false, - packageConfigPath: '.dart_tool/package_config.json', - ), + var handlerCalled = false; + await expectLater(() async { + await builder.buildGradleApp( + project: project, + androidBuildInfo: const AndroidBuildInfo( + BuildInfo( + BuildMode.release, + null, + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', ), - target: 'lib/main.dart', - isBuildingBundle: false, - configOnly: false, - localGradleErrors: [ - GradleHandledError( - test: (String line) { - if (line.contains('Some gradle message')) { - testFnCalled++; - return true; - } - return false; - }, - handler: ({String? line, FlutterProject? project, bool? usesAndroidX}) async { - return GradleBuildStatus.retry; - }, - eventLabel: 'random-event-label', - ), - ], - ); - }, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1')); + ), + target: 'lib/main.dart', + isBuildingBundle: false, + configOnly: false, + localGradleErrors: [ + GradleHandledError( + test: (String line) { + return line.contains('Some gradle message'); + }, + handler: ({String? line, FlutterProject? project, bool? usesAndroidX}) async { + handlerCalled = true; + return GradleBuildStatus.exit; + }, + eventLabel: 'random-event-label', + ), + ], + ); + }, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1')); - expect(logger.statusText, contains('Retrying Gradle Build: #1, wait time: 100ms')); - expect(logger.statusText, contains('Retrying Gradle Build: #2, wait time: 200ms')); + expect(handlerCalled, isTrue); - expect(testFnCalled, equals(maxRetries + 1)); - expect(fakeAnalytics.sentEvents, hasLength(9)); - expect( - fakeAnalytics.sentEvents, - contains( - Event.flutterBuildInfo( - label: 'gradle-random-event-label-failure', - buildType: 'gradle', - settings: 'androidGradlePluginVersion: null', - ), + expect( + fakeAnalytics.sentEvents, + containsAll([ + Event.flutterBuildInfo( + label: 'app-not-using-android-x', + buildType: 'gradle', + settings: 'androidGradlePluginVersion: null', ), - ); - }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, - ); + Event.flutterBuildInfo( + label: 'gradle-random-event-label-failure', + buildType: 'gradle', + settings: 'androidGradlePluginVersion: null', + ), + ]), + ); + + expect( + analyticsTimingEventExists( + sentEvents: fakeAnalytics.sentEvents, + workflow: 'build', + variableName: 'gradle', + ), + true, + ); + }, overrides: {AndroidStudio: () => FakeAndroidStudio()}); testUsingContext( - 'Converts recognized ProcessExceptions into tools exits', + 'Verbose mode for APKs includes Gradle stacktrace and sets debug log level', () async { final builder = AndroidGradleBuilder( java: FakeJava(), - logger: logger, + logger: BufferLogger.test(verbose: true), processManager: processManager, fileSystem: fileSystem, artifacts: Artifacts.test(), @@ -365,7 +492,9 @@ void main() { const FakeCommand( command: [ 'gradlew', - '-q', + '--full-stacktrace', + '--info', + '-Pverbose=true', '-Ptarget-platform=android-arm,android-arm64,android-x64', '-Ptarget=lib/main.dart', '-Pbase-application-name=android.app.Application', @@ -374,8 +503,6 @@ void main() { '-Ptree-shake-icons=false', 'assembleRelease', ], - exitCode: 1, - stderr: '\nSome gradle message\n', ), ); @@ -387,6 +514,14 @@ void main() { ..createSync(recursive: true) ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); + fileSystem + .directory('build') + .childDirectory('app') + .childDirectory('outputs') + .childDirectory('flutter-apk') + .childFile('app-release.apk') + .createSync(recursive: true); + final FlutterProject project = FlutterProject.fromDirectoryTest( fileSystem.currentDirectory, ); @@ -394,194 +529,171 @@ void main() { ..createSync(recursive: true) ..writeAsStringSync(minimalV2EmbeddingManifest); - var handlerCalled = false; - await expectLater(() async { - await builder.buildGradleApp( - project: project, - androidBuildInfo: const AndroidBuildInfo( - BuildInfo( - BuildMode.release, - null, - treeShakeIcons: false, - packageConfigPath: '.dart_tool/package_config.json', - ), - ), - target: 'lib/main.dart', - isBuildingBundle: false, - configOnly: false, - localGradleErrors: [ - GradleHandledError( - test: (String line) { - return line.contains('Some gradle message'); - }, - handler: ({String? line, FlutterProject? project, bool? usesAndroidX}) async { - handlerCalled = true; - return GradleBuildStatus.exit; - }, - eventLabel: 'random-event-label', - ), - ], - ); - }, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1')); - - expect(handlerCalled, isTrue); - - expect(fakeAnalytics.sentEvents, hasLength(3)); - expect( - fakeAnalytics.sentEvents, - contains( - Event.flutterBuildInfo( - label: 'gradle-random-event-label-failure', - buildType: 'gradle', - settings: 'androidGradlePluginVersion: null', + await builder.buildGradleApp( + project: project, + androidBuildInfo: const AndroidBuildInfo( + BuildInfo( + BuildMode.release, + null, + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', ), ), + target: 'lib/main.dart', + isBuildingBundle: false, + configOnly: false, + localGradleErrors: [], ); + expect(processManager, hasNoRemainingExpectations); }, overrides: {AndroidStudio: () => FakeAndroidStudio()}, ); - testUsingContext( - 'rethrows unrecognized ProcessException', - () async { - final builder = AndroidGradleBuilder( - java: FakeJava(), - logger: logger, - processManager: processManager, - fileSystem: fileSystem, - artifacts: Artifacts.test(), - analytics: fakeAnalytics, - gradleUtils: FakeGradleUtils(), - platform: FakePlatform(), - androidStudio: FakeAndroidStudio(), - ); - processManager.addCommand( - FakeCommand( - command: const [ - 'gradlew', - '-q', - '-Ptarget-platform=android-arm,android-arm64,android-x64', - '-Ptarget=lib/main.dart', - '-Pbase-application-name=android.app.Application', - '-Pdart-obfuscation=false', - '-Ptrack-widget-creation=false', - '-Ptree-shake-icons=false', - 'assembleRelease', - ], - exitCode: 1, - onRun: (_) { - throw const ProcessException('', [], 'Unrecognized'); - }, - ), - ); + testUsingContext('Can retry build on recognized exit code/stderr', () async { + final builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.test(), + analytics: fakeAnalytics, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); - fileSystem.directory('android').childFile('build.gradle').createSync(recursive: true); + const fakeCmd = FakeCommand( + command: [ + 'gradlew', + '-q', + '-Ptarget-platform=android-arm,android-arm64,android-x64', + '-Ptarget=lib/main.dart', + '-Pbase-application-name=android.app.Application', + '-Pdart-obfuscation=false', + '-Ptrack-widget-creation=false', + '-Ptree-shake-icons=false', + 'assembleRelease', + ], + exitCode: 1, + stderr: '\nSome gradle message\n', + ); - fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); + processManager.addCommand(fakeCmd); - fileSystem.directory('android').childDirectory('app').childFile('build.gradle') - ..createSync(recursive: true) - ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); + const maxRetries = 2; + for (var i = 0; i < maxRetries; i++) { + processManager.addCommand(fakeCmd); + } - final FlutterProject project = FlutterProject.fromDirectoryTest( - fileSystem.currentDirectory, - ); - project.android.appManifestFile - ..createSync(recursive: true) - ..writeAsStringSync(minimalV2EmbeddingManifest); + fileSystem.directory('android').childFile('build.gradle').createSync(recursive: true); - await expectLater(() async { - await builder.buildGradleApp( - project: project, - androidBuildInfo: const AndroidBuildInfo( - BuildInfo( - BuildMode.release, - null, - treeShakeIcons: false, - packageConfigPath: '.dart_tool/package_config.json', - ), - ), - target: 'lib/main.dart', - isBuildingBundle: false, - configOnly: false, - localGradleErrors: const [], - ); - }, throwsProcessException()); - expect(processManager, hasNoRemainingExpectations); - }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, - ); + fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); - testUsingContext( - 'logs success event after a successful retry', - () async { - final builder = AndroidGradleBuilder( - java: FakeJava(), - logger: logger, - processManager: processManager, - fileSystem: fileSystem, - artifacts: Artifacts.test(), - analytics: fakeAnalytics, - gradleUtils: FakeGradleUtils(), - platform: FakePlatform(), - androidStudio: FakeAndroidStudio(), - ); - processManager.addCommand( - const FakeCommand( - command: [ - 'gradlew', - '-q', - '-Ptarget-platform=android-arm,android-arm64,android-x64', - '-Ptarget=lib/main.dart', - '-Pbase-application-name=android.app.Application', - '-Pdart-obfuscation=false', - '-Ptrack-widget-creation=false', - '-Ptree-shake-icons=false', - 'assembleRelease', - ], - exitCode: 1, - stderr: '\nnSome gradle message\n', + fileSystem.directory('android').childDirectory('app').childFile('build.gradle') + ..createSync(recursive: true) + ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); + + final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + project.android.appManifestFile + ..createSync(recursive: true) + ..writeAsStringSync(minimalV2EmbeddingManifest); + + var testFnCalled = 0; + await expectLater(() async { + await builder.buildGradleApp( + maxRetries: maxRetries, + project: project, + androidBuildInfo: const AndroidBuildInfo( + BuildInfo( + BuildMode.release, + null, + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', + ), ), + target: 'lib/main.dart', + isBuildingBundle: false, + configOnly: false, + localGradleErrors: [ + GradleHandledError( + test: (String line) { + if (line.contains('Some gradle message')) { + testFnCalled++; + return true; + } + return false; + }, + handler: ({String? line, FlutterProject? project, bool? usesAndroidX}) async { + return GradleBuildStatus.retry; + }, + eventLabel: 'random-event-label', + ), + ], ); - processManager.addCommand( - const FakeCommand( - command: [ - 'gradlew', - '-q', - '-Ptarget-platform=android-arm,android-arm64,android-x64', - '-Ptarget=lib/main.dart', - '-Pbase-application-name=android.app.Application', - '-Pdart-obfuscation=false', - '-Ptrack-widget-creation=false', - '-Ptree-shake-icons=false', - 'assembleRelease', - ], + }, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1')); + + expect(logger.statusText, contains('Retrying Gradle Build: #1, wait time: 100ms')); + expect(logger.statusText, contains('Retrying Gradle Build: #2, wait time: 200ms')); + + expect(testFnCalled, equals(maxRetries + 1)); + expect(fakeAnalytics.sentEvents, hasLength(9)); + expect( + fakeAnalytics.sentEvents, + contains( + Event.flutterBuildInfo( + label: 'gradle-random-event-label-failure', + buildType: 'gradle', + settings: 'androidGradlePluginVersion: null', ), - ); + ), + ); + }, overrides: {AndroidStudio: () => FakeAndroidStudio()}); - fileSystem.directory('android').childFile('build.gradle').createSync(recursive: true); + testUsingContext('Converts recognized ProcessExceptions into tools exits', () async { + final builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.test(), + analytics: fakeAnalytics, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); + processManager.addCommand( + const FakeCommand( + command: [ + 'gradlew', + '-q', + '-Ptarget-platform=android-arm,android-arm64,android-x64', + '-Ptarget=lib/main.dart', + '-Pbase-application-name=android.app.Application', + '-Pdart-obfuscation=false', + '-Ptrack-widget-creation=false', + '-Ptree-shake-icons=false', + 'assembleRelease', + ], + exitCode: 1, + stderr: '\nSome gradle message\n', + ), + ); - fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); + fileSystem.directory('android').childFile('build.gradle').createSync(recursive: true); - fileSystem.directory('android').childDirectory('app').childFile('build.gradle') - ..createSync(recursive: true) - ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); + fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); - fileSystem - .directory('build') - .childDirectory('app') - .childDirectory('outputs') - .childDirectory('flutter-apk') - .childFile('app-release.apk') - .createSync(recursive: true); + fileSystem.directory('android').childDirectory('app').childFile('build.gradle') + ..createSync(recursive: true) + ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); - final FlutterProject project = FlutterProject.fromDirectoryTest( - fileSystem.currentDirectory, - ); - project.android.appManifestFile - ..createSync(recursive: true) - ..writeAsStringSync(minimalV2EmbeddingManifest); + final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + project.android.appManifestFile + ..createSync(recursive: true) + ..writeAsStringSync(minimalV2EmbeddingManifest); + var handlerCalled = false; + await expectLater(() async { await builder.buildGradleApp( project: project, androidBuildInfo: const AndroidBuildInfo( @@ -601,105 +713,76 @@ void main() { return line.contains('Some gradle message'); }, handler: ({String? line, FlutterProject? project, bool? usesAndroidX}) async { - return GradleBuildStatus.retry; + handlerCalled = true; + return GradleBuildStatus.exit; }, eventLabel: 'random-event-label', ), ], ); + }, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1')); - expect( - fakeAnalytics.sentEvents, - contains( - Event.flutterBuildInfo( - label: 'gradle-random-event-label-success', - buildType: 'gradle', - settings: 'androidGradlePluginVersion: null', - ), - ), - ); - expect(processManager, hasNoRemainingExpectations); - }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, - ); + expect(handlerCalled, isTrue); - testUsingContext( - 'performs code size analysis and sends analytics', - () async { - final builder = AndroidGradleBuilder( - java: FakeJava(), - logger: logger, - processManager: processManager, - fileSystem: fileSystem, - artifacts: Artifacts.test(), - analytics: fakeAnalytics, - gradleUtils: FakeGradleUtils(), - platform: FakePlatform(environment: {'HOME': '/home'}), - androidStudio: FakeAndroidStudio(), - ); - processManager.addCommand( - const FakeCommand( - command: [ - 'gradlew', - '-q', - '-Ptarget-platform=android-arm64', - '-Ptarget=lib/main.dart', - '-Pbase-application-name=android.app.Application', - '-Pdart-obfuscation=false', - '-Ptrack-widget-creation=false', - '-Ptree-shake-icons=false', - '-Pcode-size-directory=foo', - 'assembleRelease', - ], + expect(fakeAnalytics.sentEvents, hasLength(3)); + expect( + fakeAnalytics.sentEvents, + contains( + Event.flutterBuildInfo( + label: 'gradle-random-event-label-failure', + buildType: 'gradle', + settings: 'androidGradlePluginVersion: null', ), - ); - - fileSystem.directory('android').childFile('build.gradle').createSync(recursive: true); + ), + ); + }, overrides: {AndroidStudio: () => FakeAndroidStudio()}); - fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); - - fileSystem.directory('android').childDirectory('app').childFile('build.gradle') - ..createSync(recursive: true) - ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); + testUsingContext('rethrows unrecognized ProcessException', () async { + final builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.test(), + analytics: fakeAnalytics, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); + processManager.addCommand( + FakeCommand( + command: const [ + 'gradlew', + '-q', + '-Ptarget-platform=android-arm,android-arm64,android-x64', + '-Ptarget=lib/main.dart', + '-Pbase-application-name=android.app.Application', + '-Pdart-obfuscation=false', + '-Ptrack-widget-creation=false', + '-Ptree-shake-icons=false', + 'assembleRelease', + ], + exitCode: 1, + onRun: (_) { + throw const ProcessException('', [], 'Unrecognized'); + }, + ), + ); - final archive = Archive() - ..addFile(ArchiveFile('AndroidManifest.xml', 100, List.filled(100, 0))) - ..addFile(ArchiveFile('META-INF/CERT.RSA', 10, List.filled(10, 0))) - ..addFile(ArchiveFile('META-INF/CERT.SF', 10, List.filled(10, 0))) - ..addFile(ArchiveFile('lib/arm64-v8a/libapp.so', 50, List.filled(50, 0))) - ..addFile(ArchiveFile('lib/arm64-v8a/libflutter.so', 50, List.filled(50, 0))); + fileSystem.directory('android').childFile('build.gradle').createSync(recursive: true); - fileSystem - .directory('build') - .childDirectory('app') - .childDirectory('outputs') - .childDirectory('flutter-apk') - .childFile('app-release.apk') - ..createSync(recursive: true) - ..writeAsBytesSync(ZipEncoder().encode(archive)!); + fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); - fileSystem.file('foo/snapshot.arm64-v8a.json') - ..createSync(recursive: true) - ..writeAsStringSync(r''' -[ - { - "l": "dart:_internal", - "c": "SubListIterable", - "n": "[Optimized] skip", - "s": 2400 - } -]'''); - fileSystem.file('foo/trace.arm64-v8a.json') - ..createSync(recursive: true) - ..writeAsStringSync('{}'); + fileSystem.directory('android').childDirectory('app').childFile('build.gradle') + ..createSync(recursive: true) + ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); - final FlutterProject project = FlutterProject.fromDirectoryTest( - fileSystem.currentDirectory, - ); - project.android.appManifestFile - ..createSync(recursive: true) - ..writeAsStringSync(minimalV2EmbeddingManifest); + final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + project.android.appManifestFile + ..createSync(recursive: true) + ..writeAsStringSync(minimalV2EmbeddingManifest); + await expectLater(() async { await builder.buildGradleApp( project: project, androidBuildInfo: const AndroidBuildInfo( @@ -707,24 +790,219 @@ void main() { BuildMode.release, null, treeShakeIcons: false, - codeSizeDirectory: 'foo', packageConfigPath: '.dart_tool/package_config.json', ), - targetArchs: [AndroidArch.arm64_v8a], ), target: 'lib/main.dart', isBuildingBundle: false, configOnly: false, - localGradleErrors: [], + localGradleErrors: const [], ); + }, throwsProcessException()); + expect(processManager, hasNoRemainingExpectations); + }, overrides: {AndroidStudio: () => FakeAndroidStudio()}); - expect(fakeAnalytics.sentEvents, contains(Event.codeSizeAnalysis(platform: 'apk'))); - }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, - ); + testUsingContext('logs success event after a successful retry', () async { + final builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.test(), + analytics: fakeAnalytics, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); + processManager.addCommand( + const FakeCommand( + command: [ + 'gradlew', + '-q', + '-Ptarget-platform=android-arm,android-arm64,android-x64', + '-Ptarget=lib/main.dart', + '-Pbase-application-name=android.app.Application', + '-Pdart-obfuscation=false', + '-Ptrack-widget-creation=false', + '-Ptree-shake-icons=false', + 'assembleRelease', + ], + exitCode: 1, + stderr: '\nnSome gradle message\n', + ), + ); + processManager.addCommand( + const FakeCommand( + command: [ + 'gradlew', + '-q', + '-Ptarget-platform=android-arm,android-arm64,android-x64', + '-Ptarget=lib/main.dart', + '-Pbase-application-name=android.app.Application', + '-Pdart-obfuscation=false', + '-Ptrack-widget-creation=false', + '-Ptree-shake-icons=false', + 'assembleRelease', + ], + ), + ); + + fileSystem.directory('android').childFile('build.gradle').createSync(recursive: true); + + fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); + + fileSystem.directory('android').childDirectory('app').childFile('build.gradle') + ..createSync(recursive: true) + ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); + + fileSystem + .directory('build') + .childDirectory('app') + .childDirectory('outputs') + .childDirectory('flutter-apk') + .childFile('app-release.apk') + .createSync(recursive: true); + + final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + project.android.appManifestFile + ..createSync(recursive: true) + ..writeAsStringSync(minimalV2EmbeddingManifest); + + await builder.buildGradleApp( + project: project, + androidBuildInfo: const AndroidBuildInfo( + BuildInfo( + BuildMode.release, + null, + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', + ), + ), + target: 'lib/main.dart', + isBuildingBundle: false, + configOnly: false, + localGradleErrors: [ + GradleHandledError( + test: (String line) { + return line.contains('Some gradle message'); + }, + handler: ({String? line, FlutterProject? project, bool? usesAndroidX}) async { + return GradleBuildStatus.retry; + }, + eventLabel: 'random-event-label', + ), + ], + ); + + expect( + fakeAnalytics.sentEvents, + contains( + Event.flutterBuildInfo( + label: 'gradle-random-event-label-success', + buildType: 'gradle', + settings: 'androidGradlePluginVersion: null', + ), + ), + ); + expect(processManager, hasNoRemainingExpectations); + }, overrides: {AndroidStudio: () => FakeAndroidStudio()}); + + testUsingContext('performs code size analysis and sends analytics', () async { + final builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.test(), + analytics: fakeAnalytics, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(environment: {'HOME': '/home'}), + androidStudio: FakeAndroidStudio(), + ); + processManager.addCommand( + const FakeCommand( + command: [ + 'gradlew', + '-q', + '-Ptarget-platform=android-arm64', + '-Ptarget=lib/main.dart', + '-Pbase-application-name=android.app.Application', + '-Pdart-obfuscation=false', + '-Ptrack-widget-creation=false', + '-Ptree-shake-icons=false', + '-Pcode-size-directory=foo', + 'assembleRelease', + ], + ), + ); + + fileSystem.directory('android').childFile('build.gradle').createSync(recursive: true); + + fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); + + fileSystem.directory('android').childDirectory('app').childFile('build.gradle') + ..createSync(recursive: true) + ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); + + final archive = Archive() + ..addFile(ArchiveFile('AndroidManifest.xml', 100, List.filled(100, 0))) + ..addFile(ArchiveFile('META-INF/CERT.RSA', 10, List.filled(10, 0))) + ..addFile(ArchiveFile('META-INF/CERT.SF', 10, List.filled(10, 0))) + ..addFile(ArchiveFile('lib/arm64-v8a/libapp.so', 50, List.filled(50, 0))) + ..addFile(ArchiveFile('lib/arm64-v8a/libflutter.so', 50, List.filled(50, 0))); + + fileSystem + .directory('build') + .childDirectory('app') + .childDirectory('outputs') + .childDirectory('flutter-apk') + .childFile('app-release.apk') + ..createSync(recursive: true) + ..writeAsBytesSync(ZipEncoder().encode(archive)!); + + fileSystem.file('foo/snapshot.arm64-v8a.json') + ..createSync(recursive: true) + ..writeAsStringSync(r''' +[ + { + "l": "dart:_internal", + "c": "SubListIterable", + "n": "[Optimized] skip", + "s": 2400 + } +]'''); + fileSystem.file('foo/trace.arm64-v8a.json') + ..createSync(recursive: true) + ..writeAsStringSync('{}'); + + final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + project.android.appManifestFile + ..createSync(recursive: true) + ..writeAsStringSync(minimalV2EmbeddingManifest); + + await builder.buildGradleApp( + project: project, + androidBuildInfo: const AndroidBuildInfo( + BuildInfo( + BuildMode.release, + null, + treeShakeIcons: false, + codeSizeDirectory: 'foo', + packageConfigPath: '.dart_tool/package_config.json', + ), + targetArchs: [AndroidArch.arm64_v8a], + ), + target: 'lib/main.dart', + isBuildingBundle: false, + configOnly: false, + localGradleErrors: [], + ); + + expect(fakeAnalytics.sentEvents, contains(Event.codeSizeAnalysis(platform: 'apk'))); + }, overrides: {AndroidStudio: () => FakeAndroidStudio()}); group('Appbundle debug symbol tests', () { - final commonCommandPortion = [ + List commonCommandPortion() => [ 'gradlew', '-q', '-Ptarget-platform=android-arm64,android-arm,android-x64', @@ -733,6 +1011,8 @@ void main() { '-Pdart-obfuscation=false', '-Ptrack-widget-creation=false', '-Ptree-shake-icons=false', + '-Pflutter.androidSdkRoot=${sdkPath()}', + '-Pflutter.installedNdkVersions=', ]; // Output from `/tools/bin/apkanalyzer files list ` @@ -914,23 +1194,18 @@ void main() { gradleUtils: FakeGradleUtils(), platform: FakePlatform(environment: {'HOME': '/home'}), androidStudio: FakeAndroidStudio(), + androidSdk: globals.androidSdk, ); processManager.addCommand( - FakeCommand(command: List.of(commonCommandPortion)..add('bundleRelease')), + FakeCommand(command: List.of(commonCommandPortion())..add('bundleRelease')), ); createSharedGradleFiles(); final File aabFile = createAabFile(BuildMode.release); - final AndroidSdk sdk = AndroidSdk.locateAndroidSdk()!; processManager.addCommand( FakeCommand( - command: [ - sdk.getCmdlineToolsPath(apkAnalyzerBinaryName)!, - 'files', - 'list', - aabFile.path, - ], + command: [apkAnalyzerPath(), 'files', 'list', aabFile.path], stdout: apkanalyzerOutputWithSymFiles, ), ); @@ -963,7 +1238,22 @@ void main() { localGradleErrors: [], ); }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, + overrides: { + AndroidSdk: () { + fileSystem.directory(sdkPath()).createSync(recursive: true); + fileSystem + .directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin')) + .childFile(apkAnalyzerBinaryName) + .createSync(recursive: true); + return AndroidSdk( + fileSystem.directory(sdkPath()), + java: FakeJava(), + fileSystem: fileSystem, + ); + }, + AndroidStudio: () => FakeAndroidStudio(), + ProcessManager: () => processManager, + }, ); testUsingContext( @@ -979,23 +1269,18 @@ void main() { gradleUtils: FakeGradleUtils(), platform: FakePlatform(environment: {'HOME': '/home'}), androidStudio: FakeAndroidStudio(), + androidSdk: globals.androidSdk, ); processManager.addCommand( - FakeCommand(command: List.of(commonCommandPortion)..add('bundleRelease')), + FakeCommand(command: List.of(commonCommandPortion())..add('bundleRelease')), ); createSharedGradleFiles(); final File aabFile = createAabFile(BuildMode.release); - final AndroidSdk sdk = AndroidSdk.locateAndroidSdk()!; processManager.addCommand( FakeCommand( - command: [ - sdk.getCmdlineToolsPath(apkAnalyzerBinaryName)!, - 'files', - 'list', - aabFile.path, - ], + command: [apkAnalyzerPath(), 'files', 'list', aabFile.path], stdout: apkanalyzerOutputWithDebugInfoAndSymFiles, ), ); @@ -1028,7 +1313,22 @@ void main() { localGradleErrors: [], ); }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, + overrides: { + AndroidSdk: () { + fileSystem.directory(sdkPath()).createSync(recursive: true); + fileSystem + .directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin')) + .childFile(apkAnalyzerBinaryName) + .createSync(recursive: true); + return AndroidSdk( + fileSystem.directory(sdkPath()), + java: FakeJava(), + fileSystem: fileSystem, + ); + }, + AndroidStudio: () => FakeAndroidStudio(), + ProcessManager: () => processManager, + }, ); testUsingContext( @@ -1044,9 +1344,10 @@ void main() { gradleUtils: FakeGradleUtils(), platform: FakePlatform(environment: {'HOME': '/home'}), androidStudio: FakeAndroidStudio(), + androidSdk: globals.androidSdk, ); processManager.addCommand( - FakeCommand(command: List.of(commonCommandPortion)..add('bundleDebug')), + FakeCommand(command: List.of(commonCommandPortion())..add('bundleDebug')), ); createSharedGradleFiles(); @@ -1080,12 +1381,36 @@ void main() { localGradleErrors: [], ); }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, + overrides: { + AndroidSdk: () { + fileSystem.directory(sdkPath()).createSync(recursive: true); + fileSystem + .directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin')) + .childFile(apkAnalyzerBinaryName) + .createSync(recursive: true); + return AndroidSdk( + fileSystem.directory(sdkPath()), + java: FakeJava(), + fileSystem: fileSystem, + ); + }, + AndroidStudio: () => FakeAndroidStudio(), + }, ); testUsingContext( 'throws tool exit for missing debug symbols when building release app bundle', () async { + fileSystem.directory(sdkPath()).createSync(recursive: true); + fileSystem + .directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin')) + .childFile(apkAnalyzerBinaryName) + .createSync(recursive: true); + final sdk = AndroidSdk( + fileSystem.directory(sdkPath()), + java: FakeJava(), + fileSystem: fileSystem, + ); final builder = AndroidGradleBuilder( java: FakeJava(), logger: logger, @@ -1096,16 +1421,15 @@ void main() { gradleUtils: FakeGradleUtils(), platform: FakePlatform(environment: {'HOME': '/home'}), androidStudio: FakeAndroidStudio(), + androidSdk: sdk, ); processManager.addCommand( - FakeCommand(command: List.of(commonCommandPortion)..add('bundleRelease')), + FakeCommand(command: List.of(commonCommandPortion())..add('bundleRelease')), ); createSharedGradleFiles(); final File aabFile = createAabFile(BuildMode.release); - final AndroidSdk sdk = AndroidSdk.locateAndroidSdk()!; - processManager.addCommand( FakeCommand( command: [ @@ -1149,12 +1473,36 @@ void main() { throwsToolExit(message: failedToStripDebugSymbolsErrorMessage), ); }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, + overrides: { + AndroidSdk: () { + fileSystem.directory(sdkPath()).createSync(recursive: true); + fileSystem + .directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin')) + .childFile(apkAnalyzerBinaryName) + .createSync(recursive: true); + return AndroidSdk( + fileSystem.directory(sdkPath()), + java: FakeJava(), + fileSystem: fileSystem, + ); + }, + AndroidStudio: () => FakeAndroidStudio(), + }, ); testUsingContext( 'build aab in release mode fails when apkanalyzer exit code is non zero', () async { + fileSystem.directory(sdkPath()).createSync(recursive: true); + fileSystem + .directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin')) + .childFile(apkAnalyzerBinaryName) + .createSync(recursive: true); + final sdk = AndroidSdk( + fileSystem.directory(sdkPath()), + java: FakeJava(), + fileSystem: fileSystem, + ); final builder = AndroidGradleBuilder( java: FakeJava(), logger: logger, @@ -1165,16 +1513,15 @@ void main() { gradleUtils: FakeGradleUtils(), platform: FakePlatform(environment: {'HOME': '/home'}), androidStudio: FakeAndroidStudio(), + androidSdk: sdk, ); processManager.addCommand( - FakeCommand(command: List.of(commonCommandPortion)..add('bundleRelease')), + FakeCommand(command: List.of(commonCommandPortion())..add('bundleRelease')), ); createSharedGradleFiles(); final File aabFile = createAabFile(BuildMode.release); - final AndroidSdk sdk = AndroidSdk.locateAndroidSdk()!; - processManager.addCommand( FakeCommand( command: [ @@ -1223,82 +1570,76 @@ void main() { ); }); - testUsingContext( - 'indicates that an APK has been built successfully', - () async { - final builder = AndroidGradleBuilder( - java: FakeJava(), - logger: logger, - processManager: processManager, - fileSystem: fileSystem, - artifacts: Artifacts.test(), - analytics: fakeAnalytics, - gradleUtils: FakeGradleUtils(), - platform: FakePlatform(), - androidStudio: FakeAndroidStudio(), - ); - processManager.addCommand( - const FakeCommand( - command: [ - 'gradlew', - '-q', - '-Ptarget-platform=android-arm,android-arm64,android-x64', - '-Ptarget=lib/main.dart', - '-Pbase-application-name=android.app.Application', - '-Pdart-obfuscation=false', - '-Ptrack-widget-creation=false', - '-Ptree-shake-icons=false', - 'assembleRelease', - ], - ), - ); - fileSystem.directory('android').childFile('build.gradle').createSync(recursive: true); + testUsingContext('indicates that an APK has been built successfully', () async { + final builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.test(), + analytics: fakeAnalytics, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); + processManager.addCommand( + const FakeCommand( + command: [ + 'gradlew', + '-q', + '-Ptarget-platform=android-arm,android-arm64,android-x64', + '-Ptarget=lib/main.dart', + '-Pbase-application-name=android.app.Application', + '-Pdart-obfuscation=false', + '-Ptrack-widget-creation=false', + '-Ptree-shake-icons=false', + 'assembleRelease', + ], + ), + ); + fileSystem.directory('android').childFile('build.gradle').createSync(recursive: true); - fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); + fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); - fileSystem.directory('android').childDirectory('app').childFile('build.gradle') - ..createSync(recursive: true) - ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); - - fileSystem - .directory('build') - .childDirectory('app') - .childDirectory('outputs') - .childDirectory('flutter-apk') - .childFile('app-release.apk') - .createSync(recursive: true); + fileSystem.directory('android').childDirectory('app').childFile('build.gradle') + ..createSync(recursive: true) + ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); - final FlutterProject project = FlutterProject.fromDirectoryTest( - fileSystem.currentDirectory, - ); - project.android.appManifestFile - ..createSync(recursive: true) - ..writeAsStringSync(minimalV2EmbeddingManifest); + fileSystem + .directory('build') + .childDirectory('app') + .childDirectory('outputs') + .childDirectory('flutter-apk') + .childFile('app-release.apk') + .createSync(recursive: true); - await builder.buildGradleApp( - project: project, - androidBuildInfo: const AndroidBuildInfo( - BuildInfo( - BuildMode.release, - null, - treeShakeIcons: false, - packageConfigPath: '.dart_tool/package_config.json', - ), + final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + project.android.appManifestFile + ..createSync(recursive: true) + ..writeAsStringSync(minimalV2EmbeddingManifest); + + await builder.buildGradleApp( + project: project, + androidBuildInfo: const AndroidBuildInfo( + BuildInfo( + BuildMode.release, + null, + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', ), - target: 'lib/main.dart', - isBuildingBundle: false, - configOnly: false, - localGradleErrors: const [], - ); + ), + target: 'lib/main.dart', + isBuildingBundle: false, + configOnly: false, + localGradleErrors: const [], + ); - expect( - logger.statusText, - contains('Built build/app/outputs/flutter-apk/app-release.apk (0.0MB)'), - ); - expect(processManager, hasNoRemainingExpectations); - }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, - ); + expect( + logger.statusText, + contains('Built build/app/outputs/flutter-apk/app-release.apk (0.0MB)'), + ); + expect(processManager, hasNoRemainingExpectations); + }, overrides: {AndroidStudio: () => FakeAndroidStudio()}); testUsingContext('Uses namespace attribute if manifest lacks a package attribute', () async { final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); @@ -1419,36 +1760,32 @@ BuildVariant: paidProfile }, ); - testUsingContext( - 'getBuildOptions returns empty list if gradle returns error', - () async { - final builder = AndroidGradleBuilder( - java: FakeJava(), - logger: logger, - processManager: processManager, - fileSystem: fileSystem, - artifacts: Artifacts.test(), - analytics: fakeAnalytics, - gradleUtils: FakeGradleUtils(), - platform: FakePlatform(), - androidStudio: FakeAndroidStudio(), - ); - processManager.addCommand( - const FakeCommand( - command: ['gradlew', '-q', 'printBuildVariants'], - stderr: ''' + testUsingContext('getBuildOptions returns empty list if gradle returns error', () async { + final builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.test(), + analytics: fakeAnalytics, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); + processManager.addCommand( + const FakeCommand( + command: ['gradlew', '-q', 'printBuildVariants'], + stderr: ''' Gradle Crashed ''', - exitCode: 1, - ), - ); - final List actual = await builder.getBuildVariants( - project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), - ); - expect(actual, const []); - }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, - ); + exitCode: 1, + ), + ); + final List actual = await builder.getBuildVariants( + project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + ); + expect(actual, const []); + }, overrides: {AndroidStudio: () => FakeAndroidStudio()}); testUsingContext( 'can call custom gradle task outputFreeDebugAppLinkSettings and parse the result', @@ -1581,6 +1918,96 @@ Gradle Crashed }, ); + testUsingContext( + 'build aar passes sdkmanager path, sdk root, and validated installed ndk versions to gradle', + () async { + final builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.test(), + analytics: fakeAnalytics, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + androidSdk: globals.androidSdk, + ); + processManager.addCommand( + FakeCommand( + command: [ + 'gradlew', + '-I=/packages/flutter_tools/gradle/aar_init_script.gradle', + '-Pflutter-root=/', + '-Poutput-dir=build/', + '-Pis-plugin=false', + '-PbuildNumber=1.0', + '-q', + '-Pdart-obfuscation=false', + '-Ptrack-widget-creation=false', + '-Ptree-shake-icons=false', + '-Pflutter.androidSdkRoot=${sdkPath()}', + '-Pflutter.installedNdkVersions=29.0.13846066', + '-Pflutter.sdkManagerPath=${sdkManagerPath()}', + '-Ptarget-platform=android-arm,android-arm64,android-x64', + 'assembleAarRelease', + ], + ), + ); + + final File manifestFile = fileSystem.file('pubspec.yaml'); + manifestFile.createSync(recursive: true); + manifestFile.writeAsStringSync(''' + flutter: + module: + androidPackage: com.example.test + '''); + + fileSystem.file('.android/gradlew').createSync(recursive: true); + fileSystem.file('.android/gradle.properties').writeAsStringSync('irrelevant'); + fileSystem.file('.android/build.gradle').createSync(recursive: true); + fileSystem.directory('build/outputs/repo').createSync(recursive: true); + + await builder.buildGradleAar( + androidBuildInfo: const AndroidBuildInfo( + BuildInfo( + BuildMode.release, + null, + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', + ), + ), + project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + outputDirectory: fileSystem.directory('build/'), + target: '', + buildNumber: '1.0', + ); + + expect(processManager, hasNoRemainingExpectations); + }, + overrides: { + AndroidSdk: () { + fileSystem.directory(sdkPath()).createSync(recursive: true); + fileSystem.directory(sdkLicensesPath()).createSync(recursive: true); + fileSystem + .directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin')) + .childFile(globals.platform.isWindows ? 'sdkmanager.bat' : 'sdkmanager') + .createSync(recursive: true); + fileSystem + .directory(ndkPath('29.0.13846066')) + .childFile('source.properties') + .createSync(recursive: true); + fileSystem.directory(ndkPath('29.0.13846066-bad')).createSync(recursive: true); + return AndroidSdk( + fileSystem.directory(sdkPath()), + java: FakeJava(), + fileSystem: fileSystem, + ); + }, + AndroidStudio: () => FakeAndroidStudio(), + }, + ); + // Regression test for https://github.com/flutter/flutter/issues/162649. testUsingContext('buildAar generates tooling for each sub-build for AARs', () async { addTearDown(() { @@ -1779,119 +2206,113 @@ Gradle Crashed overrides: {AndroidStudio: () => FakeAndroidStudio()}, ); - testUsingContext( - 'gradle exit code and stderr is forwarded to tool exit', - () async { - final builder = AndroidGradleBuilder( - java: FakeJava(), - logger: logger, - processManager: processManager, - fileSystem: fileSystem, - artifacts: Artifacts.test(), - analytics: fakeAnalytics, - gradleUtils: FakeGradleUtils(), - platform: FakePlatform(), - androidStudio: FakeAndroidStudio(), - ); - processManager.addCommand( - const FakeCommand( - command: [ - 'gradlew', - '-I=/packages/flutter_tools/gradle/aar_init_script.gradle', - '-Pflutter-root=/', - '-Poutput-dir=build/', - '-Pis-plugin=false', - '-PbuildNumber=1.0', - '-q', - '-Pdart-obfuscation=false', - '-Ptrack-widget-creation=false', - '-Ptree-shake-icons=false', - '-Ptarget-platform=android-arm,android-arm64,android-x64', - 'assembleAarRelease', - ], - exitCode: 108, - stderr: 'Gradle task assembleAarRelease failed with exit code 108.', - ), - ); + testUsingContext('gradle exit code and stderr is forwarded to tool exit', () async { + final builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.test(), + analytics: fakeAnalytics, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); + processManager.addCommand( + const FakeCommand( + command: [ + 'gradlew', + '-I=/packages/flutter_tools/gradle/aar_init_script.gradle', + '-Pflutter-root=/', + '-Poutput-dir=build/', + '-Pis-plugin=false', + '-PbuildNumber=1.0', + '-q', + '-Pdart-obfuscation=false', + '-Ptrack-widget-creation=false', + '-Ptree-shake-icons=false', + '-Ptarget-platform=android-arm,android-arm64,android-x64', + 'assembleAarRelease', + ], + exitCode: 108, + stderr: 'Gradle task assembleAarRelease failed with exit code 108.', + ), + ); - final File manifestFile = fileSystem.file('pubspec.yaml'); - manifestFile.createSync(recursive: true); - manifestFile.writeAsStringSync(''' + final File manifestFile = fileSystem.file('pubspec.yaml'); + manifestFile.createSync(recursive: true); + manifestFile.writeAsStringSync(''' flutter: module: androidPackage: com.example.test '''); - fileSystem.file('.android/gradlew').createSync(recursive: true); - fileSystem.file('.android/gradle.properties').writeAsStringSync('irrelevant'); - fileSystem.file('.android/build.gradle').createSync(recursive: true); - fileSystem.directory('build/outputs/repo').createSync(recursive: true); + fileSystem.file('.android/gradlew').createSync(recursive: true); + fileSystem.file('.android/gradle.properties').writeAsStringSync('irrelevant'); + fileSystem.file('.android/build.gradle').createSync(recursive: true); + fileSystem.directory('build/outputs/repo').createSync(recursive: true); - await expectLater( - () async => builder.buildGradleAar( - androidBuildInfo: const AndroidBuildInfo( - BuildInfo( - BuildMode.release, - null, - treeShakeIcons: false, - packageConfigPath: '.dart_tool/package_config.json', - ), + await expectLater( + () async => builder.buildGradleAar( + androidBuildInfo: const AndroidBuildInfo( + BuildInfo( + BuildMode.release, + null, + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', ), - project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), - outputDirectory: fileSystem.directory('build/'), - target: '', - buildNumber: '1.0', ), - throwsToolExit( - exitCode: 108, - message: 'Gradle task assembleAarRelease failed with exit code 108.', - ), - ); - expect(processManager, hasNoRemainingExpectations); - }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, - ); + project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + outputDirectory: fileSystem.directory('build/'), + target: '', + buildNumber: '1.0', + ), + throwsToolExit( + exitCode: 108, + message: 'Gradle task assembleAarRelease failed with exit code 108.', + ), + ); + expect(processManager, hasNoRemainingExpectations); + }, overrides: {AndroidStudio: () => FakeAndroidStudio()}); - testUsingContext( - 'build apk uses selected local engine with arm32 ABI', - () async { - final builder = AndroidGradleBuilder( - java: FakeJava(), - logger: logger, - processManager: processManager, - fileSystem: fileSystem, - artifacts: Artifacts.testLocalEngine( - localEngine: 'out/android_arm', - localEngineHost: 'out/host_release', - ), - analytics: fakeAnalytics, - gradleUtils: FakeGradleUtils(), - platform: FakePlatform(), - androidStudio: FakeAndroidStudio(), - ); - processManager.addCommand( - const FakeCommand( - command: [ - 'gradlew', - '-q', - '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', - '-Plocal-engine-build-mode=release', - '-Plocal-engine-out=out/android_arm', - '-Plocal-engine-host-out=out/host_release', - '-Ptarget-platform=android-arm', - '-Ptarget=lib/main.dart', - '-Pbase-application-name=android.app.Application', - '-Pdart-obfuscation=false', - '-Ptrack-widget-creation=false', - '-Ptree-shake-icons=false', - 'assembleRelease', - ], - ), - ); + testUsingContext('build apk uses selected local engine with arm32 ABI', () async { + final builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.testLocalEngine( + localEngine: 'out/android_arm', + localEngineHost: 'out/host_release', + ), + analytics: fakeAnalytics, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); + processManager.addCommand( + const FakeCommand( + command: [ + 'gradlew', + '-q', + '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', + '-Plocal-engine-build-mode=release', + '-Plocal-engine-out=out/android_arm', + '-Plocal-engine-host-out=out/host_release', + '-Ptarget-platform=android-arm', + '-Ptarget=lib/main.dart', + '-Pbase-application-name=android.app.Application', + '-Pdart-obfuscation=false', + '-Ptrack-widget-creation=false', + '-Ptree-shake-icons=false', + 'assembleRelease', + ], + ), + ); - fileSystem.file('out/android_arm/flutter_embedding_release.pom') - ..createSync(recursive: true) - ..writeAsStringSync(''' + fileSystem.file('out/android_arm/flutter_embedding_release.pom') + ..createSync(recursive: true) + ..writeAsStringSync(''' 1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b @@ -1899,96 +2320,86 @@ Gradle Crashed '''); - fileSystem.file('out/android_arm/armeabi_v7a_release.pom').createSync(recursive: true); - fileSystem.file('out/android_arm/armeabi_v7a_release.jar').createSync(recursive: true); - fileSystem - .file('out/android_arm/armeabi_v7a_release.maven-metadata.xml') - .createSync(recursive: true); - fileSystem - .file('out/android_arm/flutter_embedding_release.jar') - .createSync(recursive: true); - fileSystem - .file('out/android_arm/flutter_embedding_release.pom') - .createSync(recursive: true); - fileSystem - .file('out/android_arm/flutter_embedding_release.maven-metadata.xml') - .createSync(recursive: true); + fileSystem.file('out/android_arm/armeabi_v7a_release.pom').createSync(recursive: true); + fileSystem.file('out/android_arm/armeabi_v7a_release.jar').createSync(recursive: true); + fileSystem + .file('out/android_arm/armeabi_v7a_release.maven-metadata.xml') + .createSync(recursive: true); + fileSystem.file('out/android_arm/flutter_embedding_release.jar').createSync(recursive: true); + fileSystem.file('out/android_arm/flutter_embedding_release.pom').createSync(recursive: true); + fileSystem + .file('out/android_arm/flutter_embedding_release.maven-metadata.xml') + .createSync(recursive: true); - fileSystem.file('android/gradlew').createSync(recursive: true); - fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); - fileSystem.file('android/build.gradle').createSync(recursive: true); - fileSystem.directory('android').childDirectory('app').childFile('build.gradle') - ..createSync(recursive: true) - ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); - final FlutterProject project = FlutterProject.fromDirectoryTest( - fileSystem.currentDirectory, - ); - project.android.appManifestFile - ..createSync(recursive: true) - ..writeAsStringSync(minimalV2EmbeddingManifest); + fileSystem.file('android/gradlew').createSync(recursive: true); + fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); + fileSystem.file('android/build.gradle').createSync(recursive: true); + fileSystem.directory('android').childDirectory('app').childFile('build.gradle') + ..createSync(recursive: true) + ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); + final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + project.android.appManifestFile + ..createSync(recursive: true) + ..writeAsStringSync(minimalV2EmbeddingManifest); - await expectLater(() async { - await builder.buildGradleApp( - project: project, - androidBuildInfo: const AndroidBuildInfo( - BuildInfo( - BuildMode.release, - null, - treeShakeIcons: false, - packageConfigPath: '.dart_tool/package_config.json', - ), + await expectLater(() async { + await builder.buildGradleApp( + project: project, + androidBuildInfo: const AndroidBuildInfo( + BuildInfo( + BuildMode.release, + null, + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', ), - target: 'lib/main.dart', - isBuildingBundle: false, - configOnly: false, - localGradleErrors: const [], - ); - }, throwsToolExit()); - expect(processManager, hasNoRemainingExpectations); - }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, - ); - - testUsingContext( - 'build apk uses selected local engine with arm64 ABI', - () async { - final builder = AndroidGradleBuilder( - java: FakeJava(), - logger: logger, - processManager: processManager, - fileSystem: fileSystem, - artifacts: Artifacts.testLocalEngine( - localEngine: 'out/android_arm64', - localEngineHost: 'out/host_release', - ), - analytics: fakeAnalytics, - gradleUtils: FakeGradleUtils(), - platform: FakePlatform(), - androidStudio: FakeAndroidStudio(), - ); - processManager.addCommand( - const FakeCommand( - command: [ - 'gradlew', - '-q', - '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', - '-Plocal-engine-build-mode=release', - '-Plocal-engine-out=out/android_arm64', - '-Plocal-engine-host-out=out/host_release', - '-Ptarget-platform=android-arm64', - '-Ptarget=lib/main.dart', - '-Pbase-application-name=android.app.Application', - '-Pdart-obfuscation=false', - '-Ptrack-widget-creation=false', - '-Ptree-shake-icons=false', - 'assembleRelease', - ], ), + target: 'lib/main.dart', + isBuildingBundle: false, + configOnly: false, + localGradleErrors: const [], ); + }, throwsToolExit()); + expect(processManager, hasNoRemainingExpectations); + }, overrides: {AndroidStudio: () => FakeAndroidStudio()}); - fileSystem.file('out/android_arm64/flutter_embedding_release.pom') - ..createSync(recursive: true) - ..writeAsStringSync(''' + testUsingContext('build apk uses selected local engine with arm64 ABI', () async { + final builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.testLocalEngine( + localEngine: 'out/android_arm64', + localEngineHost: 'out/host_release', + ), + analytics: fakeAnalytics, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); + processManager.addCommand( + const FakeCommand( + command: [ + 'gradlew', + '-q', + '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', + '-Plocal-engine-build-mode=release', + '-Plocal-engine-out=out/android_arm64', + '-Plocal-engine-host-out=out/host_release', + '-Ptarget-platform=android-arm64', + '-Ptarget=lib/main.dart', + '-Pbase-application-name=android.app.Application', + '-Pdart-obfuscation=false', + '-Ptrack-widget-creation=false', + '-Ptree-shake-icons=false', + 'assembleRelease', + ], + ), + ); + + fileSystem.file('out/android_arm64/flutter_embedding_release.pom') + ..createSync(recursive: true) + ..writeAsStringSync(''' 1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b @@ -1996,331 +2407,303 @@ Gradle Crashed '''); - fileSystem.file('out/android_arm64/arm64_v8a_release.pom').createSync(recursive: true); - fileSystem.file('out/android_arm64/arm64_v8a_release.jar').createSync(recursive: true); - fileSystem - .file('out/android_arm64/arm64_v8a_release.maven-metadata.xml') - .createSync(recursive: true); - fileSystem - .file('out/android_arm64/flutter_embedding_release.jar') - .createSync(recursive: true); - fileSystem - .file('out/android_arm64/flutter_embedding_release.pom') - .createSync(recursive: true); - fileSystem - .file('out/android_arm64/flutter_embedding_release.maven-metadata.xml') - .createSync(recursive: true); + fileSystem.file('out/android_arm64/arm64_v8a_release.pom').createSync(recursive: true); + fileSystem.file('out/android_arm64/arm64_v8a_release.jar').createSync(recursive: true); + fileSystem + .file('out/android_arm64/arm64_v8a_release.maven-metadata.xml') + .createSync(recursive: true); + fileSystem + .file('out/android_arm64/flutter_embedding_release.jar') + .createSync(recursive: true); + fileSystem + .file('out/android_arm64/flutter_embedding_release.pom') + .createSync(recursive: true); + fileSystem + .file('out/android_arm64/flutter_embedding_release.maven-metadata.xml') + .createSync(recursive: true); - fileSystem.file('android/gradlew').createSync(recursive: true); - fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); - fileSystem.file('android/build.gradle').createSync(recursive: true); - fileSystem.directory('android').childDirectory('app').childFile('build.gradle') - ..createSync(recursive: true) - ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); - final FlutterProject project = FlutterProject.fromDirectoryTest( - fileSystem.currentDirectory, - ); - project.android.appManifestFile - ..createSync(recursive: true) - ..writeAsStringSync(minimalV2EmbeddingManifest); + fileSystem.file('android/gradlew').createSync(recursive: true); + fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); + fileSystem.file('android/build.gradle').createSync(recursive: true); + fileSystem.directory('android').childDirectory('app').childFile('build.gradle') + ..createSync(recursive: true) + ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); + final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + project.android.appManifestFile + ..createSync(recursive: true) + ..writeAsStringSync(minimalV2EmbeddingManifest); - await expectLater(() async { - await builder.buildGradleApp( - project: project, - androidBuildInfo: const AndroidBuildInfo( - BuildInfo( - BuildMode.release, - null, - treeShakeIcons: false, - packageConfigPath: '.dart_tool/package_config.json', - ), + await expectLater(() async { + await builder.buildGradleApp( + project: project, + androidBuildInfo: const AndroidBuildInfo( + BuildInfo( + BuildMode.release, + null, + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', ), - target: 'lib/main.dart', - isBuildingBundle: false, - configOnly: false, - localGradleErrors: const [], - ); - }, throwsToolExit()); - expect(processManager, hasNoRemainingExpectations); - }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, - ); - - testUsingContext( - 'build apk uses selected local engine with x64 ABI', - () async { - final builder = AndroidGradleBuilder( - java: FakeJava(), - logger: logger, - processManager: processManager, - fileSystem: fileSystem, - artifacts: Artifacts.testLocalEngine( - localEngine: 'out/android_x64', - localEngineHost: 'out/host_release', - ), - analytics: fakeAnalytics, - gradleUtils: FakeGradleUtils(), - platform: FakePlatform(), - androidStudio: FakeAndroidStudio(), - ); - processManager.addCommand( - const FakeCommand( - command: [ - 'gradlew', - '-q', - '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', - '-Plocal-engine-build-mode=release', - '-Plocal-engine-out=out/android_x64', - '-Plocal-engine-host-out=out/host_release', - '-Ptarget-platform=android-x64', - '-Ptarget=lib/main.dart', - '-Pbase-application-name=android.app.Application', - '-Pdart-obfuscation=false', - '-Ptrack-widget-creation=false', - '-Ptree-shake-icons=false', - 'assembleRelease', - ], - exitCode: 1, ), + target: 'lib/main.dart', + isBuildingBundle: false, + configOnly: false, + localGradleErrors: const [], ); + }, throwsToolExit()); + expect(processManager, hasNoRemainingExpectations); + }, overrides: {AndroidStudio: () => FakeAndroidStudio()}); - fileSystem.file('out/android_x64/flutter_embedding_release.pom') - ..createSync(recursive: true) - ..writeAsStringSync(''' - - - 1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b - - - -'''); - fileSystem.file('out/android_x64/x86_64_release.pom').createSync(recursive: true); - fileSystem.file('out/android_x64/x86_64_release.jar').createSync(recursive: true); - fileSystem - .file('out/android_x64/x86_64_release.maven-metadata.xml') - .createSync(recursive: true); - fileSystem - .file('out/android_x64/flutter_embedding_release.jar') - .createSync(recursive: true); - fileSystem - .file('out/android_x64/flutter_embedding_release.pom') - .createSync(recursive: true); - fileSystem - .file('out/android_x64/flutter_embedding_release.maven-metadata.xml') - .createSync(recursive: true); + testUsingContext('build apk uses selected local engine with x64 ABI', () async { + final builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.testLocalEngine( + localEngine: 'out/android_x64', + localEngineHost: 'out/host_release', + ), + analytics: fakeAnalytics, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); + processManager.addCommand( + const FakeCommand( + command: [ + 'gradlew', + '-q', + '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', + '-Plocal-engine-build-mode=release', + '-Plocal-engine-out=out/android_x64', + '-Plocal-engine-host-out=out/host_release', + '-Ptarget-platform=android-x64', + '-Ptarget=lib/main.dart', + '-Pbase-application-name=android.app.Application', + '-Pdart-obfuscation=false', + '-Ptrack-widget-creation=false', + '-Ptree-shake-icons=false', + 'assembleRelease', + ], + exitCode: 1, + ), + ); - fileSystem.file('android/gradlew').createSync(recursive: true); - fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); - fileSystem.file('android/build.gradle').createSync(recursive: true); - fileSystem.directory('android').childDirectory('app').childFile('build.gradle') - ..createSync(recursive: true) - ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); - final FlutterProject project = FlutterProject.fromDirectoryTest( - fileSystem.currentDirectory, - ); - project.android.appManifestFile - ..createSync(recursive: true) - ..writeAsStringSync(minimalV2EmbeddingManifest); + fileSystem.file('out/android_x64/flutter_embedding_release.pom') + ..createSync(recursive: true) + ..writeAsStringSync(''' + + + 1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b + + + +'''); + fileSystem.file('out/android_x64/x86_64_release.pom').createSync(recursive: true); + fileSystem.file('out/android_x64/x86_64_release.jar').createSync(recursive: true); + fileSystem + .file('out/android_x64/x86_64_release.maven-metadata.xml') + .createSync(recursive: true); + fileSystem.file('out/android_x64/flutter_embedding_release.jar').createSync(recursive: true); + fileSystem.file('out/android_x64/flutter_embedding_release.pom').createSync(recursive: true); + fileSystem + .file('out/android_x64/flutter_embedding_release.maven-metadata.xml') + .createSync(recursive: true); - await expectLater(() async { - await builder.buildGradleApp( - project: project, - androidBuildInfo: const AndroidBuildInfo( - BuildInfo( - BuildMode.release, - null, - treeShakeIcons: false, - packageConfigPath: '.dart_tool/package_config.json', - ), - ), - target: 'lib/main.dart', - isBuildingBundle: false, - configOnly: false, - localGradleErrors: const [], - ); - }, throwsToolExit()); - expect(processManager, hasNoRemainingExpectations); - }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, - ); + fileSystem.file('android/gradlew').createSync(recursive: true); + fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); + fileSystem.file('android/build.gradle').createSync(recursive: true); + fileSystem.directory('android').childDirectory('app').childFile('build.gradle') + ..createSync(recursive: true) + ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); + final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + project.android.appManifestFile + ..createSync(recursive: true) + ..writeAsStringSync(minimalV2EmbeddingManifest); - testUsingContext( - 'honors --no-android-gradle-daemon setting', - () async { - final builder = AndroidGradleBuilder( - java: FakeJava(), - logger: logger, - processManager: processManager, - fileSystem: fileSystem, - artifacts: Artifacts.test(), - analytics: fakeAnalytics, - gradleUtils: FakeGradleUtils(), - platform: FakePlatform(), - androidStudio: FakeAndroidStudio(), - ); - processManager.addCommand( - const FakeCommand( - command: [ - 'gradlew', - '-q', - '--no-daemon', - '-Ptarget-platform=android-arm,android-arm64,android-x64', - '-Ptarget=lib/main.dart', - '-Pbase-application-name=android.app.Application', - '-Pdart-obfuscation=false', - '-Ptrack-widget-creation=false', - '-Ptree-shake-icons=false', - 'assembleRelease', - ], + await expectLater(() async { + await builder.buildGradleApp( + project: project, + androidBuildInfo: const AndroidBuildInfo( + BuildInfo( + BuildMode.release, + null, + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', + ), ), + target: 'lib/main.dart', + isBuildingBundle: false, + configOnly: false, + localGradleErrors: const [], ); - fileSystem.file('android/gradlew').createSync(recursive: true); + }, throwsToolExit()); + expect(processManager, hasNoRemainingExpectations); + }, overrides: {AndroidStudio: () => FakeAndroidStudio()}); - fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); - fileSystem.file('android/build.gradle').createSync(recursive: true); - fileSystem.directory('android').childDirectory('app').childFile('build.gradle') - ..createSync(recursive: true) - ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); - final FlutterProject project = FlutterProject.fromDirectoryTest( - fileSystem.currentDirectory, - ); - project.android.appManifestFile - ..createSync(recursive: true) - ..writeAsStringSync(minimalV2EmbeddingManifest); + testUsingContext('honors --no-android-gradle-daemon setting', () async { + final builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.test(), + analytics: fakeAnalytics, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); + processManager.addCommand( + const FakeCommand( + command: [ + 'gradlew', + '-q', + '--no-daemon', + '-Ptarget-platform=android-arm,android-arm64,android-x64', + '-Ptarget=lib/main.dart', + '-Pbase-application-name=android.app.Application', + '-Pdart-obfuscation=false', + '-Ptrack-widget-creation=false', + '-Ptree-shake-icons=false', + 'assembleRelease', + ], + ), + ); + fileSystem.file('android/gradlew').createSync(recursive: true); - await expectLater(() async { - await builder.buildGradleApp( - project: project, - androidBuildInfo: const AndroidBuildInfo( - BuildInfo( - BuildMode.release, - null, - treeShakeIcons: false, - androidGradleDaemon: false, - packageConfigPath: '.dart_tool/package_config.json', - ), - ), - target: 'lib/main.dart', - isBuildingBundle: false, - configOnly: false, - localGradleErrors: const [], - ); - }, throwsToolExit()); - expect(processManager, hasNoRemainingExpectations); - }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, - ); + fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); + fileSystem.file('android/build.gradle').createSync(recursive: true); + fileSystem.directory('android').childDirectory('app').childFile('build.gradle') + ..createSync(recursive: true) + ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); + final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + project.android.appManifestFile + ..createSync(recursive: true) + ..writeAsStringSync(minimalV2EmbeddingManifest); - testUsingContext( - 'honors --android-project-cache-dir setting', - () async { - final builder = AndroidGradleBuilder( - java: FakeJava(), - logger: logger, - processManager: processManager, - fileSystem: fileSystem, - artifacts: Artifacts.test(), - analytics: fakeAnalytics, - gradleUtils: FakeGradleUtils(), - platform: FakePlatform(), - androidStudio: FakeAndroidStudio(), - ); - processManager.addCommand( - const FakeCommand( - command: [ - 'gradlew', - '-q', - '-Ptarget-platform=android-arm,android-arm64,android-x64', - '-Ptarget=lib/main.dart', - '-Pbase-application-name=android.app.Application', - '-Pdart-obfuscation=false', - '-Ptrack-widget-creation=false', - '-Ptree-shake-icons=false', - '--project-cache-dir=/made/up/dir', - 'assembleRelease', - ], + await expectLater(() async { + await builder.buildGradleApp( + project: project, + androidBuildInfo: const AndroidBuildInfo( + BuildInfo( + BuildMode.release, + null, + treeShakeIcons: false, + androidGradleDaemon: false, + packageConfigPath: '.dart_tool/package_config.json', + ), ), + target: 'lib/main.dart', + isBuildingBundle: false, + configOnly: false, + localGradleErrors: const [], ); - fileSystem.file('android/gradlew').createSync(recursive: true); + }, throwsToolExit()); + expect(processManager, hasNoRemainingExpectations); + }, overrides: {AndroidStudio: () => FakeAndroidStudio()}); - fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); - fileSystem.file('android/build.gradle').createSync(recursive: true); - fileSystem.directory('android').childDirectory('app').childFile('build.gradle') - ..createSync(recursive: true) - ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); - final FlutterProject project = FlutterProject.fromDirectoryTest( - fileSystem.currentDirectory, - ); - project.android.appManifestFile - ..createSync(recursive: true) - ..writeAsStringSync(minimalV2EmbeddingManifest); + testUsingContext('honors --android-project-cache-dir setting', () async { + final builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.test(), + analytics: fakeAnalytics, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); + processManager.addCommand( + const FakeCommand( + command: [ + 'gradlew', + '-q', + '-Ptarget-platform=android-arm,android-arm64,android-x64', + '-Ptarget=lib/main.dart', + '-Pbase-application-name=android.app.Application', + '-Pdart-obfuscation=false', + '-Ptrack-widget-creation=false', + '-Ptree-shake-icons=false', + '--project-cache-dir=/made/up/dir', + 'assembleRelease', + ], + ), + ); + fileSystem.file('android/gradlew').createSync(recursive: true); - await expectLater(() async { - await builder.buildGradleApp( - project: project, - androidBuildInfo: const AndroidBuildInfo( - BuildInfo( - BuildMode.release, - null, - treeShakeIcons: false, - androidGradleProjectCacheDir: '/made/up/dir', - packageConfigPath: '.dart_tool/package_config.json', - ), - ), - target: 'lib/main.dart', - isBuildingBundle: false, - configOnly: false, - localGradleErrors: const [], - ); - }, throwsToolExit()); - expect(processManager, hasNoRemainingExpectations); - }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, - ); + fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); + fileSystem.file('android/build.gradle').createSync(recursive: true); + fileSystem.directory('android').childDirectory('app').childFile('build.gradle') + ..createSync(recursive: true) + ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); + final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + project.android.appManifestFile + ..createSync(recursive: true) + ..writeAsStringSync(minimalV2EmbeddingManifest); - testUsingContext( - 'build aar uses selected local engine with arm32 ABI', - () async { - final builder = AndroidGradleBuilder( - java: FakeJava(), - logger: logger, - processManager: processManager, - fileSystem: fileSystem, - artifacts: Artifacts.testLocalEngine( - localEngine: 'out/android_arm', - localEngineHost: 'out/host_release', - ), - analytics: fakeAnalytics, - gradleUtils: FakeGradleUtils(), - platform: FakePlatform(), - androidStudio: FakeAndroidStudio(), - ); - processManager.addCommand( - const FakeCommand( - command: [ - 'gradlew', - '-I=/packages/flutter_tools/gradle/aar_init_script.gradle', - '-Pflutter-root=/', - '-Poutput-dir=build/', - '-Pis-plugin=false', - '-PbuildNumber=2.0', - '-q', - '-Pdart-obfuscation=false', - '-Ptrack-widget-creation=false', - '-Ptree-shake-icons=false', - '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', - '-Plocal-engine-build-mode=release', - '-Plocal-engine-out=out/android_arm', - '-Plocal-engine-host-out=out/host_release', - '-Ptarget-platform=android-arm', - 'assembleAarRelease', - ], + await expectLater(() async { + await builder.buildGradleApp( + project: project, + androidBuildInfo: const AndroidBuildInfo( + BuildInfo( + BuildMode.release, + null, + treeShakeIcons: false, + androidGradleProjectCacheDir: '/made/up/dir', + packageConfigPath: '.dart_tool/package_config.json', + ), ), + target: 'lib/main.dart', + isBuildingBundle: false, + configOnly: false, + localGradleErrors: const [], ); + }, throwsToolExit()); + expect(processManager, hasNoRemainingExpectations); + }, overrides: {AndroidStudio: () => FakeAndroidStudio()}); - fileSystem.file('out/android_arm/flutter_embedding_release.pom') - ..createSync(recursive: true) - ..writeAsStringSync(''' + testUsingContext('build aar uses selected local engine with arm32 ABI', () async { + final builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.testLocalEngine( + localEngine: 'out/android_arm', + localEngineHost: 'out/host_release', + ), + analytics: fakeAnalytics, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); + processManager.addCommand( + const FakeCommand( + command: [ + 'gradlew', + '-I=/packages/flutter_tools/gradle/aar_init_script.gradle', + '-Pflutter-root=/', + '-Poutput-dir=build/', + '-Pis-plugin=false', + '-PbuildNumber=2.0', + '-q', + '-Pdart-obfuscation=false', + '-Ptrack-widget-creation=false', + '-Ptree-shake-icons=false', + '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', + '-Plocal-engine-build-mode=release', + '-Plocal-engine-out=out/android_arm', + '-Plocal-engine-host-out=out/host_release', + '-Ptarget-platform=android-arm', + 'assembleAarRelease', + ], + ), + ); + + fileSystem.file('out/android_arm/flutter_embedding_release.pom') + ..createSync(recursive: true) + ..writeAsStringSync(''' 1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b @@ -2328,108 +2711,100 @@ Gradle Crashed '''); - fileSystem.file('out/android_arm/armeabi_v7a_release.pom').createSync(recursive: true); - fileSystem.file('out/android_arm/armeabi_v7a_release.jar').createSync(recursive: true); - fileSystem - .file('out/android_arm/armeabi_v7a_release.maven-metadata.xml') - .createSync(recursive: true); - fileSystem - .file('out/android_arm/flutter_embedding_release.jar') - .createSync(recursive: true); - fileSystem - .file('out/android_arm/flutter_embedding_release.pom') - .createSync(recursive: true); - fileSystem - .file('out/android_arm/flutter_embedding_release.maven-metadata.xml') - .createSync(recursive: true); + fileSystem.file('out/android_arm/armeabi_v7a_release.pom').createSync(recursive: true); + fileSystem.file('out/android_arm/armeabi_v7a_release.jar').createSync(recursive: true); + fileSystem + .file('out/android_arm/armeabi_v7a_release.maven-metadata.xml') + .createSync(recursive: true); + fileSystem.file('out/android_arm/flutter_embedding_release.jar').createSync(recursive: true); + fileSystem.file('out/android_arm/flutter_embedding_release.pom').createSync(recursive: true); + fileSystem + .file('out/android_arm/flutter_embedding_release.maven-metadata.xml') + .createSync(recursive: true); - final File manifestFile = fileSystem.file('pubspec.yaml'); - manifestFile.createSync(recursive: true); - manifestFile.writeAsStringSync(''' + final File manifestFile = fileSystem.file('pubspec.yaml'); + manifestFile.createSync(recursive: true); + manifestFile.writeAsStringSync(''' flutter: module: androidPackage: com.example.test '''); - fileSystem.directory('.android/gradle').createSync(recursive: true); - fileSystem.directory('.android/gradle/wrapper').createSync(recursive: true); - fileSystem.file('.android/gradlew').createSync(recursive: true); - fileSystem.file('.android/gradle.properties').writeAsStringSync('irrelevant'); - fileSystem.file('.android/build.gradle').createSync(recursive: true); + fileSystem.directory('.android/gradle').createSync(recursive: true); + fileSystem.directory('.android/gradle/wrapper').createSync(recursive: true); + fileSystem.file('.android/gradlew').createSync(recursive: true); + fileSystem.file('.android/gradle.properties').writeAsStringSync('irrelevant'); + fileSystem.file('.android/build.gradle').createSync(recursive: true); - fileSystem.directory('build/outputs/repo').createSync(recursive: true); + fileSystem.directory('build/outputs/repo').createSync(recursive: true); - await builder.buildGradleAar( - androidBuildInfo: const AndroidBuildInfo( - BuildInfo( - BuildMode.release, - null, - treeShakeIcons: false, - packageConfigPath: '.dart_tool/package_config.json', - ), + await builder.buildGradleAar( + androidBuildInfo: const AndroidBuildInfo( + BuildInfo( + BuildMode.release, + null, + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', ), - project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), - outputDirectory: fileSystem.directory('build/'), - target: '', - buildNumber: '2.0', - ); + ), + project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + outputDirectory: fileSystem.directory('build/'), + target: '', + buildNumber: '2.0', + ); - expect( - fileSystem.link( - 'build/outputs/repo/io/flutter/flutter_embedding_release/' - '1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b/' - 'flutter_embedding_release-1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b.pom', - ), - exists, - ); - expect(processManager, hasNoRemainingExpectations); - }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, - ); + expect( + fileSystem.link( + 'build/outputs/repo/io/flutter/flutter_embedding_release/' + '1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b/' + 'flutter_embedding_release-1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b.pom', + ), + exists, + ); + expect(processManager, hasNoRemainingExpectations); + }, overrides: {AndroidStudio: () => FakeAndroidStudio()}); - testUsingContext( - 'build aar uses selected local engine with x64 ABI', - () async { - final builder = AndroidGradleBuilder( - java: FakeJava(), - logger: logger, - processManager: processManager, - fileSystem: fileSystem, - artifacts: Artifacts.testLocalEngine( - localEngine: 'out/android_arm64', - localEngineHost: 'out/host_release', - ), - analytics: fakeAnalytics, - gradleUtils: FakeGradleUtils(), - platform: FakePlatform(), - androidStudio: FakeAndroidStudio(), - ); - processManager.addCommand( - const FakeCommand( - command: [ - 'gradlew', - '-I=/packages/flutter_tools/gradle/aar_init_script.gradle', - '-Pflutter-root=/', - '-Poutput-dir=build/', - '-Pis-plugin=false', - '-PbuildNumber=2.0', - '-q', - '-Pdart-obfuscation=false', - '-Ptrack-widget-creation=false', - '-Ptree-shake-icons=false', - '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', - '-Plocal-engine-build-mode=release', - '-Plocal-engine-out=out/android_arm64', - '-Plocal-engine-host-out=out/host_release', - '-Ptarget-platform=android-arm64', - 'assembleAarRelease', - ], - ), - ); + testUsingContext('build aar uses selected local engine with x64 ABI', () async { + final builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.testLocalEngine( + localEngine: 'out/android_arm64', + localEngineHost: 'out/host_release', + ), + analytics: fakeAnalytics, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); + processManager.addCommand( + const FakeCommand( + command: [ + 'gradlew', + '-I=/packages/flutter_tools/gradle/aar_init_script.gradle', + '-Pflutter-root=/', + '-Poutput-dir=build/', + '-Pis-plugin=false', + '-PbuildNumber=2.0', + '-q', + '-Pdart-obfuscation=false', + '-Ptrack-widget-creation=false', + '-Ptree-shake-icons=false', + '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', + '-Plocal-engine-build-mode=release', + '-Plocal-engine-out=out/android_arm64', + '-Plocal-engine-host-out=out/host_release', + '-Ptarget-platform=android-arm64', + 'assembleAarRelease', + ], + ), + ); - fileSystem.file('out/android_arm64/flutter_embedding_release.pom') - ..createSync(recursive: true) - ..writeAsStringSync(''' + fileSystem.file('out/android_arm64/flutter_embedding_release.pom') + ..createSync(recursive: true) + ..writeAsStringSync(''' 1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b @@ -2437,107 +2812,103 @@ Gradle Crashed '''); - fileSystem.file('out/android_arm64/arm64_v8a_release.pom').createSync(recursive: true); - fileSystem.file('out/android_arm64/arm64_v8a_release.jar').createSync(recursive: true); - fileSystem - .file('out/android_arm64/arm64_v8a_release.maven-metadata.xml') - .createSync(recursive: true); - fileSystem - .file('out/android_arm64/flutter_embedding_release.jar') - .createSync(recursive: true); - fileSystem - .file('out/android_arm64/flutter_embedding_release.pom') - .createSync(recursive: true); - fileSystem - .file('out/android_arm64/flutter_embedding_release.maven-metadata.xml') - .createSync(recursive: true); + fileSystem.file('out/android_arm64/arm64_v8a_release.pom').createSync(recursive: true); + fileSystem.file('out/android_arm64/arm64_v8a_release.jar').createSync(recursive: true); + fileSystem + .file('out/android_arm64/arm64_v8a_release.maven-metadata.xml') + .createSync(recursive: true); + fileSystem + .file('out/android_arm64/flutter_embedding_release.jar') + .createSync(recursive: true); + fileSystem + .file('out/android_arm64/flutter_embedding_release.pom') + .createSync(recursive: true); + fileSystem + .file('out/android_arm64/flutter_embedding_release.maven-metadata.xml') + .createSync(recursive: true); - final File manifestFile = fileSystem.file('pubspec.yaml'); - manifestFile.createSync(recursive: true); - manifestFile.writeAsStringSync(''' + final File manifestFile = fileSystem.file('pubspec.yaml'); + manifestFile.createSync(recursive: true); + manifestFile.writeAsStringSync(''' flutter: module: androidPackage: com.example.test '''); - fileSystem.directory('.android/gradle').createSync(recursive: true); - fileSystem.directory('.android/gradle/wrapper').createSync(recursive: true); - fileSystem.file('.android/gradlew').createSync(recursive: true); - fileSystem.file('.android/gradle.properties').writeAsStringSync('irrelevant'); - fileSystem.file('.android/build.gradle').createSync(recursive: true); - fileSystem.directory('build/outputs/repo').createSync(recursive: true); - - await builder.buildGradleAar( - androidBuildInfo: const AndroidBuildInfo( - BuildInfo( - BuildMode.release, - null, - treeShakeIcons: false, - packageConfigPath: '.dart_tool/package_config.json', - ), + fileSystem.directory('.android/gradle').createSync(recursive: true); + fileSystem.directory('.android/gradle/wrapper').createSync(recursive: true); + fileSystem.file('.android/gradlew').createSync(recursive: true); + fileSystem.file('.android/gradle.properties').writeAsStringSync('irrelevant'); + fileSystem.file('.android/build.gradle').createSync(recursive: true); + fileSystem.directory('build/outputs/repo').createSync(recursive: true); + + await builder.buildGradleAar( + androidBuildInfo: const AndroidBuildInfo( + BuildInfo( + BuildMode.release, + null, + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', ), - project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), - outputDirectory: fileSystem.directory('build/'), - target: '', - buildNumber: '2.0', - ); + ), + project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + outputDirectory: fileSystem.directory('build/'), + target: '', + buildNumber: '2.0', + ); - expect( - fileSystem.link( - 'build/outputs/repo/io/flutter/flutter_embedding_release/' - '1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b/' - 'flutter_embedding_release-1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b.pom', - ), - exists, - ); - expect(processManager, hasNoRemainingExpectations); - }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, - ); + expect( + fileSystem.link( + 'build/outputs/repo/io/flutter/flutter_embedding_release/' + '1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b/' + 'flutter_embedding_release-1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b.pom', + ), + exists, + ); + expect(processManager, hasNoRemainingExpectations); + }, overrides: {AndroidStudio: () => FakeAndroidStudio()}); - testUsingContext( - 'build aar uses selected local engine on x64 ABI', - () async { - final builder = AndroidGradleBuilder( - java: FakeJava(), - logger: logger, - processManager: processManager, - fileSystem: fileSystem, - artifacts: Artifacts.testLocalEngine( - localEngine: 'out/android_x64', - localEngineHost: 'out/host_release', - ), - analytics: fakeAnalytics, - gradleUtils: FakeGradleUtils(), - platform: FakePlatform(), - androidStudio: FakeAndroidStudio(), - ); - processManager.addCommand( - const FakeCommand( - command: [ - 'gradlew', - '-I=/packages/flutter_tools/gradle/aar_init_script.gradle', - '-Pflutter-root=/', - '-Poutput-dir=build/', - '-Pis-plugin=false', - '-PbuildNumber=2.0', - '-q', - '-Pdart-obfuscation=false', - '-Ptrack-widget-creation=false', - '-Ptree-shake-icons=false', - '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', - '-Plocal-engine-build-mode=release', - '-Plocal-engine-out=out/android_x64', - '-Plocal-engine-host-out=out/host_release', - '-Ptarget-platform=android-x64', - 'assembleAarRelease', - ], - ), - ); + testUsingContext('build aar uses selected local engine on x64 ABI', () async { + final builder = AndroidGradleBuilder( + java: FakeJava(), + logger: logger, + processManager: processManager, + fileSystem: fileSystem, + artifacts: Artifacts.testLocalEngine( + localEngine: 'out/android_x64', + localEngineHost: 'out/host_release', + ), + analytics: fakeAnalytics, + gradleUtils: FakeGradleUtils(), + platform: FakePlatform(), + androidStudio: FakeAndroidStudio(), + ); + processManager.addCommand( + const FakeCommand( + command: [ + 'gradlew', + '-I=/packages/flutter_tools/gradle/aar_init_script.gradle', + '-Pflutter-root=/', + '-Poutput-dir=build/', + '-Pis-plugin=false', + '-PbuildNumber=2.0', + '-q', + '-Pdart-obfuscation=false', + '-Ptrack-widget-creation=false', + '-Ptree-shake-icons=false', + '-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0', + '-Plocal-engine-build-mode=release', + '-Plocal-engine-out=out/android_x64', + '-Plocal-engine-host-out=out/host_release', + '-Ptarget-platform=android-x64', + 'assembleAarRelease', + ], + ), + ); - fileSystem.file('out/android_x64/flutter_embedding_release.pom') - ..createSync(recursive: true) - ..writeAsStringSync(''' + fileSystem.file('out/android_x64/flutter_embedding_release.pom') + ..createSync(recursive: true) + ..writeAsStringSync(''' 1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b @@ -2545,63 +2916,57 @@ Gradle Crashed '''); - fileSystem.file('out/android_x64/x86_64_release.pom').createSync(recursive: true); - fileSystem.file('out/android_x64/x86_64_release.jar').createSync(recursive: true); - fileSystem - .file('out/android_x64/x86_64_release.maven-metadata.xml') - .createSync(recursive: true); - fileSystem - .file('out/android_x64/flutter_embedding_release.jar') - .createSync(recursive: true); - fileSystem - .file('out/android_x64/flutter_embedding_release.pom') - .createSync(recursive: true); - fileSystem - .file('out/android_x64/flutter_embedding_release.maven-metadata.xml') - .createSync(recursive: true); + fileSystem.file('out/android_x64/x86_64_release.pom').createSync(recursive: true); + fileSystem.file('out/android_x64/x86_64_release.jar').createSync(recursive: true); + fileSystem + .file('out/android_x64/x86_64_release.maven-metadata.xml') + .createSync(recursive: true); + fileSystem.file('out/android_x64/flutter_embedding_release.jar').createSync(recursive: true); + fileSystem.file('out/android_x64/flutter_embedding_release.pom').createSync(recursive: true); + fileSystem + .file('out/android_x64/flutter_embedding_release.maven-metadata.xml') + .createSync(recursive: true); - final File manifestFile = fileSystem.file('pubspec.yaml'); - manifestFile.createSync(recursive: true); - manifestFile.writeAsStringSync(''' + final File manifestFile = fileSystem.file('pubspec.yaml'); + manifestFile.createSync(recursive: true); + manifestFile.writeAsStringSync(''' flutter: module: androidPackage: com.example.test '''); - fileSystem.directory('.android/gradle').createSync(recursive: true); - fileSystem.directory('.android/gradle/wrapper').createSync(recursive: true); - fileSystem.file('.android/gradlew').createSync(recursive: true); - fileSystem.file('.android/gradle.properties').writeAsStringSync('irrelevant'); - fileSystem.file('.android/build.gradle').createSync(recursive: true); - fileSystem.directory('build/outputs/repo').createSync(recursive: true); - - await builder.buildGradleAar( - androidBuildInfo: const AndroidBuildInfo( - BuildInfo( - BuildMode.release, - null, - treeShakeIcons: false, - packageConfigPath: '.dart_tool/package_config.json', - ), + fileSystem.directory('.android/gradle').createSync(recursive: true); + fileSystem.directory('.android/gradle/wrapper').createSync(recursive: true); + fileSystem.file('.android/gradlew').createSync(recursive: true); + fileSystem.file('.android/gradle.properties').writeAsStringSync('irrelevant'); + fileSystem.file('.android/build.gradle').createSync(recursive: true); + fileSystem.directory('build/outputs/repo').createSync(recursive: true); + + await builder.buildGradleAar( + androidBuildInfo: const AndroidBuildInfo( + BuildInfo( + BuildMode.release, + null, + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', ), - project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), - outputDirectory: fileSystem.directory('build/'), - target: '', - buildNumber: '2.0', - ); + ), + project: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + outputDirectory: fileSystem.directory('build/'), + target: '', + buildNumber: '2.0', + ); - expect( - fileSystem.link( - 'build/outputs/repo/io/flutter/flutter_embedding_release/' - '1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b/' - 'flutter_embedding_release-1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b.pom', - ), - exists, - ); - expect(processManager, hasNoRemainingExpectations); - }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, - ); + expect( + fileSystem.link( + 'build/outputs/repo/io/flutter/flutter_embedding_release/' + '1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b/' + 'flutter_embedding_release-1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b.pom', + ), + exists, + ); + expect(processManager, hasNoRemainingExpectations); + }, overrides: {AndroidStudio: () => FakeAndroidStudio()}); }); } diff --git a/packages/flutter_tools/test/integration.shard/android_gradle_print_ndk_version_test.dart b/packages/flutter_tools/test/integration.shard/android_gradle_print_ndk_version_test.dart new file mode 100644 index 0000000000000..c9bfbce4d9389 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/android_gradle_print_ndk_version_test.dart @@ -0,0 +1,65 @@ +// 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:convert'; + +import 'package:file/file.dart'; +import 'package:flutter_tools/src/android/gradle_utils.dart' show getGradlewFileName; +import 'package:flutter_tools/src/base/io.dart'; + +import '../src/common.dart'; +import 'test_utils.dart'; + +void main() { + late Directory tempDir; + + setUp(() async { + tempDir = createResolvedTempDirectorySync('run_test.'); + }); + + tearDown(() async { + tryToDelete(tempDir); + }); + + testWithoutContext( + 'gradle task exists named printNdkVersion that prints the effective ndk version', + () async { + ProcessResult result = await processManager.run([ + flutterBin, + 'create', + tempDir.path, + '--project-name=testapp', + ], workingDirectory: tempDir.path); + expect(result.exitCode, 0, reason: 'stdout: ${result.stdout}\nstderr: ${result.stderr}'); + + result = await processManager.run([ + flutterBin, + 'build', + 'apk', + '--config-only', + ], workingDirectory: tempDir.path); + expect(result.exitCode, 0, reason: 'stdout: ${result.stdout}\nstderr: ${result.stderr}'); + + final Directory androidApp = tempDir.childDirectory('android'); + result = await processManager.run([ + '.${platform.pathSeparator}${getGradlewFileName(platform)}', + ...getLocalEngineArguments(), + '-q', + 'printNdkVersion', + ], workingDirectory: androidApp.path); + expect(result.exitCode, 0); + + final List actualLines = LineSplitter.split(result.stdout.toString()).toList(); + final Iterable ndkVersionLines = actualLines.where( + (String line) => line.startsWith('NdkVersion: '), + ); + expect(ndkVersionLines.length, 1, reason: 'actual: $actualLines'); + expect( + ndkVersionLines.single.length, + greaterThan('NdkVersion: '.length), + reason: 'actual: $actualLines', + ); + }, + ); +}