diff --git a/gradle-plugins/buildSrc/src/main/kotlin/BuildProperties.kt b/gradle-plugins/buildSrc/src/main/kotlin/BuildProperties.kt index 70c123b125d..d0ed87b3d74 100644 --- a/gradle-plugins/buildSrc/src/main/kotlin/BuildProperties.kt +++ b/gradle-plugins/buildSrc/src/main/kotlin/BuildProperties.kt @@ -23,4 +23,6 @@ object BuildProperties { fun deployVersion(project: Project): String = System.getenv("COMPOSE_GRADLE_PLUGIN_VERSION") ?: project.findProperty("deploy.version") as String + fun hotReloadVersion(project: Project): String = + project.findProperty("hotreload.version") as String } diff --git a/gradle-plugins/compose/build.gradle.kts b/gradle-plugins/compose/build.gradle.kts index fdc3022922c..fa6e630380e 100644 --- a/gradle-plugins/compose/build.gradle.kts +++ b/gradle-plugins/compose/build.gradle.kts @@ -1,5 +1,9 @@ +import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext import de.undercouch.gradle.tasks.download.Download +import org.apache.tools.zip.ZipEntry +import org.apache.tools.zip.ZipOutputStream import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask plugins { @@ -25,12 +29,16 @@ mavenPublicationConfig { val buildConfigDir get() = project.layout.buildDirectory.dir("generated/buildconfig") + +val hotReloadVersion = BuildProperties.hotReloadVersion(project) + val buildConfig = tasks.register("buildConfig", GenerateBuildConfig::class.java) { classFqName.set("org.jetbrains.compose.ComposeBuildConfig") generatedOutputDir.set(buildConfigDir) fieldsToGenerate.put("composeVersion", BuildProperties.composeVersion(project)) fieldsToGenerate.put("composeMaterial3Version", BuildProperties.composeMaterial3Version(project)) fieldsToGenerate.put("composeGradlePluginVersion", BuildProperties.deployVersion(project)) + fieldsToGenerate.put("composeHotReloadVersion", hotReloadVersion) } tasks.named("compileKotlin", KotlinCompilationTask::class) { dependsOn(buildConfig) @@ -56,6 +64,10 @@ dependencies { embeddedDependencies(dep) } + fun hotReloadDep(dep: String) = embedded( + "org.jetbrains.compose.hot-reload:$dep:$hotReloadVersion" + ) + compileOnly(gradleApi()) compileOnly(localGroovy()) compileOnly(kotlin("gradle-plugin")) @@ -69,21 +81,75 @@ dependencies { embedded(libs.download.task) embedded(libs.kotlin.poet) + hotReloadDep("hot-reload-gradle-plugin") + hotReloadDep("hot-reload-gradle-core") + hotReloadDep("hot-reload-gradle-idea") + hotReloadDep("hot-reload-core") + hotReloadDep("hot-reload-orchestration") + hotReloadDep("hot-reload-annotations-jvm") embedded(project(":preview-rpc")) embedded(project(":jdk-version-probe")) } + val packagesToRelocate = listOf("de.undercouch", "com.squareup.kotlinpoet") +val relocationPackage = "org.jetbrains.compose.internal" + +val hotReloadPackage = "org.jetbrains.compose.reload" + +val hotReloadPackageRelocated = "$relocationPackage.$hotReloadPackage" + +val hotReloadPropertiesPath = "META-INF/gradle-plugins/org.jetbrains.compose.hot-reload.properties" + +private class HotReloadPropertiesTransformer(hotReloadPackageRelocated: String) : com.github.jengelman.gradle.plugins.shadow.transformers.Transformer { + private val targetPath = "META-INF/gradle-plugins/org.jetbrains.compose.embedded.hot-reload.properties" + private val content = """ + implementation-class=$hotReloadPackageRelocated.gradle.ComposeHotReloadPlugin + """.trimIndent() + + override fun canTransformResource(element: FileTreeElement?): Boolean = false + override fun transform(context: TransformerContext?) = Unit + override fun hasTransformedResource(): Boolean = true + + override fun modifyOutputStream(os: ZipOutputStream?, preserveFileTimestamps: Boolean) { + val entry = ZipEntry(targetPath) + entry.time = TransformerContext.getEntryTimestamp(preserveFileTimestamps, entry.time) + os?.run { + putNextEntry(entry) + write(content.toByteArray()) + closeEntry() + } + } + + override fun getName(): String = "Hot reload properties transformer" +} + +fun ShadowJar.relocateHotReload() { + val relocator = object : SimpleRelocator(hotReloadPackage, hotReloadPackageRelocated, ArrayList(), ArrayList()) { + override fun canRelocatePath(path: String?): Boolean { + return super.canRelocatePath(path) && + // do not relocate orchestration as its objects are used in serialization. + path?.startsWith("org/jetbrains/compose/reload/orchestration/") == false + } + } + relocate(relocator) +} + val shadow = tasks.named("shadowJar") { for (packageToRelocate in packagesToRelocate) { - relocate(packageToRelocate, "org.jetbrains.compose.internal.$packageToRelocate") + relocate(packageToRelocate, "$relocationPackage.$packageToRelocate") } + relocateHotReload() + + transform(HotReloadPropertiesTransformer(hotReloadPackageRelocated)) + archiveBaseName.set("shadow") archiveClassifier.set("") archiveVersion.set("") configurations = listOf(embeddedDependencies) exclude("META-INF/gradle-plugins/de.undercouch.download.properties") + exclude(hotReloadPropertiesPath) exclude("META-INF/versions/**") } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt index d1d876fa201..6899a63ffac 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt @@ -64,6 +64,14 @@ abstract class ComposePlugin : Plugin { project.configureExperimentalTargetsFlagsCheck(mppExt) } } + + try { + project.pluginManager.apply("org.jetbrains.compose.hot-reload") + } catch (_: Exception) { + // If a user does not set up the hot-reload plugin explicitly, set up the embedded one. + // TODO: issue a warning/error if the embedded version is higher than explicit one + project.pluginManager.apply("org.jetbrains.compose.embedded.hot-reload") + } } @Suppress("DEPRECATION") diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/HotReloadTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/HotReloadTest.kt new file mode 100644 index 00000000000..6b3c622225d --- /dev/null +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/HotReloadTest.kt @@ -0,0 +1,83 @@ +package org.jetbrains.compose.test.tests.integration + +import org.gradle.testkit.runner.BuildResult +import org.jetbrains.compose.ComposeBuildConfig +import org.jetbrains.compose.test.utils.GradlePluginTestBase +import org.jetbrains.compose.test.utils.checks +import org.jetbrains.compose.test.utils.modify +import org.junit.jupiter.api.Test +import kotlin.concurrent.thread + +class HotReloadTest : GradlePluginTestBase() { + @Test + fun smokeTestHotRunTask() = with(testProject("application/jvm")) { + file("build.gradle").modify { + it + """ + afterEvaluate { + tasks.getByName("hotRun").doFirst { + throw new StopExecutionException("Skip hotRun task") + } + } + """.trimIndent() + } + gradle("hotRun").checks { + check.taskSuccessful(":hotRun") + } + } + + @Test + fun testHotReload() = with(testProject("application/hotReload")) { + var result: BuildResult? = null + val hotRunThread = thread { + result = gradle("hotRunJvm") + } + + while (!file("started").exists()) { + Thread.sleep(200) + } + + modifyText("src/jvmMain/kotlin/main.kt") { + it.replace("Kotlin MPP", "KMP") + } + + gradle("reload").checks { + check.taskSuccessful(":reload") + check.logContains("MainKt.class: modified") + } + + hotRunThread.join() + check(result != null) + result.checks { + check.taskSuccessful(":hotRunJvm") + check.logContains("Kotlin MPP app is running!") + check.logContains("KMP app is running!") + check.logContains("Compose Hot Reload (${ComposeBuildConfig.composeHotReloadVersion})") + } + } + + @Test + fun testExternalHotReload() = with(testProject("application/mpp")) { + val externalHotReloadVersion = "1.0.0-beta04" + modifyText("settings.gradle") { + it.replace( + "plugins {", "plugins {\n" + + """ + id 'org.jetbrains.compose.hot-reload' version '$externalHotReloadVersion' + """.trimIndent() + ) + } + modifyText("build.gradle") { + it.replace( + "plugins {", "plugins {\n" + + """ + id "org.jetbrains.compose.hot-reload" + """.trimIndent() + ) + } + gradle("hotRunJvm").checks { + check.taskSuccessful(":hotRunJvm") + check.logContains("Compose Hot Reload ($externalHotReloadVersion)") + check.logContains("Kotlin MPP app is running!") + } + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/hotReload/build.gradle b/gradle-plugins/compose/src/test/test-projects/application/hotReload/build.gradle new file mode 100644 index 00000000000..5a5fe777e79 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/hotReload/build.gradle @@ -0,0 +1,69 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + id "com.android.application" + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.compose" + id "org.jetbrains.compose" +} + +kotlin { + // empty stub (no actual android app) to detect configuration conflicts + // like https://github.com/JetBrains/compose-jb/issues/2345 + androidTarget() + + jvm() + sourceSets { + jvmMain { + dependsOn(commonMain) + + dependencies { + implementation(compose.desktop.currentOs) + } + } + } + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(11)) + } +} + +android { + namespace = "org.jetbrains.compose.testapp" + compileSdk = 35 + + defaultConfig { + minSdk = 23 + targetSdk = 35 + } +} + +compose.desktop { + application { + mainClass = "MainKt" + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + + packageVersion = "1.0.0" + packageName = "TestPackage" + description = "Test description" + copyright = "Test Copyright Holder" + vendor = "Test Vendor" + + linux { + shortcut = true + packageName = "test-package" + debMaintainer = "example@example.com" + menuGroup = "menu-group" + } + windows { + console = true + dirChooser = true + perUserInstall = true + shortcut = true + menu = true + menuGroup = "compose" + upgradeUuid = "2d6ff464-75be-40ad-a256-56420b9cc374" + } + } + } +} diff --git a/gradle-plugins/compose/src/test/test-projects/application/hotReload/gradle.properties b/gradle-plugins/compose/src/test/test-projects/application/hotReload/gradle.properties new file mode 100644 index 00000000000..2d8d1e4dd15 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/hotReload/gradle.properties @@ -0,0 +1 @@ +android.useAndroidX=true \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/hotReload/settings.gradle b/gradle-plugins/compose/src/test/test-projects/application/hotReload/settings.gradle new file mode 100644 index 00000000000..f56d133772d --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/hotReload/settings.gradle @@ -0,0 +1,34 @@ +pluginManagement { + plugins { + 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.application' version 'AGP_VERSION_PLACEHOLDER' + } + repositories { + mavenLocal() + gradlePluginPortal() + mavenCentral() + google() + maven { + url 'https://maven.pkg.jetbrains.space/public/p/compose/dev' + } + maven { + url 'https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev/' + } + } +} +dependencyResolutionManagement { + repositories { + mavenCentral() + google() + maven { + url 'https://maven.pkg.jetbrains.space/public/p/compose/dev' + } + maven { + url 'https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev/' + } + mavenLocal() + } +} +rootProject.name = "mpp" \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/hotReload/src/jvmMain/kotlin/main.kt b/gradle-plugins/compose/src/test/test-projects/application/hotReload/src/jvmMain/kotlin/main.kt new file mode 100644 index 00000000000..c4e3ed3a3d8 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/hotReload/src/jvmMain/kotlin/main.kt @@ -0,0 +1,13 @@ +import java.io.File + +fun message() = "Kotlin MPP app is running!" + +fun main() { + println(message()) + File("started").createNewFile() + //wait for reload + while(!message().startsWith("KMP")){ + Thread.sleep(200) + } + println(message()) +} \ No newline at end of file diff --git a/gradle-plugins/gradle.properties b/gradle-plugins/gradle.properties index 02c97cf741e..0f739f4bf95 100644 --- a/gradle-plugins/gradle.properties +++ b/gradle-plugins/gradle.properties @@ -25,3 +25,4 @@ compose.tests.gradle-agp.exclude=8.7/8.9.0, 8.7/9.0.0-alpha01 # A version of Gradle plugin, that will be published, # unless overridden by COMPOSE_GRADLE_PLUGIN_VERSION env var. deploy.version=9999.0.0-SNAPSHOT +hotreload.version=1.0.0-beta08