From d6ea3c63d79b5c0a422c6b1506beb619bfa346b9 Mon Sep 17 00:00:00 2001 From: Nikolay Chashnikov Date: Thu, 28 Aug 2025 11:37:57 +0200 Subject: [PATCH] [build scripts] don't rely on the runtime module repository when filtering modules compatible with the frontend (IJPL-201730) Since the runtime module repository won't be produced during compilation after migration to Bazel, the build scripts cannot use it to filter out modules incompatible with the frontend when building the frontend distribution and to put such modules in separate JARs when building a monolith distribution. Since building the runtime module repository may take noticeable time when running 'dev build' from sources, the logic in 'ContentModuleFilterImpl' and 'FrontendModuleFilterImpl' was reimplemented to use the data from JpsModel instead. IJ-MR-174028 GitOrigin-RevId: d51e6e1cde9e559a5555388a6bda7468ed95dd9a --- .../intellij/build/impl/BuildContextImpl.kt | 6 +- .../build/impl/ContentModuleFilterImpl.kt | 15 ++-- .../build/impl/FrontendModuleFilterImpl.kt | 89 ++++++++++++++----- .../impl/moduleBased/JpsProductModeMatcher.kt | 32 +++++++ .../product/src/impl/ProductModes.java | 4 +- 5 files changed, 112 insertions(+), 34 deletions(-) create mode 100644 platform/build-scripts/src/org/jetbrains/intellij/build/impl/moduleBased/JpsProductModeMatcher.kt 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 380c5204b955..0de4339cc41f 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 @@ -254,8 +254,8 @@ class BuildContextImpl internal constructor( val rootModule = productProperties.embeddedFrontendRootModule if (rootModule != null && options.enableEmbeddedFrontend) { val moduleRepository = getOriginalModuleRepository() - val productModules = moduleRepository.loadProductModules(rootModule, ProductMode.FRONTEND) - FrontendModuleFilterImpl(moduleRepository.repository, productModules) + val productModules = moduleRepository.loadRawProductModules(rootModule, ProductMode.FRONTEND) + FrontendModuleFilterImpl.create(project, productModules, jarPackagerDependencyHelper) } else { EmptyFrontendModuleFilter @@ -276,7 +276,7 @@ class BuildContextImpl internal constructor( return asyncLazy("Content Modules Filter") { val bundledPluginModules = getBundledPluginModules() - ContentModuleByProductModeFilter(getOriginalModuleRepository().repository, bundledPluginModules, productProperties.productMode) + ContentModuleByProductModeFilter(project, bundledPluginModules, productProperties.productMode) } } 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 index 045cbd2d933f..e7b17f5bc46c 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/ContentModuleFilterImpl.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/ContentModuleFilterImpl.kt @@ -2,30 +2,29 @@ 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.BuildContext import org.jetbrains.intellij.build.ContentModuleFilter +import org.jetbrains.intellij.build.impl.moduleBased.JpsProductModeMatcher +import org.jetbrains.jps.model.JpsProject /** * 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 project: JpsProject, + bundledPluginModules: List, private val productMode: ProductMode ) : ContentModuleFilter { - private val productModeMatcher = ProductModeMatcher(productMode) + private val productModeMatcher = JpsProductModeMatcher(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) + val module = project.findModuleByName(moduleName) ?: return true + return productModeMatcher.matches(module) } override fun toString(): String { diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/FrontendModuleFilterImpl.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/FrontendModuleFilterImpl.kt index ce78335b08da..9a1b568850bb 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/FrontendModuleFilterImpl.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/FrontendModuleFilterImpl.kt @@ -2,33 +2,84 @@ package org.jetbrains.intellij.build.impl import com.intellij.platform.runtime.product.ProductMode -import com.intellij.platform.runtime.product.ProductModules -import com.intellij.platform.runtime.product.impl.ProductModeMatcher -import com.intellij.platform.runtime.repository.RuntimeModuleId -import com.intellij.platform.runtime.repository.RuntimeModuleRepository +import com.intellij.platform.runtime.product.serialization.RawProductModules import org.jetbrains.intellij.build.FrontendModuleFilter +import org.jetbrains.intellij.build.JarPackagerDependencyHelper +import org.jetbrains.intellij.build.impl.moduleBased.JpsProductModeMatcher import org.jetbrains.jps.model.JpsNamedElement +import org.jetbrains.jps.model.JpsProject +import org.jetbrains.jps.model.java.JpsJavaExtensionService import org.jetbrains.jps.model.library.JpsLibrary import org.jetbrains.jps.model.module.JpsModule +import org.jetbrains.jps.model.module.JpsModuleReference -internal class FrontendModuleFilterImpl(private val moduleRepository: RuntimeModuleRepository, productModules: ProductModules): FrontendModuleFilter { - private val includedModules: Set = (sequenceOf(productModules.mainModuleGroup) + productModules.bundledPluginModuleGroups.asSequence()) - .flatMap { it.includedModules.asSequence() } - .filter { included -> included.moduleDescriptor.moduleId !in MODULES_SCRAMBLED_WITH_FRONTEND } - .mapTo(HashSet()) { it.moduleDescriptor.moduleId } - private val frontendModeMatcher = ProductModeMatcher(ProductMode.FRONTEND) +internal class FrontendModuleFilterImpl private constructor( + private val project: JpsProject, + private val frontendModeMatcher: JpsProductModeMatcher, + private val includedModuleNames: Set, + private val includedProjectLibraryNames: Set, +): FrontendModuleFilter { + companion object { + suspend fun create(project: JpsProject, productModules: RawProductModules, jarPackagerDependencyHelper: JarPackagerDependencyHelper): FrontendModuleFilter { + val frontendModeMatcher = JpsProductModeMatcher(ProductMode.FRONTEND) + val includedModuleNames = LinkedHashSet() + val includedProjectLibraryNames = LinkedHashSet() + + for (rootModuleName in productModules.mainGroupModules) { + val rootModule = project.findModuleByName(rootModuleName.moduleId.stringId) ?: continue + collectTransitiveDependenciesCompatibleWithFrontend(rootModule, frontendModeMatcher, includedModuleNames, includedProjectLibraryNames) + } + + for (mainModuleId in productModules.bundledPluginMainModules) { + val module = project.findModuleByName(mainModuleId.stringId) ?: continue + if (frontendModeMatcher.matches(module)) { + includedModuleNames.add(module.name) + jarPackagerDependencyHelper.readPluginContentFromDescriptor(module, ModuleOutputPatcher()) + .mapNotNull { project.findModuleByName(it.first) } + .filter { frontendModeMatcher.matches(it) } + .mapTo(includedModuleNames) { it.name } + } + } + + return FrontendModuleFilterImpl(project, frontendModeMatcher, includedModuleNames, includedProjectLibraryNames) + } + + private fun collectTransitiveDependenciesCompatibleWithFrontend( + module: JpsModule, + frontendModeMatcher: JpsProductModeMatcher, + includedModuleNames: MutableSet, + includedProjectLibraryNames: MutableSet, + ) { + if (isScrambledWithFrontend(module) || !frontendModeMatcher.matches(module)) { + return + } + if (!includedModuleNames.add(module.name)) { + return + } + JpsJavaExtensionService.dependencies(module).productionOnly().runtimeOnly().processModuleAndLibraries( + { depModule -> + collectTransitiveDependenciesCompatibleWithFrontend(depModule, frontendModeMatcher, includedModuleNames, includedProjectLibraryNames) + }, + { depLibrary -> + if (!isScrambledWithFrontend(depLibrary) && depLibrary.createReference().parentReference !is JpsModuleReference) { + includedProjectLibraryNames.add(depLibrary.name) + } + } + ) + } + } override fun isModuleIncluded(moduleName: String): Boolean { - return RuntimeModuleId.module(moduleName) in includedModules + return moduleName in includedModuleNames } override fun isProjectLibraryIncluded(libraryName: String): Boolean { - return RuntimeModuleId.projectLibrary(libraryName) in includedModules + return libraryName in includedProjectLibraryNames } override fun isModuleCompatibleWithFrontend(moduleName: String): Boolean { - val moduleDescriptor = moduleRepository.getModule(RuntimeModuleId.module(moduleName)) - return frontendModeMatcher.matches(moduleDescriptor) + val module = project.findModuleByName(moduleName) + return module != null && frontendModeMatcher.matches(module) } } @@ -42,15 +93,9 @@ val PROJECT_LIBRARIES_SCRAMBLED_WITH_FRONTEND: Set = setOf( ) /** - * Contains a set of runtime modules from the platform part which are also included in the frontend JARs and scrambled (differently) there. - * It's important not to include JARs for these modules in the platform part to the classpath of the frontend process, because they may - * cause clashes. + * Returns `true` if the module or library [element] from the platform part which are also included in the frontend JARs and scrambled (differently) there. + * It's important not to include JARs for these modules and libraries in the platform part to the classpath of the frontend process, because they may cause clashes. */ -val MODULES_SCRAMBLED_WITH_FRONTEND: Set by lazy { - setOf(RuntimeModuleId.module(PLATFORM_MODULE_SCRAMBLED_WITH_FRONTEND)) + - PROJECT_LIBRARIES_SCRAMBLED_WITH_FRONTEND.map { RuntimeModuleId.projectLibrary(it) } -} - fun isScrambledWithFrontend(element: JpsNamedElement): Boolean = when (element) { is JpsModule -> element.name == PLATFORM_MODULE_SCRAMBLED_WITH_FRONTEND is JpsLibrary -> element.name in PROJECT_LIBRARIES_SCRAMBLED_WITH_FRONTEND diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/moduleBased/JpsProductModeMatcher.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/moduleBased/JpsProductModeMatcher.kt new file mode 100644 index 000000000000..5633d43bb1ff --- /dev/null +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/moduleBased/JpsProductModeMatcher.kt @@ -0,0 +1,32 @@ +// 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.moduleBased + +import com.intellij.platform.runtime.product.ProductMode +import com.intellij.platform.runtime.product.impl.ProductModes +import org.jetbrains.jps.model.java.JpsJavaExtensionService +import org.jetbrains.jps.model.module.JpsModule + +/** + * This is an equivalent implementation of [com.intellij.platform.runtime.product.impl.ProductModeMatcher] based on JPS model instead of the runtime module repository. + */ +internal class JpsProductModeMatcher(productMode: ProductMode) { + private val incompatibleRootModule = ProductModes.getIncompatibleRootModule(productMode).stringId + private val cache: MutableMap = mutableMapOf() + + fun matches(module: JpsModule): Boolean { + val cached = cache[module] + if (cached != null) return cached + if (incompatibleRootModule == module.name) { + cache[module] = false + return false + } + + cache[module] = true //this is needed to prevent StackOverflowError in the case of circular dependencies + var matches = true + JpsJavaExtensionService.dependencies(module).productionOnly().runtimeOnly().processModules { + matches = matches && matches(it) + } + cache[module] = matches + return matches + } +} diff --git a/platform/runtime/product/src/impl/ProductModes.java b/platform/runtime/product/src/impl/ProductModes.java index 425c422ee7e4..da230a2f22b5 100644 --- a/platform/runtime/product/src/impl/ProductModes.java +++ b/platform/runtime/product/src/impl/ProductModes.java @@ -3,6 +3,7 @@ package com.intellij.platform.runtime.product.impl; import com.intellij.platform.runtime.product.ProductMode; import com.intellij.platform.runtime.repository.RuntimeModuleId; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** @@ -10,7 +11,8 @@ import org.jetbrains.annotations.NotNull; * Currently, this is determined by the presence of specific modules in dependencies. We can change this and require to specify modes * a module is compatible with explicitly in the future. */ -final class ProductModes { +@ApiStatus.Internal +public final class ProductModes { private ProductModes() { }