Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,25 @@ class AgpCommonExtensionWrapper(
}
}

var compileSdkPreview: String?
get() =
when (backingExtension) {
is ApplicationExtension -> backingExtension.compileSdkPreview
is LibraryExtension -> backingExtension.compileSdkPreview
is DynamicFeatureExtension -> backingExtension.compileSdkPreview
is TestExtension -> backingExtension.compileSdkPreview
else -> throw IllegalArgumentException(unsupportedMessage())
}
set(value) {
when (backingExtension) {
is ApplicationExtension -> backingExtension.compileSdkPreview = value
is LibraryExtension -> backingExtension.compileSdkPreview = value
is DynamicFeatureExtension -> backingExtension.compileSdkPreview = value
is TestExtension -> backingExtension.compileSdkPreview = value
else -> throw IllegalArgumentException(unsupportedMessage())
}
}

var namespace: String?
get() =
when (backingExtension) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -528,18 +528,21 @@ 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
internal fun getConfiguredNdkVersion(project: Project): String? = getAndroidExtension(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,
* e.g. `android-UpsideDownCake`.
* Returns the compile SDK configured on the project's Android extension: the numeric
* API level (`compileSdk = 36`) or a preview codename (`compileSdkPreview = "Baklava"`).
*/
@JvmStatic
@JvmName("getCompileSdkFromProject")
internal fun getCompileSdkFromProject(project: Project): String = getLegacyAndroidExtension(project).compileSdkVersion!!.substring(8)
internal fun getCompileSdkFromProject(project: Project): CompileSdkVersion {
val androidExtension = getAndroidExtension(project)
return CompileSdkVersion(
apiLevel = androidExtension.compileSdk,
previewCodename = androidExtension.compileSdkPreview
)
}

/**
* Returns:
Expand Down
48 changes: 38 additions & 10 deletions packages/flutter_tools/gradle/src/main/kotlin/VersionFetcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package com.flutter.gradle

import com.android.build.api.AndroidPluginVersion
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.gradle.internal.utils.getKotlinAndroidPluginVersion
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper
Expand Down Expand Up @@ -42,19 +41,16 @@ internal object VersionFetcher {
}

/**
* Returns the version of the Kotlin Gradle plugin.
* Returns the version of the Kotlin Gradle plugin, or null if it cannot be determined.
*
* Null is an expected result when the Kotlin Gradle plugin has not been applied to the
* project — most notably under AGP's built-in Kotlin support (`android.builtInKotlin`),
* where there is no standalone KGP. Callers must treat null as "unknown/not applied",
* not as an error.
*/
internal fun getKGPVersion(project: Project): Version? {
// AGP and Kgp have methods for getting kotlin version.
// AGP's method is internal, we try to use it anyway.
// KGP's version in org.jetbrains.kotlin.gradle.plugin.DefaultKotlinBasePlugin is not
// available when this method is called.
// When testing call `setAgpKotlinVersionToNull(project)`.
val agpDefinedKgpVersion = getKotlinAndroidPluginVersion(project)
if (agpDefinedKgpVersion != null && agpDefinedKgpVersion != "unknown") {
return Version.fromString(agpDefinedKgpVersion)
}

val kotlinVersionProperty = "kotlin_version"
val firstKotlinVersionFieldName = "pluginVersion"
val secondKotlinVersionFieldName = "kotlinPluginVersion"
Expand Down Expand Up @@ -127,3 +123,35 @@ internal class Version(

override fun toString(): String = "$major.$minor.$patch"
}

/**
* The compile SDK configured on a project's Android extension: either a numeric API level
* (`compileSdk = 36`) or a preview codename (`compileSdkPreview = "Baklava"`). Both are null
* when the DSL has not been configured (yet).
*/
internal data class CompileSdkVersion(
val apiLevel: Int?,
val previewCodename: String?
) {
/**
* Whether this compile SDK is known to be higher than [other].
*
* - numeric vs numeric: numeric comparison.
* - preview vs numeric: a preview codename targets an unreleased SDK, so it is
* considered higher than any numeric API level.
* - preview vs preview: codenames stopped being alphabetically ordered when the
* alphabet reset at "Baklava", so distinct codenames are incomparable and this
* returns false rather than guessing.
* - if either side is unset, returns false.
*/
fun isHigherThan(other: CompileSdkVersion): Boolean =
when {
previewCodename != null && other.previewCodename != null -> false
previewCodename != null && other.apiLevel != null -> true
apiLevel != null && other.apiLevel != null -> apiLevel > other.apiLevel
else -> false
}

/** The human-readable form used in log messages, e.g. "35" or "Baklava". */
override fun toString(): String = previewCodename ?: apiLevel?.toString() ?: "unknown"
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package com.flutter.gradle.plugins

import com.android.builder.model.BuildType
import com.flutter.gradle.CompileSdkVersion
import com.flutter.gradle.FlutterExtension
import com.flutter.gradle.FlutterPluginUtils
import com.flutter.gradle.FlutterPluginUtils.addApiDependencies
Expand Down Expand Up @@ -121,12 +122,9 @@ class PluginHandler(
// Wait until the Android plugin loaded.
pluginProject.afterEvaluate {
// Checks if there is a mismatch between the plugin compileSdkVersion and the project compileSdkVersion.
val projectCompileSdkVersion: String = getCompileSdkFromProject(project)
val pluginCompileSdkVersion: String = getCompileSdkFromProject(pluginProject)
// TODO(gmackall): This is doing a string comparison, which is odd and also can be wrong
// when comparing preview versions (against non preview, and also in the
// case of alphabet reset which happened with "Baklava".
if (pluginCompileSdkVersion > projectCompileSdkVersion) {
val projectCompileSdkVersion: CompileSdkVersion = getCompileSdkFromProject(project)
val pluginCompileSdkVersion: CompileSdkVersion = getCompileSdkFromProject(pluginProject)
if (pluginCompileSdkVersion.isHigherThan(projectCompileSdkVersion)) {
project.logger.quiet(
"Warning: The plugin $pluginName requires Android SDK version $pluginCompileSdkVersion or higher."
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import com.flutter.gradle.DependencyVersionChecker.warnAGPVersion
import com.flutter.gradle.DependencyVersionChecker.warnGradleVersion
import com.flutter.gradle.DependencyVersionChecker.warnKGPVersion
import com.flutter.gradle.DependencyVersionChecker.warnMinSdkVersion
import com.flutter.gradle.testing.setAgpKotlinVersionToNull
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
Expand Down Expand Up @@ -499,7 +498,6 @@ private object MockProjectFactory {
}
return@answers Unit
}
setAgpKotlinVersionToNull(mockProject)

return mockProject
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -616,9 +616,25 @@ class FlutterPluginUtilsTest {
@Test
fun `getCompileSdkFromProject returns the compileSdk from the project`() {
val project = mockk<Project>()
every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35"
val androidExtension = mockk<com.android.build.api.dsl.ApplicationExtension>()
every { project.extensions.findByName("android") } returns androidExtension
every { androidExtension.compileSdk } returns 35
every { androidExtension.compileSdkPreview } returns null
val result = FlutterPluginUtils.getCompileSdkFromProject(project)
assertEquals("35", result)
assertEquals(CompileSdkVersion(apiLevel = 35, previewCodename = null), result)
assertEquals("35", result.toString())
}

@Test
fun `getCompileSdkFromProject returns the preview codename from the project`() {
val project = mockk<Project>()
val androidExtension = mockk<com.android.build.api.dsl.ApplicationExtension>()
every { project.extensions.findByName("android") } returns androidExtension
every { androidExtension.compileSdk } returns null
every { androidExtension.compileSdkPreview } returns "Baklava"
val result = FlutterPluginUtils.getCompileSdkFromProject(project)
assertEquals(CompileSdkVersion(apiLevel = null, previewCodename = "Baklava"), result)
assertEquals("Baklava", result.toString())
}

@Test
Expand Down Expand Up @@ -1966,7 +1982,9 @@ class FlutterPluginUtilsTest {
every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns ""
every { project.gradle.startParameter.taskNames } returns emptyList()
every { project.gradle.startParameter.isOffline } returns false
every { project.extensions.findByType(ApplicationExtension::class.java) } returns null
val mockAndroidExtension = mockk<ApplicationExtension>()
every { project.extensions.findByName("android") } returns mockAndroidExtension
every { mockAndroidExtension.ndkVersion } returns "29.0.13846066"
every { project.serviceOf<ExecOperations>() } returns mockExecOperations
every { mockExecOperations.exec(capture(execActionSlot)) } answers {
File(tempDir.toFile(), "ndk/29.0.13846066/source.properties").apply {
Expand Down Expand Up @@ -2016,7 +2034,9 @@ class FlutterPluginUtilsTest {
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
val mockAndroidExtension = mockk<ApplicationExtension>()
every { project.extensions.findByName("android") } returns mockAndroidExtension
every { mockAndroidExtension.ndkVersion } returns "29.0.13846066"

FlutterPluginUtils.forceNdkDownload(project, "/base/path")
finalizeDslSlot.captured.invoke(Any())
Expand Down Expand Up @@ -2104,7 +2124,9 @@ class FlutterPluginUtilsTest {
} returns "26.3.11579264"
every { project.gradle.startParameter.taskNames } returns emptyList()
every { project.gradle.startParameter.isOffline } returns false
every { project.extensions.findByType(ApplicationExtension::class.java) } returns null
val mockAndroidExtension = mockk<ApplicationExtension>()
every { project.extensions.findByName("android") } returns mockAndroidExtension
every { mockAndroidExtension.ndkVersion } answers { configuredNdkVersion }
every { project.serviceOf<ExecOperations>() } returns mockExecOperations
every { mockExecOperations.exec(capture(execActionSlot)) } answers {
File(tempDir.toFile(), "ndk/27.3.13750724/source.properties").apply {
Expand Down Expand Up @@ -2166,6 +2188,7 @@ class FlutterPluginUtilsTest {
)
}
every { mockApplicationExtension.ndkVersion } answers { configuredNdkVersion }
every { project.extensions.findByName("android") } returns mockApplicationExtension
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()
Expand Down Expand Up @@ -2225,6 +2248,9 @@ class FlutterPluginUtilsTest {
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()
val mockAndroidExtension = mockk<ApplicationExtension>()
every { project.extensions.findByName("android") } returns mockAndroidExtension
every { mockAndroidExtension.ndkVersion } returns "29.0.13846066"

FlutterPluginUtils.forceNdkDownload(project, "/base/path")
finalizeDslSlot.captured.invoke(Any())
Expand Down Expand Up @@ -2252,6 +2278,7 @@ class FlutterPluginUtilsTest {
throw AssertionError("legacy ndkVersion should not be read when ApplicationExtension is available")
}
every { mockApplicationExtension.ndkVersion } returns "29.0.13846066"
every { project.extensions.findByName("android") } returns mockApplicationExtension
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"
Expand Down Expand Up @@ -2289,7 +2316,9 @@ class FlutterPluginUtilsTest {
every { project.findProperty(FlutterPluginUtils.PROP_INSTALLED_NDK_VERSIONS) } returns ""
every { project.gradle.startParameter.taskNames } returns emptyList()
every { project.gradle.startParameter.isOffline } returns false
every { project.extensions.findByType(ApplicationExtension::class.java) } returns null
val mockAndroidExtension = mockk<ApplicationExtension>()
every { project.extensions.findByName("android") } returns mockAndroidExtension
every { mockAndroidExtension.ndkVersion } returns "29.0.13846066"
every { project.serviceOf<ExecOperations>() } returns mockExecOperations
every { mockExecOperations.exec(any<Action<ExecSpec>>()) } returns mockExecResult
every { mockExecResult.assertNormalExitValue() } returns mockExecResult
Expand Down Expand Up @@ -2496,7 +2525,7 @@ class FlutterPluginUtilsTest {
val project = mockk<Project>(relaxed = true)
val androidExtension = mockk<ApplicationExtension>()
every { androidExtension.ndkVersion } returns "29.0.13846066"
every { project.extensions.findByType(ApplicationExtension::class.java) } returns androidExtension
every { project.extensions.findByName("android") } returns androidExtension
every { project.tasks.register(any(), eq(PrintTask::class.java), any()) } returns mockk()
val captureSlot = slot<Action<PrintTask>>()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package com.flutter.gradle

import com.android.build.api.AndroidPluginVersion
import com.android.build.api.variant.AndroidComponentsExtension
import com.flutter.gradle.testing.setAgpKotlinVersionToNull
import io.mockk.every
import io.mockk.mockk
import org.gradle.api.Project
Expand Down Expand Up @@ -47,7 +46,6 @@ class VersionFetcherTest {
fun `getKGPVersion returns version when kotlin_version is set`() {
val kgpVersion = Version(1, 9, 20)
val project = mockk<Project>()
setAgpKotlinVersionToNull(project)
every { project.hasProperty(eq("kotlin_version")) } returns true
every { project.properties["kotlin_version"] } returns kgpVersion.toString()
val result = VersionFetcher.getKGPVersion(project)
Expand All @@ -58,7 +56,6 @@ class VersionFetcherTest {
fun `getKGPVersion returns version from KotlinAndroidPluginWrapper`() {
val kgpVersion = Version(1, 9, 20)
val project = mockk<Project>()
setAgpKotlinVersionToNull(project)
every { project.hasProperty(eq("kotlin_version")) } returns false
every { project.plugins.findPlugin(KotlinAndroidPluginWrapper::class.java) } returns
mockk<KotlinAndroidPluginWrapper> {
Expand All @@ -67,4 +64,51 @@ class VersionFetcherTest {
val result = VersionFetcher.getKGPVersion(project)
assertEquals(kgpVersion, result!!)
}

@Test
fun `getKGPVersion returns null when the Kotlin Gradle plugin is absent`() {
// Expected under AGP's built-in Kotlin support, where no standalone KGP is applied.
val project = mockk<Project>()
every { project.hasProperty(eq("kotlin_version")) } returns false
every { project.plugins.findPlugin(KotlinAndroidPluginWrapper::class.java) } returns null
val result = VersionFetcher.getKGPVersion(project)
assertEquals(null, result)
}

// CompileSdkVersion.isHigherThan
@Test
fun `isHigherThan compares numeric api levels numerically`() {
val sdk35 = CompileSdkVersion(apiLevel = 35, previewCodename = null)
val sdk36 = CompileSdkVersion(apiLevel = 36, previewCodename = null)
assertEquals(true, sdk36.isHigherThan(sdk35))
assertEquals(false, sdk35.isHigherThan(sdk36))
assertEquals(false, sdk35.isHigherThan(sdk35))
}

@Test
fun `isHigherThan treats a preview codename as higher than any numeric api level`() {
val preview = CompileSdkVersion(apiLevel = null, previewCodename = "Baklava")
val numeric = CompileSdkVersion(apiLevel = 36, previewCodename = null)
assertEquals(true, preview.isHigherThan(numeric))
assertEquals(false, numeric.isHigherThan(preview))
}

@Test
fun `isHigherThan treats distinct preview codenames as incomparable`() {
// Codenames stopped being alphabetically ordered at the "Baklava" alphabet reset,
// so neither side may claim to be higher.
val baklava = CompileSdkVersion(apiLevel = null, previewCodename = "Baklava")
val vanilla = CompileSdkVersion(apiLevel = null, previewCodename = "VanillaIceCream")
assertEquals(false, baklava.isHigherThan(vanilla))
assertEquals(false, vanilla.isHigherThan(baklava))
assertEquals(false, baklava.isHigherThan(baklava))
}

@Test
fun `isHigherThan returns false when either side is unset`() {
val unset = CompileSdkVersion(apiLevel = null, previewCodename = null)
val numeric = CompileSdkVersion(apiLevel = 36, previewCodename = null)
assertEquals(false, unset.isHigherThan(numeric))
assertEquals(false, numeric.isHigherThan(unset))
}
}
Loading