[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
This commit is contained in:
Nikolay Chashnikov
2025-02-04 20:26:52 +00:00
committed by intellij-monorepo-bot
parent 3402d737d7
commit f9ca7cac87
6 changed files with 93 additions and 1 deletions
@@ -144,6 +144,8 @@ interface BuildContext : CompilationContext {
}
suspend fun getFrontendModuleFilter(): FrontendModuleFilter
suspend fun getContentModuleFilter(): ContentModuleFilter
val isEmbeddedFrontendEnabled: Boolean
@@ -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
}
@@ -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
@@ -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<ContentModuleFilter> {
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
@@ -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<String>,
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
}
@@ -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)) {