Skip to content

Commit

Permalink
[gradle] Support new AGP with androidLibrary target (#5157)
Browse files Browse the repository at this point in the history
Since AGP `8.8.0-alpha08` there was added support generated assets in
the new `androidLibrary` target.
We have to support a new target configuration and work with compose
multiplatform resources

Fixes https://youtrack.jetbrains.com/issue/CMP-6982

## Testing
- Added gradle tests

## Release Notes
### Features - Gradle Plugin
- Support compose resources in `androidLibrary` target
  • Loading branch information
terrakok authored Oct 30, 2024
1 parent c52b4a9 commit 5e60212
Show file tree
Hide file tree
Showing 21 changed files with 300 additions and 55 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/gradle-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-20.04, macos-14, windows-2022]
gradle: [7.4, 8.8]
agp: [8.1.0, 8.5.0]
gradle: [7.4, 8.10.2]
agp: [8.1.0, 8.5.0, 8.8.0-alpha08]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
Expand Down
19 changes: 11 additions & 8 deletions gradle-plugins/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import com.gradle.publish.PluginBundleExtension
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile

plugins {
Expand All @@ -20,8 +21,8 @@ subprojects {

plugins.withId("java") {
configureIfExists<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11

withJavadocJar()
withSourcesJar()
Expand All @@ -30,11 +31,13 @@ subprojects {

plugins.withId("org.jetbrains.kotlin.jvm") {
tasks.withType(KotlinJvmCompile::class).configureEach {
// must be set to a language version of the kotlin compiler & runtime,
// which is bundled to the oldest supported Gradle
kotlinOptions.languageVersion = "1.5"
kotlinOptions.apiVersion = "1.5"
kotlinOptions.jvmTarget = "1.8"
compilerOptions {
// must be set to a language version of the kotlin compiler & runtime,
// which is bundled to the oldest supported Gradle
languageVersion.set(KotlinVersion.KOTLIN_1_5)
apiVersion.set(KotlinVersion.KOTLIN_1_5)
jvmTarget.set(JvmTarget.JVM_11)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package org.jetbrains.compose.resources

import com.android.build.api.dsl.KotlinMultiplatformAndroidTarget
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.Component
import com.android.build.api.variant.HasAndroidTest
import com.android.build.api.variant.KotlinMultiplatformAndroidComponentsExtension
import com.android.build.api.variant.Sources
import com.android.build.gradle.internal.lint.AndroidLintAnalysisTask
import com.android.build.gradle.internal.lint.LintModelWriterTask
import org.gradle.api.DefaultTask
Expand All @@ -25,37 +27,130 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget
import java.io.File
import javax.inject.Inject

internal fun Project.configureAndroidComposeResources(moduleResourceDir: Provider<File>? = null) {
//copy all compose resources to android assets
val androidComponents = project.extensions.findByType(AndroidComponentsExtension::class.java) ?: return
//copy all compose resources to android assets
internal fun Project.configureAndroidComposeResources(
agpPluginId: String,
moduleResourceDir: Provider<File>? = null
) {
val kotlinExtension = extensions.findByType(KotlinMultiplatformExtension::class.java) ?: return

if (agpPluginId != AGP_KMP_LIB_ID) {
extensions.findByType(AndroidComponentsExtension::class.java)?.let { androidComponents ->
configureAndroidComposeResources(kotlinExtension, androidComponents, moduleResourceDir)
}
} else {
@Suppress("UnstableApiUsage")
extensions.findByType(KotlinMultiplatformAndroidComponentsExtension::class.java)?.let { androidComponents ->
configureAndroidComposeResources(kotlinExtension, androidComponents, moduleResourceDir)
}
}
}

private fun Project.configureAndroidComposeResources(
kotlinExtension: KotlinMultiplatformExtension,
androidComponents: AndroidComponentsExtension<*, *, *>,
moduleResourceDir: Provider<File>?
) {
logger.info("Configure compose resources with AndroidComponentsExtension")
androidComponents.onVariants { variant ->
configureGeneratedAndroidComponentAssets(variant, moduleResourceDir)
val componentAssets = getAndroidComponentComposeResources(kotlinExtension, variant.name)
configureGeneratedAndroidComponentAssets(
variant.name,
variant.sources,
componentAssets,
moduleResourceDir
)

if (variant is HasAndroidTest) {
variant.androidTest?.let { androidTest ->
configureGeneratedAndroidComponentAssets(androidTest, moduleResourceDir)
val androidTestAssets = getAndroidComponentComposeResources(kotlinExtension, androidTest.name)
configureGeneratedAndroidComponentAssets(
androidTest.name,
androidTest.sources,
androidTestAssets,
moduleResourceDir
)
}
}
}
}

private fun Project.getAndroidComponentComposeResources(
kotlinExtension: KotlinMultiplatformExtension,
componentName: String
): FileCollection = project.files({
kotlinExtension.targets.withType(KotlinAndroidTarget::class.java).flatMap { androidTarget ->
androidTarget.compilations.flatMap { compilation ->
if (compilation.androidVariant.name == componentName) {
compilation.allKotlinSourceSets.map { kotlinSourceSet ->
getPreparedComposeResourcesDir(kotlinSourceSet)
}
} else emptyList()
}
}
})

@Suppress("UnstableApiUsage")
private fun Project.configureAndroidComposeResources(
kotlinExtension: KotlinMultiplatformExtension,
androidComponents: KotlinMultiplatformAndroidComponentsExtension,
moduleResourceDir: Provider<File>?
) {
logger.info("Configure compose resources with KotlinMultiplatformAndroidComponentsExtension")
androidComponents.onVariant { variant ->
val variantAssets = getAndroidKmpComponentComposeResources(kotlinExtension, variant.name)
configureGeneratedAndroidComponentAssets(
variant.name,
variant.sources,
variantAssets,
moduleResourceDir
)

variant.androidTest?.let { androidTest ->
val androidTestAssets = getAndroidKmpComponentComposeResources(kotlinExtension, androidTest.name)
configureGeneratedAndroidComponentAssets(
androidTest.name,
androidTest.sources,
androidTestAssets,
moduleResourceDir
)
}
}
}

@Suppress("UnstableApiUsage")
private fun Project.getAndroidKmpComponentComposeResources(
kotlinExtension: KotlinMultiplatformExtension,
componentName: String
): FileCollection = project.files({
kotlinExtension.targets.withType(KotlinMultiplatformAndroidTarget::class.java).flatMap { androidTarget ->
androidTarget.compilations.flatMap { compilation ->
if (compilation.componentName == componentName) {
compilation.allKotlinSourceSets.map { kotlinSourceSet ->
getPreparedComposeResourcesDir(kotlinSourceSet)
}
} else emptyList()
}
}
})

private fun Project.configureGeneratedAndroidComponentAssets(
component: Component,
componentName: String,
componentSources: Sources,
componentAssets: FileCollection,
moduleResourceDir: Provider<File>?
) {
val kotlinExtension = project.extensions.findByType(KotlinMultiplatformExtension::class.java) ?: return
val camelComponentName = component.name.uppercaseFirstChar()
val componentAssets = getAndroidComponentComposeResources(kotlinExtension, component.name)
logger.info("Configure ${component.name} resources for 'android' target")
logger.info("Configure $componentName resources for 'android' target")

val camelComponentName = componentName.uppercaseFirstChar()
val copyComponentAssets = registerTask<CopyResourcesToAndroidAssetsTask>(
"copy${camelComponentName}ComposeResourcesToAndroidAssets"
) {
from.set(componentAssets)
moduleResourceDir?.let { relativeResourcePlacement.set(it) }
}

component.sources.assets?.addGeneratedSourceDirectory(
componentSources.assets?.addGeneratedSourceDirectory(
copyComponentAssets,
CopyResourcesToAndroidAssetsTask::outputDirectory
)
Expand All @@ -71,21 +166,6 @@ private fun Project.configureGeneratedAndroidComponentAssets(
}
}

private fun Project.getAndroidComponentComposeResources(
kotlinExtension: KotlinMultiplatformExtension,
componentName: String
): FileCollection = project.files({
kotlinExtension.targets.withType(KotlinAndroidTarget::class.java).flatMap { androidTarget ->
androidTarget.compilations.flatMap { compilation ->
if (compilation.androidVariant.name == componentName) {
compilation.allKotlinSourceSets.map { kotlinSourceSet ->
getPreparedComposeResourcesDir(kotlinSourceSet)
}
} else emptyList()
}
}
})

//Copy task doesn't work with 'variant.sources?.assets?.addGeneratedSourceDirectory' API
internal abstract class CopyResourcesToAndroidAssetsTask : DefaultTask() {
@get:Inject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ internal const val COMPOSE_RESOURCES_DIR = "composeResources"
internal const val RES_GEN_DIR = "generated/compose/resourceGenerator"
internal const val KMP_RES_EXT = "multiplatformResourcesPublication"
private const val MIN_GRADLE_VERSION_FOR_KMP_RESOURCES = "7.6"
private val androidPluginIds = listOf(
"com.android.application",
"com.android.library"
)
private const val AGP_APP_ID = "com.android.application"
private const val AGP_LIB_ID = "com.android.library"
internal const val AGP_KMP_LIB_ID = "com.android.kotlin.multiplatform.library"

internal fun Project.configureComposeResources(extension: ResourcesExtension) {
val config = provider { extension }
Expand Down Expand Up @@ -64,10 +63,8 @@ internal fun Project.onKotlinJvmApplied(config: Provider<ResourcesExtension>) {
configureJvmOnlyResources(kotlinExtension, config)
}

internal fun Project.onAgpApplied(block: () -> Unit) {
androidPluginIds.forEach { pluginId ->
plugins.withId(pluginId) {
block()
}
internal fun Project.onAgpApplied(block: (pluginId: String) -> Unit) {
listOf(AGP_APP_ID, AGP_LIB_ID, AGP_KMP_LIB_ID).forEach { pluginId ->
plugins.withId(pluginId) { block(pluginId) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ internal fun Project.configureMultimoduleResources(
.all { target -> configureTargetResources(target, moduleIsolationDirectory) }

//configure ANDROID resources
onAgpApplied {
configureAndroidComposeResources(moduleIsolationDirectory)
onAgpApplied { agpId ->
configureAndroidComposeResources(agpId, moduleIsolationDirectory)
fixAndroidLintTaskDependencies()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ internal fun Project.configureSinglemoduleResources(
}
}

onAgpApplied {
configureAndroidComposeResources()
onAgpApplied { agpId ->
configureAndroidComposeResources(agpId)
fixAndroidLintTaskDependencies()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import org.gradle.util.GradleVersion
import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.PreviewLogger
import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.RemoteConnection
import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.receiveConfigFromGradle
import org.jetbrains.compose.internal.Version
import org.jetbrains.compose.test.utils.GradlePluginTestBase
import org.jetbrains.compose.test.utils.checkExists
import org.jetbrains.compose.test.utils.checks
Expand Down Expand Up @@ -77,7 +78,8 @@ class GradlePluginTest : GradlePluginTestBase() {

@Test
fun newAndroidTarget() {
Assumptions.assumeTrue(defaultTestEnvironment.parsedGradleVersion >= GradleVersion.version("8.0.0"))
Assumptions.assumeTrue(defaultTestEnvironment.parsedGradleVersion >= GradleVersion.version("8.10.2"))
Assumptions.assumeTrue(Version.fromString(defaultTestEnvironment.agpVersion) >= Version.fromString("8.8.0-alpha08"))
with(testProject("application/newAndroidTarget")) {
gradle("build", "--dry-run").checks {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.jetbrains.compose.test.tests.integration

import org.gradle.util.GradleVersion
import org.jetbrains.compose.desktop.application.internal.ComposeProperties
import org.jetbrains.compose.internal.Version
import org.jetbrains.compose.internal.utils.Arch
import org.jetbrains.compose.internal.utils.OS
import org.jetbrains.compose.internal.utils.currentArch
Expand Down Expand Up @@ -315,6 +316,27 @@ class ResourcesTest : GradlePluginTestBase() {
}
}

@Test
fun testNewAgpResources() {
Assumptions.assumeTrue(defaultTestEnvironment.parsedGradleVersion >= GradleVersion.version("8.10.2"))
Assumptions.assumeTrue(Version.fromString(defaultTestEnvironment.agpVersion) >= Version.fromString("8.8.0-alpha08"))

with(testProject("misc/newAgpResources", defaultTestEnvironment)) {
gradle(":appModule:assembleDebug").checks {
check.logContains("Configure compose resources with KotlinMultiplatformAndroidComponentsExtension")

val resourcesFiles = sequenceOf(
"assets/composeResources/newagpresources.appmodule.generated.resources/values/strings.commonMain.cvr",
"assets/composeResources/newagpresources.featuremodule.generated.resources/values/strings.commonMain.cvr"
)
val apk = file("appModule/build/outputs/apk/debug/appModule-debug.apk")

//isAndroid = false, because the new AGP has an issue with duplicate resources for now
checkResourcesZip(apk, resourcesFiles, false)
}
}
}

@Test
fun testDisableMultimoduleResourcesWithNewKotlin() {
with(testProject("misc/kmpResourcePublication")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pluginManagement {
id 'org.jetbrains.kotlin.multiplatform' version 'KOTLIN_VERSION_PLACEHOLDER'
id 'org.jetbrains.kotlin.plugin.compose' version 'KOTLIN_VERSION_PLACEHOLDER'
id 'org.jetbrains.compose' version 'COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER'
id 'com.android.kotlin.multiplatform.library' version '8.2.0-alpha13'
id 'com.android.kotlin.multiplatform.library' version 'AGP_VERSION_PLACEHOLDER'
}
repositories {
mavenLocal()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
plugins {
id("org.jetbrains.compose")
kotlin("multiplatform")
kotlin("plugin.compose")
id("com.android.application")
}

kotlin {
jvmToolchain(11)
androidTarget()
jvm()

sourceSets {
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.material3)
implementation(compose.components.resources)
implementation(project(":featureModule"))
}

jvmMain.dependencies {
implementation(compose.desktop.currentOs)
}
}
}

android {
namespace = "me.sample.app"
compileSdk = 35
defaultConfig {
applicationId = "org.example.project"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
<application/>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<resources>
<string name="str_1">App text str_1</string>
</resources>
Loading

0 comments on commit 5e60212

Please sign in to comment.