From f9ca7cac875ebb66603eec35bf835590a5db00e8 Mon Sep 17 00:00:00 2001 From: Nikolay Chashnikov Date: Tue, 4 Feb 2025 12:55:38 +0100 Subject: [PATCH] [build scripts] exclude content modules from distribution if they aren't compatible with the current product mode (IJPL-165692) ContentModuleFilter interface is introduced which can be used to filter out some optional content modules from the distribution. It's used to exclude modules not compatible with the frontend mode when building a standalone frontend distribution (JetBrains Client). This is needed to be able to use the same plugin.xml for the backend and the frontend parts without increasing the size of the standalone frontend distribution. ContentModuleFilter can be also used for other cases when we need to exclude unavailable modules from the distribution, see IJPL-176806. GitOrigin-RevId: 9e6b6dac1753d586199c39e96998760df17db5c7 --- .../jetbrains/intellij/build/BuildContext.kt | 2 + .../intellij/build/ContentModuleFilter.kt | 21 +++++++++++ .../jetbrains/intellij/build/autoLayout.kt | 7 ++++ .../intellij/build/impl/BuildContextImpl.kt | 14 +++++++ .../build/impl/ContentModuleFilterImpl.kt | 37 +++++++++++++++++++ .../intellij/build/impl/PlatformModules.kt | 13 ++++++- 6 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 platform/build-scripts/src/org/jetbrains/intellij/build/ContentModuleFilter.kt create mode 100644 platform/build-scripts/src/org/jetbrains/intellij/build/impl/ContentModuleFilterImpl.kt 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)) {