diff --git a/packages/flutter_tools/gradle/build.gradle.kts b/packages/flutter_tools/gradle/build.gradle.kts index 4486392a70..56d4629b9b 100644 --- a/packages/flutter_tools/gradle/build.gradle.kts +++ b/packages/flutter_tools/gradle/build.gradle.kts @@ -2,14 +2,33 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { `java-gradle-plugin` groovy + `kotlin-dsl` + kotlin("jvm") version "1.9.20" } group = "dev.flutter.plugin" version = "1.0.0" +// Optional: enable stricter validation, to ensure Gradle configuration is correct +tasks.validatePlugins { + enableStricterValidation.set(true) +} + +// We need to compile Kotlin first so we can call it from Groovy. See https://stackoverflow.com/q/36214437/7009800 +tasks.withType { + dependsOn(tasks.compileKotlin) + classpath += files(tasks.compileKotlin.get().destinationDirectory) +} + +tasks.classes { + dependsOn(tasks.compileGroovy) +} + gradlePlugin { plugins { // The "flutterPlugin" name isn't used anywhere. @@ -25,10 +44,28 @@ gradlePlugin { } } +tasks.withType { + options.release.set(11) +} + +tasks.test { + useJUnitPlatform() +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } +} + dependencies { // When bumping, also update: // * ndkVersion in FlutterExtension in packages/flutter_tools/gradle/src/main/groovy/flutter.groovy - // * AGP version in the buildscript block in packages/flutter_tools/gradle/src/main/kotlin/dependency_version_checker.gradle.kts + // * AGP version in the buildscript block in packages/flutter_tools/gradle/src/main/kotlin_scripts/dependency_version_checker.gradle.kts // * AGP version constants in packages/flutter_tools/lib/src/android/gradle_utils.dart - compileOnly("com.android.tools.build:gradle:7.3.0") + compileOnly("com.android.tools.build:gradle:8.7.3") + + testImplementation(kotlin("test")) + testImplementation("com.android.tools.build:gradle:8.7.3") + testImplementation("org.mockito:mockito-core:4.8.0") } diff --git a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy index b5edf1f98c..3d21b75ac0 100644 --- a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy +++ b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy @@ -4,6 +4,7 @@ // found in the LICENSE file. import com.android.build.OutputFile +import com.flutter.gradle.BaseApplicationNameHandler import groovy.json.JsonGenerator import groovy.xml.QName import java.nio.file.Paths @@ -299,7 +300,7 @@ class FlutterPlugin implements Plugin { if (!shouldSkipDependencyChecks) { try { final String dependencyCheckerPluginPath = Paths.get(flutterRoot.absolutePath, - "packages", "flutter_tools", "gradle", "src", "main", "kotlin", + "packages", "flutter_tools", "gradle", "src", "main", "kotlin_scripts", "dependency_version_checker.gradle.kts") project.apply from: dependencyCheckerPluginPath } catch (Exception e) { @@ -317,8 +318,8 @@ class FlutterPlugin implements Plugin { } } - // Use Kotlin DSL to handle baseApplicationName logic due to Groovy dynamic dispatch bug. - project.apply from: Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools", "gradle", "src", "main", "kotlin", "flutter.gradle.kts") + // Use Kotlin source to handle baseApplicationName logic due to Groovy dynamic dispatch bug. + BaseApplicationNameHandler.setBaseName(project) String flutterProguardRules = Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools", "gradle", "flutter_proguard_rules.pro") diff --git a/packages/flutter_tools/gradle/src/main/kotlin/BaseApplicationNameHandler.kt b/packages/flutter_tools/gradle/src/main/kotlin/BaseApplicationNameHandler.kt new file mode 100644 index 0000000000..60c94aa72f --- /dev/null +++ b/packages/flutter_tools/gradle/src/main/kotlin/BaseApplicationNameHandler.kt @@ -0,0 +1,28 @@ +package com.flutter.gradle + +import com.android.build.api.dsl.ApplicationExtension +import org.gradle.api.Project + +// TODO(gmackall): maybe migrate this to a package-level function when FGP conversion is done. +object BaseApplicationNameHandler { + internal const val DEFAULT_BASE_APPLICATION_NAME: String = "android.app.Application" + + internal const val GRADLE_BASE_APPLICATION_NAME_PROPERTY: String = "base-application-name" + + @JvmStatic fun setBaseName(project: Project) { + // Only set the base application name for apps, skip otherwise (LibraryExtension, DynamicFeatureExtension). + val androidComponentsExtension: ApplicationExtension = + project.extensions.findByType(ApplicationExtension::class.java) ?: return + + // Setting to android.app.Application is the same as omitting the attribute. + var baseApplicationName: String = DEFAULT_BASE_APPLICATION_NAME + + // Respect this property if it set by the Flutter tool. + if (project.hasProperty(GRADLE_BASE_APPLICATION_NAME_PROPERTY)) { + baseApplicationName = project.property(GRADLE_BASE_APPLICATION_NAME_PROPERTY).toString() + } + + androidComponentsExtension.defaultConfig.manifestPlaceholders["applicationName"] = + baseApplicationName + } +} diff --git a/packages/flutter_tools/gradle/src/main/kotlin/flutter.gradle.kts b/packages/flutter_tools/gradle/src/main/kotlin/flutter.gradle.kts deleted file mode 100644 index 9070d285ee..0000000000 --- a/packages/flutter_tools/gradle/src/main/kotlin/flutter.gradle.kts +++ /dev/null @@ -1,25 +0,0 @@ -// 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. - -apply() - -class FlutterPluginKts : Plugin { - override fun apply(project: Project) { - // Use withGroovyBuilder and getProperty() to access Groovy metaprogramming. - project.withGroovyBuilder { - getProperty("android").withGroovyBuilder { - getProperty("defaultConfig").withGroovyBuilder { - var baseApplicationName: String = "android.app.Application" - if (project.hasProperty("base-application-name")) { - baseApplicationName = project.property("base-application-name").toString() - } - // Setting to android.app.Application is the same as omitting the attribute. - getProperty("manifestPlaceholders").withGroovyBuilder { - setProperty("applicationName", baseApplicationName) - } - } - } - } - } -} diff --git a/packages/flutter_tools/gradle/src/main/kotlin/dependency_version_checker.gradle.kts b/packages/flutter_tools/gradle/src/main/kotlin_scripts/dependency_version_checker.gradle.kts similarity index 100% rename from packages/flutter_tools/gradle/src/main/kotlin/dependency_version_checker.gradle.kts rename to packages/flutter_tools/gradle/src/main/kotlin_scripts/dependency_version_checker.gradle.kts diff --git a/packages/flutter_tools/gradle/src/test/kotlin/BaseApplicationNameHandlerTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/BaseApplicationNameHandlerTest.kt new file mode 100644 index 0000000000..18604feb3c --- /dev/null +++ b/packages/flutter_tools/gradle/src/test/kotlin/BaseApplicationNameHandlerTest.kt @@ -0,0 +1,60 @@ +package com.flutter.gradle +import com.android.build.api.dsl.ApplicationDefaultConfig +import com.android.build.api.dsl.ApplicationExtension +import com.flutter.gradle.BaseApplicationNameHandler.GRADLE_BASE_APPLICATION_NAME_PROPERTY +import org.gradle.api.Project +import org.gradle.api.plugins.ExtensionContainer +import org.junit.jupiter.api.Assertions.assertEquals +import org.mockito.Mockito +import kotlin.test.Test + +class BaseApplicationNameHandlerTest { + @Test + fun `setBaseName respects Flutter tool property`() { + val baseApplicationNamePassedByFlutterTool = "toolSetBaseApplicationName" + + // Set up mocks. + val mockProject: Project = Mockito.mock(Project::class.java) + val mockAndroidComponentsExtension: ApplicationExtension = Mockito.mock(ApplicationExtension::class.java) + val mockExtensionContainer: ExtensionContainer = Mockito.mock(ExtensionContainer::class.java) + val mockDefaultConfig = Mockito.mock(ApplicationDefaultConfig::class.java) + val mockManifestPlaceholders = HashMap() + + Mockito.`when`(mockProject.hasProperty(GRADLE_BASE_APPLICATION_NAME_PROPERTY)).thenReturn(true) + Mockito.`when`(mockProject.property(GRADLE_BASE_APPLICATION_NAME_PROPERTY)).thenReturn(baseApplicationNamePassedByFlutterTool) + + Mockito.`when`(mockProject.extensions).thenReturn(mockExtensionContainer) + Mockito.`when`(mockExtensionContainer.findByType(ApplicationExtension::class.java)).thenReturn(mockAndroidComponentsExtension) + Mockito.`when`(mockAndroidComponentsExtension.defaultConfig).thenReturn(mockDefaultConfig) + Mockito.`when`(mockDefaultConfig.manifestPlaceholders).thenReturn(mockManifestPlaceholders) + + // Call the base name handler. + BaseApplicationNameHandler.setBaseName(mockProject) + + // Make sure we set the value passed by the tool. + assertEquals(mockManifestPlaceholders["applicationName"], baseApplicationNamePassedByFlutterTool) + } + + @Test + fun `setBaseName defaults to correct value`() { + // Set up mocks. + val mockProject: Project = Mockito.mock(Project::class.java) + val mockAndroidComponentsExtension: ApplicationExtension = Mockito.mock(ApplicationExtension::class.java) + val mockExtensionContainer: ExtensionContainer = Mockito.mock(ExtensionContainer::class.java) + val mockDefaultConfig = Mockito.mock(ApplicationDefaultConfig::class.java) + val mockManifestPlaceholders = HashMap() + + Mockito.`when`(mockProject.hasProperty(GRADLE_BASE_APPLICATION_NAME_PROPERTY)).thenReturn(false) + + Mockito.`when`(mockProject.extensions).thenReturn(mockExtensionContainer) + Mockito.`when`(mockExtensionContainer.findByType(ApplicationExtension::class.java)).thenReturn(mockAndroidComponentsExtension) + Mockito.`when`(mockAndroidComponentsExtension.defaultConfig).thenReturn(mockDefaultConfig) + Mockito.`when`(mockDefaultConfig.manifestPlaceholders).thenReturn(mockManifestPlaceholders) + + // Call the base name handler. + BaseApplicationNameHandler.setBaseName(mockProject) + + // Make sure we default to the correct value. + assertEquals(mockManifestPlaceholders["applicationName"], BaseApplicationNameHandler.DEFAULT_BASE_APPLICATION_NAME) + } +} diff --git a/packages/flutter_tools/test/integration.shard/android_run_flutter_gradle_plugin_tests_test.dart b/packages/flutter_tools/test/integration.shard/android_run_flutter_gradle_plugin_tests_test.dart new file mode 100644 index 0000000000..c21fdd4ff9 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/android_run_flutter_gradle_plugin_tests_test.dart @@ -0,0 +1,42 @@ +// 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:io'; + +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/process.dart'; +import 'package:flutter_tools/src/globals.dart' as globals; + +import '../src/common.dart'; +import '../src/context.dart'; +import 'test_utils.dart'; + +void main() { + testUsingContext('Flutter Gradle Plugin unit tests pass', () async { + final String gradleFileName = Platform.isWindows ? 'gradlew.bat' : 'gradlew'; + final String gradleExecutable = Platform.isWindows ? '.\\$gradleFileName' : './$gradleFileName'; + final Directory flutterGradlePluginDirectory = fileSystem + .directory(getFlutterRoot()) + .childDirectory('packages') + .childDirectory('flutter_tools') + .childDirectory('gradle'); + globals.gradleUtils?.injectGradleWrapperIfNeeded(flutterGradlePluginDirectory); + makeExecutable(flutterGradlePluginDirectory.childFile(gradleFileName)); + final RunResult runResult = await globals.processUtils.run([ + gradleExecutable, + 'test', + ], workingDirectory: flutterGradlePluginDirectory.path); + expect(runResult.exitCode, 0); + }); +} + +void makeExecutable(File file) { + if (Platform.isWindows) { + // no op. + return; + } + + final ProcessResult result = processManager.runSync(['chmod', '+x', file.path]); + expect(result, const ProcessResultMatcher()); +}