IJPL-217708 Support plugins with use-idea-classloader by allowing platform modules to use them

GitOrigin-RevId: 96d07824221e2c4a6aea81eb7ce375eedc6676f2
This commit is contained in:
Nikolay Rykunov
2025-11-13 19:08:20 +00:00
committed by intellij-monorepo-bot
parent 75fb9c760b
commit 4bcef8a3f2
5 changed files with 138 additions and 37 deletions
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.bootstrap
import com.intellij.ide.plugins.*
@@ -6,6 +6,8 @@ import com.intellij.idea.AppMode
import com.intellij.openapi.diagnostic.debug
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.platform.plugins.parser.impl.PluginDescriptorFromXmlStreamConsumer
import com.intellij.platform.plugins.parser.impl.consume
import com.intellij.platform.runtime.product.IncludedRuntimeModule
import com.intellij.platform.runtime.product.PluginModuleGroup
import com.intellij.platform.runtime.product.ProductMode
@@ -136,7 +138,7 @@ internal class ModuleBasedProductLoadingStrategy(internal val moduleRepository:
}
val bundled = productModules.bundledPluginModuleGroups.map { moduleGroup ->
scope.async {
if (moduleGroup.includedModules.none { it.moduleDescriptor.moduleId in mainGroupModulesSet }) {
if (moduleGroup.includedModules.none { it.moduleDescriptor.moduleId in mainGroupModulesSet } || isPlatformPlugin(moduleGroup, context)) {
val serviceModuleMapping = serviceModuleMappingDeferred.await()
loadPluginDescriptorFromRuntimeModule(
pluginModuleGroup = moduleGroup,
@@ -161,6 +163,25 @@ internal class ModuleBasedProductLoadingStrategy(internal val moduleRepository:
return scope.async { DiscoveredPluginsList(bundled.awaitAll().filterNotNull(), PluginsSourceContext.Bundled) }
}
/**
* Returns true if [pluginModuleGroup] is a plugin with `use-idea-classloader` which should be loaded by platform.
* Content modules of these plugins always should be loaded.
*/
private fun isPlatformPlugin(
pluginModuleGroup: PluginModuleGroup,
loadingContext: PluginDescriptorLoadingContext,
): Boolean {
val mainResourceRoot = pluginModuleGroup.mainModule.resourceRootPaths.singleOrNull() ?: return false
val input = Files.readAllBytes(mainResourceRoot.resolve(PluginManagerCore.PLUGIN_XML_PATH))
// TODO: do we need to support xIncludes in this case?
@Suppress("TestOnlyProblems")
val rawDescriptor = PluginDescriptorFromXmlStreamConsumer(loadingContext.readContext, xIncludeLoader = null).let {
it.consume(input, mainResourceRoot.toString())
it.build()
}
return rawDescriptor.isUseIdeaClassLoader
}
private fun loadCustomPluginDescriptors(
scope: CoroutineScope,
customPluginDir: Path,
@@ -14,6 +14,7 @@ import org.jetbrains.intellij.build.PLATFORM_LOADER_JAR
import org.jetbrains.intellij.build.PLUGIN_XML_RELATIVE_PATH
import org.jetbrains.intellij.build.UTIL_8_JAR
import org.jetbrains.intellij.build.UTIL_JAR
import org.jetbrains.intellij.build.getUnprocessedPluginXmlContent
import org.jetbrains.intellij.build.impl.DescriptorCacheContainer
import org.jetbrains.intellij.build.impl.ModuleIncludeReasons
import org.jetbrains.intellij.build.impl.PRODUCT_DESCRIPTOR_META_PATH
@@ -25,6 +26,7 @@ import org.jetbrains.intellij.build.impl.PlatformJarNames.PRODUCT_BACKEND_JAR
import org.jetbrains.intellij.build.impl.PlatformLayout
import org.jetbrains.intellij.build.impl.PluginLayout
import org.jetbrains.intellij.build.impl.ScopedCachedDescriptorContainer
import org.jetbrains.intellij.build.impl.filterAndProcessContentModules
import org.jetbrains.intellij.build.impl.projectStructureMapping.CustomAssetEntry
import org.jetbrains.intellij.build.impl.projectStructureMapping.DistributionFileEntry
import org.jetbrains.intellij.build.impl.projectStructureMapping.ModuleOutputEntry
@@ -91,6 +93,51 @@ internal fun generateClassPathByLayoutReport(libDir: Path, entries: List<Distrib
return result
}
/**
* Provides a set of paths that should be included in the core classpath.
* This generation is based on the plugins distribution,
* so we would like to include all distribution entities of **embedded** modules (and their libraries) from plugins marked with `use-idea-classloader`.
*/
internal fun generateCoreClasspathFromPlugins(
context: BuildContext,
pluginEntities: List<PluginBuildDescriptor>,
): Set<Path> {
val classPathResult = mutableSetOf<Path>()
for (pluginEntity in pluginEntities) {
val pluginLayout = pluginEntity.layout
val classPathModules = extractPlatformPluginsModules(context, pluginLayout.mainModule)
for (distributionEntry in pluginEntity.distribution) {
if (distributionEntry is ModuleOwnedFileEntry && distributionEntry.owner?.moduleName in classPathModules) {
classPathResult.add(distributionEntry.path)
}
}
}
return classPathResult
}
/**
* Provides a set of content modules ("embedded" ones) and the module of the plugin itself, if it uses `use-idea-classloader`.
* These modules should be included in the core classpath, also their libraries should be treated as platform libraries.
*/
internal fun extractPlatformPluginsModules(
context: BuildContext,
pluginMainModule: String,
): Set<String> {
val pluginModule = context.findRequiredModule(pluginMainModule)
val pluginXmlContent = getUnprocessedPluginXmlContent(pluginModule, context).decodeToString()
val rootElement = JDOMUtil.load(pluginXmlContent)
if (rootElement.getAttribute("use-idea-classloader")?.value?.toBoolean() != true) {
return emptySet()
}
val embeddedModules = mutableSetOf(pluginMainModule)
filterAndProcessContentModules(rootElement, pluginMainModule, context) { _, moduleName, loadingRule ->
if (loadingRule == "embedded") {
embeddedModules.add(moduleName)
}
}
return embeddedModules
}
internal data class PluginBuildDescriptor(
@JvmField val dir: Path,
@JvmField val os: OsFamily?,
@@ -37,6 +37,7 @@ import org.jetbrains.intellij.build.ScrambleTool
import org.jetbrains.intellij.build.SearchableOptionSetDescriptor
import org.jetbrains.intellij.build.WindowsDistributionCustomizer
import org.jetbrains.intellij.build.classPath.generateClassPathByLayoutReport
import org.jetbrains.intellij.build.classPath.generateCoreClasspathFromPlugins
import org.jetbrains.intellij.build.classPath.generatePluginClassPath
import org.jetbrains.intellij.build.classPath.generatePluginClassPathFromPrebuiltPluginFiles
import org.jetbrains.intellij.build.classPath.writePluginClassPathHeader
@@ -178,9 +179,8 @@ internal suspend fun buildProduct(request: BuildRequest, createProductProperties
}
val searchableOptionSet = getSearchableOptionSet(context)
val platformLayoutResultDeferred = async(CoroutineName("platform distribution entries")) {
val generateFilesInBinDirJob = launch(Dispatchers.IO) {
launch(Dispatchers.IO) {
// PathManager.getBinPath() is used as a working dir for maven
val binDir = Files.createDirectories(runDir.resolve("bin"))
val oldFiles = Files.newDirectoryStream(binDir).use { it.toCollection(HashSet()) }
@@ -207,7 +207,7 @@ internal suspend fun buildProduct(request: BuildRequest, createProductProperties
}
val platformLayoutAwaited = platformLayout.await()
val platformLayoutResult = spanBuilder("layout platform").use {
spanBuilder("layout platform").use {
layoutPlatform(
runDir = runDir,
platformLayout = platformLayoutAwaited,
@@ -217,17 +217,6 @@ internal suspend fun buildProduct(request: BuildRequest, createProductProperties
request = request,
)
}
if (request.writeCoreClasspath) {
val classPathString = platformLayoutResult.coreClassPath.joinToString(separator = "\n")
launch(Dispatchers.IO) {
Files.writeString(runDir.resolve("core-classpath.txt"), classPathString)
}
}
generateFilesInBinDirJob.join()
request.platformClassPathConsumer?.invoke(context.ideMainClassName, platformLayoutResult.coreClassPath, runDir)
platformLayoutResult
}
val pluginDistributionEntriesDeferred = async(CoroutineName("build plugins")) {
@@ -242,6 +231,23 @@ internal suspend fun buildProduct(request: BuildRequest, createProductProperties
)
}
// write and update core classpath from platform and plugins distribution
launch {
val platformClasspath = platformLayoutResultDeferred.await().coreClassPath
val pluginDistributionEntities = pluginDistributionEntriesDeferred.await().pluginEntries
val coreClasspathFromPlugins = generateCoreClasspathFromPlugins(context, pluginDistributionEntities)
val classPath = platformClasspath + coreClasspathFromPlugins
if (request.writeCoreClasspath) {
val classPathString = classPath.joinToString(separator = "\n")
launch(Dispatchers.IO) {
Files.writeString(runDir.resolve("core-classpath.txt"), classPathString)
}
}
request.platformClassPathConsumer?.invoke(context.ideMainClassName, classPath, runDir)
}
if (context.generateRuntimeModuleRepository) {
launch {
val allDistributionEntries = platformLayoutResultDeferred.await().distributionEntries.asSequence() +
@@ -36,6 +36,7 @@ import org.jetbrains.intellij.build.SearchableOptionSetDescriptor
import org.jetbrains.intellij.build.buildSearchableOptions
import org.jetbrains.intellij.build.classPath.PluginBuildDescriptor
import org.jetbrains.intellij.build.classPath.generateClassPathByLayoutReport
import org.jetbrains.intellij.build.classPath.generateCoreClasspathFromPlugins
import org.jetbrains.intellij.build.classPath.generatePluginClassPath
import org.jetbrains.intellij.build.classPath.generatePluginClassPathFromPrebuiltPluginFiles
import org.jetbrains.intellij.build.classPath.writePluginClassPathHeader
@@ -136,7 +137,10 @@ internal suspend fun buildDistribution(
context = context,
)
ContentReport(platform = buildPlatformJob.await(), bundledPlugins = bundledPluginItems, nonBundledPlugins = buildNonBundledPlugins.await())
val platformItems = buildPlatformJob.await()
context.bootClassPathJarNames = generateCoreClassPath(context, platformItems, bundledPluginItems)
ContentReport(platform = platformItems, bundledPlugins = bundledPluginItems, nonBundledPlugins = buildNonBundledPlugins.await())
}
coroutineScope {
@@ -162,6 +166,27 @@ internal suspend fun buildDistribution(
contentReport
}
private fun generateCoreClassPath(
context: BuildContext,
platformDistribution: List<DistributionFileEntry>,
bundledPluginsDistribution: List<PluginBuildDescriptor>,
): List<String> {
val platformClassPath = if (context.useModularLoader) {
listOf(PLATFORM_LOADER_JAR)
}
else {
val libDir = context.paths.distAllDir.resolve("lib")
generateClassPathByLayoutReport(
libDir = libDir,
entries = platformDistribution,
skipNioFs = isMultiRoutingFileSystemEnabledForProduct(context.productProperties.platformPrefix)
).map { libDir.relativize(it).toString() }
}
val pluginsDir = context.paths.distAllDir.resolve("plugins")
val coreClassPathFromPlugins = generateCoreClasspathFromPlugins(context, bundledPluginsDistribution).map { pluginsDir.resolve(it).toString() }
return platformClassPath + coreClassPathFromPlugins
}
@VisibleForTesting
suspend fun buildPlatform(
moduleOutputPatcher: ModuleOutputPatcher,
@@ -185,17 +210,6 @@ suspend fun buildPlatform(
tool.scramble(platformLayout = state.platformLayout, platformFileEntries = distributionFileEntries, context = context)
}
}
context.bootClassPathJarNames = if (context.useModularLoader) {
listOf(PLATFORM_LOADER_JAR)
}
else {
val libDir = context.paths.distAllDir.resolve("lib")
generateClassPathByLayoutReport(
libDir = libDir,
entries = distributionFileEntries,
skipNioFs = isMultiRoutingFileSystemEnabledForProduct(context.productProperties.platformPrefix)
).map { libDir.relativize(it).toString() }
}
return distributionFileEntries
}
@@ -15,6 +15,7 @@ import org.jetbrains.intellij.build.PLATFORM_LOADER_JAR
import org.jetbrains.intellij.build.UTIL_8_JAR
import org.jetbrains.intellij.build.UTIL_JAR
import org.jetbrains.intellij.build.UTIL_RT_JAR
import org.jetbrains.intellij.build.classPath.extractPlatformPluginsModules
import org.jetbrains.intellij.build.impl.PlatformJarNames.TEST_FRAMEWORK_JAR
import org.jetbrains.intellij.build.isModuleNameLikeFilename
import org.jetbrains.intellij.build.productLayout.ProductModulesLayout
@@ -378,12 +379,14 @@ fun collectExportedLibrariesFromLibraryModules(
): Map<String, String> {
val javaExtensionService = JpsJavaExtensionService.getInstance()
val result = mutableMapOf<String, String>()
val includedModuleNames = layout.includedModules.map { it.moduleName }
val corePluginsContentModuleNames = computePlatformPluginsContentModules(context)
layout.includedModules
(includedModuleNames + corePluginsContentModuleNames)
.asSequence()
.filter { it.moduleName.startsWith(LIB_MODULE_PREFIX) }
.forEach { moduleItem ->
val module = context.findRequiredModule(moduleItem.moduleName)
.filter { it.startsWith(LIB_MODULE_PREFIX) }
.forEach { moduleName ->
val module = context.findRequiredModule(moduleName)
// get all library dependencies from the module
module.dependenciesList.dependencies
.asSequence()
@@ -394,7 +397,7 @@ fun collectExportedLibrariesFromLibraryModules(
}
.mapNotNull { it.library?.name }
.forEach { libName ->
result.put(libName, moduleItem.moduleName)
result.put(libName, moduleName)
}
}
@@ -492,12 +495,16 @@ private suspend fun computeImplicitRequiredModules(
unique.add("intellij.pycharm.ds")
unique.add("intellij.notebooks.visualization")
val result = mutableListOf<Pair<String, PersistentList<String>>>()
computeTransitive(list = rootList, context = context, unique = unique, result = result)
// we should filter out modules which are included in plugins with `use-idea-classloader`
val corePluginContents = computePlatformPluginsContentModules(context)
val requiredDependencies = mutableListOf<Pair<String, PersistentList<String>>>()
computeTransitive(list = rootList, context = context, unique = unique, result = requiredDependencies)
val requiredModules = requiredDependencies.filter { it.first !in corePluginContents }
if (validateImplicitPlatformModule) {
withContext(Dispatchers.IO) {
for ((name, chain) in result) {
for ((name, chain) in requiredModules) {
launch(CoroutineName("validating the implicit platform module $name")) {
val file = context.findFileInModuleSources(name, "META-INF/plugin.xml")
check(file == null) {
@@ -508,7 +515,13 @@ private suspend fun computeImplicitRequiredModules(
}
}
return result
return requiredModules
}
private fun computePlatformPluginsContentModules(context: BuildContext): Set<String> {
val bundledPlugins = getPluginLayoutsByJpsModuleNames(modules = context.getBundledPluginModules(), productLayout = context.productProperties.productLayout)
val corePluginContents = bundledPlugins.flatMap { extractPlatformPluginsModules(context, it.mainModule) }.toSet()
return corePluginContents
}
private fun computeTransitive(