mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-16 14:23:28 +07:00
[jdk] Introduce ExternalJavaConfigurationService and migrate SdkmanrcWatcher
#IDEA-355295 GitOrigin-RevId: bbcefd977b5d88e3a8b0d137b1b5cfda6eb3050a
This commit is contained in:
committed by
intellij-monorepo-bot
parent
4b6a424f1c
commit
7ccd3af88d
@@ -363,6 +363,9 @@
|
||||
<extensionPoint qualifiedName="com.intellij.execution.applicationRunLineMarkerHider"
|
||||
interface="com.intellij.execution.ApplicationRunLineMarkerHider" dynamic="true"/>
|
||||
<extensionPoint qualifiedName="com.intellij.jvm.logging" interface="com.intellij.lang.logging.JvmLogger" dynamic="true"/>
|
||||
|
||||
<extensionPoint qualifiedName="com.intellij.openapi.projectRoots.externalJavaConfigurationProvider"
|
||||
interface="com.intellij.openapi.projectRoots.impl.ExternalJavaConfigurationProvider" dynamic="true"/>
|
||||
</extensionPoints>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
@@ -537,7 +540,9 @@
|
||||
|
||||
<updateSettingsUIProvider implementation="com.intellij.openapi.projectRoots.impl.JdkUpdaterConfigurable"/>
|
||||
<postStartupActivity implementation="com.intellij.openapi.projectRoots.impl.ExistingJdkConfigurationActivity"/>
|
||||
<postStartupActivity implementation="com.intellij.openapi.projectRoots.impl.SdkmanrcWatcher"/>
|
||||
<openapi.projectRoots.externalJavaConfigurationProvider implementation="com.intellij.openapi.projectRoots.impl.SdkmanrcConfigurationProvider"/>
|
||||
<postStartupActivity implementation="com.intellij.openapi.projectRoots.impl.ExternalJavaConfigurationActivity"/>
|
||||
|
||||
<registryKey key="jdk.configure.existing" defaultValue="false" description="Attempt to add an existing SDK to the SDK table."/>
|
||||
|
||||
<applicationService serviceInterface="com.intellij.execution.runners.ProcessProxyFactory"
|
||||
@@ -2751,7 +2756,7 @@
|
||||
key="notification.group.preview.features"/>
|
||||
<notificationGroup id="Remove redundant exports/opens" displayType="BALLOON" bundle="messages.JavaBundle"
|
||||
key="notification.group.redundant.exports"/>
|
||||
<notificationGroup id="Setup SDK" displayType="BALLOON" bundle="messages.JavaBundle" key="notification.group.setup.sdk"/>
|
||||
<notificationGroup id="Setup JDK" displayType="BALLOON" bundle="messages.JavaBundle" key="notification.group.setup.jdk"/>
|
||||
<notificationGroup id="External annotations" displayType="BALLOON" bundle="messages.JavaBundle"
|
||||
key="notification.group.setup.external.annotations"/>
|
||||
<notificationGroup id="Test integration" displayType="BALLOON" bundle="messages.JavaBundle" key="notification.group.testintegration"/>
|
||||
|
||||
@@ -75,7 +75,7 @@ public abstract class JavaCreateTemplateInPackageAction<T extends PsiElement> ex
|
||||
public void run(@NotNull ProgressIndicator indicator) {
|
||||
SdkLookupUtil.lookupAndSetupSdkBlocking(project, indicator, JavaSdk.getInstance(), sdk -> {
|
||||
JavaSdkUtil.applyJdkToProject(project, sdk);
|
||||
Notifications.Bus.notify(new Notification("Setup SDK", JavaBundle.message("notification.content.was.set.up", sdk.getVersionString()), NotificationType.INFORMATION).addAction(
|
||||
Notifications.Bus.notify(new Notification("Setup JDK", JavaBundle.message("notification.content.was.set.up", sdk.getVersionString()), NotificationType.INFORMATION).addAction(
|
||||
new NotificationAction(JavaBundle.message("notification.content.change.jdk")) {
|
||||
@Override
|
||||
public void actionPerformed(@NotNull AnActionEvent e,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.projectRoots.impl
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.extensions.ExtensionNotApplicableException
|
||||
import com.intellij.openapi.extensions.ExtensionPointListener
|
||||
import com.intellij.openapi.extensions.PluginDescriptor
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.startup.ProjectActivity
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Configures the project JDK according to configuration files from external config tools.
|
||||
*/
|
||||
class ExternalJavaConfigurationActivity : ProjectActivity {
|
||||
|
||||
private val disposableMap = mutableMapOf<Class<*>, Disposable>()
|
||||
|
||||
init {
|
||||
if (ApplicationManager.getApplication().isUnitTestMode) {
|
||||
throw ExtensionNotApplicableException.create()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun execute(project: Project) {
|
||||
delay(5.seconds)
|
||||
|
||||
val configWatcherService = project.service<ExternalJavaConfigurationService>()
|
||||
|
||||
ExternalJavaConfigurationProvider.EP_NAME.addExtensionPointListener(object : ExtensionPointListener<ExternalJavaConfigurationProvider<*>> {
|
||||
override fun extensionAdded(extension: ExternalJavaConfigurationProvider<*>, pluginDescriptor: PluginDescriptor) {
|
||||
setupExtension(project, extension)
|
||||
}
|
||||
|
||||
override fun extensionRemoved(extension: ExternalJavaConfigurationProvider<*>, pluginDescriptor: PluginDescriptor) {
|
||||
val clazz = extension::class.java
|
||||
disposableMap[clazz]?.let { Disposer.dispose(it) }
|
||||
disposableMap.remove(extension::class.java)
|
||||
}
|
||||
}, configWatcherService)
|
||||
|
||||
for (configProvider in ExternalJavaConfigurationProvider.EP_NAME.extensionList) {
|
||||
val file = configProvider.getConfigurationFile(project)
|
||||
setupExtension(project, configProvider)
|
||||
if (file.exists()) configWatcherService.updateJdkFromConfig(configProvider)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupExtension(project: Project, extension: ExternalJavaConfigurationProvider<*>) {
|
||||
val key = extension::class.java
|
||||
if (disposableMap.containsKey(key)) return
|
||||
val configWatcherService = project.service<ExternalJavaConfigurationService>()
|
||||
val disposable = Disposer.newDisposable(configWatcherService)
|
||||
configWatcherService.registerListener(disposable, extension)
|
||||
disposableMap[extension::class.java] = disposable
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.projectRoots.impl
|
||||
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* This extension point makes it possible to select which JDK should be configured for the project,
|
||||
* based on the configuration file of an external SDK manager.
|
||||
*
|
||||
* @param T Release data representation for the configuration provider.
|
||||
*/
|
||||
interface ExternalJavaConfigurationProvider<T> {
|
||||
|
||||
companion object {
|
||||
val EP_NAME: ExtensionPointName<ExternalJavaConfigurationProvider<*>> = ExtensionPointName.create("com.intellij.openapi.projectRoots.externalJavaConfigurationProvider")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configuration file supported by this provider.
|
||||
*/
|
||||
fun getConfigurationFile(project: Project): File
|
||||
|
||||
/**
|
||||
* Returns the release data [T] corresponding to the [text] content of the configuration file.
|
||||
*/
|
||||
fun getReleaseData(text: String): T?
|
||||
|
||||
/**
|
||||
* Returns true if the release data matches the given SDK.
|
||||
*/
|
||||
fun matchAgainstSdk(releaseData: T, sdk: Sdk): Boolean
|
||||
|
||||
/**
|
||||
* Returns true if the release data matches the given path.
|
||||
*/
|
||||
fun matchAgainstPath(releaseData: T, path: String): Boolean
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.projectRoots.impl
|
||||
|
||||
import com.intellij.java.JavaBundle
|
||||
import com.intellij.notification.NotificationGroupManager
|
||||
import com.intellij.notification.NotificationType
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.application.readAction
|
||||
import com.intellij.openapi.application.writeAction
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.JdkFinder
|
||||
import com.intellij.openapi.projectRoots.ProjectJdkTable
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.roots.ProjectRootManager
|
||||
import com.intellij.openapi.vfs.VirtualFileManager
|
||||
import com.intellij.openapi.vfs.newvfs.BulkFileListener
|
||||
import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent
|
||||
import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent
|
||||
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private val LOG = logger<ExternalJavaConfigurationService>()
|
||||
|
||||
/**
|
||||
* The JDK needed for a project can be described in a configuration file written by an external tool.
|
||||
* This service is used to find a matching JDK candidate for a given release data.
|
||||
*
|
||||
* @see ExternalJavaConfigurationProvider
|
||||
*/
|
||||
@Service(Service.Level.PROJECT)
|
||||
class ExternalJavaConfigurationService(val project: Project, private val scope: CoroutineScope) : Disposable {
|
||||
|
||||
sealed class JdkCandidate<T> {
|
||||
data class Jdk<T>(val releaseData: T, val jdk: Sdk, val project: Boolean) : JdkCandidate<T>()
|
||||
data class Path<T>(val releaseData: T, val path: String) : JdkCandidate<T>()
|
||||
}
|
||||
|
||||
internal fun <T> registerListener(disposable: Disposable, configProvider: ExternalJavaConfigurationProvider<T>) {
|
||||
project.messageBus.connect(disposable).subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener {
|
||||
override fun after(events: MutableList<out VFileEvent>) {
|
||||
for (event in events) {
|
||||
if (!event.path.endsWith(configProvider.getConfigurationFile(project).name)) continue
|
||||
if (event !is VFileContentChangeEvent && event !is VFileCreateEvent) continue
|
||||
|
||||
updateJdkFromConfig(configProvider)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the project JDK according to the configuration file of [configProvider].
|
||||
*/
|
||||
fun <T> updateJdkFromConfig(configProvider: ExternalJavaConfigurationProvider<T>) {
|
||||
scope.launch {
|
||||
val releaseData: T = getReleaseData(configProvider) ?: return@launch
|
||||
val file = configProvider.getConfigurationFile(project)
|
||||
val suggestion = findCandidate(releaseData, configProvider)
|
||||
|
||||
when (suggestion) {
|
||||
is JdkCandidate.Jdk -> if (!suggestion.project) configure(suggestion.jdk, file.name, releaseData)
|
||||
is JdkCandidate.Path -> service<AddJdkService>().createJdkFromPath(suggestion.path) {
|
||||
configure(it, file.name, releaseData)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the configuration file and returns a candidate.
|
||||
*/
|
||||
suspend fun <T> getReleaseData(configProvider: ExternalJavaConfigurationProvider<T>): T? {
|
||||
val text = readAction {
|
||||
val virtualFile = VirtualFileManager.getInstance().findFileByNioPath(configProvider.getConfigurationFile(project).toPath().toAbsolutePath())
|
||||
virtualFile?.let { FileDocumentManager.getInstance().getDocument(it)?.text }
|
||||
} ?: return null
|
||||
|
||||
return configProvider.getReleaseData(text)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a matching JDK candidate for the release data among registered and detected JDKs.
|
||||
*/
|
||||
fun <T> findCandidate(releaseData: T, configProvider: ExternalJavaConfigurationProvider<T>): JdkCandidate<T>? {
|
||||
val fileName = configProvider.getConfigurationFile(project).name
|
||||
|
||||
// Match against the project SDK
|
||||
val projectSdk = ProjectRootManager.getInstance(project).projectSdk
|
||||
if (projectSdk != null && configProvider.matchAgainstSdk(releaseData, projectSdk)) {
|
||||
return JdkCandidate.Jdk(releaseData, projectSdk, true)
|
||||
} else {
|
||||
LOG.info("[$fileName] $releaseData - Project JDK doesn't match (${projectSdk?.versionString})")
|
||||
}
|
||||
|
||||
// Match against the project JDK table
|
||||
val jdks = ProjectJdkTable.getInstance().allJdks
|
||||
for (jdk in jdks) {
|
||||
if (configProvider.matchAgainstSdk(releaseData, jdk)) {
|
||||
LOG.info("[$fileName] $releaseData - Candidate found: ${jdk.versionString}")
|
||||
return JdkCandidate.Jdk(releaseData, jdk, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Match against JdkFinder
|
||||
JdkFinder.getInstance().suggestHomePaths().forEach { path ->
|
||||
if (configProvider.matchAgainstPath(releaseData, path)) {
|
||||
LOG.info("[$fileName] $releaseData - Candidate found to register")
|
||||
return JdkCandidate.Path(releaseData, path)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun <T> configure(jdk: Sdk, fileName: String, candidate: T) {
|
||||
scope.launch(Dispatchers.EDT) {
|
||||
val rootManager = ProjectRootManager.getInstance(project)
|
||||
writeAction { rootManager.projectSdk = jdk }
|
||||
LOG.info("[$fileName] $candidate - JDK registered: ${jdk.versionString}")
|
||||
|
||||
NotificationGroupManager.getInstance()
|
||||
.getNotificationGroup("Setup JDK")
|
||||
.createNotification(
|
||||
JavaBundle.message("sdk.configured.external.config.title", fileName),
|
||||
JavaBundle.message("sdk.configured", jdk.versionString),
|
||||
NotificationType.INFORMATION
|
||||
)
|
||||
.notify(project)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {}
|
||||
}
|
||||
@@ -18,14 +18,21 @@ internal class ParseSdkmanrcAction: AnAction() {
|
||||
return
|
||||
}
|
||||
|
||||
val watcher = project.service<SdkmanrcWatcherService>()
|
||||
val sdkmanrcConfigProvider = ExternalJavaConfigurationProvider.EP_NAME.extensionList.find { it is SdkmanrcConfigurationProvider }
|
||||
|
||||
if (sdkmanrcConfigProvider == null) {
|
||||
e.presentation.isEnabledAndVisible = false
|
||||
return
|
||||
}
|
||||
|
||||
val file: VirtualFile? = CommonDataKeys.VIRTUAL_FILE.getData(e.dataContext)
|
||||
|
||||
e.presentation.isEnabledAndVisible = file != null && file.path == watcher.file.absolutePath
|
||||
e.presentation.isEnabledAndVisible = file != null && file.path == sdkmanrcConfigProvider.getConfigurationFile(project).absolutePath
|
||||
}
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val project = e.project ?: return
|
||||
project.service<SdkmanrcWatcherService>().configureSdkFromSdkmanrc()
|
||||
val sdkmanrcConfigProvider = ExternalJavaConfigurationProvider.EP_NAME.extensionList.find { it is SdkmanrcConfigurationProvider } ?: return
|
||||
project.service<ExternalJavaConfigurationService>().updateJdkFromConfig(sdkmanrcConfigProvider)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.projectRoots.impl
|
||||
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import org.jetbrains.jps.model.java.JdkVersionDetector
|
||||
import java.io.File
|
||||
import java.util.Properties
|
||||
|
||||
private val LOG = logger<SdkmanrcConfigurationProvider>()
|
||||
|
||||
data class SdkmanReleaseData(val target: String,
|
||||
val version: String,
|
||||
val flavour: String? = null,
|
||||
val vendor: String? = null) {
|
||||
companion object {
|
||||
private val regex: Regex = Regex("(\\d+(?:\\.\\d+)*)(?:\\.([^-]+))?-?(.*)?")
|
||||
|
||||
fun parse(text: String): SdkmanReleaseData? {
|
||||
val matchResult = regex.find(text) ?: return null
|
||||
return SdkmanReleaseData(
|
||||
text,
|
||||
matchResult.groups[1]?.value ?: return null,
|
||||
matchResult.groups[2]?.value,
|
||||
matchResult.groups[3]?.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun matchVersionString(versionString: @NlsSafe String): Boolean {
|
||||
LOG.info("Matching '$versionString'")
|
||||
if (version !in versionString) return false
|
||||
|
||||
val variant = when {
|
||||
vendor == "adpt" && flavour == "hs" -> JdkVersionDetector.Variant.AdoptOpenJdk_HS
|
||||
vendor == "adpt" && flavour == "j9" -> JdkVersionDetector.Variant.AdoptOpenJdk_J9
|
||||
vendor == "amzn" -> JdkVersionDetector.Variant.Corretto
|
||||
vendor == "graal" -> JdkVersionDetector.Variant.GraalVM
|
||||
vendor == "graalce" -> JdkVersionDetector.Variant.GraalVMCE
|
||||
vendor == "jbr" -> JdkVersionDetector.Variant.JBR
|
||||
vendor == "librca" -> JdkVersionDetector.Variant.Liberica
|
||||
vendor == "oracle" -> JdkVersionDetector.Variant.Oracle
|
||||
vendor == "open" -> JdkVersionDetector.Variant.Oracle
|
||||
vendor == "sapmchn" -> JdkVersionDetector.Variant.SapMachine
|
||||
vendor == "sem" -> JdkVersionDetector.Variant.Semeru
|
||||
vendor == "tem" -> JdkVersionDetector.Variant.Temurin
|
||||
vendor == "zulu" -> JdkVersionDetector.Variant.Zulu
|
||||
else -> JdkVersionDetector.Variant.Unknown
|
||||
}
|
||||
|
||||
// Check vendor
|
||||
val variantName = variant.displayName
|
||||
return variantName != null && versionString.contains(variantName)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SdkmanrcConfigurationProvider: ExternalJavaConfigurationProvider<SdkmanReleaseData> {
|
||||
override fun getConfigurationFile(project: Project): File = File(project.basePath, ".sdkmanrc")
|
||||
|
||||
override fun getReleaseData(text: String): SdkmanReleaseData? {
|
||||
val properties = Properties().apply {
|
||||
load(text.byteInputStream())
|
||||
}
|
||||
return SdkmanReleaseData.parse(properties.getProperty("java"))
|
||||
}
|
||||
|
||||
override fun matchAgainstSdk(releaseData: SdkmanReleaseData, sdk: Sdk): Boolean {
|
||||
val versionString = sdk.versionString ?: return false
|
||||
return releaseData.matchVersionString(versionString)
|
||||
}
|
||||
|
||||
override fun matchAgainstPath(releaseData: SdkmanReleaseData, path: String): Boolean {
|
||||
val info = SdkVersionUtil.getJdkVersionInfo(path) ?: return false
|
||||
return releaseData.matchVersionString(info.displayVersionString())
|
||||
}
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.projectRoots.impl
|
||||
|
||||
import com.intellij.java.JavaBundle
|
||||
import com.intellij.notification.NotificationGroupManager
|
||||
import com.intellij.notification.NotificationType
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.application.readAction
|
||||
import com.intellij.openapi.application.writeAction
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.serviceAsync
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.extensions.ExtensionNotApplicableException
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.options.advanced.AdvancedSettings
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.JavaSdk
|
||||
import com.intellij.openapi.projectRoots.JdkFinder
|
||||
import com.intellij.openapi.projectRoots.ProjectJdkTable
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.roots.ProjectRootManager
|
||||
import com.intellij.openapi.startup.ProjectActivity
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.openapi.vfs.VirtualFileManager
|
||||
import com.intellij.openapi.vfs.newvfs.BulkFileListener
|
||||
import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent
|
||||
import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent
|
||||
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
|
||||
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.jps.model.java.JdkVersionDetector
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.contract
|
||||
|
||||
private val LOG = logger<SdkmanrcWatcher>()
|
||||
|
||||
data class SdkmanCandidate(val target: String,
|
||||
val version: String,
|
||||
val flavour: String? = null,
|
||||
val vendor: String? = null) {
|
||||
companion object {
|
||||
private val regex: Regex = Regex("(\\d+(?:\\.\\d+)*)(?:\\.([^-]+))?-?(.*)?")
|
||||
|
||||
fun parse(text: String): SdkmanCandidate? {
|
||||
val matchResult = regex.find(text) ?: return null
|
||||
return SdkmanCandidate(
|
||||
text,
|
||||
matchResult.groups[1]?.value ?: return null,
|
||||
matchResult.groups[2]?.value,
|
||||
matchResult.groups[3]?.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun matchVersionString(versionString: @NlsSafe String): Boolean {
|
||||
LOG.info("Matching '$versionString'")
|
||||
if (version !in versionString) return false
|
||||
|
||||
val variant = when {
|
||||
vendor == "adpt" && flavour == "hs" -> JdkVersionDetector.Variant.AdoptOpenJdk_HS
|
||||
vendor == "adpt" && flavour == "j9" -> JdkVersionDetector.Variant.AdoptOpenJdk_J9
|
||||
vendor == "amzn" -> JdkVersionDetector.Variant.Corretto
|
||||
vendor == "graal" -> JdkVersionDetector.Variant.GraalVM
|
||||
vendor == "graalce" -> JdkVersionDetector.Variant.GraalVMCE
|
||||
vendor == "jbr" -> JdkVersionDetector.Variant.JBR
|
||||
vendor == "librca" -> JdkVersionDetector.Variant.Liberica
|
||||
vendor == "oracle" -> JdkVersionDetector.Variant.Oracle
|
||||
vendor == "open" -> JdkVersionDetector.Variant.Oracle
|
||||
vendor == "sapmchn" -> JdkVersionDetector.Variant.SapMachine
|
||||
vendor == "sem" -> JdkVersionDetector.Variant.Semeru
|
||||
vendor == "tem" -> JdkVersionDetector.Variant.Temurin
|
||||
vendor == "zulu" -> JdkVersionDetector.Variant.Zulu
|
||||
else -> JdkVersionDetector.Variant.Unknown
|
||||
}
|
||||
|
||||
// Check vendor
|
||||
val variantName = variant.displayName
|
||||
return variantName != null && versionString.contains(variantName)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Service(Service.Level.PROJECT)
|
||||
class SdkmanrcWatcherService(private val project: Project, private val scope: CoroutineScope): Disposable {
|
||||
val file: File = File(project.basePath, ".sdkmanrc")
|
||||
|
||||
fun registerListener(project: Project) {
|
||||
project.messageBus.connect(this).subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener {
|
||||
override fun after(events: MutableList<out VFileEvent>) {
|
||||
for (event in events) {
|
||||
when {
|
||||
!event.path.endsWith(".sdkmanrc") -> {}
|
||||
event is VFileContentChangeEvent || event is VFileCreateEvent -> configureSdkFromSdkmanrc()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
sealed class SdkSuggestion {
|
||||
data class Jdk(val target: SdkmanCandidate, val jdk: Sdk, val project: Boolean) : SdkSuggestion()
|
||||
data class Path(val target: SdkmanCandidate, val path: String) : SdkSuggestion()
|
||||
}
|
||||
|
||||
fun configureSdkFromSdkmanrc() {
|
||||
scope.launch {
|
||||
val result = suggestSdkFromSdkmanrc()
|
||||
withContext(Dispatchers.EDT) {
|
||||
when {
|
||||
result is SdkSuggestion.Jdk && !result.project -> configure(result.jdk, result.target)
|
||||
result is SdkSuggestion.Path -> configure(result.path, result.target)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresBackgroundThread
|
||||
suspend fun suggestSdkFromSdkmanrc(): SdkSuggestion? {
|
||||
if (!file.exists()) return null
|
||||
|
||||
// Parse .sdkmanrc
|
||||
val properties = Properties().apply {
|
||||
val vfText = readAction {
|
||||
findVirtualFile()?.let { FileDocumentManager.getInstance().getDocument(it)?.text }
|
||||
}
|
||||
load(vfText?.byteInputStream() ?: file.inputStream())
|
||||
}
|
||||
|
||||
val java = properties.getProperty("java") ?: return null
|
||||
val target = SdkmanCandidate.parse(java) ?: return null
|
||||
LOG.info(".sdkmanrc found: $target")
|
||||
|
||||
val testedPaths = mutableListOf<String>()
|
||||
|
||||
// Match against the project SDK
|
||||
val projectSdk = ProjectRootManager.getInstance(project).projectSdk
|
||||
if (target.match(projectSdk)) {
|
||||
return SdkSuggestion.Jdk(target, projectSdk, true)
|
||||
} else {
|
||||
projectSdk?.homePath?.let { testedPaths.add(it) }
|
||||
LOG.info("$java - Project JDK doesn't match .sdkmanrc (${projectSdk?.versionString})")
|
||||
}
|
||||
|
||||
// Match against the project JDK table
|
||||
val jdks = ProjectJdkTable.getInstance().allJdks
|
||||
for (jdk in jdks) {
|
||||
if (jdk.homePath !in testedPaths && target.match(jdk)) {
|
||||
jdk.homePath?.let { testedPaths.add(it) }
|
||||
LOG.info("$java - Candidate found: ${jdk.versionString}")
|
||||
return SdkSuggestion.Jdk(target, jdk, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Match against JdkFinder
|
||||
JdkFinder.getInstance().suggestHomePaths().forEach { path ->
|
||||
if (path !in testedPaths && target.match(path)) {
|
||||
testedPaths.add(path)
|
||||
return SdkSuggestion.Path(target, path)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun findVirtualFile() = VirtualFileManager.getInstance().findFileByNioPath(file.toPath().toAbsolutePath())
|
||||
|
||||
@RequiresEdt
|
||||
private suspend fun configure(path: String, target: SdkmanCandidate) {
|
||||
LOG.info("${target.target} - Candidate found: ${SdkVersionUtil.getJdkVersionInfo(path)?.displayVersionString()}")
|
||||
|
||||
val jdk = SdkConfigurationUtil.createAndAddSDK(path, JavaSdk.getInstance())
|
||||
|
||||
if (jdk != null) {
|
||||
LOG.info("${target.target} - Registered ${jdk.name}")
|
||||
configure(jdk, target)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private suspend fun configure(jdk: Sdk, target: SdkmanCandidate) {
|
||||
val rootManager = ProjectRootManager.getInstance(project)
|
||||
|
||||
writeAction {
|
||||
rootManager.projectSdk = jdk
|
||||
}
|
||||
|
||||
LOG.info("${target.target} - Configured ${jdk.name}")
|
||||
|
||||
NotificationGroupManager.getInstance()
|
||||
.getNotificationGroup("Setup SDK")
|
||||
.createNotification(
|
||||
JavaBundle.message("sdk.configured.sdkmanrc.title"),
|
||||
JavaBundle.message("sdk.configured.sdkmanrc", jdk.versionString),
|
||||
NotificationType.INFORMATION
|
||||
)
|
||||
.notify(project)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
private fun SdkmanCandidate.match(sdk: Sdk?): Boolean {
|
||||
contract { returns(true) implies (sdk != null) }
|
||||
if (sdk == null) return false
|
||||
val versionString = sdk.versionString ?: return false
|
||||
return matchVersionString(versionString)
|
||||
}
|
||||
|
||||
private fun SdkmanCandidate.match(path: String): Boolean {
|
||||
val info = SdkVersionUtil.getJdkVersionInfo(path) ?: return false
|
||||
return matchVersionString(info.displayVersionString())
|
||||
}
|
||||
|
||||
override fun dispose() {}
|
||||
}
|
||||
|
||||
private class SdkmanrcWatcher : ProjectActivity {
|
||||
init {
|
||||
if (ApplicationManager.getApplication().isUnitTestMode) {
|
||||
throw ExtensionNotApplicableException.create()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun execute(project: Project) {
|
||||
if (!AdvancedSettings.getBoolean("java.sdkmanrc.watcher")) {
|
||||
return
|
||||
}
|
||||
|
||||
val watcherService = project.serviceAsync<SdkmanrcWatcherService>()
|
||||
watcherService.registerListener(project)
|
||||
watcherService.configureSdkFromSdkmanrc()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.projectRoots.impl
|
||||
|
||||
import com.intellij.openapi.application.runWriteActionAndWait
|
||||
import com.intellij.openapi.projectRoots.ProjectJdkTable
|
||||
import com.intellij.testFramework.HeavyPlatformTestCase
|
||||
import com.intellij.testFramework.IdeaTestUtil
|
||||
import com.intellij.testFramework.VfsTestUtil.createFile
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
abstract class ExternalJavaConfigurationTest: HeavyPlatformTestCase() {
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
|
||||
for (versionString in mockJdkVersions) {
|
||||
val jdk = IdeaTestUtil.createMockJdk(versionString, "")
|
||||
SdkConfigurationUtil.addSdk(jdk)
|
||||
}
|
||||
}
|
||||
|
||||
abstract val mockJdkVersions: List<String>
|
||||
|
||||
fun <T> checkSuggestion(
|
||||
watcher: ExternalJavaConfigurationService,
|
||||
configProvider: ExternalJavaConfigurationProvider<T>,
|
||||
configFileContent: String,
|
||||
expectedVersionString: String,
|
||||
) {
|
||||
val file = configProvider.getConfigurationFile(project)
|
||||
createFile(getOrCreateProjectBaseDir(), file.name, configFileContent)
|
||||
val suggestion = runBlocking { watcher.getReleaseData(configProvider) }
|
||||
assert(suggestion != null)
|
||||
|
||||
val jdk = watcher.findCandidate<T>(suggestion!!, configProvider)
|
||||
assert(jdk is ExternalJavaConfigurationService.JdkCandidate.Jdk)
|
||||
assertEquals((jdk as ExternalJavaConfigurationService.JdkCandidate.Jdk).jdk.versionString, expectedVersionString)
|
||||
}
|
||||
|
||||
override fun tearDown() {
|
||||
try {
|
||||
runWriteActionAndWait {
|
||||
ProjectJdkTable.getInstance().apply {
|
||||
allJdks.forEach { removeJdk(it) }
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
addSuppressedException(e)
|
||||
} finally {
|
||||
super.tearDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,30 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.projectRoots.impl
|
||||
|
||||
import com.intellij.openapi.application.runWriteActionAndWait
|
||||
import com.intellij.openapi.projectRoots.ProjectJdkTable
|
||||
import com.intellij.platform.util.coroutines.childScope
|
||||
import com.intellij.testFramework.HeavyPlatformTestCase
|
||||
import com.intellij.testFramework.IdeaTestUtil
|
||||
import com.intellij.testFramework.VfsTestUtil.createFile
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class SdkmanrcWatcherHeavyTests : HeavyPlatformTestCase() {
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
|
||||
val graal21 = IdeaTestUtil.createMockJdk("GraalVM CE 23.1.2 - Java 21.0.2", "")
|
||||
SdkConfigurationUtil.addSdk(graal21)
|
||||
|
||||
val jbr17 = IdeaTestUtil.createMockJdk("JetBrains Runtime 17.0.7", "")
|
||||
SdkConfigurationUtil.addSdk(jbr17)
|
||||
|
||||
val open11 = IdeaTestUtil.createMockJdk("Oracle OpenJDK 11.0.2", "")
|
||||
SdkConfigurationUtil.addSdk(open11)
|
||||
}
|
||||
class SdkmanrcWatcherHeavyTests : ExternalJavaConfigurationTest() {
|
||||
override val mockJdkVersions: List<String> = listOf(
|
||||
"GraalVM CE 23.1.2 - Java 21.0.2",
|
||||
"Oracle OpenJDK 11.0.2",
|
||||
"JetBrains Runtime 17.0.7",
|
||||
)
|
||||
|
||||
fun `test jdk suggestion after sdkmanrc change`() {
|
||||
runBlocking {
|
||||
val scope = childScope()
|
||||
val scope = childScope("")
|
||||
try {
|
||||
val watcher = SdkmanrcWatcherService(project, scope)
|
||||
val watcher = ExternalJavaConfigurationService(project, scope)
|
||||
val configProvider = SdkmanrcConfigurationProvider()
|
||||
|
||||
assert(watcher.suggestSdkFromSdkmanrc() == null)
|
||||
assert(watcher.getReleaseData(configProvider) == null)
|
||||
|
||||
createFile(getOrCreateProjectBaseDir(), ".sdkmanrc", "java=21.0.2-graalce")
|
||||
val suggestion1 = watcher.suggestSdkFromSdkmanrc()
|
||||
assert(suggestion1 is SdkmanrcWatcherService.SdkSuggestion.Jdk)
|
||||
assert((suggestion1 as SdkmanrcWatcherService.SdkSuggestion.Jdk).jdk.versionString == "GraalVM CE 23.1.2 - Java 21.0.2")
|
||||
|
||||
createFile(getOrCreateProjectBaseDir(), ".sdkmanrc", "java=11.0.2-open")
|
||||
val suggestion2 = watcher.suggestSdkFromSdkmanrc()
|
||||
assert(suggestion2 is SdkmanrcWatcherService.SdkSuggestion.Jdk)
|
||||
assert((suggestion2 as SdkmanrcWatcherService.SdkSuggestion.Jdk).jdk.versionString == "Oracle OpenJDK 11.0.2")
|
||||
|
||||
createFile(getOrCreateProjectBaseDir(), ".sdkmanrc", "java=17.0.7-jbr")
|
||||
val suggestion3 = watcher.suggestSdkFromSdkmanrc()
|
||||
assert(suggestion3 is SdkmanrcWatcherService.SdkSuggestion.Jdk)
|
||||
assert((suggestion3 as SdkmanrcWatcherService.SdkSuggestion.Jdk).jdk.versionString == "JetBrains Runtime 17.0.7")
|
||||
checkSuggestion(watcher, configProvider, "java=21.0.2-graalce", "GraalVM CE 23.1.2 - Java 21.0.2")
|
||||
checkSuggestion(watcher, configProvider, "java=11.0.2-open", "Oracle OpenJDK 11.0.2")
|
||||
checkSuggestion(watcher, configProvider, "java=17.0.7-jbr", "JetBrains Runtime 17.0.7")
|
||||
}
|
||||
catch (_: Exception) {}
|
||||
finally {
|
||||
@@ -54,39 +32,25 @@ class SdkmanrcWatcherHeavyTests : HeavyPlatformTestCase() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun tearDown() {
|
||||
try {
|
||||
runWriteActionAndWait {
|
||||
ProjectJdkTable.getInstance().apply {
|
||||
allJdks.forEach { removeJdk(it) }
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
addSuppressedException(e)
|
||||
} finally {
|
||||
super.tearDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SdkmanrcWatcherLightTests : BasePlatformTestCase() {
|
||||
|
||||
fun `test candidates parsing`() {
|
||||
assertEquals(SdkmanCandidate.parse("8"),
|
||||
SdkmanCandidate("8", "8", null, ""))
|
||||
assertEquals(SdkmanReleaseData.parse("8"),
|
||||
SdkmanReleaseData("8", "8", null, ""))
|
||||
|
||||
assertEquals(SdkmanCandidate.parse("19-zulu"),
|
||||
SdkmanCandidate("19-zulu", "19", null, "zulu"))
|
||||
assertEquals(SdkmanReleaseData.parse("19-zulu"),
|
||||
SdkmanReleaseData("19-zulu", "19", null, "zulu"))
|
||||
|
||||
assertEquals(SdkmanCandidate.parse("11.0.17-tem"),
|
||||
SdkmanCandidate("11.0.17-tem", "11.0.17", null, "tem"))
|
||||
assertEquals(SdkmanReleaseData.parse("11.0.17-tem"),
|
||||
SdkmanReleaseData("11.0.17-tem", "11.0.17", null, "tem"))
|
||||
|
||||
assertEquals(SdkmanCandidate.parse("11.0.9.fx-librca"),
|
||||
SdkmanCandidate("11.0.9.fx-librca", "11.0.9", "fx", "librca"))
|
||||
assertEquals(SdkmanReleaseData.parse("11.0.9.fx-librca"),
|
||||
SdkmanReleaseData("11.0.9.fx-librca", "11.0.9", "fx", "librca"))
|
||||
|
||||
assertEquals(SdkmanCandidate.parse("16.0.1.hs-adpt"),
|
||||
SdkmanCandidate("16.0.1.hs-adpt", "16.0.1", "hs", "adpt"))
|
||||
assertEquals(SdkmanReleaseData.parse("16.0.1.hs-adpt"),
|
||||
SdkmanReleaseData("16.0.1.hs-adpt", "16.0.1", "hs", "adpt"))
|
||||
}
|
||||
|
||||
fun `test candidates matching`() {
|
||||
@@ -108,7 +72,7 @@ class SdkmanrcWatcherLightTests : BasePlatformTestCase() {
|
||||
"22.ea.2-open" to "Oracle OpenJDK 22",
|
||||
)) {
|
||||
assert(
|
||||
SdkmanCandidate.parse(candidate)?.matchVersionString(version) == true
|
||||
SdkmanReleaseData.parse(candidate)?.matchVersionString(version) == true
|
||||
) { "$candidate doesn't match $version" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1244,8 +1244,8 @@ scheduled.thread.pool.executor.with.zero.core.threads.description='ScheduledThre
|
||||
scope.hierarchy=Hierarchy of {0}
|
||||
sdk.cannot.create=Cannot Create SDK
|
||||
sdk.java.no.classes=Cannot find JDK classes in ''{0}''
|
||||
sdk.configured.sdkmanrc.title=JDK is configured (.sdkmanrc)
|
||||
sdk.configured.sdkmanrc=''{0}'' is set as project JDK.
|
||||
sdk.configured.external.config.title=JDK is configured ({0})
|
||||
sdk.configured=''{0}'' is set as project JDK.
|
||||
section.title.inspection.suspicious.names.ignore.methods=Ignore methods:
|
||||
set.language.level=Set language level
|
||||
set.language.level.to.0=Set language level to {0}
|
||||
@@ -1823,7 +1823,7 @@ notification.group.source.searcher=Failed to find sources for JAR file
|
||||
notification.group.language.level=Preview Java language level requires accepting license
|
||||
notification.group.preview.features=Preview Java language level may be discontinued
|
||||
notification.group.redundant.exports=Redundant exports/opens can be removed
|
||||
notification.group.setup.sdk=JDK configured
|
||||
notification.group.setup.jdk=JDK configured
|
||||
notification.group.setup.external.annotations=Failed to load external annotations
|
||||
notification.group.testintegration=Failed to generate tests for @TestDataPath
|
||||
notification.group.legacy.library=Legacy library depends on IDE installation
|
||||
|
||||
Reference in New Issue
Block a user