From 4bcef8a3f24981570df7897a344e05e208579eae Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Wed, 12 Nov 2025 18:25:27 +0100 Subject: [PATCH] IJPL-217708 Support plugins with use-idea-classloader by allowing platform modules to use them GitOrigin-RevId: 96d07824221e2c4a6aea81eb7ce375eedc6676f2 --- .../ModuleBasedProductLoadingStrategy.kt | 25 +++++++++- .../intellij/build/classPath/classpath.kt | 47 +++++++++++++++++++ .../intellij/build/dev/IdeBuilder.kt | 34 ++++++++------ .../build/impl/DistributionJARsBuilder.kt | 38 ++++++++++----- .../intellij/build/impl/PlatformModules.kt | 31 ++++++++---- 5 files changed, 138 insertions(+), 37 deletions(-) diff --git a/platform/bootstrap/src/com/intellij/platform/bootstrap/ModuleBasedProductLoadingStrategy.kt b/platform/bootstrap/src/com/intellij/platform/bootstrap/ModuleBasedProductLoadingStrategy.kt index 11363314229a..44fea0b46858 100644 --- a/platform/bootstrap/src/com/intellij/platform/bootstrap/ModuleBasedProductLoadingStrategy.kt +++ b/platform/bootstrap/src/com/intellij/platform/bootstrap/ModuleBasedProductLoadingStrategy.kt @@ -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, diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/classPath/classpath.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/classPath/classpath.kt index 41f4be6aff5c..6a71be4632a5 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/classPath/classpath.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/classPath/classpath.kt @@ -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, +): Set { + val classPathResult = mutableSetOf() + 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 { + 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?, diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/dev/IdeBuilder.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/dev/IdeBuilder.kt index 5b02062c1fa8..94ef6f5a1786 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/dev/IdeBuilder.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/dev/IdeBuilder.kt @@ -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() + diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/DistributionJARsBuilder.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/DistributionJARsBuilder.kt index 764700889f15..e6d27c7d09b8 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/DistributionJARsBuilder.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/DistributionJARsBuilder.kt @@ -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, + bundledPluginsDistribution: List, +): List { + 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 } diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/PlatformModules.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/PlatformModules.kt index e17ff54b5d2a..4a99d8dcee66 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/PlatformModules.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/PlatformModules.kt @@ -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 { val javaExtensionService = JpsJavaExtensionService.getInstance() val result = mutableMapOf() + 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>>() - 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>>() + 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 { + val bundledPlugins = getPluginLayoutsByJpsModuleNames(modules = context.getBundledPluginModules(), productLayout = context.productProperties.productLayout) + val corePluginContents = bundledPlugins.flatMap { extractPlatformPluginsModules(context, it.mainModule) }.toSet() + return corePluginContents } private fun computeTransitive(