From 0fddc37b72c083c91328d39445ae93c2a6c2de54 Mon Sep 17 00:00:00 2001 From: Matt Boetger Date: Tue, 12 May 2026 02:52:16 +0000 Subject: [PATCH] Resolve issue #172093: Prevent duplicate registration of generateLockfiles task --- .../gradle/src/main/kotlin/FlutterPlugin.kt | 3 + .../src/test/kotlin/FlutterPluginTest.kt | 108 +++++++++++++++++- 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt index 7d09171f31e33..fc41c84e6f0a9 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt @@ -274,6 +274,9 @@ class FlutterPlugin : Plugin { } private fun addTaskForLockfileGeneration(rootProject: Project) { + if (rootProject.tasks.findByName("generateLockfiles") != null) { + return + } rootProject.tasks.register("generateLockfiles") { doLast { rootProject.subprojects.forEach { subproject -> diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt index c1cbdceb41c8f..99d94509125d1 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt @@ -116,12 +116,113 @@ class FlutterPluginTest { // mock method calls that are invoked by the args to NativePluginLoaderReflectionBridge every { project.extraProperties } returns mockk() every { project.file(flutterExtension.source!!) } returns mockk() + val mockTaskContainer = mockk(relaxed = true) + every { project.tasks } returns mockTaskContainer + every { mockTaskContainer.findByName("generateLockfiles") } returns null val flutterPlugin = FlutterPlugin() flutterPlugin.apply(project) - verify { project.tasks.register("generateLockfiles", any()) } - verify { project.tasks.register("javaVersion", any()) } - verify { project.tasks.register("printBuildVariants", any()) } + verify { mockTaskContainer.register("generateLockfiles", any()) } + verify { mockTaskContainer.register("javaVersion", any()) } + verify { mockTaskContainer.register("printBuildVariants", any()) } + } + + @Test + fun `FlutterPlugin apply() does not register generateLockfiles task if already exists`( + @TempDir tempDir: Path + ) { + val projectDir = tempDir.resolve("project-dir").resolve("android").resolve("app") + projectDir.toFile().mkdirs() + val settingsFile = projectDir.parent.resolve("settings.gradle") + settingsFile.writeText("empty for now") + val fakeFlutterSdkDir = tempDir.resolve("fake-flutter-sdk") + fakeFlutterSdkDir.toFile().mkdirs() + val fakeCacheDir = fakeFlutterSdkDir.resolve("bin").resolve("cache") + fakeCacheDir.toFile().mkdirs() + val fakeEngineStampFile = fakeCacheDir.resolve("engine.stamp") + fakeEngineStampFile.writeText(FAKE_ENGINE_STAMP) + val fakeEngineRealmFile = fakeCacheDir.resolve("engine.realm") + fakeEngineRealmFile.writeText(FAKE_ENGINE_REALM) + val project = mockk(relaxed = true) + val mockAbstractAppExtension = + mockk( + moreInterfaces = arrayOf(ApplicationExtension::class), + relaxed = true + ) + val mockLibraryExtension = mockk(relaxed = true) + every { project.extensions.findByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension + val mockAndroidComponentsExtension = mockk>(relaxed = true) + every { project.extensions.getByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension + every { mockAndroidComponentsExtension.selector() } returns + mockk { + every { all() } returns mockk() + } + every { project.extensions.getByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension + every { project.extensions.getByType(LibraryExtension::class.java) } returns mockLibraryExtension + every { project.extensions.findByName("android") } returns mockAbstractAppExtension + every { project.projectDir } returns projectDir.toFile() + every { project.findProperty("flutter.sdk") } returns fakeFlutterSdkDir.toString() + every { project.file(fakeFlutterSdkDir.toString()) } returns fakeFlutterSdkDir.toFile() + val flutterExtension = FlutterExtension() + every { project.extensions.create("flutter", any>()) } returns flutterExtension + every { project.extensions.findByType(FlutterExtension::class.java) } returns flutterExtension + val mockBaseExtension = mockk(relaxed = true) + val mockCommonExtension = mockk>(relaxed = true) + val mockDebugBuildType = mockk(relaxed = true) + val mockReleaseBuildType = mockk(relaxed = true) + + // Cast our multi-interface mock instead of creating a brand new one + val mockApplicationExtension = mockAbstractAppExtension as ApplicationExtension + + // Mock buildTypes on our new dual-purpose mock so AgpCommonExtensionWrapper can read them + every { mockApplicationExtension.buildTypes.getByName("debug") } returns mockDebugBuildType + every { mockApplicationExtension.buildTypes.getByName("release") } returns mockReleaseBuildType + + // Keep the CommonExtension mocks just in case other parts of the plugin look for it + every { mockCommonExtension.buildTypes.getByName("debug") } returns mockDebugBuildType + every { mockCommonExtension.buildTypes.getByName("release") } returns mockReleaseBuildType + + every { project.extensions.findByType(BaseExtension::class.java) } returns mockBaseExtension + every { project.extensions.findByType(CommonExtension::class.java) } returns mockCommonExtension + + // Pass the dual-purpose mock for any ApplicationExtension lookups + every { project.extensions.findByType(ApplicationExtension::class.java) } returns mockApplicationExtension + every { project.extensions.getByType(ApplicationExtension::class.java) } returns mockApplicationExtension + + val mockApplicationDefaultConfig = + mockk( + moreInterfaces = arrayOf(ApplicationDefaultConfig::class), + relaxed = true + ) + every { mockApplicationExtension.defaultConfig } returns mockApplicationDefaultConfig + every { project.rootProject } returns project + every { project.state.failure as Throwable? } returns null + val mockDirectory = mockk(relaxed = true) + every { project.layout.buildDirectory.get() } returns mockDirectory + val mockAndroidSourceSet = mockk(relaxed = true) + val mockAndroidSourceDirectorySet = mockk(relaxed = true) + every { mockAndroidSourceSet.jniLibs.srcDir(any()) } returns mockAndroidSourceDirectorySet + every { mockAbstractAppExtension.sourceSets.getByName("main") } returns mockAndroidSourceSet + // mock return of NativePluginLoaderReflectionBridge.getPlugins + mockkObject(NativePluginLoaderReflectionBridge) + every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns + listOf() + // mock method calls that are invoked by the args to NativePluginLoaderReflectionBridge + every { project.extraProperties } returns mockk() + every { project.file(flutterExtension.source!!) } returns mockk() + + // MOCK generateLockfiles task ALREADY EXISTS: + val mockTask = mockk(relaxed = true) + val mockTaskContainer = mockk(relaxed = true) + every { project.tasks } returns mockTaskContainer + every { mockTaskContainer.findByName("generateLockfiles") } returns mockTask + + val flutterPlugin = FlutterPlugin() + flutterPlugin.apply(project) + + verify(exactly = 0) { mockTaskContainer.register("generateLockfiles", any()) } + verify { mockTaskContainer.register("javaVersion", any()) } + verify { mockTaskContainer.register("printBuildVariants", any()) } } @Test @@ -208,6 +309,7 @@ class FlutterPluginTest { // Set up the task container and our task capture val taskContainer = mockk(relaxed = true) every { project.tasks } returns taskContainer + every { taskContainer.findByName("generateLockfiles") } returns null val copyTaskActionCaptor = slot>() val copyTask = mockk(relaxed = true) val mockVariant = mockk(relaxed = true)