From 176a12058faa2dc3a6347446eb362bd86093694f Mon Sep 17 00:00:00 2001 From: kyungil Date: Fri, 24 Apr 2026 18:47:52 +0900 Subject: [PATCH 1/6] [flutter_tools] Preprovision Android NDK only for flavored builds --- .../bin/tasks/gradle_plugin_fat_apk_test.dart | 7 +- .../gradle/src/main/kotlin/FlutterPlugin.kt | 1 + .../src/main/kotlin/FlutterPluginUtils.kt | 44 ++ .../src/test/kotlin/FlutterPluginTest.kt | 1 + .../src/test/kotlin/FlutterPluginUtilsTest.kt | 69 +++ .../lib/src/android/android_sdk.dart | 144 ++++-- .../flutter_tools/lib/src/android/gradle.dart | 147 +++++- .../lib/src/android/gradle_errors.dart | 3 + .../android/android_gradle_builder_test.dart | 462 +++++++++++++++++- ...android_gradle_print_ndk_version_test.dart | 65 +++ 10 files changed, 865 insertions(+), 78 deletions(-) create mode 100644 packages/flutter_tools/test/integration.shard/android_gradle_print_ndk_version_test.dart 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..4f57a07d77b5f 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt @@ -39,6 +39,9 @@ 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_PREPROVISIONED_NDK_VERSION = "flutter-preprovisioned-ndk-version" + internal const val TASK_PRINT_NDK_VERSION = "printNdkVersion" + internal const val NDK_VERSION_OUTPUT_PREFIX = "NdkVersion: " /** * The URL for documentation for general information on migration to built-in Kotlin. @@ -787,6 +790,14 @@ object FlutterPluginUtils { gradleProject: Project, flutterSdkRootPath: String ) { + if (isFlutterAppProject(gradleProject) && isInvokingMetadataNdkVersionTask(gradleProject)) { + return + } + + if (isFlutterAppProject(gradleProject) && hasPreprovisionedNdkVersion(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 = @@ -829,6 +840,18 @@ object FlutterPluginUtils { } } + @JvmStatic + @JvmName("hasPreprovisionedNdkVersion") + internal fun hasPreprovisionedNdkVersion(project: Project): Boolean = + project.findProperty(PROP_PREPROVISIONED_NDK_VERSION)?.toString() != null + + @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 +984,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 used by Flutter tool Android NDK preflight. + @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 ?: FlutterExtension().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..082f776b135f2 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt @@ -1616,6 +1616,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 +1635,50 @@ class FlutterPluginUtilsTest { verify { mockDefaultConfig wasNot called } } + @Test + fun `forceNdkDownload skips when project has a preprovisioned ndk property`() { + val project = mockk() + val mockCmakeOptions = mockk() + val mockDefaultConfig = mockk() + every { + project.extensions + .findByType(BaseExtension::class.java)!! + .externalNativeBuild.cmake + } returns mockCmakeOptions + every { project.extensions.findByType(BaseExtension::class.java)!!.defaultConfig } returns mockDefaultConfig + every { mockCmakeOptions.path } returns null + every { project.findProperty(FlutterPluginUtils.PROP_PREPROVISIONED_NDK_VERSION) } returns "29.0.13846066" + every { project.gradle.startParameter.taskNames } returns emptyList() + 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 skips when invoking the ndk metadata task`() { + val project = mockk() + val mockCmakeOptions = mockk() + val mockDefaultConfig = mockk() + every { + project.extensions + .findByType(BaseExtension::class.java)!! + .externalNativeBuild.cmake + } returns mockCmakeOptions + every { project.extensions.findByType(BaseExtension::class.java)!!.defaultConfig } returns mockDefaultConfig + every { mockCmakeOptions.path } returns null + every { project.findProperty(FlutterPluginUtils.PROP_PREPROVISIONED_NDK_VERSION) } 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 sets externalNativeBuild properties`() { val project = mockk() @@ -1641,6 +1686,7 @@ class FlutterPluginUtilsTest { val mockDefaultConfig = mockk() val mockDirectoryProperty = mockk() val mockDirectory = mockk() + every { project.extensions.findByType(ApplicationExtension::class.java) } returns null every { project.extensions .findByType(BaseExtension::class.java)!! @@ -1684,6 +1730,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..de12399e8cb83 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,10 @@ import 'migrations/top_level_gradle_build_file_migration.dart'; final _kBuildVariantRegex = RegExp('^BuildVariant: (?<$_kBuildVariantRegexGroupName>.*)\$'); const _kBuildVariantRegexGroupName = 'variant'; const _kBuildVariantTaskName = 'printBuildVariants'; +final _kNdkVersionRegex = RegExp('^NdkVersion: (?<$_kNdkVersionRegexGroupName>.*)\$'); +const _kNdkVersionRegexGroupName = 'ndkVersion'; +const _kNdkVersionTaskName = 'printNdkVersion'; +const _kPreprovisionedNdkVersionProperty = 'flutter-preprovisioned-ndk-version'; @visibleForTesting const failedToStripDebugSymbolsErrorMessage = r''' Release app bundle failed to strip debug symbols from native libraries. @@ -273,6 +278,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 +341,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 +401,7 @@ class AndroidGradleBuilder implements AndroidBuilder { postRunTask: postRunTask, localGradleErrors: localGradleErrors, gradleExecutablePath: gradleExecutablePath, + printOutput: printOutput, retry: retry, project: project, maxRetries: maxRetries, @@ -568,6 +575,16 @@ class AndroidGradleBuilder implements AndroidBuilder { if (androidBuildInfo.splitPerAbi) { options.add('-Psplit-per-abi=true'); } + final String? preprovisionedNdkVersion = _shouldPreprovisionAndroidNdk(buildInfo) + ? await _preprovisionAndroidNdkIfNeeded( + project: project, + gradleExecutablePath: gradleExecutablePath, + buildInfo: buildInfo, + ) + : null; + if (preprovisionedNdkVersion != null) { + options.add('-P$_kPreprovisionedNdkVersionProperty=$preprovisionedNdkVersion'); + } late Stopwatch sw; final int exitCode = await _runGradleTask( assembleTask, @@ -907,6 +924,129 @@ class AndroidGradleBuilder implements AndroidBuilder { ); } + Future _preprovisionAndroidNdkIfNeeded({ + required FlutterProject project, + required String gradleExecutablePath, + required BuildInfo buildInfo, + }) async { + final AndroidSdk? androidSdk = globals.androidSdk; + if (androidSdk == null || !androidSdk.directory.existsSync()) { + return null; + } + + final String? requiredNdkVersion = await _getNdkVersion( + project: project, + gradleExecutablePath: gradleExecutablePath, + buildInfo: buildInfo, + ); + if (requiredNdkVersion == null) { + return null; + } + + if (androidSdk.hasNdkVersion(requiredNdkVersion)) { + return requiredNdkVersion; + } + + if (!androidSdk.cmdlineToolsAvailable) { + throwToolExit( + 'Android sdkmanager not found. Update to the latest Android SDK and ensure that ' + 'the cmdline-tools are installed to resolve this.', + ); + } + + if (!androidSdk.licensesAvailable) { + throwToolExit( + 'Unable to download needed Android SDK components because the Android SDK licenses have ' + 'not been accepted.\n\nTo resolve this, please run the following command in a Terminal:\n' + 'flutter doctor --android-licenses', + ); + } + + final Status status = _logger.startProgress( + "Ensuring Android NDK '$requiredNdkVersion' is installed...", + ); + try { + final RunResult result = await androidSdk.installNdkVersion( + requiredNdkVersion, + java: _java, + processUtils: _processUtils, + ); + if (result.exitCode != 0) { + _logger.printTrace( + 'Android sdkmanager failed while installing NDK $requiredNdkVersion.\n' + 'stdout: ${result.stdout}\n' + 'stderr: ${result.stderr}', + ); + throwToolExit( + 'Unable to download needed Android NDK $requiredNdkVersion.\n' + 'Please check that the Android SDK command-line tools are installed and that SDK ' + 'licenses have been accepted with `flutter doctor --android-licenses`.', + ); + } + } finally { + status.stop(); + } + + if (!androidSdk.hasNdkVersion(requiredNdkVersion)) { + throwToolExit( + 'Android NDK $requiredNdkVersion could not be found in ${androidSdk.directory.path} ' + 'after sdkmanager completed.', + ); + } + + return requiredNdkVersion; + } + + Future _getNdkVersion({ + required FlutterProject project, + required String gradleExecutablePath, + required BuildInfo buildInfo, + }) async { + late Stopwatch sw; + var exitCode = 1; + String? result; + + try { + exitCode = await _runGradleTask( + _kNdkVersionTaskName, + preRunTask: () { + sw = Stopwatch()..start(); + }, + postRunTask: () { + final Duration elapsedDuration = sw.elapsed; + _analytics.send( + Event.timing( + workflow: 'print', + variableName: 'android ndk version', + elapsedMilliseconds: elapsedDuration.inMilliseconds, + ), + ); + }, + options: [ + '-q', + if (buildInfo.androidSkipBuildDependencyValidation) '-PskipDependencyChecks=true', + ], + project: project, + localGradleErrors: gradleErrors, + gradleExecutablePath: gradleExecutablePath, + printOutput: false, + outputParser: (String line) { + if (_kNdkVersionRegex.firstMatch(line) case final RegExpMatch match) { + result = match.namedGroup(_kNdkVersionRegexGroupName); + } + }, + ); + } on Error catch (error) { + _logger.printTrace('Failed to query Android ndkVersion: $error'); + } + + if (exitCode != 0) { + return null; + } + + return result; + } + @override Future> getBuildVariants({required FlutterProject project}) async { late Stopwatch sw; @@ -1364,3 +1504,8 @@ String _getTargetPlatformByLocalEnginePath(String engineOutPath) { } return result; } + +bool _shouldPreprovisionAndroidNdk(BuildInfo buildInfo) { + final String? flavor = buildInfo.flavor; + return flavor != null && flavor.isNotEmpty; +} 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/test/general.shard/android/android_gradle_builder_test.dart b/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart index f8d9fbf8f1ce4..d3f17fb81bc92 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,6 +61,362 @@ 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, + }, + ); + } + + late AndroidSdk sdkForPreprovisionBuild; + testUsingContext( + 'build apk preprovisions the configured ndk and passes the gradle property', + () 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', 'printNdkVersion'], + stdout: 'NdkVersion: 29.0.13846066\n', + ), + ); + processManager.addCommand( + FakeCommand( + command: [ + sdkManagerPath(), + '--sdk_root=${sdkPath()}', + '--install', + 'ndk;29.0.13846066', + ], + onRun: (_) { + fileSystem + .directory(ndkPath('29.0.13846066')) + .childFile('source.properties') + .createSync(recursive: true); + }, + ), + ); + 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', + '-Pflutter-preprovisioned-ndk-version=29.0.13846066', + '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(sdkForPreprovisionBuild.hasNdkVersion('29.0.13846066'), isTrue); + 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); + sdkForPreprovisionBuild = AndroidSdk( + fileSystem.directory(sdkPath()), + java: FakeJava(), + fileSystem: fileSystem, + ); + return sdkForPreprovisionBuild; + }, + AndroidStudio: () => FakeAndroidStudio(), + }, + ); + + testUsingContext( + 'build apk forwards skip dependency checks to printNdkVersion', + () 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', '-PskipDependencyChecks=true', 'printNdkVersion'], + stdout: 'NdkVersion: 29.0.13846066\n', + ), + ); + processManager.addCommand( + FakeCommand( + command: [ + sdkManagerPath(), + '--sdk_root=${sdkPath()}', + '--install', + 'ndk;29.0.13846066', + ], + onRun: (_) { + fileSystem + .directory(ndkPath('29.0.13846066')) + .childFile('source.properties') + .createSync(recursive: true); + }, + ), + ); + processManager.addCommand( + const FakeCommand( + command: [ + 'gradlew', + '-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', + '-Pflutter-preprovisioned-ndk-version=29.0.13846066', + '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', + androidSkipBuildDependencyValidation: 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); + return AndroidSdk( + fileSystem.directory(sdkPath()), + java: FakeJava(), + fileSystem: fileSystem, + ); + }, + AndroidStudio: () => FakeAndroidStudio(), + }, + ); + + late AndroidSdk sdkForFailedQuery; + testUsingContext( + 'build apk continues without ndk property when printNdkVersion fails', + () 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', 'printNdkVersion'], + stderr: 'Task failed\n', + exitCode: 1, + ), + ); + 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', + '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(sdkForFailedQuery.hasNdkVersion('29.0.13846066'), isFalse); + 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); + sdkForFailedQuery = AndroidSdk( + fileSystem.directory(sdkPath()), + java: FakeJava(), + fileSystem: fileSystem, + ); + return sdkForFailedQuery; + }, + AndroidStudio: () => FakeAndroidStudio(), + }, + ); + testUsingContext( 'Can immediately tool exit on recognized exit code/stderr', () async { @@ -921,16 +1279,10 @@ void main() { 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 +1315,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( @@ -986,16 +1353,10 @@ void main() { 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 +1389,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( @@ -1080,7 +1456,21 @@ 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( @@ -1104,7 +1494,16 @@ void main() { createSharedGradleFiles(); final File aabFile = createAabFile(BuildMode.release); - final AndroidSdk sdk = AndroidSdk.locateAndroidSdk()!; + fileSystem.directory(sdkPath()).createSync(recursive: true); + fileSystem + .directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin')) + .childFile(apkAnalyzerBinaryName) + .createSync(recursive: true); + final AndroidSdk sdk = AndroidSdk( + fileSystem.directory(sdkPath()), + java: FakeJava(), + fileSystem: fileSystem, + ); processManager.addCommand( FakeCommand( @@ -1149,7 +1548,21 @@ 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( @@ -1173,7 +1586,16 @@ void main() { createSharedGradleFiles(); final File aabFile = createAabFile(BuildMode.release); - final AndroidSdk sdk = AndroidSdk.locateAndroidSdk()!; + fileSystem.directory(sdkPath()).createSync(recursive: true); + fileSystem + .directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin')) + .childFile(apkAnalyzerBinaryName) + .createSync(recursive: true); + final AndroidSdk sdk = AndroidSdk( + fileSystem.directory(sdkPath()), + java: FakeJava(), + fileSystem: fileSystem, + ); processManager.addCommand( FakeCommand( 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', + ); + }, + ); +} From 1e4bde9d47477951d8bb072829be292278685188 Mon Sep 17 00:00:00 2001 From: kyungil Date: Fri, 24 Apr 2026 20:13:03 +0900 Subject: [PATCH 2/6] [flutter_tools] Reuse project FlutterExtension for printNdkVersion --- .../flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt index 4f57a07d77b5f..932a930a4d7f2 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt @@ -999,7 +999,7 @@ object FlutterPluginUtils { project.tasks.register(TASK_PRINT_NDK_VERSION) { description = "Prints out the configured ndkVersion for this Android project" doLast { - val configuredNdkVersion = androidExtension.ndkVersion ?: FlutterExtension().ndkVersion + val configuredNdkVersion = androidExtension.ndkVersion ?: getFlutterExtensionOrNull(project)?.ndkVersion println("$NDK_VERSION_OUTPUT_PREFIX$configuredNdkVersion") } } From cd46bf4d32dd3d03a67c68b0345215d8b35f5213 Mon Sep 17 00:00:00 2001 From: kyungil Date: Sat, 25 Apr 2026 08:58:53 +0900 Subject: [PATCH 3/6] [flutter_tools] Remove obvious local types in gradle builder tests --- .../general.shard/android/android_gradle_builder_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 d3f17fb81bc92..74e03c081c6eb 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 @@ -1499,7 +1499,7 @@ void main() { .directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin')) .childFile(apkAnalyzerBinaryName) .createSync(recursive: true); - final AndroidSdk sdk = AndroidSdk( + final sdk = AndroidSdk( fileSystem.directory(sdkPath()), java: FakeJava(), fileSystem: fileSystem, @@ -1591,7 +1591,7 @@ void main() { .directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin')) .childFile(apkAnalyzerBinaryName) .createSync(recursive: true); - final AndroidSdk sdk = AndroidSdk( + final sdk = AndroidSdk( fileSystem.directory(sdkPath()), java: FakeJava(), fileSystem: fileSystem, From 7fffcf9842e1b6a3b090f217405fc45dd35240d7 Mon Sep 17 00:00:00 2001 From: kyungil Date: Sun, 3 May 2026 23:07:23 +0900 Subject: [PATCH 4/6] [devicelab] Keep non-flavored fat APK .cxx expectation --- dev/devicelab/bin/tasks/gradle_plugin_fat_apk_test.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 e7499901c53f3..1097196a9ecdd 100644 --- a/dev/devicelab/bin/tasks/gradle_plugin_fat_apk_test.dart +++ b/dev/devicelab/bin/tasks/gradle_plugin_fat_apk_test.dart @@ -158,8 +158,10 @@ Future main() async { if (Directory(defaultPath).existsSync()) { throw TaskResult.failure('Producing unexpected build artifacts in $defaultPath'); } - if (Directory(modifiedPath).existsSync()) { - throw TaskResult.failure('Producing unexpected build artifacts in $modifiedPath'); + if (!Directory(modifiedPath).existsSync()) { + throw TaskResult.failure( + 'Not producing external native build output directory in $modifiedPath', + ); } }); From 1aaca5bf0929a618b2ed73ae32b255975b37d059 Mon Sep 17 00:00:00 2001 From: kyungil Date: Mon, 11 May 2026 14:40:20 +0900 Subject: [PATCH 5/6] [flutter_tools] Provision Android NDK in the main Gradle invocation --- .../bin/tasks/gradle_plugin_fat_apk_test.dart | 6 +- .../src/main/kotlin/FlutterPluginUtils.kt | 107 ++++++++-- .../src/test/kotlin/FlutterPluginUtilsTest.kt | 185 +++++++++++++++-- .../flutter_tools/lib/src/android/gradle.dart | 189 +++++------------- .../flutter_tools/lib/src/context_runner.dart | 1 + .../android/android_gradle_builder_test.dart | 149 +++++--------- 6 files changed, 367 insertions(+), 270 deletions(-) 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 1097196a9ecdd..e7499901c53f3 100644 --- a/dev/devicelab/bin/tasks/gradle_plugin_fat_apk_test.dart +++ b/dev/devicelab/bin/tasks/gradle_plugin_fat_apk_test.dart @@ -158,10 +158,8 @@ Future main() async { 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/FlutterPluginUtils.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt index 932a930a4d7f2..7c09a40e59bf5 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt @@ -39,10 +39,18 @@ 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_PREPROVISIONED_NDK_VERSION = "flutter-preprovisioned-ndk-version" + 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. */ @@ -794,10 +802,6 @@ object FlutterPluginUtils { return } - if (isFlutterAppProject(gradleProject) && hasPreprovisionedNdkVersion(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 = @@ -806,7 +810,89 @@ object FlutterPluginUtils { return } - // Otherwise, point to an empty CMakeLists.txt, and ignore associated warnings. + val toolNdkProvisioningProperties = getToolNdkProvisioningProperties(gradleProject) + if (toolNdkProvisioningProperties != null) { + 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 = getLegacyAndroidExtension(gradleProject).ndkVersion + 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" ) @@ -840,11 +926,6 @@ object FlutterPluginUtils { } } - @JvmStatic - @JvmName("hasPreprovisionedNdkVersion") - internal fun hasPreprovisionedNdkVersion(project: Project): Boolean = - project.findProperty(PROP_PREPROVISIONED_NDK_VERSION)?.toString() != null - @JvmStatic @JvmName("isInvokingMetadataNdkVersionTask") internal fun isInvokingMetadataNdkVersionTask(project: Project): Boolean = @@ -991,7 +1072,7 @@ object FlutterPluginUtils { // // NdkVersion: 28.2.13676358 // - // Format of the output of this task is used by Flutter tool Android NDK preflight. + // Format of the output of this task is kept for diagnostics and targeted testing. @JvmStatic @JvmName("addTaskForPrintNdkVersion") internal fun addTaskForPrintNdkVersion(project: Project) { @@ -999,7 +1080,7 @@ object FlutterPluginUtils { project.tasks.register(TASK_PRINT_NDK_VERSION) { description = "Prints out the configured ndkVersion for this Android project" doLast { - val configuredNdkVersion = androidExtension.ndkVersion ?: getFlutterExtensionOrNull(project)?.ndkVersion + val configuredNdkVersion = androidExtension.ndkVersion println("$NDK_VERSION_OUTPUT_PREFIX$configuredNdkVersion") } } diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt index 082f776b135f2..07d8d399db0f3 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 @@ -1636,23 +1638,119 @@ class FlutterPluginUtilsTest { } @Test - fun `forceNdkDownload skips when project has a preprovisioned ndk property`() { + 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() - every { - project.extensions - .findByType(BaseExtension::class.java)!! - .externalNativeBuild.cmake - } returns mockCmakeOptions - every { project.extensions.findByType(BaseExtension::class.java)!!.defaultConfig } returns mockDefaultConfig + 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_PREPROVISIONED_NDK_VERSION) } returns "29.0.13846066" + 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 mockk(relaxed = true) + 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 projectActionSlot = slot>() + 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 + every { project.afterEvaluate(capture(projectActionSlot)) } returns Unit FlutterPluginUtils.forceNdkDownload(project, "/base/path") + verify { project.afterEvaluate(capture(projectActionSlot)) } + projectActionSlot.captured.execute(project) + + 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 } } @@ -1662,14 +1760,14 @@ class FlutterPluginUtilsTest { val project = mockk() val mockCmakeOptions = mockk() val mockDefaultConfig = mockk() - every { - project.extensions - .findByType(BaseExtension::class.java)!! - .externalNativeBuild.cmake - } returns mockCmakeOptions - every { project.extensions.findByType(BaseExtension::class.java)!!.defaultConfig } returns mockDefaultConfig + 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_PREPROVISIONED_NDK_VERSION) } 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) @@ -1679,6 +1777,58 @@ class FlutterPluginUtilsTest { verify { mockDefaultConfig wasNot called } } + @Test + fun `forceNdkDownload falls back when tool properties are present but sdkmanager is unavailable`() { + val project = mockk() + val projectActionSlot = slot>() + 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.afterEvaluate(capture(projectActionSlot)) } returns Unit + 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 { project.afterEvaluate(capture(projectActionSlot)) } + projectActionSlot.captured.execute(project) + + 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() @@ -1687,6 +1837,9 @@ class FlutterPluginUtilsTest { 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)!! diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index de12399e8cb83..99397d249141d 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -56,10 +56,9 @@ import 'migrations/top_level_gradle_build_file_migration.dart'; final _kBuildVariantRegex = RegExp('^BuildVariant: (?<$_kBuildVariantRegexGroupName>.*)\$'); const _kBuildVariantRegexGroupName = 'variant'; const _kBuildVariantTaskName = 'printBuildVariants'; -final _kNdkVersionRegex = RegExp('^NdkVersion: (?<$_kNdkVersionRegexGroupName>.*)\$'); -const _kNdkVersionRegexGroupName = 'ndkVersion'; -const _kNdkVersionTaskName = 'printNdkVersion'; -const _kPreprovisionedNdkVersionProperty = 'flutter-preprovisioned-ndk-version'; +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. @@ -170,6 +169,7 @@ class AndroidGradleBuilder implements AndroidBuilder { required GradleUtils gradleUtils, required Platform platform, required AndroidStudio? androidStudio, + AndroidSdk? androidSdk, }) : _java = java, _logger = logger, _fileSystem = fileSystem, @@ -177,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); @@ -189,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 @@ -575,16 +577,8 @@ class AndroidGradleBuilder implements AndroidBuilder { if (androidBuildInfo.splitPerAbi) { options.add('-Psplit-per-abi=true'); } - final String? preprovisionedNdkVersion = _shouldPreprovisionAndroidNdk(buildInfo) - ? await _preprovisionAndroidNdkIfNeeded( - project: project, - gradleExecutablePath: gradleExecutablePath, - buildInfo: buildInfo, - ) - : null; - if (preprovisionedNdkVersion != null) { - options.add('-P$_kPreprovisionedNdkVersionProperty=$preprovisionedNdkVersion'); - } + + options.addAll(_getAndroidNdkProvisioningProperties()); late Stopwatch sw; final int exitCode = await _runGradleTask( assembleTask, @@ -690,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.', @@ -924,129 +918,6 @@ class AndroidGradleBuilder implements AndroidBuilder { ); } - Future _preprovisionAndroidNdkIfNeeded({ - required FlutterProject project, - required String gradleExecutablePath, - required BuildInfo buildInfo, - }) async { - final AndroidSdk? androidSdk = globals.androidSdk; - if (androidSdk == null || !androidSdk.directory.existsSync()) { - return null; - } - - final String? requiredNdkVersion = await _getNdkVersion( - project: project, - gradleExecutablePath: gradleExecutablePath, - buildInfo: buildInfo, - ); - if (requiredNdkVersion == null) { - return null; - } - - if (androidSdk.hasNdkVersion(requiredNdkVersion)) { - return requiredNdkVersion; - } - - if (!androidSdk.cmdlineToolsAvailable) { - throwToolExit( - 'Android sdkmanager not found. Update to the latest Android SDK and ensure that ' - 'the cmdline-tools are installed to resolve this.', - ); - } - - if (!androidSdk.licensesAvailable) { - throwToolExit( - 'Unable to download needed Android SDK components because the Android SDK licenses have ' - 'not been accepted.\n\nTo resolve this, please run the following command in a Terminal:\n' - 'flutter doctor --android-licenses', - ); - } - - final Status status = _logger.startProgress( - "Ensuring Android NDK '$requiredNdkVersion' is installed...", - ); - try { - final RunResult result = await androidSdk.installNdkVersion( - requiredNdkVersion, - java: _java, - processUtils: _processUtils, - ); - if (result.exitCode != 0) { - _logger.printTrace( - 'Android sdkmanager failed while installing NDK $requiredNdkVersion.\n' - 'stdout: ${result.stdout}\n' - 'stderr: ${result.stderr}', - ); - throwToolExit( - 'Unable to download needed Android NDK $requiredNdkVersion.\n' - 'Please check that the Android SDK command-line tools are installed and that SDK ' - 'licenses have been accepted with `flutter doctor --android-licenses`.', - ); - } - } finally { - status.stop(); - } - - if (!androidSdk.hasNdkVersion(requiredNdkVersion)) { - throwToolExit( - 'Android NDK $requiredNdkVersion could not be found in ${androidSdk.directory.path} ' - 'after sdkmanager completed.', - ); - } - - return requiredNdkVersion; - } - - Future _getNdkVersion({ - required FlutterProject project, - required String gradleExecutablePath, - required BuildInfo buildInfo, - }) async { - late Stopwatch sw; - var exitCode = 1; - String? result; - - try { - exitCode = await _runGradleTask( - _kNdkVersionTaskName, - preRunTask: () { - sw = Stopwatch()..start(); - }, - postRunTask: () { - final Duration elapsedDuration = sw.elapsed; - _analytics.send( - Event.timing( - workflow: 'print', - variableName: 'android ndk version', - elapsedMilliseconds: elapsedDuration.inMilliseconds, - ), - ); - }, - options: [ - '-q', - if (buildInfo.androidSkipBuildDependencyValidation) '-PskipDependencyChecks=true', - ], - project: project, - localGradleErrors: gradleErrors, - gradleExecutablePath: gradleExecutablePath, - printOutput: false, - outputParser: (String line) { - if (_kNdkVersionRegex.firstMatch(line) case final RegExpMatch match) { - result = match.namedGroup(_kNdkVersionRegexGroupName); - } - }, - ); - } on Error catch (error) { - _logger.printTrace('Failed to query Android ndkVersion: $error'); - } - - if (exitCode != 0) { - return null; - } - - return result; - } - @override Future> getBuildVariants({required FlutterProject project}) async { late Stopwatch sw; @@ -1135,6 +1006,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. @@ -1505,7 +1396,19 @@ String _getTargetPlatformByLocalEnginePath(String engineOutPath) { return result; } -bool _shouldPreprovisionAndroidNdk(BuildInfo buildInfo) { - final String? flavor = buildInfo.flavor; - return flavor != null && flavor.isNotEmpty; +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) => androidSdk.hasNdkVersion(version)) + .toList() + ..sort(); + return installedNdkVersions; } 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 74e03c081c6eb..93091761a6169 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 @@ -95,9 +95,8 @@ void main() { ); } - late AndroidSdk sdkForPreprovisionBuild; testUsingContext( - 'build apk preprovisions the configured ndk and passes the gradle property', + 'build apk passes sdkmanager path, sdk root, and validated installed ndk versions to gradle', () async { final builder = AndroidGradleBuilder( java: FakeJava(), @@ -109,31 +108,10 @@ void main() { gradleUtils: FakeGradleUtils(), platform: FakePlatform(), androidStudio: FakeAndroidStudio(), - ); - processManager.addCommand( - const FakeCommand( - command: ['gradlew', '-q', 'printNdkVersion'], - stdout: 'NdkVersion: 29.0.13846066\n', - ), + androidSdk: globals.androidSdk, ); processManager.addCommand( FakeCommand( - command: [ - sdkManagerPath(), - '--sdk_root=${sdkPath()}', - '--install', - 'ndk;29.0.13846066', - ], - onRun: (_) { - fileSystem - .directory(ndkPath('29.0.13846066')) - .childFile('source.properties') - .createSync(recursive: true); - }, - ), - ); - processManager.addCommand( - const FakeCommand( command: [ 'gradlew', '-q', @@ -143,7 +121,9 @@ void main() { '-Pdart-obfuscation=false', '-Ptrack-widget-creation=false', '-Ptree-shake-icons=false', - '-Pflutter-preprovisioned-ndk-version=29.0.13846066', + '-Pflutter.androidSdkRoot=${sdkPath()}', + '-Pflutter.installedNdkVersions=29.0.13846066', + '-Pflutter.sdkManagerPath=${sdkManagerPath()}', 'assembleDevRelease', ], ), @@ -186,7 +166,6 @@ void main() { localGradleErrors: const [], ); - expect(sdkForPreprovisionBuild.hasNdkVersion('29.0.13846066'), isTrue); expect(processManager, hasNoRemainingExpectations); }, overrides: { @@ -197,19 +176,23 @@ void main() { .directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin')) .childFile(globals.platform.isWindows ? 'sdkmanager.bat' : 'sdkmanager') .createSync(recursive: true); - sdkForPreprovisionBuild = AndroidSdk( + 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, ); - return sdkForPreprovisionBuild; }, AndroidStudio: () => FakeAndroidStudio(), }, ); testUsingContext( - 'build apk forwards skip dependency checks to printNdkVersion', + 'build apk keeps skip dependency checks on the main gradle invocation when ndk provisioning is unavailable', () async { final builder = AndroidGradleBuilder( java: FakeJava(), @@ -221,31 +204,10 @@ void main() { gradleUtils: FakeGradleUtils(), platform: FakePlatform(), androidStudio: FakeAndroidStudio(), - ); - processManager.addCommand( - const FakeCommand( - command: ['gradlew', '-q', '-PskipDependencyChecks=true', 'printNdkVersion'], - stdout: 'NdkVersion: 29.0.13846066\n', - ), + androidSdk: globals.androidSdk, ); processManager.addCommand( FakeCommand( - command: [ - sdkManagerPath(), - '--sdk_root=${sdkPath()}', - '--install', - 'ndk;29.0.13846066', - ], - onRun: (_) { - fileSystem - .directory(ndkPath('29.0.13846066')) - .childFile('source.properties') - .createSync(recursive: true); - }, - ), - ); - processManager.addCommand( - const FakeCommand( command: [ 'gradlew', '-q', @@ -256,7 +218,8 @@ void main() { '-Pdart-obfuscation=false', '-Ptrack-widget-creation=false', '-Ptree-shake-icons=false', - '-Pflutter-preprovisioned-ndk-version=29.0.13846066', + '-Pflutter.androidSdkRoot=${sdkPath()}', + '-Pflutter.installedNdkVersions=', 'assembleDevRelease', ], ), @@ -305,7 +268,6 @@ void main() { 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') @@ -320,9 +282,8 @@ void main() { }, ); - late AndroidSdk sdkForFailedQuery; testUsingContext( - 'build apk continues without ndk property when printNdkVersion fails', + 'build apk passes an empty installed ndk version list when no valid ndks are installed', () async { final builder = AndroidGradleBuilder( java: FakeJava(), @@ -334,16 +295,10 @@ void main() { gradleUtils: FakeGradleUtils(), platform: FakePlatform(), androidStudio: FakeAndroidStudio(), + androidSdk: globals.androidSdk, ); processManager.addCommand( - const FakeCommand( - command: ['gradlew', '-q', 'printNdkVersion'], - stderr: 'Task failed\n', - exitCode: 1, - ), - ); - processManager.addCommand( - const FakeCommand( + FakeCommand( command: [ 'gradlew', '-q', @@ -353,6 +308,9 @@ void main() { '-Pdart-obfuscation=false', '-Ptrack-widget-creation=false', '-Ptree-shake-icons=false', + '-Pflutter.androidSdkRoot=${sdkPath()}', + '-Pflutter.installedNdkVersions=', + '-Pflutter.sdkManagerPath=${sdkManagerPath()}', 'assembleDevRelease', ], ), @@ -395,7 +353,6 @@ void main() { localGradleErrors: const [], ); - expect(sdkForFailedQuery.hasNdkVersion('29.0.13846066'), isFalse); expect(processManager, hasNoRemainingExpectations); }, overrides: { @@ -406,12 +363,11 @@ void main() { .directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin')) .childFile(globals.platform.isWindows ? 'sdkmanager.bat' : 'sdkmanager') .createSync(recursive: true); - sdkForFailedQuery = AndroidSdk( + return AndroidSdk( fileSystem.directory(sdkPath()), java: FakeJava(), fileSystem: fileSystem, ); - return sdkForFailedQuery; }, AndroidStudio: () => FakeAndroidStudio(), }, @@ -1082,7 +1038,7 @@ void main() { ); group('Appbundle debug symbol tests', () { - final commonCommandPortion = [ + List commonCommandPortion() => [ 'gradlew', '-q', '-Ptarget-platform=android-arm64,android-arm,android-x64', @@ -1091,6 +1047,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 ` @@ -1272,9 +1230,10 @@ 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(); @@ -1346,9 +1305,10 @@ 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(); @@ -1420,9 +1380,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(); @@ -1476,6 +1437,16 @@ void main() { 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, @@ -1486,25 +1457,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); - 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, - ); - processManager.addCommand( FakeCommand( command: [ @@ -1568,6 +1529,16 @@ void main() { 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, @@ -1578,25 +1549,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); - 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, - ); - processManager.addCommand( FakeCommand( command: [ From a3d5731e67c108b00c81730ab3287e946af167e8 Mon Sep 17 00:00:00 2001 From: kyungil Date: Wed, 13 May 2026 11:58:09 +0900 Subject: [PATCH 6/6] [flutter_tools] Tighten main-invocation Android NDK provisioning --- .../src/main/kotlin/FlutterPluginUtils.kt | 26 +- .../src/test/kotlin/FlutterPluginUtilsTest.kt | 138 +- .../flutter_tools/lib/src/android/gradle.dart | 6 +- .../android/android_gradle_builder_test.dart | 2636 ++++++++--------- 4 files changed, 1464 insertions(+), 1342 deletions(-) diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt index 7c09a40e59bf5..2a488ff5f7669 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt @@ -511,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, @@ -714,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() @@ -745,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, @@ -812,6 +814,20 @@ object FlutterPluginUtils { 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, @@ -854,7 +870,7 @@ object FlutterPluginUtils { gradleProject: Project, toolNdkProvisioningProperties: ToolNdkProvisioningProperties ): Boolean { - val configuredNdkVersion = getLegacyAndroidExtension(gradleProject).ndkVersion + val configuredNdkVersion = getConfiguredNdkVersion(gradleProject) if (configuredNdkVersion.isNullOrBlank()) { return false } diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt index 07d8d399db0f3..b13100079905c 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt @@ -599,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)) @@ -640,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( @@ -667,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) @@ -749,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() @@ -756,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") @@ -1694,7 +1768,6 @@ class FlutterPluginUtilsTest { @Test fun `forceNdkDownload skips sdkmanager install when the requested ndk is already installed`() { val project = mockk() - val projectActionSlot = slot>() val mockCmakeOptions = mockk() val mockDefaultConfig = mockk() val mockBaseExtension = mockk() @@ -1708,13 +1781,64 @@ class FlutterPluginUtilsTest { 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 - every { project.afterEvaluate(capture(projectActionSlot)) } returns Unit FlutterPluginUtils.forceNdkDownload(project, "/base/path") - verify { project.afterEvaluate(capture(projectActionSlot)) } - projectActionSlot.captured.execute(project) + 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 } @@ -1780,7 +1904,6 @@ class FlutterPluginUtilsTest { @Test fun `forceNdkDownload falls back when tool properties are present but sdkmanager is unavailable`() { val project = mockk() - val projectActionSlot = slot>() val mockCmakeOptions = mockk() val mockDefaultConfig = mockk() val mockDirectoryProperty = mockk() @@ -1798,7 +1921,6 @@ class FlutterPluginUtilsTest { 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.afterEvaluate(capture(projectActionSlot)) } returns Unit every { project.layout.buildDirectory } returns mockDirectoryProperty every { mockDirectoryProperty.dir(any()) } returns mockDirectoryProperty every { mockDirectoryProperty.get() } returns mockDirectory @@ -1812,9 +1934,7 @@ class FlutterPluginUtilsTest { FlutterPluginUtils.forceNdkDownload(project, basePath) - verify { project.afterEvaluate(capture(projectActionSlot)) } - projectActionSlot.captured.execute(project) - + 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") diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index 99397d249141d..f012a57493a1e 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -832,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}' @@ -1407,7 +1408,10 @@ List _getInstalledNdkVersionsForGradle(AndroidSdk androidSdk) { .listSync() .whereType() .map((Directory dir) => dir.basename) - .where((String version) => androidSdk.hasNdkVersion(version)) + .where( + (String version) => + ndkDir.childDirectory(version).childFile('source.properties').existsSync(), + ) .toList() ..sort(); return installedNdkVersions; 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 93091761a6169..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 @@ -373,112 +373,106 @@ void main() { }, ); - 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', - '-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('build.gradle').createSync(recursive: true); - - fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); + 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', + '-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').childDirectory('app').childFile('build.gradle') - ..createSync(recursive: true) - ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); + fileSystem.directory('android').childFile('build.gradle').createSync(recursive: true); - final FlutterProject project = FlutterProject.fromDirectoryTest( - fileSystem.currentDirectory, - ); - project.android.appManifestFile - ..createSync(recursive: true) - ..writeAsStringSync(minimalV2EmbeddingManifest); + fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); - 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')); + fileSystem.directory('android').childDirectory('app').childFile('build.gradle') + ..createSync(recursive: true) + ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); - expect(handlerCalled, isTrue); + final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + project.android.appManifestFile + ..createSync(recursive: true) + ..writeAsStringSync(minimalV2EmbeddingManifest); - expect( - fakeAnalytics.sentEvents, - containsAll([ - Event.flutterBuildInfo( - label: 'app-not-using-android-x', - buildType: 'gradle', - settings: 'androidGradlePluginVersion: null', + 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', ), - Event.flutterBuildInfo( - label: 'gradle-random-event-label-failure', - buildType: 'gradle', - settings: 'androidGradlePluginVersion: null', + ), + 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( - analyticsTimingEventExists( - sentEvents: fakeAnalytics.sentEvents, - workflow: 'build', - variableName: 'gradle', + expect(handlerCalled, isTrue); + + expect( + fakeAnalytics.sentEvents, + containsAll([ + Event.flutterBuildInfo( + label: 'app-not-using-android-x', + buildType: 'gradle', + settings: 'androidGradlePluginVersion: null', ), - true, - ); - }, - 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( 'Verbose mode for APKs includes Gradle stacktrace and sets debug log level', @@ -555,22 +549,120 @@ void main() { overrides: {AndroidStudio: () => FakeAndroidStudio()}, ); - 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(), + 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(), + ); + + 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', + ); + + 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('gradle.properties').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); + + 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', + ), + ], ); + }, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1')); - const fakeCmd = FakeCommand( + 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()}); + + 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', @@ -584,318 +676,113 @@ 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('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').childFile('build.gradle').createSync(recursive: true); - final FlutterProject project = FlutterProject.fromDirectoryTest( - fileSystem.currentDirectory, - ); - project.android.appManifestFile - ..createSync(recursive: true) - ..writeAsStringSync(minimalV2EmbeddingManifest); + fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); - 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', - ), - ], - ); - }, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1')); + fileSystem.directory('android').childDirectory('app').childFile('build.gradle') + ..createSync(recursive: true) + ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); - expect(logger.statusText, contains('Retrying Gradle Build: #1, wait time: 100ms')); - expect(logger.statusText, contains('Retrying Gradle Build: #2, wait time: 200ms')); + final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + project.android.appManifestFile + ..createSync(recursive: true) + ..writeAsStringSync(minimalV2EmbeddingManifest); - 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', + 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', ), ), - ); - }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, - ); - - 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('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 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( - 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', + 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', ), - ), - ); - }, - 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'); - }, - ), - ); - - 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 FlutterProject project = FlutterProject.fromDirectoryTest( - fileSystem.currentDirectory, + ], ); - project.android.appManifestFile - ..createSync(recursive: true) - ..writeAsStringSync(minimalV2EmbeddingManifest); + }, throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1')); - 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()}, - ); + expect(handlerCalled, isTrue); - 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', - ], + expect(fakeAnalytics.sentEvents, hasLength(3)); + 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('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'); + }, + ), + ); - 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); + await expectLater(() async { await builder.buildGradleApp( project: project, androidBuildInfo: const AndroidBuildInfo( @@ -909,92 +796,173 @@ void main() { 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', - ), - ), + localGradleErrors: const [], ); - expect(processManager, hasNoRemainingExpectations); - }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, - ); + }, throwsProcessException()); + 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', - ], + 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', + ), + ], + ); - fileSystem.directory('android').childFile('build.gradle').createSync(recursive: true); + expect( + fakeAnalytics.sentEvents, + contains( + Event.flutterBuildInfo( + label: 'gradle-random-event-label-success', + buildType: 'gradle', + settings: 'androidGradlePluginVersion: null', + ), + ), + ); + expect(processManager, hasNoRemainingExpectations); + }, overrides: {AndroidStudio: () => FakeAndroidStudio()}); - fileSystem.directory('android').childFile('gradle.properties').createSync(recursive: true); + 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').childDirectory('app').childFile('build.gradle') - ..createSync(recursive: true) - ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); + fileSystem.directory('android').childFile('build.gradle').createSync(recursive: true); - 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('gradle.properties').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').childDirectory('app').childFile('build.gradle') + ..createSync(recursive: true) + ..writeAsStringSync('apply from: irrelevant/flutter.gradle'); - fileSystem.file('foo/snapshot.arm64-v8a.json') - ..createSync(recursive: true) - ..writeAsStringSync(r''' + 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", @@ -1003,39 +971,35 @@ void main() { "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); + fileSystem.file('foo/trace.arm64-v8a.json') + ..createSync(recursive: true) + ..writeAsStringSync('{}'); - 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], + 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', ), - target: 'lib/main.dart', - isBuildingBundle: false, - configOnly: false, - localGradleErrors: [], - ); + targetArchs: [AndroidArch.arm64_v8a], + ), + target: 'lib/main.dart', + isBuildingBundle: false, + configOnly: false, + localGradleErrors: [], + ); - expect(fakeAnalytics.sentEvents, contains(Event.codeSizeAnalysis(platform: 'apk'))); - }, - overrides: {AndroidStudio: () => FakeAndroidStudio()}, - ); + expect(fakeAnalytics.sentEvents, contains(Event.codeSizeAnalysis(platform: 'apk'))); + }, overrides: {AndroidStudio: () => FakeAndroidStudio()}); group('Appbundle debug symbol tests', () { List commonCommandPortion() => [ @@ -1606,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); - - fileSystem.directory('android').childFile('gradle.properties').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').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); + 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); @@ -1802,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', @@ -1964,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(() { @@ -2162,216 +2206,200 @@ 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()}, - ); - - 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', - ], ), - ); + 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()}); - fileSystem.file('out/android_arm/flutter_embedding_release.pom') - ..createSync(recursive: true) - ..writeAsStringSync(''' - - - 1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b - - + 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(''' + + + 1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b + + '''); - 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 @@ -2379,97 +2407,91 @@ 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(''' + 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('out/android_x64/flutter_embedding_release.pom') + ..createSync(recursive: true) + ..writeAsStringSync(''' 1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b @@ -2477,233 +2499,211 @@ 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); - 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( - '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', - ], ), + 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 @@ -2711,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 @@ -2820,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 @@ -2928,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()}); }); }