diff --git a/hot-reload-agent/src/main/kotlin/org/jetbrains/compose/reload/agent/devTools.kt b/hot-reload-agent/src/main/kotlin/org/jetbrains/compose/reload/agent/devTools.kt index 9f5e87948..5e193cb08 100644 --- a/hot-reload-agent/src/main/kotlin/org/jetbrains/compose/reload/agent/devTools.kt +++ b/hot-reload-agent/src/main/kotlin/org/jetbrains/compose/reload/agent/devTools.kt @@ -10,6 +10,7 @@ import org.jetbrains.compose.reload.core.HotReloadEnvironment import org.jetbrains.compose.reload.core.HotReloadEnvironment.devToolsDetached import org.jetbrains.compose.reload.core.HotReloadProperty import org.jetbrains.compose.reload.core.HotReloadProperty.Environment.DevTools +import org.jetbrains.compose.reload.core.JavaHome import org.jetbrains.compose.reload.core.Os import org.jetbrains.compose.reload.core.createLogger import org.jetbrains.compose.reload.core.debug @@ -18,9 +19,7 @@ import org.jetbrains.compose.reload.core.issueNewDebugSessionJvmArguments import org.jetbrains.compose.reload.core.subprocessSystemProperties import org.jetbrains.compose.reload.core.withHotReloadEnvironmentVariables import java.io.File -import java.nio.file.Path import kotlin.concurrent.thread -import kotlin.io.path.Path import kotlin.io.path.absolutePathString private val logger = createLogger() @@ -46,6 +45,7 @@ private fun tryStartDevToolsProcess(): DevToolsHandle? { resolveDevtoolsJavaBinary(), *platformSpecificJvmArguments(), "-XX:+UseZGC", "-Xmx256M", "-XX:SoftMaxHeapSize=128M", + "--enable-native-access=ALL-UNNAMED", "-cp", classpath.joinToString(File.pathSeparator), *subprocessSystemProperties(DevTools).toTypedArray(), *issueNewDebugSessionJvmArguments("DevTools"), @@ -73,20 +73,8 @@ private fun tryStartDevToolsProcess(): DevToolsHandle? { return DevToolsHandle(orchestrationPort) } -private fun resolveDevtoolsJavaBinary(): String? { - fun Path.resolveJavaHome(): Path = resolve( - if (Os.currentOrNull() == Os.Windows) "bin/java.exe" else "bin/java" - ) - - System.getProperty("java.home")?.let { javaHome -> - return Path(javaHome).resolveJavaHome().absolutePathString() - } - - System.getenv("JAVA_HOME")?.let { javaHome -> - return Path(javaHome).resolveJavaHome().absolutePathString() - } - - return null +private fun resolveDevtoolsJavaBinary(): String { + return JavaHome.current().javaExecutable.absolutePathString() } private fun platformSpecificJvmArguments(): Array = when (Os.current()) { diff --git a/hot-reload-core/src/main/kotlin/org/jetbrains/compose/reload/core/jdk.kt b/hot-reload-core/src/main/kotlin/org/jetbrains/compose/reload/core/jdk.kt new file mode 100644 index 000000000..0e7667d96 --- /dev/null +++ b/hot-reload-core/src/main/kotlin/org/jetbrains/compose/reload/core/jdk.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2024-2025 JetBrains s.r.o. and Compose Hot Reload contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +package org.jetbrains.compose.reload.core + +import org.jetbrains.compose.reload.InternalHotReloadApi +import java.nio.file.Path +import kotlin.io.path.Path +import kotlin.io.path.absolutePathString +import kotlin.io.path.readText + +@InternalHotReloadApi +public class JavaHome(public val path: Path) { + /** + * The 'java' ('java.exe') executable binary inside the java distribution + */ + public val javaExecutable: Path + get() = path.resolve("bin").resolve(if (Os.currentOrNull() == Os.Windows) "java.exe" else "java") + + public val releaseFile: Path + get() = path.resolve("release") + + public fun readReleaseFile(): JavaReleaseFileContent { + return parseJavaReleaseFile(releaseFile) + } + + override fun toString(): String { + return "JavaHome(${path.absolutePathString()})" + } + + override fun hashCode(): Int { + return path.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is JavaHome) return false + return path.absolutePathString() == other.path.absolutePathString() + } + + @InternalHotReloadApi + public companion object { + /** + * @param path: The 'bin/java' or 'bin/java.exe' inside a java distribution + */ + public fun fromExecutable(path: Path): JavaHome { + return JavaHome(path.parent.parent) + } + + /** + * @return The java home, currently running this code + */ + public fun current(): JavaHome { + System.getProperty("java.home")?.let { javaHome -> + return JavaHome(Path(javaHome)) + } + + System.getenv("JAVA_HOME")?.let { javaHome -> + return JavaHome(Path(javaHome)) + } + + throw IllegalStateException("Missing 'java.home' System property or 'JAVA_HOME' environment variable") + } + } +} + +@InternalHotReloadApi +public data class JavaReleaseFileContent( + val values: Map +) { + + /** + * Expected to match the 'java.vendor' System property + * Example: + * IMPLEMENTOR -> JetBrains s.r.o. + */ + val implementor: String? get() = values[IMPLEMENTOR_KEY] + + /** + * + * Example: + * IMPLEMENTOR_VERSION -> JBRSDK-21.0.8+9-1038.68-jcef + */ + val implementorVersion: String? get() = values[IMPLEMENTOR_VERSION_KEY] + + /** + * Expected to match the 'java.version' System property + * Example: + * JAVA_VERSION -> 21.0.8 + */ + val javaVersion: String? get() = values[JAVA_VERSION_KEY] + + /** + * Expected to match the 'java.vm.version' System property + * Example: + * JAVA_RUNTIME_VERSION -> 21.0.8+9-b1038.68 + */ + val javaRuntimeVersion: String? get() = values[JAVA_RUNTIME_VERSION] + + /** + * Example: + * OS_ARCH -> aarch64 + */ + val osArch: String? get() = values[OS_ARCH_KEY] + + /** + * Example: + * OS_NAME -> Darwin + */ + val osName: String? get() = values[OS_NAME_KEY] + + @InternalHotReloadApi + public companion object { + public const val IMPLEMENTOR_KEY: String = "IMPLEMENTOR" + public const val IMPLEMENTOR_VERSION_KEY: String = "IMPLEMENTOR_VERSION" + public const val JAVA_VERSION_KEY: String = "JAVA_VERSION" + public const val JAVA_RUNTIME_VERSION: String = "JAVA_RUNTIME_VERSION" + public const val OS_ARCH_KEY: String = "OS_ARCH" + public const val OS_NAME_KEY: String = "OS_NAME" + } + + override fun toString(): String { + return buildString { + for ((key, value) in values) { + append("$key=\"$value\"\n") + } + } + } +} + +private fun parseJavaReleaseFile(path: Path): JavaReleaseFileContent { + val map = buildMap { + path.readText().lines().mapNotNull { line -> + val keyValue = line.split("=", limit = 2) + if (keyValue.size != 2) return@mapNotNull null + val key = keyValue[0].trim() + val value = keyValue[1].trim().removeSurrounding("\"") + put(key, value) + } + }.toMap() + + return JavaReleaseFileContent(map) +} diff --git a/hot-reload-core/src/test/kotlin/JavaHomeTest.kt b/hot-reload-core/src/test/kotlin/JavaHomeTest.kt new file mode 100644 index 000000000..7af1cacc5 --- /dev/null +++ b/hot-reload-core/src/test/kotlin/JavaHomeTest.kt @@ -0,0 +1,16 @@ +import org.jetbrains.compose.reload.core.JavaHome +import kotlin.io.path.absolutePathString +import kotlin.test.Test +import kotlin.test.assertEquals + +/* + * Copyright 2024-2025 JetBrains s.r.o. and Compose Hot Reload contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +class JavaHomeTest { + @Test + fun `test - current`() { + assertEquals(System.getProperty("java.home"), JavaHome.current().path.absolutePathString()) + } +} diff --git a/hot-reload-core/src/test/kotlin/JavaReleaseFileContentTest.kt b/hot-reload-core/src/test/kotlin/JavaReleaseFileContentTest.kt new file mode 100644 index 000000000..74641bc61 --- /dev/null +++ b/hot-reload-core/src/test/kotlin/JavaReleaseFileContentTest.kt @@ -0,0 +1,21 @@ +import org.jetbrains.compose.reload.core.JavaHome +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +/* + * Copyright 2024-2025 JetBrains s.r.o. and Compose Hot Reload contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ + +class JavaReleaseFileContentTest { + @Test + fun `test current jvm`() { + val javaHome = JavaHome.current() + val releaseFile = javaHome.readReleaseFile() + + assertEquals(System.getProperty("java.version"), assertNotNull(releaseFile.javaVersion)) + assertEquals(System.getProperty("java.vendor"), assertNotNull(releaseFile.implementor)) + assertEquals(System.getProperty("java.vm.version"), releaseFile.javaRuntimeVersion) + } +} diff --git a/hot-reload-gradle-core/src/main/kotlin/org/jetbrains/compose/reload/gradle/InternalHotReloadGradleApi.kt b/hot-reload-gradle-core/src/main/kotlin/org/jetbrains/compose/reload/gradle/InternalHotReloadGradleApi.kt deleted file mode 100644 index d3622c00a..000000000 --- a/hot-reload-gradle-core/src/main/kotlin/org/jetbrains/compose/reload/gradle/InternalHotReloadGradleApi.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2024-2025 JetBrains s.r.o. and Compose Hot Reload contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. - */ - -package org.jetbrains.compose.reload.gradle - -import org.jetbrains.compose.reload.InternalHotReloadApi - -@RequiresOptIn( - "This API is internal to hot-reload-gradle-plugin and should not be used from outside.", - level = RequiresOptIn.Level.ERROR -) -@Deprecated(level = DeprecationLevel.HIDDEN, message = "Use 'InternalHotReloadApi' instead.") -@OptIn(InternalHotReloadApi::class) -annotation class InternalHotReloadGradleApi() diff --git a/hot-reload-gradle-core/src/main/kotlin/org/jetbrains/compose/reload/gradle/jetbrainsRuntime.kt b/hot-reload-gradle-core/src/main/kotlin/org/jetbrains/compose/reload/gradle/jetbrainsRuntime.kt index 3b09bb201..e7b4df13b 100644 --- a/hot-reload-gradle-core/src/main/kotlin/org/jetbrains/compose/reload/gradle/jetbrainsRuntime.kt +++ b/hot-reload-gradle-core/src/main/kotlin/org/jetbrains/compose/reload/gradle/jetbrainsRuntime.kt @@ -6,19 +6,85 @@ package org.jetbrains.compose.reload.gradle import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.file.RegularFile +import org.gradle.api.plugins.JavaPluginExtension import org.gradle.api.provider.Provider +import org.gradle.jvm.toolchain.JavaInstallationMetadata import org.gradle.jvm.toolchain.JavaLanguageVersion import org.gradle.jvm.toolchain.JavaLauncher import org.gradle.jvm.toolchain.JavaToolchainService import org.gradle.jvm.toolchain.JvmVendorSpec +import org.gradle.kotlin.dsl.findByType import org.gradle.kotlin.dsl.support.serviceOf import org.jetbrains.compose.reload.InternalHotReloadApi +import org.jetbrains.compose.reload.core.JavaHome +import org.jetbrains.compose.reload.core.JavaReleaseFileContent +import kotlin.io.path.absolutePathString + +@InternalHotReloadApi +fun Project.jetbrainsRuntimeVersion(): Provider { + val defaultVersion = JavaLanguageVersion.of(composeReloadJetBrainsRuntimeVersion) + return project.provider { + val projectLevel = extensions.findByType()?.toolchain?.languageVersion?.orNull + if (projectLevel != null && projectLevel > defaultVersion) return@provider projectLevel + defaultVersion + } +} @InternalHotReloadApi fun Project.jetbrainsRuntimeLauncher(): Provider { - return serviceOf().launcherFor { spec -> + val provisionedLauncher = serviceOf().launcherFor { spec -> @Suppress("UnstableApiUsage") spec.vendor.set(JvmVendorSpec.JETBRAINS) - spec.languageVersion.set(JavaLanguageVersion.of(21)) + spec.languageVersion.set((jetbrainsRuntimeVersion())) + } + + return project.provider { + try { + provisionedLauncher.get() + } catch (e: Throwable) { + createJavaLauncherFromProvidedJetBrainsRuntimeBinaryPath() ?: throw e + } + } +} + +/** + * Builds a [JavaLauncher] from the JetBrains Runtime provided by the + * [org.jetbrains.compose.reload.core.HotReloadProperty.JetBrainsRuntimeBinary] property. + * The 'executable' path is specified by the user, the [JavaInstallationMetadata] is then inferred + * by introspecting the distribution. + */ +private fun Project.createJavaLauncherFromProvidedJetBrainsRuntimeBinaryPath(): JavaLauncher? { + val layout = project.layout + val executablePath = composeReloadJetBrainsRuntimeBinary ?: return null + val javaHome = JavaHome.fromExecutable(executablePath) + val releaseFileContent = javaHome.readReleaseFile() + val javaVersion = releaseFileContent.javaVersion + ?: error("Missing '${JavaReleaseFileContent.JAVA_VERSION_KEY}' in '$javaHome'") + + return object : JavaLauncher { + override fun getMetadata(): JavaInstallationMetadata = object : JavaInstallationMetadata { + override fun getLanguageVersion(): JavaLanguageVersion = + JavaLanguageVersion.of(javaVersion.split(".").first().toInt()) + + override fun getJavaRuntimeVersion(): String = + releaseFileContent.javaRuntimeVersion ?: "N/A" + + override fun getJvmVersion(): String = + releaseFileContent.implementorVersion ?: "N/A" + + override fun getVendor(): String = + releaseFileContent.implementor ?: "N/A" + + override fun getInstallationPath(): Directory = + layout.projectDirectory.dir(javaHome.path.absolutePathString()) + + @Suppress("UnstableApiUsage") + override fun isCurrentJvm(): Boolean = JavaHome.current() == javaHome + } + + override fun getExecutablePath(): RegularFile = + layout.projectDirectory.file(executablePath.absolutePathString()) } } diff --git a/hot-reload-gradle-plugin/src/main/kotlin/org/jetbrains/compose/reload/gradle/arguments.kt b/hot-reload-gradle-plugin/src/main/kotlin/org/jetbrains/compose/reload/gradle/arguments.kt index cb58fd970..7b80ae641 100644 --- a/hot-reload-gradle-plugin/src/main/kotlin/org/jetbrains/compose/reload/gradle/arguments.kt +++ b/hot-reload-gradle-plugin/src/main/kotlin/org/jetbrains/compose/reload/gradle/arguments.kt @@ -57,12 +57,11 @@ fun T.withComposeHotReload(arguments: ComposeHotReloadArgumentsBuilder.() -> jvmArgumentProviders.add(arguments) /* Ensure the task is launched with a proper JetBrains Runtime */ - if (project.composeReloadJetBrainsRuntimeBinary != null) { - this.executable(project.composeReloadJetBrainsRuntimeBinary) - } else if (this is JavaExec) { - javaLauncher.set(project.jetbrainsRuntimeLauncher()) + if (this is JavaExec) { + javaLauncher.convention(project.jetbrainsRuntimeLauncher()) } + /** * Wire up the orchestration listener ports: * Note: This is not done in the [ComposeHotReloadArguments] as these properties can change frequently and diff --git a/hot-reload-gradle-plugin/src/main/kotlin/org/jetbrains/compose/reload/gradle/hotRunTasks.kt b/hot-reload-gradle-plugin/src/main/kotlin/org/jetbrains/compose/reload/gradle/hotRunTasks.kt index b68be6266..7656d004f 100644 --- a/hot-reload-gradle-plugin/src/main/kotlin/org/jetbrains/compose/reload/gradle/hotRunTasks.kt +++ b/hot-reload-gradle-plugin/src/main/kotlin/org/jetbrains/compose/reload/gradle/hotRunTasks.kt @@ -147,6 +147,11 @@ internal fun JavaExec.configureJavaExecTaskForHotReload(compilation: Provider + if (vendor.contains("jetbrains", ignoreCase = true)) "JetBrains Runtime" + else "⚠️ VM: $vendor (JetBrains Runtime required)" + } ?: "⚠️ Unknown VM" + logger.quiet(buildString { append( """ @@ -154,7 +159,7 @@ internal fun JavaExec.configureJavaExecTaskForHotReload(compilation: Provider