diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/BuildContext.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/BuildContext.kt index b96605d93a88..75688dee977b 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/BuildContext.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/BuildContext.kt @@ -144,6 +144,8 @@ interface BuildContext : CompilationContext { } suspend fun getFrontendModuleFilter(): FrontendModuleFilter + + suspend fun getContentModuleFilter(): ContentModuleFilter val isEmbeddedFrontendEnabled: Boolean diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/ContentModuleFilter.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/ContentModuleFilter.kt new file mode 100644 index 000000000000..1aa63aaa09fc --- /dev/null +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/ContentModuleFilter.kt @@ -0,0 +1,21 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.intellij.build + +import org.jetbrains.annotations.ApiStatus + +/** + * Provides a way to exclude from the distribution some modules registered as an optional 'content module' in a plugin. + * It can be used to reduce the installation size by removing modules which will never be compatible with it, e.g., for a standalone frontend variant of an IDE. + */ +@ApiStatus.Experimental +interface ContentModuleFilter { + /** + * Returns `true` if a module with name [moduleName] which is registered in `content` tag in `plugin.xml` should be added to the plugin distribution, and `false` otherwise. + * If the function returns `false`, the corresponding tag is also removed from `plugin.xml`. + * + * Note, that modules with `loading` rule set to `required` or `embedded` must be always included; this function will be called only for modules `optional` and `on-demand` + * loading rules. + * @param pluginMainModuleName specifies the plugin where [moduleName] is included, or `null` if [moduleName] is part of the core (platform) plugin + */ + fun isOptionalModuleIncluded(moduleName: String, pluginMainModuleName: String?): Boolean +} diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/autoLayout.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/autoLayout.kt index 58b6cd7863c2..e00b372f5f96 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/autoLayout.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/autoLayout.kt @@ -2,6 +2,7 @@ package org.jetbrains.intellij.build import com.intellij.util.xml.dom.readXmlAsModel +import io.opentelemetry.api.trace.Span import org.jetbrains.intellij.build.impl.* private const val VERIFIER_MODULE = "intellij.platform.commercial.verifier" @@ -62,7 +63,13 @@ internal suspend fun computeModuleSourcesByContent( searchableOptionSet: SearchableOptionSetDescriptor? ) { val frontendModuleFilter = context.getFrontendModuleFilter() + val contentModuleFilter = context.getContentModuleFilter() for ((moduleName, loadingRule) in helper.readPluginContentFromDescriptor(context.findRequiredModule(layout.mainModule), jarPackager.moduleOutputPatcher)) { + if (loadingRule != "required" && loadingRule != "embedded" && !contentModuleFilter.isOptionalModuleIncluded(moduleName, pluginMainModuleName = layout.mainModule)) { + Span.current().addEvent("Module '$moduleName' is excluded from plugin '${layout.mainModule}' by $contentModuleFilter") + continue + } + // CWM plugin is overcomplicated without any valid reason - it must be refactored if (moduleName == "intellij.driver.backend.split" || !addedModules.add(moduleName)) { continue diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/BuildContextImpl.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/BuildContextImpl.kt index b7a91d6d9d82..0ba96a40bb07 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/BuildContextImpl.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/BuildContextImpl.kt @@ -234,6 +234,20 @@ class BuildContextImpl internal constructor( override suspend fun getFrontendModuleFilter(): FrontendModuleFilter = _frontendModuleFilter.await() + private val contentModuleFilter = computeContentModuleFilter() + + @OptIn(DelicateCoroutinesApi::class) + private fun computeContentModuleFilter(): Deferred { + if (productProperties.productMode == ProductMode.MONOLITH) return CompletableDeferred(IncludeAllContentModuleFilter) + + return GlobalScope.async(Dispatchers.Unconfined + CoroutineName("Content Modules Filter"), start = CoroutineStart.LAZY) { + val bundledPluginModules = getBundledPluginModules() + ContentModuleByProductModeFilter(getOriginalModuleRepository().repository, bundledPluginModules, productProperties.productMode) + } + } + + override suspend fun getContentModuleFilter(): ContentModuleFilter = contentModuleFilter.await() + override val isEmbeddedFrontendEnabled: Boolean get() = productProperties.embeddedFrontendRootModule != null && options.enableEmbeddedFrontend diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/ContentModuleFilterImpl.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/ContentModuleFilterImpl.kt new file mode 100644 index 000000000000..a42263ce2b36 --- /dev/null +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/ContentModuleFilterImpl.kt @@ -0,0 +1,37 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.intellij.build.impl + +import com.intellij.platform.runtime.product.ProductMode +import com.intellij.platform.runtime.product.impl.ProductModeMatcher +import com.intellij.platform.runtime.repository.RuntimeModuleId +import com.intellij.platform.runtime.repository.RuntimeModuleRepository +import org.jetbrains.intellij.build.ContentModuleFilter + +/** + * An instance of [ContentModuleFilter] which excludes modules not compatible with the given [ProductMode] from the platform part and bundled plugins. + */ +internal class ContentModuleByProductModeFilter( + private val moduleRepository: RuntimeModuleRepository, + bundledPluginModules: List, + private val productMode: ProductMode +) : ContentModuleFilter { + + private val productModeMatcher = ProductModeMatcher(productMode) + private val bundledPluginMainModules = bundledPluginModules.toSet() + + override fun isOptionalModuleIncluded(moduleName: String, pluginMainModuleName: String?): Boolean { + if (pluginMainModuleName != null && pluginMainModuleName !in bundledPluginMainModules) { + return true + } + val moduleDescriptor = moduleRepository.getModule(RuntimeModuleId.module(moduleName)) + return productModeMatcher.matches(moduleDescriptor) + } + + override fun toString(): String { + return "ContentModuleByProductModeFilter{productMode=${productMode.id}}" + } +} + +internal object IncludeAllContentModuleFilter : ContentModuleFilter { + override fun isOptionalModuleIncluded(moduleName: String, pluginMainModuleName: String?): Boolean = true +} \ No newline at end of file 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 20ff2cadd60b..f43ff40139a8 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 @@ -3,6 +3,7 @@ package org.jetbrains.intellij.build.impl import com.intellij.openapi.util.JDOMUtil +import io.opentelemetry.api.trace.Span import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @@ -579,12 +580,22 @@ private fun embedAndCollectProductModules(file: Path, xIncludePathResolver: XInc suspend fun embedContentModules(file: Path, xIncludePathResolver: XIncludePathResolver, xml: Element, layout: PluginLayout?, context: BuildContext) { val frontendModuleFilter = context.getFrontendModuleFilter() + val contentModuleFilter = context.getContentModuleFilter() resolveNonXIncludeElement(original = xml, base = file, pathResolver = xIncludePathResolver) - for (moduleElement in xml.getChildren("content").asSequence().flatMap { it.getChildren("module") }) { + + val moduleElements = xml.getChildren("content").flatMap { it.getChildren("module") } + for (moduleElement in moduleElements) { val moduleName = moduleElement.getAttributeValue("name") ?: continue check(moduleElement.content.isEmpty()) val jpsModuleName = moduleName.substringBeforeLast('/') + val loadingRule = moduleElement.getAttributeValue("loading") + if (loadingRule != "required" && loadingRule != "embedded" && !contentModuleFilter.isOptionalModuleIncluded(jpsModuleName, pluginMainModuleName = layout?.mainModule)) { + Span.current().addEvent("Tag for module '$moduleName' is removed from plugin.xml file for '${layout?.mainModule}' by $contentModuleFilter") + moduleElement.parent.removeContent(moduleElement) + continue + } + val descriptor = getModuleDescriptor(moduleName = moduleName, jpsModuleName = jpsModuleName, xIncludePathResolver = xIncludePathResolver, context = context) if (jpsModuleName == moduleName && (context as BuildContextImpl).jarPackagerDependencyHelper.isPluginModulePackedIntoSeparateJar(context.findRequiredModule(jpsModuleName.removeSuffix("._test")), layout, frontendModuleFilter)) {