Skip to content
Draft
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 @@ -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
Expand All @@ -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()
Expand All @@ -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"),
Expand Down Expand Up @@ -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<String> = when (Os.current()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String>
) {

/**
* 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)
}
16 changes: 16 additions & 0 deletions hot-reload-core/src/test/kotlin/JavaHomeTest.kt
Original file line number Diff line number Diff line change
@@ -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())
}
}
21 changes: 21 additions & 0 deletions hot-reload-core/src/test/kotlin/JavaReleaseFileContentTest.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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<JavaLanguageVersion> {
val defaultVersion = JavaLanguageVersion.of(composeReloadJetBrainsRuntimeVersion)
return project.provider {
val projectLevel = extensions.findByType<JavaPluginExtension>()?.toolchain?.languageVersion?.orNull
if (projectLevel != null && projectLevel > defaultVersion) return@provider projectLevel
defaultVersion
}
}

@InternalHotReloadApi
fun Project.jetbrainsRuntimeLauncher(): Provider<JavaLauncher> {
return serviceOf<JavaToolchainService>().launcherFor { spec ->
val provisionedLauncher = serviceOf<JavaToolchainService>().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())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,11 @@ fun <T> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,19 @@ internal fun JavaExec.configureJavaExecTaskForHotReload(compilation: Provider<Ko
jvmArgs = jvmArgs.orEmpty() + "-D${HotReloadProperty.LaunchMode.key}=${LaunchMode.GradleBlocking.name}"


val vmTitle = javaLauncher.orNull?.metadata?.vendor?.let { vendor ->
if (vendor.contains("jetbrains", ignoreCase = true)) "JetBrains Runtime"
else "⚠️ VM: $vendor (JetBrains Runtime required)"
} ?: "⚠️ Unknown VM"

logger.quiet(buildString {
append(
"""
________________________________________________________________________________________________
|
| Compose Hot Reload ($HOT_RELOAD_VERSION)
| Running '${mainClass.get()}'
| JetBrains Runtime: ${javaLauncher.orNull?.executablePath?.asFile?.path}
| $vmTitle: ${javaLauncher.orNull?.executablePath?.asFile?.path}
| ________________________________________________________________________________________________
""".trimIndent()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import kotlinx.coroutines.future.await
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import org.jetbrains.compose.reload.core.Future
import org.jetbrains.compose.reload.core.JavaHome
import org.jetbrains.compose.reload.core.Logger
import org.jetbrains.compose.reload.core.Os
import org.jetbrains.compose.reload.core.Queue
import org.jetbrains.compose.reload.core.complete
import org.jetbrains.compose.reload.core.completeExceptionally
Expand Down Expand Up @@ -77,8 +77,7 @@ fun CoroutineScope.launchIsolate(
): IsolateHandle {
val previousClasspath = classpath + System.getProperty("testClasspath").split(File.pathSeparator).map { Path(it) }

val javaHome = Path(System.getProperty("java.home"))
val java = javaHome.resolve(if (Os.current() == Os.Windows) "bin/java.exe" else "bin/java")
val java = JavaHome.current().javaExecutable

val process = ProcessBuilder(
java.absolutePathString(),
Expand Down
Loading