From 2c3ef39f5067548c3233f99467d864eaac2cc775 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Thu, 20 Nov 2025 14:55:27 +0100 Subject: [PATCH] IJPL-218803 fix scrambling due to packaging of extra classes to scrambled jar - convert core and lang to product modules, extract intellij.platform.credentialStore.impl GitOrigin-RevId: 1dcc0ea54bf15e4d53d5fb3700daa77c37f3885c --- .../intellij/build/IdeaCommunityProperties.kt | 8 +- .../resources/META-INF/IdeaPlugin.xml | 1 - fleet/andel/resources/fleet.andel.xml | 6 +- fleet/kernel/resources/fleet.kernel.xml | 14 +- .../rpc.server/resources/fleet.rpc.server.xml | 5 +- fleet/rpc/resources/fleet.rpc.xml | 4 +- fleet/util/core/resources/fleet.util.core.xml | 6 +- .../resources/intellij.platform.analysis.xml | 11 +- platform/analysis-impl/module-content.yaml | 5 + .../intellij.platform.analysis.impl.xml | 21 + .../build-scripts/product-dsl/BUILD.bazel | 2 + ...ellij.platform.buildScripts.productDsl.iml | 1 + .../build-scripts/product-dsl/module-sets.md | 334 +++++++++++++-- .../product-dsl/programmatic-content.md | 306 ++++++++++++++ .../product-dsl/src/AnsiColors.kt | 16 + .../src/DuplicateIncludeDetector.kt | 3 +- .../product-dsl/src/JpsModuleDependencies.kt | 42 ++ .../ModuleDescriptorDependencyGenerator.kt | 325 +++++++++++++++ .../product-dsl/src/ModuleSetBuilder.kt | 176 +++++++- .../product-dsl/src/ModuleSetDiscovery.kt | 35 +- .../product-dsl/src/ModuleSetIndexing.kt | 102 +++++ .../product-dsl/src/ModuleSetJsonExport.kt | 315 +++++++++----- .../src/ProductModulesContentSpec.kt | 11 + .../product-dsl/src/XmlGenerationUtils.kt | 57 +-- .../src/analysis/AnalysisModels.kt | 189 ++++++++- .../src/analysis/DependencyValidation.kt | 365 ++++++++++++++++ .../src/analysis/ModuleDependencyAnalysis.kt | 336 +++++++++++++++ .../src/analysis/ModulePathAnalysis.kt | 2 +- .../src/analysis/ModuleSetTraversal.kt | 226 ++++++++++ .../src/analysis/SimilarityAnalysis.kt | 41 +- .../src/analysis/UnificationAnalysis.kt | 208 ++++++--- .../src/analysis/ValidationAnalysis.kt | 30 +- .../product-dsl/src/generator.kt | 12 +- .../product-dsl/src/generatorStats.kt | 240 +++++++---- .../src/json/AnalysisJsonWriter.kt | 293 +++++-------- .../product-dsl/src/json/JsonWriterUtils.kt | 68 +++ .../src/json/ModuleSetJsonWriter.kt | 10 +- .../product-dsl/src/json/ProductJsonWriter.kt | 3 + .../src/json/ValidationJsonWriter.kt | 33 +- .../product-dsl/src/productDiscovery.kt | 314 ++++++++++++++ .../build/JarPackagerDependencyHelper.kt | 36 +- .../intellij/build/ProductProperties.kt | 11 - .../build/classPath/contentModuleEmbedding.kt | 19 - .../intellij/build/impl/PlatformModules.kt | 78 +++- .../build/impl/productModuleLayout.kt | 5 + .../productLayout/CommunityModuleSets.kt | 272 +++--------- .../CommunityProductFragments.kt | 23 +- .../build/productLayout/CoreModuleSets.kt | 394 ++++++++++++++++++ .../build/productLayout/ModuleSetRunner.kt | 48 ++- .../ProductConfigurationRegistry.kt | 16 +- .../productLayout/productXmlFileGenerator.kt | 143 +------ .../ModuleBasedPluginLayoutProvider.kt | 46 +- .../pluginModel/PluginLayoutDescription.kt | 11 +- .../built-in-server-api/module-content.yaml | 3 + .../intellij.platform.builtInServer.xml | 2 + .../intellij.platform.builtInServer.impl.xml | 17 + platform/core-api/module-content.yaml | 5 + .../resources/intellij.platform.core.xml | 7 + platform/core-impl/module-content.yaml | 5 + .../resources/intellij.platform.core.impl.xml | 11 + .../ide/plugins/PluginDescriptorLoader.kt | 1 + platform/core-ui/BUILD.bazel | 9 +- .../core-ui/intellij.platform.core.ui.iml | 1 + platform/core-ui/module-content.yaml | 3 + .../resources/intellij.platform.core.ui.xml | 9 + .../credential-store-impl/module-content.yaml | 7 + ...ntellij.platform.credentialStore.impl.xml} | 15 + platform/credential-store-ui/BUILD.bazel | 9 +- .../intellij.platform.credentialStore.ui.iml | 1 + .../credential-store-ui/module-content.yaml | 3 + .../intellij.platform.credentialStore.ui.xml | 13 + platform/diff-api/module-content.yaml | 3 + .../resources/intellij.platform.diff.xml | 2 + .../resources/intellij.platform.diff.impl.xml | 16 + .../resources/intellij.platform.eel.impl.xml | 9 + ...lij.platform.externalProcessAuthHelper.xml | 9 + ...tform.externalSystem.dependencyUpdater.xml | 6 + .../external-system-api/module-content.yaml | 3 + .../intellij.platform.externalSystem.xml | 2 + .../intellij.platform.externalSystem.impl.xml | 18 + platform/ide-core/module-content.yaml | 4 + .../intellij.moduleSets.core.platform.xml} | 60 +-- .../intellij.moduleSets.rpc.minimal.xml} | 13 +- .../resources/intellij.platform.ide.core.xml | 15 + .../intellij.platform.kernel.backend.xml | 3 +- .../rpc/resources/intellij.platform.rpc.xml | 5 +- platform/kernel/shared/module-content.yaml | 3 +- .../resources/intellij.platform.kernel.xml | 6 +- platform/lang-api/module-content.yaml | 7 + .../resources/intellij.platform.lang.xml | 14 + platform/lang-core/module-content.yaml | 3 + .../resources/intellij.platform.lang.core.xml | 13 + platform/lang-impl/module-content.yaml | 1 - .../resources/intellij.platform.lang.impl.xml | 44 ++ platform/platform-api/module-content.yaml | 13 + .../resources/intellij.platform.ide.xml | 16 + .../intellij.platform.ide.concurrency.xml | 9 + platform/platform-impl/module-content.yaml | 22 - .../resources/intellij.platform.ide.impl.xml | 55 +++ .../META-INF/intellij.moduleSets.core.ide.xml | 127 ++++++ .../intellij.moduleSets.core.lang.xml | 149 +++++++ .../META-INF/intellij.moduleSets.debugger.xml | 6 +- .../intellij.moduleSets.essential.minimal.xml | 131 +++--- .../intellij.moduleSets.essential.xml | 148 ++++--- ...leet.xml => intellij.moduleSets.fleet.xml} | 5 +- .../intellij.moduleSets.ide.common.xml | 146 ++++--- .../intellij.moduleSets.libraries.ide.xml | 16 + .../intellij.moduleSets.libraries.misc.xml | 1 + ...ntellij.moduleSets.libraries.platform.xml} | 10 +- .../intellij.moduleSets.platformLangBase.xml | 13 - .../intellij.moduleSets.recentFiles.xml | 11 + ...tellij.moduleSets.rpc.backend.extended.xml | 14 + .../META-INF/intellij.moduleSets.vcs.xml | 1 - .../src/META-INF/PlatformLangPlugin.xml | 4 - .../ide/plugins/PluginModelValidator.kt | 97 +++-- platform/projectModel-api/module-content.yaml | 9 + .../intellij.platform.projectModel.xml | 7 + .../projectModel-impl/module-content.yaml | 15 +- .../intellij.platform.projectModel.impl.xml | 13 + .../resources/intellij.platform.smRunner.xml | 11 + platform/util-ex/BUILD.bazel | 9 +- .../util-ex/intellij.platform.util.ex.iml | 1 + platform/util-ex/module-content.yaml | 3 + .../resources/intellij.platform.util.ex.xml | 13 + platform/util/ui/module-content.yaml | 3 + .../resources/intellij.platform.util.ui.xml | 17 + .../resources/intellij.platform.debugger.xml | 12 + .../intellij.platform.debugger.impl.xml | 28 ++ .../intellij.debugger.streams.backend.xml | 4 +- .../intellij.debugger.streams.core.xml | 2 +- .../pycharm/PyCharmCommunityProperties.kt | 8 +- .../resources/META-INF/PyCharmCorePlugin.xml | 1 - 132 files changed, 5355 insertions(+), 1453 deletions(-) create mode 100644 platform/analysis-impl/module-content.yaml create mode 100644 platform/analysis-impl/resources/intellij.platform.analysis.impl.xml create mode 100644 platform/build-scripts/product-dsl/src/AnsiColors.kt create mode 100644 platform/build-scripts/product-dsl/src/JpsModuleDependencies.kt create mode 100644 platform/build-scripts/product-dsl/src/ModuleDescriptorDependencyGenerator.kt create mode 100644 platform/build-scripts/product-dsl/src/ModuleSetIndexing.kt create mode 100644 platform/build-scripts/product-dsl/src/analysis/DependencyValidation.kt create mode 100644 platform/build-scripts/product-dsl/src/analysis/ModuleDependencyAnalysis.kt create mode 100644 platform/build-scripts/product-dsl/src/analysis/ModuleSetTraversal.kt create mode 100644 platform/build-scripts/product-dsl/src/json/JsonWriterUtils.kt create mode 100644 platform/build-scripts/product-dsl/src/productDiscovery.kt create mode 100644 platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/CoreModuleSets.kt create mode 100644 platform/built-in-server-api/module-content.yaml create mode 100644 platform/built-in-server-api/resources/intellij.platform.builtInServer.xml create mode 100644 platform/core-api/module-content.yaml create mode 100644 platform/core-api/resources/intellij.platform.core.xml create mode 100644 platform/core-impl/module-content.yaml create mode 100644 platform/core-impl/resources/intellij.platform.core.impl.xml create mode 100644 platform/core-ui/module-content.yaml create mode 100644 platform/core-ui/resources/intellij.platform.core.ui.xml create mode 100644 platform/credential-store-impl/module-content.yaml rename platform/credential-store-impl/resources/{META-INF/credential-store.xml => intellij.platform.credentialStore.impl.xml} (56%) create mode 100644 platform/credential-store-ui/module-content.yaml create mode 100644 platform/credential-store-ui/resources/intellij.platform.credentialStore.ui.xml create mode 100644 platform/diff-api/module-content.yaml create mode 100644 platform/diff-api/resources/intellij.platform.diff.xml create mode 100644 platform/external-system-api/module-content.yaml create mode 100644 platform/external-system-api/resources/intellij.platform.externalSystem.xml create mode 100644 platform/ide-core/module-content.yaml rename platform/{platform-resources/generated/META-INF/intellij.moduleSets.libraries.xml => ide-core/resources/META-INF/intellij.moduleSets.core.platform.xml} (71%) rename platform/{platform-resources/generated/META-INF/intellij.moduleSets.rpc.xml => ide-core/resources/META-INF/intellij.moduleSets.rpc.minimal.xml} (67%) create mode 100644 platform/ide-core/resources/intellij.platform.ide.core.xml create mode 100644 platform/lang-api/module-content.yaml create mode 100644 platform/lang-api/resources/intellij.platform.lang.xml create mode 100644 platform/lang-core/module-content.yaml create mode 100644 platform/lang-core/resources/intellij.platform.lang.core.xml create mode 100644 platform/platform-api/module-content.yaml create mode 100644 platform/platform-api/resources/intellij.platform.ide.xml create mode 100644 platform/platform-resources/generated/META-INF/intellij.moduleSets.core.ide.xml create mode 100644 platform/platform-resources/generated/META-INF/intellij.moduleSets.core.lang.xml rename platform/platform-resources/generated/META-INF/{intellij.moduleSets.libraries.fleet.xml => intellij.moduleSets.fleet.xml} (83%) create mode 100644 platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.ide.xml rename platform/platform-resources/generated/META-INF/{intellij.moduleSets.libraries.core.xml => intellij.moduleSets.libraries.platform.xml} (88%) delete mode 100644 platform/platform-resources/generated/META-INF/intellij.moduleSets.platformLangBase.xml create mode 100644 platform/platform-resources/generated/META-INF/intellij.moduleSets.recentFiles.xml create mode 100644 platform/platform-resources/generated/META-INF/intellij.moduleSets.rpc.backend.extended.xml create mode 100644 platform/projectModel-api/module-content.yaml create mode 100644 platform/projectModel-api/resources/intellij.platform.projectModel.xml create mode 100644 platform/util-ex/module-content.yaml create mode 100644 platform/util-ex/resources/intellij.platform.util.ex.xml create mode 100644 platform/util/ui/module-content.yaml create mode 100644 platform/util/ui/resources/intellij.platform.util.ui.xml diff --git a/build/src/org/jetbrains/intellij/build/IdeaCommunityProperties.kt b/build/src/org/jetbrains/intellij/build/IdeaCommunityProperties.kt index 458c669f4457..1ed26a0aa465 100644 --- a/build/src/org/jetbrains/intellij/build/IdeaCommunityProperties.kt +++ b/build/src/org/jetbrains/intellij/build/IdeaCommunityProperties.kt @@ -9,7 +9,10 @@ import org.jetbrains.intellij.build.impl.BuildContextImpl import org.jetbrains.intellij.build.impl.qodana.QodanaProductProperties import org.jetbrains.intellij.build.io.copyDir import org.jetbrains.intellij.build.io.copyFileToDir -import org.jetbrains.intellij.build.productLayout.* +import org.jetbrains.intellij.build.productLayout.CommunityModuleSets +import org.jetbrains.intellij.build.productLayout.CommunityProductFragments +import org.jetbrains.intellij.build.productLayout.ProductModulesContentSpec +import org.jetbrains.intellij.build.productLayout.productModules import java.nio.file.Path val MAVEN_ARTIFACTS_ADDITIONAL_MODULES: PersistentList = persistentListOf( @@ -110,9 +113,6 @@ open class IdeaCommunityProperties(private val communityHomeDir: Path) : JetBrai additionalVmOptions = persistentListOf("-Dllm.show.ai.promotion.window.on.start=false") } - override val moduleSetsProviders: List - get() = listOf(CommunityModuleSets) - override val baseFileName: String get() = "idea" diff --git a/community-resources/resources/META-INF/IdeaPlugin.xml b/community-resources/resources/META-INF/IdeaPlugin.xml index 3805cb6dc698..48434777df4c 100644 --- a/community-resources/resources/META-INF/IdeaPlugin.xml +++ b/community-resources/resources/META-INF/IdeaPlugin.xml @@ -25,7 +25,6 @@ - diff --git a/fleet/andel/resources/fleet.andel.xml b/fleet/andel/resources/fleet.andel.xml index c188f1d533e0..0567c8e12265 100644 --- a/fleet/andel/resources/fleet.andel.xml +++ b/fleet/andel/resources/fleet.andel.xml @@ -1,8 +1,10 @@ + - - + + + diff --git a/fleet/kernel/resources/fleet.kernel.xml b/fleet/kernel/resources/fleet.kernel.xml index 1011c0f1752c..8f0103ba74bb 100644 --- a/fleet/kernel/resources/fleet.kernel.xml +++ b/fleet/kernel/resources/fleet.kernel.xml @@ -1,12 +1,14 @@ + - - - - - - + + + + + + + diff --git a/fleet/rpc.server/resources/fleet.rpc.server.xml b/fleet/rpc.server/resources/fleet.rpc.server.xml index 86f1643ddb25..9a2d91dfc9f0 100644 --- a/fleet/rpc.server/resources/fleet.rpc.server.xml +++ b/fleet/rpc.server/resources/fleet.rpc.server.xml @@ -1,8 +1,11 @@ + + - + + diff --git a/fleet/rpc/resources/fleet.rpc.xml b/fleet/rpc/resources/fleet.rpc.xml index 86f1643ddb25..fd5a270fdaec 100644 --- a/fleet/rpc/resources/fleet.rpc.xml +++ b/fleet/rpc/resources/fleet.rpc.xml @@ -1,8 +1,10 @@ + + - + diff --git a/fleet/util/core/resources/fleet.util.core.xml b/fleet/util/core/resources/fleet.util.core.xml index a88df9709cef..19f94caf41bd 100644 --- a/fleet/util/core/resources/fleet.util.core.xml +++ b/fleet/util/core/resources/fleet.util.core.xml @@ -1,9 +1,11 @@ + - + - + + diff --git a/platform/analysis-api/resources/intellij.platform.analysis.xml b/platform/analysis-api/resources/intellij.platform.analysis.xml index 6fb76a66265d..1cf8142c2ee2 100644 --- a/platform/analysis-api/resources/intellij.platform.analysis.xml +++ b/platform/analysis-api/resources/intellij.platform.analysis.xml @@ -1,2 +1,11 @@ - + + + + + + + + + + \ No newline at end of file diff --git a/platform/analysis-impl/module-content.yaml b/platform/analysis-impl/module-content.yaml new file mode 100644 index 000000000000..0fce98938505 --- /dev/null +++ b/platform/analysis-impl/module-content.yaml @@ -0,0 +1,5 @@ +- name: dist.all/lib/intellij.platform.analysis.impl.jar + modules: + - name: intellij.platform.analysis.impl + - name: intellij.platform.indexing.impl + - name: intellij.platform.util.io.storages \ No newline at end of file diff --git a/platform/analysis-impl/resources/intellij.platform.analysis.impl.xml b/platform/analysis-impl/resources/intellij.platform.analysis.impl.xml new file mode 100644 index 000000000000..98c778961257 --- /dev/null +++ b/platform/analysis-impl/resources/intellij.platform.analysis.impl.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/build-scripts/product-dsl/BUILD.bazel b/platform/build-scripts/product-dsl/BUILD.bazel index 58ed9f5c6c15..90bca0501acc 100644 --- a/platform/build-scripts/product-dsl/BUILD.bazel +++ b/platform/build-scripts/product-dsl/BUILD.bazel @@ -28,6 +28,7 @@ jvm_library( "//platform/util", "//platform/util/jdom", "//libraries/jackson/jackson", + "@lib//:kotlinx-coroutines-core", ] ) @@ -51,6 +52,7 @@ jvm_library( "@lib//:junit5", "@lib//:assert_j", "//libraries/jackson/jackson", + "@lib//:kotlinx-coroutines-core", ] ) ### auto-generated section `build intellij.platform.buildScripts.productDsl` end diff --git a/platform/build-scripts/product-dsl/intellij.platform.buildScripts.productDsl.iml b/platform/build-scripts/product-dsl/intellij.platform.buildScripts.productDsl.iml index 1ade7fd58b9b..805b4cd20fe4 100644 --- a/platform/build-scripts/product-dsl/intellij.platform.buildScripts.productDsl.iml +++ b/platform/build-scripts/product-dsl/intellij.platform.buildScripts.productDsl.iml @@ -42,5 +42,6 @@ + \ No newline at end of file diff --git a/platform/build-scripts/product-dsl/module-sets.md b/platform/build-scripts/product-dsl/module-sets.md index 32895263360b..d55d3cd31551 100644 --- a/platform/build-scripts/product-dsl/module-sets.md +++ b/platform/build-scripts/product-dsl/module-sets.md @@ -1,14 +1,29 @@ # Module Sets -Module sets are collections of modules that can be referenced as a single entity in product configurations. +## What Are Module Sets? -## Overview +Module sets are **reusable collections of modules** that can be referenced as a single entity in product configurations. They solve the problem of: +- **Duplication**: Avoiding repetitive module lists across products +- **Maintainability**: Changing a module set once updates all products that use it +- **Organization**: Grouping related modules by functionality (e.g., VCS, XML, essential platform) +- **Composition**: Building complex products from simple, composable building blocks + +## How Module Sets Work + +Module sets are **defined in Kotlin code** and **auto-generate XML files**: + +1. **Define**: Write Kotlin functions in `CommunityModuleSets.kt` or `UltimateModuleSets.kt` +2. **Generate**: Run the generator to create XML files from Kotlin definitions +3. **Use**: Products reference module sets via `moduleSet()` in their content descriptors + +**Key principle**: Code is the source of truth, XML files are generated artifacts. + +## Module Set Locations -Module sets are now **defined in Kotlin code** in the following locations: - **Community module sets**: `community/platform/build-scripts/product-dsl/src/CommunityModuleSets.kt` - **Ultimate module sets**: `platform/buildScripts/src/productLayout/UltimateModuleSets.kt` -Each module set is defined as a Kotlin function returning a `ModuleSet` object. The XML files (following the pattern `intellij.moduleSets...xml`) are **auto-generated** from this Kotlin code. +Each module set is defined as a Kotlin function (e.g., `fun essential(): ModuleSet`) that returns a `ModuleSet` object. The XML files (pattern: `intellij.moduleSets..xml`) are **auto-generated** from this Kotlin code. ### Generating XML Files @@ -19,15 +34,6 @@ To regenerate XML files from Kotlin code: mcp__jetbrains__execute_run_configuration(configurationName="Generate Product Layouts") ``` -**Using Gradle:** -```bash -# For all products (community + ultimate) -./gradlew :platform.buildScripts:runIde -PmainClass=com.intellij.platform.commercial.buildScripts.productLayout.UltimateModuleSets - -# For community products only -./gradlew :intellij.platform.buildScripts:runIde -PmainClass=org.jetbrains.intellij.build.productLayout.CommunityModuleSets -``` - **From IDE:** Run the "Generate Product Layouts" run configuration. @@ -44,21 +50,301 @@ See the [Programmatic Content](programmatic-content.md#json-analysis-endpoint) d See `/create-module-set` slash command for detailed instructions on creating a new module set. -## Available Module Sets +## Discovering Available Module Sets -Module sets are defined in Kotlin code: +To find available module sets and understand their contents: -- **Community module sets**: See `community/platform/build-scripts/product-dsl/src/CommunityModuleSets.kt` -- **Ultimate module sets**: See `platform/buildScripts/src/productLayout/UltimateModuleSets.kt` +1. **Browse the source code** - Open the Kotlin files to see all module set functions: + - `community/platform/build-scripts/product-dsl/src/CommunityModuleSets.kt` + - `platform/buildScripts/src/productLayout/UltimateModuleSets.kt` + +2. **Read KDoc comments** - Each module set function has comprehensive documentation including: + - What it contains + - When to use it + - Example products using it + - Relationships to other module sets -Each module set is defined as a function (e.g., `fun essential(): ModuleSet`) within these classes. +3. **Use the JSON endpoint** - Run `UltimateModuleSets.main(args = ["--json"])` for programmatic analysis: + - Complete module composition + - Nesting hierarchy + - Product usage + - Module distribution across sets -### Querying Module Sets +4. **Check generated XML** - The generated XML files show expanded module lists: + - Pattern: `intellij.moduleSets..xml` + - Locations: `community/platform/platform-resources/generated/META-INF/` (community) or `licenseCommon/generated/` (ultimate) -To discover available module sets and their composition: +**Best practice**: Start by browsing the Kotlin source files and reading KDoc comments. They are the canonical source of truth. -1. **Browse the source code** - Open the Kotlin files listed above to see all available module sets -2. **Use the JSON endpoint** - Run `UltimateModuleSets.main(args = ["--json"])` to get comprehensive analysis -3. **Check generated XML** - The generated XML files (e.g., `intellij.moduleSets.essential.xml`) contain expanded module lists +## Best Practices -For detailed module composition, includes/includedBy relationships, and product usage, use the [JSON analysis endpoint](#querying-module-sets). \ No newline at end of file +### When to Create a New Module Set + +Create a new module set when: +- **Multiple products** need the same group of modules +- The modules form a **cohesive functional unit** (e.g., VCS support, XML support, SSH support) +- You want to **enforce consistency** across products using these modules +- The group is likely to be **reused or evolved** over time + +Don't create a module set if: +- Only **one product** needs these specific modules +- The modules are **product-specific** customizations +- The grouping is **arbitrary** without functional cohesion + +### Naming Conventions + +- Use **functional names** that describe what the modules do: `vcs`, `xml`, `ssh`, `essential` +- Use **dot notation** for hierarchical relationships: `libraries.core`, `ide.common`, `essential.minimal` +- Avoid **product names** in module set names (sets should be reusable) +- Keep names **concise** and **memorable** + +### Module Set Composition + +Module sets can: +- **Include individual modules**: `module("intellij.platform.vcs.impl")` +- **Nest other module sets**: `moduleSet(corePlatform())` +- **Use embedded loading**: `embeddedModule("intellij.platform.core")` for core classloader +- **Include dependencies**: `includeDependencies = true` to automatically pull in module dependencies + +**Tip**: Prefer nesting existing module sets over duplicating modules. This creates a clean hierarchy and ensures consistency. + +### Content Modules vs Implementation Modules + +**Critical distinction** for understanding `includeDependencies`: + +#### Content Modules +- **Have XML descriptors** (e.g., `resources/fleet.andel.xml`) +- Define extensions, services, components, listeners +- **Must** be declared in module sets or product content descriptors +- Cannot be loaded via `productImplementationModules` +- Example: `fleet.andel`, `fleet.rpc`, `intellij.platform.vcs.impl` + +#### Implementation Modules +- **No XML descriptors** - just code (classes, resources) +- Provide implementation classes loaded into classloader +- Can be in `productImplementationModules` OR loaded via `includeDependencies` +- Example: `fleet.util.multiplatform`, `fleet.backend`, `intellij.platform.webide.impl` + +**To check if a module has a descriptor:** +```bash +# Using JetBrains MCP +find_files_by_glob("**/moduleName.xml") + +# Or check the module's resources directory +ls community/modulePath/resources/ # Look for .xml files +``` + +### How `includeDependencies` Works + +When you declare `embeddedModule("someModule", includeDependencies = true)`: + +1. **Collects ALL transitive JPS dependencies** (not just direct!) + - Uses BFS traversal through the full dependency graph + - Example: `fleet.andel` → `fleet.util.core` → `fleet.multiplatform.shims` → etc. + +2. **Filters to implementation modules only** + - Includes ONLY modules WITHOUT XML descriptors + - Content modules are assumed to be in module sets already + - This prevents duplication of content modules + +3. **Loads into classloader** + - Implementation code becomes available at runtime + - No plugin descriptor processing (since there are no descriptors) + +**Example:** +```kotlin +embeddedModule("fleet.andel", includeDependencies = true) +``` + +This automatically includes these implementation dependencies: +- `fleet.util.multiplatform` (no descriptor) +- Plus any other transitive implementation modules + +But does NOT duplicate these content modules (already in `fleetMinimal()`): +- `fleet.util.core` (has descriptor) +- `fleet.bifurcan` (has descriptor) +- etc. + +**Best practice**: Use `includeDependencies = true` for modules with implementation-only transitive dependencies to avoid explicit listing in `productImplementationModules`. + +## Module Set Validation + +### Two-Tier Validation System + +The build system uses a **two-tier validation approach** to ensure module dependencies are resolvable while avoiding false positives: + +#### Tier 1: Product-Level Validation + +**What it validates**: All products with their complete module composition + +**How it works**: +- Collects all modules from a product's complete module set hierarchy +- Validates that each module's dependencies are available within that product context +- Reports errors per-product with clear affected modules + +**Why this tier exists**: Products are the actual deployment units. Most module sets (like `debugger()`, `vcs()`, `xml()`) are **composable building blocks** designed to work together. Validating them in isolation would produce false positives because they intentionally depend on modules from other sets (e.g., `debugger()` depends on `intellij.platform.core` from `essential()`). + +**Example**: +- Product `GoLand` uses: `ide.ultimate`, `ssh`, `rd.common` +- Validation checks: Can all modules in `ide.ultimate` + `ssh` + `rd.common` resolve their dependencies within this combined set? + +#### Tier 2: Self-Contained Validation + +**What it validates**: Module sets marked with `selfContained = true` + +**How it works**: +- Validates the module set in isolation without considering other module sets +- Ensures all dependencies are resolvable within the set itself +- Reports errors specific to that module set + +**Why this tier exists**: Some module sets are designed to be **standalone/self-contained** and used directly by products without composition with other sets. These sets must have all their dependencies available internally. + +**When to use `selfContained = true`**: +- Module set is used directly by products as the primary/only module set +- Module set represents a complete, independent runtime environment +- You want to enforce that the set doesn't leak dependencies on external sets + +**When NOT to use `selfContained = true`**: +- Module set is designed to be composed with other sets (debugger, vcs, xml, ssh) +- Module set intentionally depends on modules from base sets like `essential()` +- Module set is a specialized feature addition to a larger base + +### Example: core.platform (Self-Contained) + +```kotlin +/** + * Core platform modules without IDE or language support. + * Used by: CodeServer (analysis tool) + */ +fun corePlatform(): ModuleSet = moduleSet( + name = "core.platform", + selfContained = true, // ✅ Must be resolvable in isolation + outputModule = "intellij.platform.ide.core" +) { + moduleSet(librariesPlatform()) + moduleSet(rpcMinimal()) // Provides kernel + fleet deps + + embeddedModule("intellij.platform.core", includeDependencies = true) + embeddedModule("intellij.platform.ide.core", includeDependencies = true) + // ... +} +``` + +**Why selfContained**: CodeServer uses `core.platform` alone without other module sets. It must contain everything needed for the platform runtime. + +### Example: debugger() (Composable, Not Self-Contained) + +```kotlin +/** + * Debugger platform modules. + * Used by: All IDE products via essential() + */ +fun debugger(): ModuleSet = moduleSet( + name = "debugger" + // ❌ NOT selfContained - designed to compose with essential() +) { + module("intellij.platform.debugger.impl.backend") + embeddedModule("intellij.platform.debugger") + // Depends on intellij.platform.core from essential() ✅ OK +} +``` + +**Why NOT selfContained**: Debugger is always used together with `essential()` which provides core platform modules. Validating debugger in isolation would fail because it depends on `intellij.platform.core`, but that's correct by design. + +### Validation Error Examples + +#### Product-Level Error + +``` +❌ Unresolvable dependencies in products + + Product: GoLand + ✗ Missing: 'intellij.platform.polySymbols' + Needed by: intellij.platform.vcs.impl + Chain: intellij.platform.vcs.impl → intellij.platform.polySymbols +``` + +**What this means**: The product `GoLand` is missing a module that one of its included modules depends on. Fix by adding the missing module or a module set that contains it. + +#### Self-Contained Error + +``` +❌ Module set 'core.platform' is marked selfContained but has unresolvable dependencies + + ✗ Missing: 'fleet.kernel' + Needed by: intellij.platform.kernel + Suggestion: Include fleet() or add fleet.kernel directly +``` + +**What this means**: A self-contained module set is missing a dependency. Fix by adding the missing module/set to make it truly self-contained. + +### Troubleshooting Dependency Errors + +When you encounter a dependency validation error, **resist the temptation to simply add the missing module to the failing module set**. This creates technical debt and breaks the isolation principle. + +#### ❌ Wrong Approach + +```kotlin +// BAD: Adding a library directly to debugger module set +fun debugger(): ModuleSet = moduleSet("debugger") { + module("intellij.platform.debugger.impl") + embeddedModule("intellij.libraries.kotlinx.serialization.core") // ❌ Wrong! +} +``` + +**Why it's wrong**: The debugger module set should contain only debugger-related modules. Adding serialization libraries pollutes its domain and creates maintenance burden. + +#### ✅ Correct Approach + +1. **Identify the dependency chain**: Use PMA MCP to trace dependencies + ``` + mcp__PluginModelAnalyzer__find_dependency_path( + fromModule="intellij.platform.debugger.impl", + toModule="intellij.libraries.kotlinx.serialization.core" + ) + ``` + +2. **Find which module set provides the dependency**: Check existing module sets + ``` + mcp__PluginModelAnalyzer__suggest_module_set_for_modules( + moduleNames=["intellij.libraries.kotlinx.serialization.core"] + ) + ``` + +3. **Analyze the product's module set hierarchy**: Trace what the product includes + - Does `essentialMinimal()` → `coreLang()` → `coreIde()` → `corePlatform()` → `librariesPlatform()` include it? + - Is there a missing link in the chain? + +4. **Fix at the appropriate level**: + - If a base module set is missing a nested set → add the nested set + - If a product is missing a required module set → add to product + - If the module truly belongs in the domain → then add it directly (rare) + +#### Example: Serialization Library Missing + +**Error**: `debugger` module set missing `intellij.libraries.kotlinx.serialization.core` + +**Investigation**: +- `kotlinx.serialization.core` is in `librariesPlatform()` module set +- `librariesPlatform()` is nested in `corePlatform()` +- `corePlatform()` is nested in `coreIde()` → `coreLang()` → `essentialMinimal()` +- Product uses `essentialMinimal()` ✅ so it should have the library + +**Root cause options**: +1. Product doesn't include `essentialMinimal()` - add it +2. The chain is broken somewhere - fix the nesting +3. Validation logic has a bug - investigate the validator + +**Key insight**: The fix is rarely "add module X to module set Y". It's usually "ensure the module set hierarchy correctly provides module X through proper nesting". + +### Best Practices + +1. **Start without `selfContained`**: Most module sets should NOT be self-contained. Only flag sets that truly need isolation. + +2. **Keep self-contained sets minimal**: If a set needs `selfContained = true`, try to minimize dependencies to keep it lightweight. + +3. **Document why self-contained**: When using `selfContained = true`, add a KDoc comment explaining why this set needs isolation. + +4. **Use product-level validation as guide**: If validation suggests adding many dependencies to a self-contained set, consider whether it should really be self-contained. + +5. **Test with real products**: After changes, run "Generate Product Layouts" to validate both tiers work correctly. \ No newline at end of file diff --git a/platform/build-scripts/product-dsl/programmatic-content.md b/platform/build-scripts/product-dsl/programmatic-content.md index 7727da4e8f2a..f03651fc19bc 100644 --- a/platform/build-scripts/product-dsl/programmatic-content.md +++ b/platform/build-scripts/product-dsl/programmatic-content.md @@ -134,6 +134,142 @@ This will generate a complete plugin.xml file like: ``` +## Best Practices + +### Module Sets vs. deprecatedInclude() vs. direct modules + +Choose the appropriate mechanism based on your needs: + +**Use `moduleSet()` when:** +- You need a cohesive group of modules (e.g., `essentialMinimal`, `vcs`, `ssh`) +- You want to reuse a common set across multiple products +- You need the full module functionality, not just XML extension points +- The module set provides platform infrastructure (e.g., `corePlatform` for core platform modules) + +**Use `deprecatedInclude()` when:** +- You only need XML extension points/components from a specific module +- The module is not part of any module set yet +- You need to reference product-specific customization XML files +- The XML file doesn't define content modules (just extensions/listeners) + +**Use `module()` or `embeddedModule()` when:** +- You need a single content module not in any module set +- The module is specific to your product +- You want explicit control over loading mode + +**Key principle**: Prefer module sets for core platform functionality, use `deprecatedInclude()` only for XML-only includes. + +### Migrating from PLATFORM_CORE_MODULES + +The `PLATFORM_CORE_MODULES` constant in `PlatformModules.kt` is **deprecated**. It was a hard-coded list of modules that are included in lib/app.jar in all IntelliJ-based IDEs. + +**Why it's deprecated:** +- Hard-coded list, not composable or reusable +- No clear hierarchy or structure +- String-based, error-prone +- Cannot be customized per product + +**Migration path:** + +Replace `PLATFORM_CORE_MODULES` references with module sets: + +```kotlin +// OLD (deprecated) +for (module in PLATFORM_CORE_MODULES) { + embeddedModule(module) +} + +// NEW (recommended) +moduleSet(CommunityModuleSets.essentialMinimal()) +// or for minimal products: +moduleSet(CommunityModuleSets.corePlatform()) // when public +``` + +**What module set to use:** +- **`essentialMinimal()`**: For lightweight IDE products (includes coreLang, backend/frontend split, editor, search) +- **`essential()`**: For full IDE products with language support +- **`ide.common`**: For IDE products with VCS, XML, and common IDE features +- **`corePlatform()`** (when public): For minimal products that need core platform without lang/ide.impl + +### Choosing the Right Module Set for Your Product + +Use this decision tree to select the appropriate module set: + +``` +┌─────────────────────────────────────────────────┐ +│ What type of product are you building? │ +└───────────────────┬─────────────────────────────┘ + │ + ┌───────────┴───────────────────┬─────────────────────┐ + │ │ │ + Minimal tool Lightweight IDE Full-featured IDE + (analysis/inspection) (basic editing) (all features) + │ │ │ + ▼ ▼ ▼ + corePlatform essentialMinimal ide.common + (when public) + specific sets or ide.ultimate +``` + +**Example products:** +- **CodeServer** (analysis/inspection tool without editing): Uses `corePlatform` +- **GitClient** (lightweight IDE with editing): Uses `essentialMinimal` + `vcs()` +- **WebStorm** (full IDE): Uses `ide.common` + web-specific modules +- **IntelliJ IDEA Ultimate**: Uses `ide.ultimate` + all language plugins + +### Example: Migrating CodeServer + +**Current approach** (not recommended): +```kotlin +override fun getProductContentDescriptor(): ProductModulesContentSpec = productModules { + alias("com.intellij.codeServer") + + // Only XML includes - modules not available at runtime + deprecatedInclude("intellij.platform.analysis", "META-INF/Analysis.xml") + deprecatedInclude("intellij.platform.core", "META-INF/Core.xml") + deprecatedInclude("intellij.platform.projectModel", "META-INF/ProjectModel.xml") + // ... 11 more deprecatedInclude calls + + // Only 5 modules total + module("intellij.grid") + module("intellij.libraries.jettison") +} +``` + +**Recommended approach** (for analysis tools without editing): +```kotlin +override fun getProductContentDescriptor(): ProductModulesContentSpec = productModules { + alias("com.intellij.codeServer") + + // Use corePlatform for analysis tools (core platform without language editing) + moduleSet(CommunityModuleSets.corePlatform()) + + // Keep deprecatedInclude only for modules NOT in corePlatform + deprecatedInclude("intellij.platform.indexing", "META-INF/Indexing.xml") + deprecatedInclude("intellij.platform.codeStyle.impl", "META-INF/CodeStyle.xml") + deprecatedInclude("intellij.platform.refactoring", "META-INF/RefactoringExtensionPoints.xml") + deprecatedInclude("intellij.codeServer.core", "META-INF/codeserver-customization.xml") + + // Product-specific modules + module("intellij.grid") + module("intellij.grid.types") + module("intellij.grid.csv.core.impl") + module("intellij.grid.core.impl") + module("intellij.libraries.jettison") +} +``` + +**Why corePlatform (not essentialMinimal)?** +CodeServer is an analysis/inspection tool that doesn't provide language editing capabilities: +- ✅ Needs: Core platform, analysis APIs, IDE extension points +- ❌ Doesn't need: Language support (lang.*), IDE editing (ide.impl), editor UI, search +- **corePlatform provides exactly what's needed** without unnecessary dependencies + +**Benefits of using module sets:** +- Modules are actually available at runtime (not just XML extension points) +- Clear separation: analysis tools use corePlatform, editing IDEs use essentialMinimal +- Easier to maintain (fewer deprecatedInclude calls) +- Automatic updates when core platform evolves + ## How It Works ### File Generation Strategy @@ -450,6 +586,176 @@ The JSON generation is implemented in: - `ModuleSetJsonExport.kt` - JSON generation logic - `ModuleSetDiscovery.kt` - Module set discovery via reflection +## Migrating from productImplementationModules + +### Understanding the Difference + +**`productImplementationModules` (deprecated):** +- Lists implementation modules (no XML descriptors) to bundle into product JARs +- Just loads classes into classloader +- Modules are NOT content modules (no plugin descriptors processed) +- Being phased out in favor of programmatic content descriptors + +**`getProductContentDescriptor()` (modern):** +- Declares content modules via module sets and `module()`/`embeddedModule()` +- Content modules = have XML descriptors with extensions/services +- Implementation dependencies come via `includeDependencies = true` + +### Migration Steps + +**1. Identify content vs implementation modules** + +Content modules (have .xml descriptors): +```bash +# Check if module has a descriptor +find_files_by_glob("**/moduleName.xml") + +# Or look in resources directory +ls community/modulePath/resources/*.xml +``` + +Implementation modules (no descriptors): +- Just provide classes/resources +- Examples: `fleet.util.multiplatform`, `intellij.platform.webide.impl` + +**2. Move content modules to programmatic descriptor** + +If a module in `productImplementationModules` has a descriptor, it's incorrectly placed: + +```kotlin +// ❌ WRONG - content module in productImplementationModules +productLayout.productImplementationModules = listOf( + "fleet.andel" // Has fleet.andel.xml descriptor! +) + +// ✅ CORRECT - content module in programmatic descriptor +override fun getProductContentDescriptor() = productModules { + module("fleet.andel") + // Or better: use module set that already includes it + moduleSet(CoreModuleSets.essential()) +} +``` + +**3. Keep implementation-only modules in productImplementationModules** + +Implementation modules without descriptors can stay: + +```kotlin +// ✅ OK - implementation modules without descriptors +productLayout.productImplementationModules = listOf( + "intellij.platform.webide.impl", // No descriptor + "fleet.backend", // No descriptor + "fleet.util.network" // No descriptor +) +``` + +**4. Use includeDependencies for transitive implementation deps** + +Instead of listing all implementation dependencies explicitly: + +```kotlin +// ❌ OLD - manually list all transitive implementation modules +productLayout.productImplementationModules = listOf( + "fleet.andel", // Content module (has descriptor) + "fleet.util.multiplatform", // Implementation dep of fleet.andel + "fleet.backend" +) + +// ✅ NEW - let includeDependencies handle transitive implementation modules +override fun getProductContentDescriptor() = productModules { + embeddedModule("fleet.andel", includeDependencies = true) + // This automatically includes fleet.util.multiplatform and other implementation deps +} + +productLayout.productImplementationModules = listOf( + "fleet.backend" // Only product-specific implementation module +) +``` + +### Common Pitfalls + +**Pitfall 1: Mixing content modules in productImplementationModules** + +```kotlin +// ❌ BAD - fleet.rpc has descriptor, causes duplicates +productLayout.productImplementationModules = listOf( + "fleet.rpc" // Also comes from essential() → fleetMinimal() +) + +override fun getProductContentDescriptor() = productModules { + moduleSet(CoreModuleSets.essential()) // Includes fleet.rpc +} +// Result: Duplicate content module declaration! +``` + +**Fix:** Remove content modules from `productImplementationModules`. + +**Pitfall 2: Not checking transitive dependencies** + +```kotlin +// ❌ BAD - assuming no duplicates without checking +productLayout.productImplementationModules = listOf( + "fleet.util.multiplatform" // Might come via includeDependencies! +) + +override fun getProductContentDescriptor() = productModules { + embeddedModule("fleet.andel", includeDependencies = true) + // fleet.andel → fleet.util.core → fleet.util.multiplatform +} +``` + +**Fix:** Use Plugin Model Analyzer MCP to check transitive deps: + +```kotlin +// Check ALL transitive dependencies +get_module_dependencies( + moduleName = "fleet.andel", + includeTransitive = true +) +``` + +**Pitfall 3: Forgetting includeDependencies only gets implementation modules** + +```kotlin +// ❓ QUESTION - will this include fleet.util.core? +embeddedModule("fleet.andel", includeDependencies = true) + +// ✅ ANSWER - NO! +// fleet.util.core has a descriptor, so it's filtered out +// Only implementation modules (no descriptors) are included +``` + +### Verification Checklist + +Before committing changes: + +1. **Run Generate Product Layouts** + ```bash + # Via JetBrains MCP + execute_run_configuration(name="Generate Product Layouts") + + # Or directly + bazel run //platform/buildScripts:product-model-tool + ``` + +2. **Check for duplicate content modules** + - The generator will error if content modules are declared twice + - Look for: "Plugin 'X' has duplicated content modules declarations" + +3. **Verify tests pass** + ```bash + bazel test //platform/build-scripts/tests/testSrc/org/jetbrains/intellij/build:UltimatePluginModelTest + ``` + +4. **Use MCP to analyze transitive dependencies** + ```kotlin + // Check what includeDependencies will include + get_module_dependencies( + moduleName = "your.module", + includeTransitive = true + ) + ``` + ## Benefits 1. **Type safety**: Kotlin code with IDE support (autocomplete, refactoring) diff --git a/platform/build-scripts/product-dsl/src/AnsiColors.kt b/platform/build-scripts/product-dsl/src/AnsiColors.kt new file mode 100644 index 000000000000..26a85ef76d73 --- /dev/null +++ b/platform/build-scripts/product-dsl/src/AnsiColors.kt @@ -0,0 +1,16 @@ +// 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.productLayout + +/** + * ANSI color codes for terminal formatting + */ +object AnsiColors { + const val RESET = "\u001B[0m" + const val BOLD = "\u001B[1m" + const val RED = "\u001B[31m" + const val GREEN = "\u001B[32m" + const val YELLOW = "\u001B[33m" + const val BLUE = "\u001B[34m" + const val CYAN = "\u001B[36m" + const val GRAY = "\u001B[90m" +} diff --git a/platform/build-scripts/product-dsl/src/DuplicateIncludeDetector.kt b/platform/build-scripts/product-dsl/src/DuplicateIncludeDetector.kt index 915846cf3cb6..b9cc70e74557 100644 --- a/platform/build-scripts/product-dsl/src/DuplicateIncludeDetector.kt +++ b/platform/build-scripts/product-dsl/src/DuplicateIncludeDetector.kt @@ -13,8 +13,7 @@ import kotlin.io.path.readText * Detects duplicate xi:include references in product plugin.xml files. * Analyzes both direct xi:includes and nested includes from deprecatedInclude XML files. */ -object DuplicateIncludeDetector { - +internal object DuplicateIncludeDetector { /** * Detects duplicate xi:include references across all provided product XML files. * diff --git a/platform/build-scripts/product-dsl/src/JpsModuleDependencies.kt b/platform/build-scripts/product-dsl/src/JpsModuleDependencies.kt new file mode 100644 index 000000000000..c0124caff390 --- /dev/null +++ b/platform/build-scripts/product-dsl/src/JpsModuleDependencies.kt @@ -0,0 +1,42 @@ +// 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.productLayout + +import org.jetbrains.jps.model.java.JpsJavaClasspathKind +import org.jetbrains.jps.model.java.JpsJavaExtensionService +import org.jetbrains.jps.model.module.JpsDependencyElement +import org.jetbrains.jps.model.module.JpsModule +import org.jetbrains.jps.model.module.JpsModuleDependency + +/** + * Checks if a dependency element is included in production runtime classpath. + * + * @param element The dependency element to check + * @param withTests If true, also includes test runtime dependencies + * @return true if the dependency should be included in the runtime + */ +fun isProductionRuntimeDependency(element: JpsDependencyElement, javaExtensionService: JpsJavaExtensionService, withTests: Boolean = false): Boolean { + val scope = javaExtensionService.getDependencyExtension(element)?.scope ?: return false + if (withTests && scope.isIncludedIn(JpsJavaClasspathKind.TEST_RUNTIME)) { + return true + } + return scope.isIncludedIn(JpsJavaClasspathKind.PRODUCTION_RUNTIME) +} + +/** + * Returns a sequence of module dependencies that are included in production runtime. + * This filters the module's dependencies to only include those that should be + * packaged with the application at runtime. + * + * @param withTests If true, also includes test runtime dependencies + * @return Sequence of production runtime module dependencies + */ +fun JpsModule.getProductionModuleDependencies(withTests: Boolean = false): Sequence { + return sequence { + val javaExtensionService = JpsJavaExtensionService.getInstance() + for (element in dependenciesList.dependencies) { + if (element is JpsModuleDependency && isProductionRuntimeDependency(element, javaExtensionService, withTests)) { + yield(element) + } + } + } +} diff --git a/platform/build-scripts/product-dsl/src/ModuleDescriptorDependencyGenerator.kt b/platform/build-scripts/product-dsl/src/ModuleDescriptorDependencyGenerator.kt new file mode 100644 index 000000000000..719c1e293378 --- /dev/null +++ b/platform/build-scripts/product-dsl/src/ModuleDescriptorDependencyGenerator.kt @@ -0,0 +1,325 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@file:Suppress("ReplacePutWithAssignment", "ReplaceGetOrSet") + +package org.jetbrains.intellij.build.productLayout + +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import org.jetbrains.intellij.build.ModuleOutputProvider +import org.jetbrains.intellij.build.findFileInModuleSources +import org.jetbrains.intellij.build.productLayout.analysis.validateProductModuleSets +import org.jetbrains.intellij.build.productLayout.analysis.validateSelfContainedModuleSets +import java.nio.file.Files +import java.nio.file.Path +import java.util.concurrent.ConcurrentHashMap + +/** + * Generates module descriptor dependencies for all modules with includeDependencies=true. + * + * **Validation Strategy:** + * This function not only generates dependencies but also validates that all dependencies + * (both direct and transitive) are reachable within the module set hierarchy. + * + * **Why Transitive Validation Matters:** + * Before this fix, only direct dependencies were validated. This caused runtime failures when: + * - Module A depends on Module B (validated ✓) + * - Module B depends on Module C (NOT validated ✗) + * - Module C is missing from the module set + * - At runtime: Module A loads → Module B loads → Module C fails! + * + * **Example Bug Fixed:** + * - `intellij.platform.kernel` (in `corePlatform()`) depends on `fleet.kernel` + * - But `corePlatform()` doesn't include `fleet()` module set + * - Modules in `corePlatform()` with `includeDependencies=true` would generate descriptors + * listing `intellij.platform.kernel` as a dependency + * - At runtime, `intellij.platform.kernel` would fail to load because `fleet.kernel` is missing + * - OLD: No validation error (only checked direct dependencies) + * - NEW: Validation catches the missing `fleet.kernel` transitive dependency + */ +suspend fun generateModuleDescriptorDependencies( + communityModuleSets: List, + ultimateModuleSets: List, + coreModuleSets: List = emptyList(), + moduleOutputProvider: ModuleOutputProvider, + productSpecs: List> = emptyList(), +): DependencyGenerationResult = coroutineScope { + val allModuleSets = communityModuleSets + coreModuleSets + ultimateModuleSets + val modulesToProcess = collectModulesWithIncludeDependencies(allModuleSets) + if (modulesToProcess.isEmpty()) { + return@coroutineScope DependencyGenerationResult(emptyList()) + } + + val cache = ModuleDescriptorCache(moduleOutputProvider) + + // Validate self-contained module sets in isolation + // Module sets marked with selfContained=true must be resolvable without other sets + validateSelfContainedModuleSets(allModuleSets, cache) + + // Tier 2: Validate product-level dependencies + // This ensures all products can load without missing dependency errors + validateProductModuleSets( + allModuleSets = allModuleSets, + productSpecs = productSpecs, + descriptorCache = cache, + allowUnresolvableProducts = setOf( + // TODO: Fix these products' module dependencies + "AppCode", + "CLion", + "DataSpell", + "RustRover", + "Rider", + "WebStorm", + "GoLand", + ) + ) + + // Write XML files in parallel + val results = modulesToProcess.map { moduleName -> + async { + val info = cache.getOrAnalyze(moduleName) ?: return@async null + val status = updateModuleDescriptor(info.descriptorPath, info.dependencies) + DependencyFileResult( + moduleName = moduleName, + descriptorPath = info.descriptorPath, + status = status, + dependencyCount = info.dependencies.size + ) + } + }.awaitAll().filterNotNull() + + DependencyGenerationResult(results) +} + +/** + * Cache for module descriptor information to avoid redundant file system lookups. + * Made public for access from analysis package. + */ +class ModuleDescriptorCache(private val moduleOutputProvider: ModuleOutputProvider) { + data class DescriptorInfo( + val descriptorPath: Path, + val dependencies: List, + ) + + // Wrapper to allow caching null results (ConcurrentHashMap doesn't support null values) + private class CacheValue(@JvmField val info: DescriptorInfo?) + + private val cache = ConcurrentHashMap() + + /** + * Gets cached descriptor info or analyzes the module if not yet cached. + * Caches both positive (has descriptor) and negative (no descriptor) results. + * Thread-safe: ensures exactly one analysis per module using double-checked locking. + */ + fun getOrAnalyze(moduleName: String): DescriptorInfo? { + return (cache.get(moduleName) ?: synchronized(moduleName.intern()) { + cache.getOrPut(moduleName) { CacheValue(analyzeModule(moduleName)) } + }).info + } + + /** + * Analyzes a module to find its descriptor and production dependencies. + */ + private fun analyzeModule(moduleName: String): DescriptorInfo? { + val jpsModule = moduleOutputProvider.findRequiredModule(moduleName) + val descriptorPath = findFileInModuleSources( + module = jpsModule, + relativePath = "$moduleName.xml", + onlyProductionSources = true + ) ?: return null + + // Skip modules with IJPL-210868 marker (not registered as content modules) + if (shouldSkipDescriptor(descriptorPath)) { + return null + } + + val deps = mutableListOf() + for (dep in jpsModule.getProductionModuleDependencies(withTests = false)) { + val depName = dep.moduleReference.moduleName + if (hasDescriptor(depName)) { + deps.add(depName) + } + } + + // Deduplicate MODULE DEPENDENCIES (not content modules!) before sorting. + // Handles cases where the same dependency appears multiple times in JPS module graph. + // Note: Content module duplicates are caught by validateProductModuleSets() during generation. + return DescriptorInfo(descriptorPath, deps.distinct().sorted()) + } + + private fun shouldSkipDescriptor(descriptorPath: Path): Boolean { + val content = Files.readString(descriptorPath) + return content.contains("" + val generatedEnd = "" + + // Legacy markers for backward compatibility + val legacyStart = "" + val legacyEnd = "" + + // Generate new dependencies block + val newDepsBlock = if (dependencies.isEmpty()) { + "" + } + else { + buildString { + append(" $generatedStart\n") + append(" \n") + for (dep in dependencies) { + append(" \n") + } + append(" \n") + append(" $generatedEnd\n") + } + } + + // Case 1: Check if current editor-fold markers exist + val startIndex = xmlContent.indexOf(generatedStart) + if (startIndex >= 0) { + val endIndex = xmlContent.indexOf(generatedEnd, startIndex) + if (endIndex >= 0) { + val result = replaceBlockBetweenMarkers(xmlContent, startIndex, endIndex, generatedEnd.length, newDepsBlock) + // Clean up any orphaned legacy markers + return result.replace(" $legacyStart\n", "").replace(" $legacyEnd\n", "") + } + } + + // Case 2: Check if legacy markers exist and replace them + val legacyStartIndex = xmlContent.indexOf(legacyStart) + if (legacyStartIndex >= 0) { + val legacyEndIndex = xmlContent.indexOf(legacyEnd, legacyStartIndex) + if (legacyEndIndex >= 0) { + return replaceBlockBetweenMarkers(xmlContent, legacyStartIndex, legacyEndIndex, legacyEnd.length, newDepsBlock) + } + } + + // Case 3: No markers found, try to replace entire section using string parsing + val depsTagStart = xmlContent.indexOf("") + if (depsTagStart >= 0) { + val depsTagEnd = xmlContent.indexOf("", depsTagStart) + if (depsTagEnd >= 0) { + // Find line boundaries to replace entire block including indentation + val lineStart = xmlContent.lastIndexOf('\n', depsTagStart - 1) + 1 + val lineEnd = xmlContent.indexOf('\n', depsTagEnd + "".length) + val actualEnd = if (lineEnd >= 0) lineEnd + 1 else xmlContent.length + return xmlContent.substring(0, lineStart) + newDepsBlock + xmlContent.substring(actualEnd) + } + } + + // Case 4: No dependencies section exists, insert after opening tag + if (dependencies.isEmpty()) { + return xmlContent + } + + val insertionPoint = findInsertionPointAfterPluginTag(xmlContent) + return if (insertionPoint > 0) { + xmlContent.substring(0, insertionPoint) + newDepsBlock + xmlContent.substring(insertionPoint) + } + else { + xmlContent + } +} + +/** + * Replaces content between markers, preserving line boundaries. + */ +private fun replaceBlockBetweenMarkers( + content: String, + markerStart: Int, + markerEnd: Int, + markerEndLength: Int, + newBlock: String +): String { + // Find the start of the line (after previous newline) to preserve correct indentation + val lineStartIndex = content.lastIndexOf('\n', markerStart - 1) + 1 + // Find end of line containing the end marker + val endLineIndex = content.indexOf('\n', markerEnd + markerEndLength) + val actualEndIndex = if (endLineIndex >= 0) endLineIndex + 1 else content.length + return content.substring(0, lineStartIndex) + newBlock + content.substring(actualEndIndex) +} + +/** + * Finds the insertion point after the opening tag. + * Returns -1 if the tag is not found. + */ +private fun findInsertionPointAfterPluginTag(xmlContent: String): Int { + val pluginTagStart = xmlContent.indexOf("', pluginTagStart) + if (pluginTagEnd < 0) return -1 + + val newlineAfterTag = xmlContent.indexOf('\n', pluginTagEnd) + return if (newlineAfterTag >= 0) newlineAfterTag + 1 else -1 +} diff --git a/platform/build-scripts/product-dsl/src/ModuleSetBuilder.kt b/platform/build-scripts/product-dsl/src/ModuleSetBuilder.kt index 53a80ffb013f..77160e662834 100644 --- a/platform/build-scripts/product-dsl/src/ModuleSetBuilder.kt +++ b/platform/build-scripts/product-dsl/src/ModuleSetBuilder.kt @@ -2,8 +2,12 @@ package org.jetbrains.intellij.build.productLayout import com.intellij.platform.plugins.parser.impl.elements.ModuleLoadingRuleValue +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.serialization.Serializable -import org.jetbrains.intellij.build.BuildPaths +import org.jetbrains.intellij.build.ModuleOutputProvider +import org.jetbrains.jps.model.java.JavaResourceRootType import java.nio.file.Files import java.nio.file.Path @@ -28,6 +32,8 @@ data class ContentModule( * @param modules List of content modules in this set * @param nestedSets List of nested module sets (for xi:include generation) * @param alias Optional module alias for `` declaration (e.g., "com.intellij.modules.xml") + * @param outputModule Optional module name whose resources directory should be used for generating this module set's XML + * @param selfContained If true, this module set will be validated in isolation to ensure all dependencies are resolvable within the set itself. Use for module sets that are designed to be standalone (e.g., core.platform). Default: false. */ @Serializable data class ModuleSet( @@ -35,19 +41,10 @@ data class ModuleSet( @JvmField val modules: List, @JvmField val nestedSets: List = emptyList(), @JvmField val alias: String? = null, + @JvmField @kotlinx.serialization.Transient val outputModule: String? = null, + @JvmField val selfContained: Boolean = false, ) -/** - * Interface for module set providers that can generate XML files. - * Provides the output directory where generated module set XML files are stored. - */ -interface ModuleSetProvider { - /** - * Returns the path to the META-INF directory where this provider's module set XML files are generated. - */ - fun getOutputDirectory(paths: BuildPaths): Path -} - /** * DSL builder for creating ModuleSets with reduced boilerplate. */ @@ -97,11 +94,21 @@ class ModuleSetBuilder { * embeddedModule("intellij.xml.dom") * // ... * } + * + * // With custom output module: + * fun corePlatform() = moduleSet("core.platform", outputModule = "intellij.platform.ide.core") { + * // ... + * } + * + * // With selfContained flag: + * fun corePlatform() = moduleSet("core.platform", selfContained = true, outputModule = "intellij.platform.ide.core") { + * // Must include all dependencies - validated in isolation + * } * ``` */ -inline fun moduleSet(name: String, alias: String? = null, block: ModuleSetBuilder.() -> Unit): ModuleSet { +inline fun moduleSet(name: String, alias: String? = null, outputModule: String? = null, selfContained: Boolean = false, block: ModuleSetBuilder.() -> Unit): ModuleSet { val (modules, nestedSets) = ModuleSetBuilder().apply(block).build() - return ModuleSet(name, modules, nestedSets, alias) + return ModuleSet(name = name, modules = modules, nestedSets = nestedSets, alias = alias, outputModule = outputModule, selfContained = selfContained) } /** @@ -201,41 +208,166 @@ internal fun buildModuleSetXml(moduleSet: ModuleSet, label: String): ModuleSetBu return ModuleSetBuildResult(xml, moduleSet.modules.size) } +/** + * Cleans up orphaned module set XML files that no longer have corresponding Kotlin module set functions. + * Scans each output directory and deletes files matching the module set pattern that aren't in the generated set. + * + * @param outputDirToGeneratedFiles Map of output directory to set of generated file names + * @return List of deleted file results + */ +internal fun cleanupOrphanedModuleSetFiles( + outputDirToGeneratedFiles: Map> +): List { + val deletedFiles = mutableListOf() + + for ((dir, generatedFiles) in outputDirToGeneratedFiles) { + if (Files.exists(dir)) { + // First, collect all module set files in the directory + val allModuleSetFiles = Files.list(dir).use { stream -> + stream + .filter { Files.isRegularFile(it) } + .filter { it.fileName.toString().startsWith(MODULE_SET_PREFIX) && it.fileName.toString().endsWith(".xml") } + .toList() + } + + // Identify orphaned files + val orphanedFiles = allModuleSetFiles.filter { it.fileName.toString() !in generatedFiles } + + // Safety check: prevent mass deletion if >50% of files appear orphaned + if (orphanedFiles.size > allModuleSetFiles.size * 0.5 && orphanedFiles.isNotEmpty()) { + error(""" + |Safety check failed: Too many orphaned module set files detected in $dir + | Total module set files: ${allModuleSetFiles.size} + | Orphaned files: ${orphanedFiles.size} + | Generated files tracked: $generatedFiles + | Orphaned file names: ${orphanedFiles.map { it.fileName }} + |This might indicate a bug in module set discovery or tracking. Aborting cleanup. + """.trimMargin()) + } + + // Delete orphaned files with logging + for (filePath in orphanedFiles) { + val fileName = filePath.fileName.toString() + println("Deleting orphaned module set file: $fileName from $dir") + println(" Reason: File not in generated set: $generatedFiles") + Files.delete(filePath) + deletedFiles.add(ModuleSetFileResult(fileName, FileChangeStatus.DELETED, 0)) + } + } + } + + return deletedFiles +} + /** * Generates all module set XMLs for the given object. * Discovers all ModuleSet functions via reflection, generates XML files, and prints results. * + * Automatically cleans up outdated module set XML files that no longer have corresponding + * Kotlin module set functions. + * * @param obj The object containing module set functions (e.g., CommunityModuleSets, UltimateModuleSets) * @param outputDir Directory where XML files will be generated * @param label Description label for logging (e.g., "community", "ultimate") + * @param projectRoot Project root path for relativizing paths in output + * @param moduleOutputProvider Optional provider for resolving module output directories (required if module sets use outputModule) * @param printSummary Whether to print generation summary (default: true) */ -fun generateAllModuleSets(obj: Any, outputDir: Path, label: String, printSummary: Boolean = true) { +fun generateAllModuleSets( + obj: Any, + outputDir: Path, + label: String, + projectRoot: Path, + moduleOutputProvider: ModuleOutputProvider? = null, + printSummary: Boolean = true, +) { val startTime = System.currentTimeMillis() Files.createDirectories(outputDir) val moduleSets = discoverModuleSets(obj) + + // Generate all module set XML files first val fileResults = moduleSets.map { moduleSet -> - generateModuleSetXml(moduleSet, outputDir, label) + val targetOutputDir = resolveOutputDir(moduleSet, outputDir, moduleOutputProvider) + generateModuleSetXml(moduleSet, targetOutputDir, label) } - if (printSummary) { - val result = ModuleSetGenerationResult(label, outputDir, fileResults) - printGenerationSummary(moduleSetResults = listOf(result), productResult = null, durationMs = System.currentTimeMillis() - startTime) + // Build map of output directory -> generated file names for cleanup (from generation results) + val outputDirToGeneratedFiles = mutableMapOf>() + for ((moduleSet, fileResult) in moduleSets.zip(fileResults)) { + val targetOutputDir = resolveOutputDir(moduleSet, outputDir, moduleOutputProvider) + outputDirToGeneratedFiles.computeIfAbsent(targetOutputDir) { mutableSetOf() }.add(fileResult.fileName) } + + // Clean up orphaned files after generation (safe for standalone runs) + val deletedFiles = cleanupOrphanedModuleSetFiles(outputDirToGeneratedFiles) + + // Combine generated files and deleted files + val allResults = fileResults + deletedFiles + + if (printSummary) { + val result = ModuleSetGenerationResult(label, outputDir, allResults, outputDirToGeneratedFiles) + printGenerationSummary( + moduleSetResults = listOf(result), + dependencyResult = null, + productResult = null, + projectRoot = projectRoot, + durationMs = System.currentTimeMillis() - startTime + ) + } +} + +/** + * Resolves the output directory for a module set. + * If the module set has an outputModule specified, uses that module's resources directory. + * Otherwise, uses the default outputDir. + */ +private fun resolveOutputDir(moduleSet: ModuleSet, defaultOutputDir: Path, moduleOutputProvider: ModuleOutputProvider?): Path { + val outputModuleName = moduleSet.outputModule + if (outputModuleName != null) { + require(moduleOutputProvider != null) { + "ModuleOutputProvider is required when module set '${moduleSet.name}' specifies outputModule='$outputModuleName'" + } + val module = moduleOutputProvider.findRequiredModule(outputModuleName) + val resourceRoot = module.sourceRoots.firstOrNull { it.rootType == JavaResourceRootType.RESOURCE } + ?: error("No resource root found for module '$outputModuleName' (required by module set '${moduleSet.name}')") + return resourceRoot.path.resolve("META-INF") + } + return defaultOutputDir } /** * Generates all module set XMLs for the given object and returns statistics. * Internal function for use by multi-set generators. + * + * Automatically cleans up outdated module set XML files that no longer have corresponding + * Kotlin module set functions. */ -fun doGenerateAllModuleSetsInternal(obj: Any, outputDir: Path, label: String): ModuleSetGenerationResult { +suspend fun doGenerateAllModuleSetsInternal( + obj: Any, + outputDir: Path, + label: String, + moduleOutputProvider: ModuleOutputProvider? = null, +): ModuleSetGenerationResult = coroutineScope { Files.createDirectories(outputDir) val moduleSets = discoverModuleSets(obj) + + // Generate all module set XML files first (in parallel) val fileResults = moduleSets.map { moduleSet -> - generateModuleSetXml(moduleSet = moduleSet, outputDir = outputDir, label = label) + async { + val targetOutputDir = resolveOutputDir(moduleSet, outputDir, moduleOutputProvider) + generateModuleSetXml(moduleSet = moduleSet, outputDir = targetOutputDir, label = label) + } + }.awaitAll() + + // Build map of output directory -> generated file names (for cleanup aggregation) + val outputDirToGeneratedFiles = mutableMapOf>() + for ((moduleSet, fileResult) in moduleSets.zip(fileResults)) { + val targetOutputDir = resolveOutputDir(moduleSet, outputDir, moduleOutputProvider) + outputDirToGeneratedFiles.computeIfAbsent(targetOutputDir) { mutableSetOf() }.add(fileResult.fileName) } - return ModuleSetGenerationResult(label = label, outputDir = outputDir, files = fileResults) + // Return results with tracking map (cleanup will be done after aggregating all labels) + ModuleSetGenerationResult(label = label, outputDir = outputDir, files = fileResults, trackingMap = outputDirToGeneratedFiles) } \ No newline at end of file diff --git a/platform/build-scripts/product-dsl/src/ModuleSetDiscovery.kt b/platform/build-scripts/product-dsl/src/ModuleSetDiscovery.kt index c0f8f82affbc..568be1e475b5 100644 --- a/platform/build-scripts/product-dsl/src/ModuleSetDiscovery.kt +++ b/platform/build-scripts/product-dsl/src/ModuleSetDiscovery.kt @@ -1,29 +1,52 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@file:Suppress("ReplaceGetOrSet") + package org.jetbrains.intellij.build.productLayout +import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandles import java.lang.invoke.MethodType +import java.util.concurrent.ConcurrentHashMap + +/** + * Cache for reflected method handles per class. + * Avoids repeated reflection and method handle lookup for the same provider class. + */ +private val methodHandleCache = ConcurrentHashMap, List>() /** * Discovers all module sets from an object using reflection. * Returns all public functions that return ModuleSet and take no parameters. * + * Method handles are cached per class to avoid repeated reflection overhead. * This consolidates the duplicated discovery logic that was in both UltimateModuleSets and ultimateGenerator. */ fun discoverModuleSets(provider: Any): List { - val lookup = MethodHandles.lookup() val clazz = provider.javaClass - val methodType = MethodType.methodType(ModuleSet::class.java) + val handles = methodHandleCache.computeIfAbsent(clazz) { discoverMethodHandles(it) } + + val result = ArrayList(handles.size) + for (handle in handles) { + result.add(handle.invoke(provider) as ModuleSet) + } + return result +} +/** + * Discovers and caches method handles for all public no-arg methods returning ModuleSet. + */ +private fun discoverMethodHandles(clazz: Class<*>): List { + val lookup = MethodHandles.lookup() + val methodType = MethodType.methodType(ModuleSet::class.java) + val declaredMethods = clazz.declaredMethods - val result = ArrayList(declaredMethods.size) + val handles = ArrayList() for (method in declaredMethods) { if (method.parameterCount == 0 && java.lang.reflect.Modifier.isPublic(method.modifiers) && method.returnType == ModuleSet::class.java) { - val moduleSet = lookup.findVirtual(clazz, method.name, methodType).invoke(provider) as ModuleSet - result.add(moduleSet) + handles.add(lookup.findVirtual(clazz, method.name, methodType)) } } - return result + return handles } diff --git a/platform/build-scripts/product-dsl/src/ModuleSetIndexing.kt b/platform/build-scripts/product-dsl/src/ModuleSetIndexing.kt new file mode 100644 index 000000000000..34d5d16234a2 --- /dev/null +++ b/platform/build-scripts/product-dsl/src/ModuleSetIndexing.kt @@ -0,0 +1,102 @@ +// 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.productLayout + +import org.jetbrains.intellij.build.productLayout.analysis.ModuleSetTraversal + +/** + * Index of all modules in module sets for dependency validation. + * Includes reachability information to support scope-aware validation. + * + * @param allModules All modules found across all module sets + * @param moduleToDirectSets Map from module name to module sets that directly contain it + * @param moduleToReachableModules Map from module name to all modules reachable from its containing sets + */ +internal data class ModuleSetIndex( + val allModules: Set, + val moduleToDirectSets: Map>, + val moduleToReachableModules: Map> +) + +/** + * Builds an index of all modules across all module sets (recursively). + * Used for validating that dependencies reference modules that actually exist in module sets. + * Tracks direct containment (module in moduleSet.modules) and builds a reachability graph + * to determine which modules are accessible from each module's perspective. + * + * @param allModuleSets List of all module sets to index + * @return Module set index with containment and reachability information + */ +internal fun buildModuleSetIndex(allModuleSets: List): ModuleSetIndex { + val allModules = mutableSetOf() + val moduleToDirectSets = mutableMapOf>() + + // Build a map from module set name to the ModuleSet object for reachability computation + val moduleSetsByName = mutableMapOf() + + /** + * Processes a single module set: records its modules and processes nested sets recursively. + */ + fun processModuleSet(moduleSet: ModuleSet) { + moduleSetsByName.put(moduleSet.name, moduleSet) + + // Record direct modules (those in this moduleSet.modules) + for (module in moduleSet.modules) { + allModules.add(module.name) + moduleToDirectSets.getOrPut(module.name) { mutableSetOf() }.add(moduleSet.name) + } + + // Process nested sets recursively + for (nestedSet in moduleSet.nestedSets) { + processModuleSet(nestedSet) + } + } + + // Process all module sets + allModuleSets.forEach { processModuleSet(it) } + + // Build reachability graph + val moduleToReachableModules = buildReachabilityGraph( + moduleToDirectSets = moduleToDirectSets, + moduleSetsByName = moduleSetsByName + ) + + return ModuleSetIndex( + allModules = allModules, + moduleToDirectSets = moduleToDirectSets.mapValues { it.value.toSet() }, + moduleToReachableModules = moduleToReachableModules + ) +} + +/** + * Builds a reachability graph showing which modules are accessible from each module's perspective. + * For each module, collects all modules reachable through its containing module sets + * (i.e., all modules in the same module set hierarchy). + * + * @param moduleToDirectSets Map from module to sets that directly contain it + * @param moduleSetsByName Map from module set name to ModuleSet object + * @return Map from module name to set of all reachable modules + */ +private fun buildReachabilityGraph( + moduleToDirectSets: Map>, + moduleSetsByName: Map +): Map> { + val moduleToReachableModules = mutableMapOf>() + // Cache for repeated traversals of the same module set + val cache = mutableMapOf>() + + // For each module, collect all modules reachable from its containing sets + for ((moduleName, directSets) in moduleToDirectSets) { + val reachable = mutableSetOf() + + for (setName in directSets) { + val moduleSet = moduleSetsByName.get(setName) + if (moduleSet != null) { + reachable.addAll(ModuleSetTraversal.collectAllModuleNames(moduleSet, cache)) + } + } + + moduleToReachableModules.put(moduleName, reachable) + } + + return moduleToReachableModules +} diff --git a/platform/build-scripts/product-dsl/src/ModuleSetJsonExport.kt b/platform/build-scripts/product-dsl/src/ModuleSetJsonExport.kt index 9d4966d15147..efafa4593104 100644 --- a/platform/build-scripts/product-dsl/src/ModuleSetJsonExport.kt +++ b/platform/build-scripts/product-dsl/src/ModuleSetJsonExport.kt @@ -4,23 +4,33 @@ package org.jetbrains.intellij.build.productLayout import com.fasterxml.jackson.core.JsonEncoding import com.fasterxml.jackson.core.JsonFactory import com.fasterxml.jackson.core.JsonGenerator +import org.jetbrains.intellij.build.ModuleOutputProvider import org.jetbrains.intellij.build.productLayout.analysis.JsonFilter +import org.jetbrains.intellij.build.productLayout.analysis.MergeOperation import org.jetbrains.intellij.build.productLayout.analysis.ModuleSetMetadata +import org.jetbrains.intellij.build.productLayout.analysis.ParseResult import org.jetbrains.intellij.build.productLayout.analysis.ProductSpec import org.jetbrains.intellij.build.productLayout.analysis.analyzeMergeImpact import org.jetbrains.intellij.build.productLayout.analysis.analyzeProductSimilarity +import org.jetbrains.intellij.build.productLayout.analysis.analyzeProductUsage +import org.jetbrains.intellij.build.productLayout.analysis.checkModuleReachability import org.jetbrains.intellij.build.productLayout.analysis.detectModuleSetOverlap +import org.jetbrains.intellij.build.productLayout.analysis.findDependencyPath import org.jetbrains.intellij.build.productLayout.analysis.findModulePaths +import org.jetbrains.intellij.build.productLayout.analysis.getModuleDependencies import org.jetbrains.intellij.build.productLayout.analysis.parseModulesXml import org.jetbrains.intellij.build.productLayout.analysis.suggestModuleSetUnification import org.jetbrains.intellij.build.productLayout.analysis.validateCommunityProducts import org.jetbrains.intellij.build.productLayout.analysis.validateModuleSetLocations import org.jetbrains.intellij.build.productLayout.json.enrichProductsWithMetrics import org.jetbrains.intellij.build.productLayout.json.writeCommunityProductViolations +import org.jetbrains.intellij.build.productLayout.json.writeDependencyPathResult import org.jetbrains.intellij.build.productLayout.json.writeDuplicateAnalysis import org.jetbrains.intellij.build.productLayout.json.writeMergeImpactAnalysis +import org.jetbrains.intellij.build.productLayout.json.writeModuleDependenciesResult import org.jetbrains.intellij.build.productLayout.json.writeModuleDistribution import org.jetbrains.intellij.build.productLayout.json.writeModulePathsResult +import org.jetbrains.intellij.build.productLayout.json.writeModuleReachabilityResult import org.jetbrains.intellij.build.productLayout.json.writeModuleSet import org.jetbrains.intellij.build.productLayout.json.writeModuleSetHierarchy import org.jetbrains.intellij.build.productLayout.json.writeModuleSetLocationViolations @@ -29,6 +39,7 @@ import org.jetbrains.intellij.build.productLayout.json.writeModuleUsageIndex import org.jetbrains.intellij.build.productLayout.json.writeProduct import org.jetbrains.intellij.build.productLayout.json.writeProductCompositionAnalysis import org.jetbrains.intellij.build.productLayout.json.writeProductSimilarityAnalysis +import org.jetbrains.intellij.build.productLayout.json.writeProductUsageAnalysis import org.jetbrains.intellij.build.productLayout.json.writeUnificationSuggestions import java.nio.file.Path import java.time.Instant @@ -61,12 +72,14 @@ import java.time.Instant * @param products List of ProductSpec instances * @param projectRoot Project root path for resolving .idea/modules.xml * @param filter Optional filter: null (full), or JsonFilter object with filter type and optional value + * @param moduleOutputProvider Optional provider for accessing JPS modules (required for dependency analysis filters) */ fun streamModuleAnalysisJson( allModuleSets: List, products: List, projectRoot: Path, - filter: JsonFilter? = null + filter: JsonFilter? = null, + moduleOutputProvider: ModuleOutputProvider? = null ) { // Validate product specifications using shared validation (always validate, even when filtering) val moduleSets = allModuleSets.map { it.moduleSet } @@ -86,105 +99,8 @@ fun streamModuleAnalysisJson( gen.writeStartObject() gen.writeStringField("timestamp", Instant.now().toString()) - // Apply filter - when { - filter == null -> { - // Full JSON (no filter) - writeAllSections(gen, allModuleSets, enrichedProducts, projectRoot) - } - filter.filter == "products" -> { - gen.writeArrayFieldStart("products") - for (product in enrichedProducts) { - writeProduct(gen, product) - } - gen.writeEndArray() - } - filter.filter == "moduleSets" -> { - gen.writeArrayFieldStart("moduleSets") - for ((moduleSet, location, sourceFilePath) in allModuleSets) { - writeModuleSet(gen, moduleSet, location, sourceFilePath, moduleSets) - } - gen.writeEndArray() - } - filter.filter == "composition" -> { - gen.writeObjectFieldStart("productCompositionAnalysis") - writeProductCompositionAnalysis(gen, enrichedProducts) - gen.writeEndObject() - } - filter.filter == "duplicates" -> { - gen.writeObjectFieldStart("duplicateAnalysis") - writeDuplicateAnalysis(gen, allModuleSets, enrichedProducts, projectRoot) - gen.writeEndObject() - } - filter.filter == "product" && filter.value != null -> { - val productName = filter.value - val product = enrichedProducts.firstOrNull { it.name == productName } - if (product != null) { - gen.writeFieldName("product") - gen.writeStartObject() - // Copy writeProduct logic inline but without outer object wrapper - gen.writeStringField("name", product.name) - gen.writeStringField("className", product.className) - gen.writeStringField("sourceFile", product.sourceFile) - if (product.pluginXmlPath != null) { - gen.writeStringField("pluginXmlPath", product.pluginXmlPath) - } - if (product.contentSpec != null) { - val contentSpecJson = org.jetbrains.intellij.build.productLayout.json.kotlinxJson.encodeToString(product.contentSpec) - gen.writeFieldName("contentSpec") - gen.writeRawValue(contentSpecJson) - } - gen.writeArrayFieldStart("buildModules") - for (buildModule in product.buildModules) { - gen.writeString(buildModule) - } - gen.writeEndArray() - gen.writeEndObject() - } else { - gen.writeStringField("error", "Product '$productName' not found") - } - } - filter.filter == "moduleSet" && filter.value != null -> { - val moduleSetName = filter.value - val moduleSetEntry = allModuleSets.firstOrNull { it.moduleSet.name == moduleSetName } - if (moduleSetEntry != null) { - gen.writeFieldName("moduleSet") - gen.writeStartObject() - gen.writeStringField("name", moduleSetEntry.moduleSet.name) - gen.writeStringField("location", moduleSetEntry.location) - gen.writeStringField("sourceFile", moduleSetEntry.sourceFile) - val moduleSetJson = org.jetbrains.intellij.build.productLayout.json.kotlinxJson.encodeToString(moduleSetEntry.moduleSet) - gen.writeFieldName("moduleSet") - gen.writeRawValue(moduleSetJson) - gen.writeEndObject() - } else { - gen.writeStringField("error", "Module set '$moduleSetName' not found") - } - } - filter.filter == "mergeImpact" && filter.source != null -> { - val sourceSet = filter.source - val targetSet = filter.target - val operation = filter.operation ?: "merge" - - val impact = analyzeMergeImpact(sourceSet, targetSet, operation, allModuleSets, enrichedProducts) - gen.writeFieldName("mergeImpact") - gen.writeStartObject() - writeMergeImpactAnalysis(gen, impact) - gen.writeEndObject() - } - filter.filter == "modulePaths" && filter.module != null -> { - val moduleName = filter.module - - val pathsResult = findModulePaths(moduleName, allModuleSets, enrichedProducts, projectRoot) - gen.writeFieldName("modulePaths") - gen.writeStartObject() - writeModulePathsResult(gen, pathsResult) - gen.writeEndObject() - } - else -> { - gen.writeStringField("error", "Unknown filter: ${filter.filter}") - } - } + // Apply filter (dispatches to handler functions) + applyFilter(gen, filter, allModuleSets, enrichedProducts, moduleSets, projectRoot, moduleOutputProvider) gen.writeEndObject() } @@ -193,6 +109,185 @@ fun streamModuleAnalysisJson( System.out.flush() } +/** + * Dispatches filter handling to appropriate handler functions. + */ +private fun applyFilter( + gen: JsonGenerator, + filter: JsonFilter?, + allModuleSets: List, + products: List, + moduleSets: List, + projectRoot: Path, + moduleOutputProvider: ModuleOutputProvider? +) { + when (filter?.filter) { + null -> writeAllSections(gen, allModuleSets, products, projectRoot) + "products" -> handleProductsFilter(gen, products) + "moduleSets" -> handleModuleSetsFilter(gen, allModuleSets, moduleSets) + "composition" -> handleCompositionFilter(gen, products) + "duplicates" -> handleDuplicatesFilter(gen, allModuleSets, products, projectRoot) + "product" -> handleSingleProductFilter(gen, filter.value, products) + "moduleSet" -> handleSingleModuleSetFilter(gen, filter.value, allModuleSets) + "mergeImpact" -> handleMergeImpactFilter(gen, filter, allModuleSets, products) + "modulePaths" -> handleModulePathsFilter(gen, filter.module, allModuleSets, products, projectRoot) + "moduleDependencies" -> handleModuleDependenciesFilter(gen, filter, moduleOutputProvider) + "moduleReachability" -> handleModuleReachabilityFilter(gen, filter, allModuleSets, moduleOutputProvider) + "dependencyPath" -> handleDependencyPathFilter(gen, filter, moduleOutputProvider) + "productUsage" -> handleProductUsageFilter(gen, filter.moduleSet, allModuleSets, products) + else -> gen.writeStringField("error", "Unknown filter: ${filter.filter}") + } +} + +private fun handleProductsFilter(gen: JsonGenerator, products: List) { + gen.writeArrayFieldStart("products") + for (product in products) { + writeProduct(gen, product) + } + gen.writeEndArray() +} + +private fun handleModuleSetsFilter(gen: JsonGenerator, allModuleSets: List, moduleSets: List) { + gen.writeArrayFieldStart("moduleSets") + for ((moduleSet, location, sourceFilePath) in allModuleSets) { + writeModuleSet(gen, moduleSet, location, sourceFilePath, moduleSets) + } + gen.writeEndArray() +} + +private fun handleCompositionFilter(gen: JsonGenerator, products: List) { + gen.writeObjectFieldStart("productCompositionAnalysis") + writeProductCompositionAnalysis(gen, products) + gen.writeEndObject() +} + +private fun handleDuplicatesFilter(gen: JsonGenerator, allModuleSets: List, products: List, projectRoot: Path) { + gen.writeObjectFieldStart("duplicateAnalysis") + writeDuplicateAnalysis(gen, allModuleSets, products, projectRoot) + gen.writeEndObject() +} + +private fun handleSingleProductFilter(gen: JsonGenerator, productName: String?, products: List) { + if (productName == null) { + gen.writeStringField("error", "Product name required for 'product' filter") + return + } + val product = products.firstOrNull { it.name == productName } + if (product != null) { + gen.writeFieldName("product") + writeProduct(gen, product) + } + else { + gen.writeStringField("error", "Product '$productName' not found") + } +} + +private fun handleSingleModuleSetFilter(gen: JsonGenerator, moduleSetName: String?, allModuleSets: List) { + if (moduleSetName == null) { + gen.writeStringField("error", "Module set name required for 'moduleSet' filter") + return + } + val moduleSetEntry = allModuleSets.firstOrNull { it.moduleSet.name == moduleSetName } + if (moduleSetEntry != null) { + gen.writeFieldName("moduleSet") + gen.writeStartObject() + gen.writeStringField("name", moduleSetEntry.moduleSet.name) + gen.writeStringField("location", moduleSetEntry.location) + gen.writeStringField("sourceFile", moduleSetEntry.sourceFile) + gen.writeArrayFieldStart("directNestedSets") + for (nestedSet in moduleSetEntry.directNestedSets) { + gen.writeString(nestedSet) + } + gen.writeEndArray() + val moduleSetJson = org.jetbrains.intellij.build.productLayout.json.kotlinxJson.encodeToString(moduleSetEntry.moduleSet) + gen.writeFieldName("moduleSet") + gen.writeRawValue(moduleSetJson) + gen.writeEndObject() + } + else { + gen.writeStringField("error", "Module set '$moduleSetName' not found") + } +} + +private fun handleMergeImpactFilter(gen: JsonGenerator, filter: JsonFilter, allModuleSets: List, products: List) { + val sourceSet = filter.source + if (sourceSet == null) { + gen.writeStringField("error", "Source module set required for 'mergeImpact' filter") + return + } + val operation = MergeOperation.fromString(filter.operation ?: "merge") + val impact = analyzeMergeImpact(sourceSet, filter.target, operation, allModuleSets, products) + gen.writeFieldName("mergeImpact") + writeMergeImpactAnalysis(gen, impact) +} + +private fun handleModulePathsFilter(gen: JsonGenerator, moduleName: String?, allModuleSets: List, products: List, projectRoot: Path) { + if (moduleName == null) { + gen.writeStringField("error", "Module name required for 'modulePaths' filter") + return + } + val pathsResult = findModulePaths(moduleName, allModuleSets, products, projectRoot) + gen.writeFieldName("modulePaths") + writeModulePathsResult(gen, pathsResult) +} + +private fun handleModuleDependenciesFilter(gen: JsonGenerator, filter: JsonFilter, moduleOutputProvider: ModuleOutputProvider?) { + val moduleName = filter.module + if (moduleName == null) { + gen.writeStringField("error", "Module name required for 'moduleDependencies' filter") + return + } + if (moduleOutputProvider == null) { + gen.writeStringField("error", "ModuleOutputProvider required for moduleDependencies filter") + return + } + val result = getModuleDependencies(moduleName, moduleOutputProvider, filter.includeTransitive) + gen.writeFieldName("moduleDependencies") + writeModuleDependenciesResult(gen, result) +} + +private fun handleModuleReachabilityFilter(gen: JsonGenerator, filter: JsonFilter, allModuleSets: List, moduleOutputProvider: ModuleOutputProvider?) { + val moduleName = filter.module + val moduleSetName = filter.moduleSet + if (moduleName == null || moduleSetName == null) { + gen.writeStringField("error", "Module name and module set name required for 'moduleReachability' filter") + return + } + if (moduleOutputProvider == null) { + gen.writeStringField("error", "ModuleOutputProvider required for moduleReachability filter") + return + } + val result = checkModuleReachability(moduleName, moduleSetName, allModuleSets, moduleOutputProvider) + gen.writeFieldName("moduleReachability") + writeModuleReachabilityResult(gen, result) +} + +private fun handleDependencyPathFilter(gen: JsonGenerator, filter: JsonFilter, moduleOutputProvider: ModuleOutputProvider?) { + val fromModule = filter.fromModule + val toModule = filter.toModule + if (fromModule == null || toModule == null) { + gen.writeStringField("error", "Both fromModule and toModule required for 'dependencyPath' filter") + return + } + if (moduleOutputProvider == null) { + gen.writeStringField("error", "ModuleOutputProvider required for dependencyPath filter") + return + } + val result = findDependencyPath(fromModule, toModule, moduleOutputProvider) + gen.writeFieldName("dependencyPath") + writeDependencyPathResult(gen, result) +} + +private fun handleProductUsageFilter(gen: JsonGenerator, moduleSetName: String?, allModuleSets: List, products: List) { + if (moduleSetName == null) { + gen.writeStringField("error", "Module set name required for 'productUsage' filter") + return + } + val usage = analyzeProductUsage(moduleSetName, products, allModuleSets) + gen.writeFieldName("productUsage") + writeProductUsageAnalysis(gen, usage) +} + /** * Writes all sections of the analysis JSON (used when no filter is specified). */ @@ -206,7 +301,7 @@ private fun writeAllSections( val moduleSets = allModuleSets.map { it.moduleSet } gen.writeArrayFieldStart("moduleSets") for ((moduleSet, location, sourceFilePath) in allModuleSets) { - writeModuleSet(gen, moduleSet, location, sourceFilePath, moduleSets) + writeModuleSet(gen = gen, moduleSet = moduleSet, location = location, sourceFilePath = sourceFilePath, allModuleSets = moduleSets) } gen.writeEndArray() @@ -228,7 +323,14 @@ private fun writeAllSections( gen.writeEndObject() // Write module distribution analysis - val moduleLocations = parseModulesXml(projectRoot) + val moduleLocationsResult = parseModulesXml(projectRoot) + val moduleLocations = when (moduleLocationsResult) { + is ParseResult.Success -> moduleLocationsResult.value + is ParseResult.Failure -> { + gen.writeStringField("moduleLocationsWarning", moduleLocationsResult.error) + moduleLocationsResult.partial ?: emptyMap() + } + } gen.writeObjectFieldStart("moduleDistribution") writeModuleDistribution(gen, allModuleSets, products, moduleLocations) gen.writeEndObject() @@ -257,15 +359,13 @@ private fun writeAllSections( // Analyze product similarity for refactoring recommendations val similarityPairs = analyzeProductSimilarity(products, similarityThreshold = 0.7) - gen.writeObjectFieldStart("productSimilarity") + gen.writeFieldName("productSimilarity") writeProductSimilarityAnalysis(gen, similarityPairs, 0.7) - gen.writeEndObject() // Detect module set overlaps (with nested set filtering to avoid false positives) val moduleSetOverlaps = detectModuleSetOverlap(allModuleSets, minOverlapPercent = 50) - gen.writeObjectFieldStart("moduleSetOverlap") + gen.writeFieldName("moduleSetOverlap") writeModuleSetOverlapAnalysis(gen, moduleSetOverlaps, 50) - gen.writeEndObject() // Generate unification suggestions based on overlaps and similarity val unificationSuggestions = suggestModuleSetUnification( @@ -276,7 +376,6 @@ private fun writeAllSections( maxSuggestions = 10, strategy = "all" ) - gen.writeObjectFieldStart("unificationSuggestions") + gen.writeFieldName("unificationSuggestions") writeUnificationSuggestions(gen, unificationSuggestions) - gen.writeEndObject() } diff --git a/platform/build-scripts/product-dsl/src/ProductModulesContentSpec.kt b/platform/build-scripts/product-dsl/src/ProductModulesContentSpec.kt index 00977f812293..3590c577ac7a 100644 --- a/platform/build-scripts/product-dsl/src/ProductModulesContentSpec.kt +++ b/platform/build-scripts/product-dsl/src/ProductModulesContentSpec.kt @@ -1,4 +1,15 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + +/** + * Programmatic content DSL for defining product module content in Kotlin instead of static XML. + * + * This file contains the core types for the programmatic content system, which allows products + * to define their module composition, XML includes, and module sets using type-safe Kotlin code. + * + * For comprehensive documentation on how to use this system: + * @see Programmatic Content Documentation - Complete guide with examples + * @see Module Sets Documentation - How module sets work and best practices + */ @file:Suppress("ReplacePutWithAssignment") package org.jetbrains.intellij.build.productLayout diff --git a/platform/build-scripts/product-dsl/src/XmlGenerationUtils.kt b/platform/build-scripts/product-dsl/src/XmlGenerationUtils.kt index c9eabcd13ef3..76f02b319117 100644 --- a/platform/build-scripts/product-dsl/src/XmlGenerationUtils.kt +++ b/platform/build-scripts/product-dsl/src/XmlGenerationUtils.kt @@ -3,6 +3,8 @@ package org.jetbrains.intellij.build.productLayout +import org.jetbrains.intellij.build.productLayout.analysis.ModuleSetTraversal + /** * Shared utilities for generating plugin.xml content for both products and module sets. * Both products and module sets are "content containers" that: @@ -56,6 +58,7 @@ internal fun buildModuleAliasesXml(aliases: List): String { /** * Wraps content with editor-fold comments for collapsible sections in IDE. + * Uses try-finally to ensure the closing tag is always written, even if block throws. * * @param sb StringBuilder to append to * @param indent Indentation string (e.g., " ") @@ -64,33 +67,12 @@ internal fun buildModuleAliasesXml(aliases: List): String { */ internal inline fun withEditorFold(sb: StringBuilder, indent: String, description: String, block: () -> Unit) { sb.append("$indent\n") - block() - sb.append("$indent\n") -} - -/** - * Recursively collects all module names from a module set and its nested sets. - * - * @param moduleSet The module set to collect modules from - * @param excludedModules Set of module names to exclude - * @return Set of all module names found in the module set hierarchy - */ -internal fun collectAllModuleNames(moduleSet: ModuleSet, excludedModules: Set = emptySet()): Set { - val result = HashSet() - - fun collect(set: ModuleSet) { - for (module in set.modules) { - if (module.name !in excludedModules) { - result.add(module.name) - } - } - for (nestedSet in set.nestedSets) { - collect(nestedSet) - } + try { + block() + } + finally { + sb.append("$indent\n") } - - collect(moduleSet) - return result } /** @@ -110,32 +92,19 @@ internal fun visitAllModules(moduleSet: ModuleSet, visitor: (ContentModule) -> U } /** - * Recursively collects all module names from a module set and its nested sets. - * Helper function for module distribution and usage analysis. + * Finds a module set by name and collects all module names from it and its nested sets. + * Delegates to [ModuleSetTraversal.collectAllModuleNames] for the actual traversal. * * @param moduleSets List of all module sets to search in * @param setName Name of the module set to start collecting from - * @param visited Set of already visited module set names to prevent infinite recursion - * @return Set of all module names found in the module set hierarchy + * @return Set of all module names found in the module set hierarchy, or empty set if not found */ -fun collectAllModuleNamesFromSet( +internal fun collectAllModuleNamesFromSet( moduleSets: List, setName: String, - visited: MutableSet = mutableSetOf() ): Set { - if (setName in visited) return emptySet() - visited.add(setName) - val moduleSet = moduleSets.firstOrNull { it.name == setName } ?: return emptySet() - - val allModules = moduleSet.modules.map { it.name }.toMutableSet() - - // Recursively collect from nested sets - for (nestedSet in moduleSet.nestedSets) { - allModules.addAll(collectAllModuleNamesFromSet(moduleSets, nestedSet.name, visited)) - } - - return allModules + return ModuleSetTraversal.collectAllModuleNames(moduleSet) } /** diff --git a/platform/build-scripts/product-dsl/src/analysis/AnalysisModels.kt b/platform/build-scripts/product-dsl/src/analysis/AnalysisModels.kt index c62f57a029d5..b9405cca3e59 100644 --- a/platform/build-scripts/product-dsl/src/analysis/AnalysisModels.kt +++ b/platform/build-scripts/product-dsl/src/analysis/AnalysisModels.kt @@ -5,34 +5,123 @@ import kotlinx.serialization.Serializable import org.jetbrains.intellij.build.productLayout.ModuleSet import org.jetbrains.intellij.build.productLayout.ProductModulesContentSpec +/** + * Operation type for module set merge/move/inline analysis. + * Using enum instead of String for type safety. + */ +enum class MergeOperation { + /** Combine source modules into target module set */ + MERGE, + /** Move source module set to a different location */ + MOVE, + /** Remove module set and add modules directly to products */ + INLINE; + + companion object { + fun fromString(value: String): MergeOperation = when (value.lowercase()) { + "merge" -> MERGE + "move" -> MOVE + "inline" -> INLINE + else -> error("Unknown merge operation: '$value'. Valid values: merge, move, inline") + } + } +} + +/** + * Result type for operations that can fail with partial data. + * Use instead of exceptions for recoverable errors or when partial results are useful. + */ +sealed class ParseResult { + /** Successful parse with complete data */ + data class Success(val value: T) : ParseResult() + + /** Failed parse, optionally with partial data */ + data class Failure( + val error: String, + val partial: T? = null + ) : ParseResult() + + fun isSuccess(): Boolean = this is Success + fun isFailure(): Boolean = this is Failure + + fun getOrNull(): T? = when (this) { + is Success -> value + is Failure -> partial + } + + fun getOrThrow(): T = when (this) { + is Success -> value + is Failure -> error(error) + } + + inline fun map(transform: (T) -> R): ParseResult = when (this) { + is Success -> Success(transform(value)) + is Failure -> Failure(error, partial?.let(transform)) + } +} + +/** + * Dependency resolution error for a single module. + * Used by validation functions to report missing dependencies. + */ +@Serializable +data class DependencyError( + val dependency: String, + val existsGlobally: Boolean, + val affectedModules: List, + val suggestedModuleSets: List, + val isTransitive: Boolean = false, + val transitiveChains: List = emptyList() +) + /** * Metadata about a module set including its location and source file. * * @param moduleSet The module set instance * @param location The location category ("community" or "ultimate") * @param sourceFile The Kotlin source file path relative to project root where this module set is defined + * @param directNestedSets Names of directly nested module sets (immediate children only, not transitive) */ data class ModuleSetMetadata( val moduleSet: ModuleSet, val location: String, - val sourceFile: String + val sourceFile: String, + val directNestedSets: List = emptyList() ) /** * JSON filter for selective analysis output. - * Supports various filter types: products, moduleSets, composition, duplicates, mergeImpact, modulePaths, or specific items. + * Supports various filter types: products, moduleSets, composition, duplicates, mergeImpact, modulePaths, + * moduleDependencies, moduleReachability, dependencyPath, productUsage, or specific items. */ @Serializable data class JsonFilter( - val filter: String, // "products", "moduleSets", "composition", "duplicates", "mergeImpact", "modulePaths", "product", "moduleSet" + val filter: String, // "products", "moduleSets", "composition", "duplicates", "mergeImpact", "modulePaths", "product", "moduleSet", "moduleDependencies", "moduleReachability", "dependencyPath", "productUsage" val value: String? = null, // Product/module set name when filter is "product" or "moduleSet" - val module: String? = null, // Module name for "modulePaths" filter + val module: String? = null, // Module name for "modulePaths", "moduleDependencies" filters + val moduleSet: String? = null, // Module set name for "moduleReachability" or "productUsage" filter + val fromModule: String? = null, // Starting module for "dependencyPath" filter + val toModule: String? = null, // Target module for "dependencyPath" filter val source: String? = null, // Source module set name for "mergeImpact" filter val target: String? = null, // Target module set name for "mergeImpact" filter (null for inline operation) val operation: String? = null, // Operation type for "mergeImpact": "merge", "move", or "inline" (default: "merge") - val includeDuplicates: Boolean = false // Include duplicate xi:include detection in output (for future unification) + val includeDuplicates: Boolean = false, // Include duplicate xi:include detection in output (for future unification) + val includeTransitive: Boolean = false // Include ALL transitive dependencies in moduleDependencies filter (BFS traversal) ) +/** + * Product category based on architecture and module sets. + */ +@Serializable +enum class ProductCategory { + /** Ultimate IDE products (uses ide.ultimate module set) */ + ULTIMATE, + /** Community IDE products (uses ide.common module set) */ + COMMUNITY, + /** Backend/specialized products (neither ultimate nor community) */ + BACKEND +} + /** * Product specification for JSON output. * Contains essential product metadata with source file paths for AI navigation, @@ -45,6 +134,7 @@ data class ProductSpec( val pluginXmlPath: String?, val contentSpec: ProductModulesContentSpec?, val buildModules: List, + val category: ProductCategory = ProductCategory.BACKEND, // Product architecture category val totalModuleCount: Int = 0, // All modules including from module sets val directModuleCount: Int = 0, // Just additionalModules count val moduleSetCount: Int = 0, // Number of module sets included @@ -95,7 +185,8 @@ data class ModuleSetLocationViolation( * Similarity between two products based on module set overlap. * Used for identifying merge candidates and refactoring opportunities. */ -data class ProductSimilarityPair( +@Serializable +internal data class ProductSimilarityPair( val product1: String, val product2: String, val similarity: Double, @@ -110,7 +201,8 @@ data class ProductSimilarityPair( * Correctly identifies intentional nested set inclusions vs actual duplications. * Intentional nesting (e.g., libraries includes libraries.core) is filtered out. */ -data class ModuleSetOverlap( +@Serializable +internal data class ModuleSetOverlap( val moduleSet1: String, val moduleSet2: String, val location1: String, @@ -123,11 +215,26 @@ data class ModuleSetOverlap( val recommendation: String ) +/** + * Impact metrics for unification suggestions. + * Contains various metrics depending on the strategy type. + */ +@Serializable +internal data class UnificationImpact( + val moduleSetsSaved: Int? = null, + val overlapPercent: Int? = null, + val moduleCount: Int? = null, + val affectedProducts: List? = null, + val similarity: Double? = null, + val sharedModuleSets: Int? = null +) + /** * Suggestion for module set unification (merge, inline, factor, split). * Generated by analyzing overlap, product similarity, and module set usage patterns. */ -data class UnificationSuggestion( +@Serializable +internal data class UnificationSuggestion( val priority: String, // "high", "medium", "low" val strategy: String, // "merge", "inline", "factor", "split" val type: String?, // For merge: "subset", "superset", "high-overlap" @@ -137,14 +244,15 @@ data class UnificationSuggestion( val products: List?, // For factor: products with shared sets val sharedModuleSets: List?, // For factor: shared module sets val reason: String, - val impact: Map + val impact: UnificationImpact ) /** * File reference in a module path trace. * Points to either a module set file or a product file that includes a module. */ -data class PathFileReference( +@Serializable +internal data class PathFileReference( val type: String, // "module-set" or "product" val path: String?, // Absolute file path val name: String, // Name of the module set or product @@ -155,7 +263,8 @@ data class PathFileReference( * A single path showing how a module reaches a product. * Either directly or through module set(s). */ -data class ModulePath( +@Serializable +internal data class ModulePath( val type: String, // "direct" or "module-set" val path: String, // Human-readable path string like "module → set → product" val files: List // File references involved in this path @@ -164,30 +273,80 @@ data class ModulePath( /** * Complete tracing result for a module showing all paths to products. */ -data class ModulePathsResult( +@Serializable +internal data class ModulePathsResult( val module: String, val paths: List, val moduleSets: List, // Module sets that contain this module val products: List // Products that include this module (directly or indirectly) ) +/** + * Validation violation during merge impact analysis. + * Indicates issues that would be introduced by the operation. + */ +@Serializable +internal data class MergeViolation( + val type: String, // "location", "community-uses-ultimate", "notFound", "validation" + val severity: String, // "error", "warning" + val message: String, + val affectedProducts: List? = null, + val fix: String? = null +) + +/** + * Size impact metrics for merge operations. + * Shows how module counts change as a result of the operation. + */ +@Serializable +internal data class SizeImpact( + val sourceModuleCount: Int, + val targetModuleCount: Int, + val newModulesToTarget: Int, + val duplicateModules: Int, + val resultingModuleCount: Int +) + /** * Impact analysis result for merging, moving, or inlining module sets. * Used to assess safety and predict consequences before refactoring. */ -data class MergeImpactResult( +@Serializable +internal data class MergeImpactResult( val operation: String, // "merge", "move", "inline" val sourceSet: String, val targetSet: String?, val productsUsingSource: List, val productsUsingTarget: List, val productsThatWouldChange: List, - val sizeImpact: Map, - val violations: List>, + val sizeImpact: SizeImpact, + val violations: List, val recommendation: String, val safe: Boolean ) +/** + * Product usage information showing how a product uses a module set. + */ +@Serializable +internal data class ProductUsageEntry( + val product: String, + val usageType: String, // "direct" or "indirect" + val inclusionChain: List? // For indirect: chain of module sets from product to target +) + +/** + * Analysis of which products use a specific module set. + * Distinguishes direct usage (top-level reference) from indirect usage (nested within other module sets). + */ +@Serializable +internal data class ProductUsageAnalysis( + val moduleSet: String, + val directUsage: List, // Products that directly reference this module set + val indirectUsage: List, // Products that use it transitively through other sets + val totalProducts: Int +) + // Private helper data classes for internal analysis /** diff --git a/platform/build-scripts/product-dsl/src/analysis/DependencyValidation.kt b/platform/build-scripts/product-dsl/src/analysis/DependencyValidation.kt new file mode 100644 index 000000000000..357e07299de9 --- /dev/null +++ b/platform/build-scripts/product-dsl/src/analysis/DependencyValidation.kt @@ -0,0 +1,365 @@ +// 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.productLayout.analysis + +import org.jetbrains.intellij.build.productLayout.AnsiColors +import org.jetbrains.intellij.build.productLayout.ModuleDescriptorCache +import org.jetbrains.intellij.build.productLayout.ModuleSet +import org.jetbrains.intellij.build.productLayout.ProductModulesContentSpec + +/** + * Known missing dependencies that are temporarily allowed. + * These are typically provided by plugin layouts rather than module sets. + * TODO: Move these to proper module sets or fix product definitions + */ +internal val KNOWN_MISSING_DEPENDENCIES = setOf( + "intellij.cidr.core", + "intellij.cidr.util.execution", + "intellij.cidr.debugger.core", + "intellij.cidr.debugger.backend", + "intellij.cidr.runner" +) + +/** + * Validates module sets marked with selfContained=true in isolation. + * + * Self-contained module sets must be resolvable without other module sets. + * This ensures they have all their dependencies available internally. + * + * Example: core.platform is self-contained because CodeServer uses it alone + * without other module sets, so it must contain everything needed. + * + * Only validates self-contained sets to avoid false positives from composable + * module sets like debugger(), vcs(), xml() that depend on modules from other sets. + * + * **Key difference from normal validation**: For self-contained sets, ALL modules + * in the entire hierarchy (including nested sets) can see ALL other modules. + * This matches runtime behavior where order doesn't matter (topological sort). + */ +internal fun validateSelfContainedModuleSets( + allModuleSets: List, + descriptorCache: ModuleDescriptorCache +) { + // Collect all self-contained module sets (recursively check nested sets too) + val selfContainedSets = mutableListOf() + fun collectSelfContained(moduleSet: ModuleSet) { + if (moduleSet.selfContained) { + selfContainedSets.add(moduleSet) + } + for (nestedSet in moduleSet.nestedSets) { + collectSelfContained(nestedSet) + } + } + for (moduleSet in allModuleSets) { + collectSelfContained(moduleSet) + } + + if (selfContainedSets.isEmpty()) { + return + } + + // Validate each self-contained set in isolation + for (moduleSet in selfContainedSets) { + // Collect ALL modules from the entire hierarchy (flatten nested sets) + val allModulesInSet = mutableSetOf() + fun collectAllModules(ms: ModuleSet) { + for (module in ms.modules) { + allModulesInSet.add(module.name) + } + for (nestedSet in ms.nestedSets) { + collectAllModules(nestedSet) + } + } + collectAllModules(moduleSet) + + // Collect modules with descriptors + val modulesWithDescriptors = allModulesInSet.mapNotNull { moduleName -> + descriptorCache.getOrAnalyze(moduleName)?.let { moduleName to it } + } + + if (modulesWithDescriptors.isEmpty()) { + continue + } + + // Validate dependencies: each module must be able to reach its dependencies + // within the flattened set (all modules can see all other modules) + val missingDeps = mutableMapOf>() + + for ((moduleName, info) in modulesWithDescriptors) { + // Check direct and transitive dependencies + val visited = mutableSetOf(moduleName) + val queue = ArrayDeque(info.dependencies.map { it to listOf(moduleName) }) + + while (queue.isNotEmpty()) { + val (dep, chain) = queue.removeFirst() + + if (dep in visited) { + continue + } + visited.add(dep) + + // Check if dependency is in the flattened set + if (dep !in allModulesInSet) { + missingDeps.getOrPut(dep) { mutableSetOf() }.add(chain.first()) + } + + // Add transitive dependencies + val depInfo = descriptorCache.getOrAnalyze(dep) + if (depInfo != null) { + for (transitiveDep in depInfo.dependencies) { + queue.add(transitiveDep to (chain + dep)) + } + } + } + } + + if (missingDeps.isNotEmpty()) { + error(buildString { + appendLine("${AnsiColors.RED}${AnsiColors.BOLD}❌ Module set '${moduleSet.name}' is marked selfContained but has unresolvable dependencies${AnsiColors.RESET}") + appendLine() + + for ((dep, needingModules) in missingDeps.entries.sortedByDescending { it.value.size }) { + appendLine(" ${AnsiColors.RED}✗${AnsiColors.RESET} Missing: ${AnsiColors.BOLD}'$dep'${AnsiColors.RESET}") + appendLine(" Needed by: ${needingModules.sorted().joinToString(", ")}") + } + + appendLine() + appendLine("${AnsiColors.YELLOW}💡 To fix:${AnsiColors.RESET}") + appendLine("1. Add the missing modules/sets to '${moduleSet.name}' to make it truly self-contained") + appendLine("2. Or remove selfContained=true if this set is designed to compose with other sets") + }) + } + } +} + +/** + * Validates that all products have resolvable module set dependencies. + * + * This is Tier 2 validation that ensures products can actually load at runtime. + * It validates that: + * 1. All module sets referenced by a product exist and are resolvable + * 2. All modules in those sets can have their dependencies satisfied within the product's composition + * 3. No module references dependencies outside the product's available modules + * + * This catches the class of errors like: + * "Plugin 'Java' has dependency on 'com.intellij.modules.vcs' which is not installed" + * + * @param allModuleSets All available module sets (community + ultimate) + * @param productSpecs List of (productName, ProductModulesContentSpec) pairs + * @param descriptorCache Cache for module descriptor information + * @param allowUnresolvableProducts Set of product names explicitly allowed to have unresolvable dependencies (e.g., test products) + * @throws IllegalStateException if any product has unresolvable dependencies + */ +internal fun validateProductModuleSets( + allModuleSets: List, + productSpecs: List>, + descriptorCache: ModuleDescriptorCache, + allowUnresolvableProducts: Set = setOf() +) { + data class ProductError( + val productName: String, + val missingModules: Map>, // module -> set of dependencies it needs + ) + + val productErrors = mutableListOf() + val moduleSetsByName = allModuleSets.associateBy { it.name } + + for ((productName, spec) in productSpecs) { + // Skip products without specs + if (spec == null) { + continue + } + + // Build index of modules available in this product + val productIndex = buildProductModuleIndex(productName, spec) + + // Validate that all referenced module sets exist + val missingModuleSets = productIndex.referencedModuleSets.filter { it !in moduleSetsByName } + if (missingModuleSets.isNotEmpty()) { + error(buildString { + appendLine("${AnsiColors.RED}${AnsiColors.BOLD}❌ Product '$productName' references non-existent module sets${AnsiColors.RESET}") + appendLine() + for (setName in missingModuleSets.sorted()) { + appendLine(" ${AnsiColors.RED}✗${AnsiColors.RESET} Module set '${AnsiColors.BOLD}$setName${AnsiColors.RESET}' does not exist") + } + appendLine() + appendLine("${AnsiColors.BLUE}💡 Fix: Remove the reference or define the module set${AnsiColors.RESET}") + }) + } + + // Validate no duplicate content modules in product + // This validation ALWAYS runs, even for products in allowUnresolvableProducts + val allContentModules = mutableListOf() + for (moduleSetWithOverrides in spec.moduleSets) { + val moduleSet = moduleSetsByName[moduleSetWithOverrides.moduleSet.name] + if (moduleSet != null) { + allContentModules.addAll(ModuleSetTraversal.collectAllModuleNamesAsList(moduleSet)) + } + } + for (module in spec.additionalModules) { + allContentModules.add(module.name) + } + + val duplicateModules = allContentModules.groupingBy { it }.eachCount().filter { it.value > 1 } + if (duplicateModules.isNotEmpty()) { + error(buildString { + appendLine("${AnsiColors.RED}${AnsiColors.BOLD}❌ Product '$productName' has duplicate content modules${AnsiColors.RESET}") + appendLine() + appendLine("${AnsiColors.YELLOW}Duplicated modules (appearing ${AnsiColors.BOLD}${duplicateModules.values.max()}${AnsiColors.RESET}${AnsiColors.YELLOW} times):${AnsiColors.RESET}") + for ((moduleName, count) in duplicateModules.entries.sortedBy { it.key }) { + appendLine(" ${AnsiColors.RED}✗${AnsiColors.RESET} ${AnsiColors.BOLD}$moduleName${AnsiColors.RESET} (appears $count times)") + } + appendLine() + appendLine("${AnsiColors.BLUE}💡 This causes runtime error: \"Plugin has duplicated content modules declarations\"${AnsiColors.RESET}") + appendLine("${AnsiColors.BLUE}Fix: Remove duplicate moduleSet() nesting or redundant module() calls${AnsiColors.RESET}") + }) + } + + // Skip missing dependency validation for explicitly allowed products + if (productName in allowUnresolvableProducts) { + continue + } + + // Validate module dependencies within product scope + val missingDependencies = mutableMapOf>() + + for (moduleName in productIndex.allModules) { + val info = descriptorCache.getOrAnalyze(moduleName) ?: continue + val reachableModules = productIndex.moduleToReachableModules[moduleName] ?: emptySet() + + // Check direct dependencies + for (dependency in info.dependencies) { + if (dependency !in reachableModules && dependency !in KNOWN_MISSING_DEPENDENCIES) { + missingDependencies.getOrPut(moduleName) { mutableSetOf() }.add(dependency) + } + } + + // Check transitive dependencies + val visited = mutableSetOf(moduleName) + val queue = ArrayDeque(info.dependencies) + + while (queue.isNotEmpty()) { + val dep = queue.removeFirst() + if (dep in visited) continue + visited.add(dep) + + if (dep !in reachableModules && dep !in KNOWN_MISSING_DEPENDENCIES) { + missingDependencies.getOrPut(moduleName) { mutableSetOf() }.add(dep) + } + + val depInfo = descriptorCache.getOrAnalyze(dep) + if (depInfo != null) { + queue.addAll(depInfo.dependencies) + } + } + } + + if (missingDependencies.isNotEmpty()) { + productErrors.add(ProductError(productName, missingDependencies)) + } + } + + // Report all errors + if (productErrors.isNotEmpty()) { + error(buildString { + appendLine("${AnsiColors.RED}${AnsiColors.BOLD}❌ Product-level validation failed: Unresolvable module dependencies${AnsiColors.RESET}") + appendLine() + + for (productError in productErrors) { + appendLine("${AnsiColors.BOLD}Product: ${productError.productName}${AnsiColors.RESET}") + appendLine() + + // Group by missing dependency for clearer output + val depToModules = mutableMapOf>() + for ((module, deps) in productError.missingModules) { + for (dep in deps) { + depToModules.getOrPut(dep) { mutableSetOf() }.add(module) + } + } + + for ((missingDep, needingModules) in depToModules.entries.sortedByDescending { it.value.size }) { + appendLine(" ${AnsiColors.RED}✗${AnsiColors.RESET} Missing: ${AnsiColors.BOLD}'$missingDep'${AnsiColors.RESET}") + appendLine(" Needed by: ${needingModules.sorted().take(5).joinToString(", ")}") + if (needingModules.size > 5) { + appendLine(" ... and ${needingModules.size - 5} more modules") + } + + // Suggest which module sets contain this dependency + val containingSets = allModuleSets.filter { moduleSet -> + ModuleSetTraversal.containsModule(moduleSet, missingDep) + }.map { it.name } + + if (containingSets.isNotEmpty()) { + appendLine(" ${AnsiColors.BLUE}Suggestion:${AnsiColors.RESET} Add module set: ${containingSets.joinToString(" or ")}") + } + appendLine() + } + + appendLine() + } + + appendLine("${AnsiColors.BLUE}💡 This will cause runtime errors: \"Plugin X has dependency on Y which is not installed\"${AnsiColors.RESET}") + appendLine() + appendLine("${AnsiColors.BOLD}To fix:${AnsiColors.RESET}") + appendLine("${AnsiColors.BLUE}1.${AnsiColors.RESET} Add the required module sets to the product's getProductContentDescriptor()") + appendLine("${AnsiColors.BLUE}2.${AnsiColors.RESET} Or add individual modules via module()/embeddedModule()") + appendLine("${AnsiColors.BLUE}3.${AnsiColors.RESET} Or add the product to allowUnresolvableProducts if this is intentional") + }) + } +} + +// Helper functions + +/** + * Builds an index of all modules available in a specific product. + * This is product-scoped, unlike the global ModuleSetIndex. + */ +private data class ProductModuleIndex( + val productName: String, + val allModules: Set, // All modules available in this product + val moduleToReachableModules: Map>, // Product-scoped reachability + val referencedModuleSets: Set, // Module set names used by product +) + +private fun buildProductModuleIndex( + productName: String, + spec: ProductModulesContentSpec, +): ProductModuleIndex { + val allModules = mutableSetOf() + val referencedModuleSets = mutableSetOf() + + // Recursively collect all modules from a module set + fun collectModulesFromSet(moduleSet: ModuleSet) { + for (module in moduleSet.modules) { + allModules.add(module.name) + } + for (nestedSet in moduleSet.nestedSets) { + collectModulesFromSet(nestedSet) + } + } + + // Process all module sets referenced by the product + // Use moduleSetWithOverrides.moduleSet directly - it already contains the full ModuleSet + for (moduleSetWithOverrides in spec.moduleSets) { + val moduleSet = moduleSetWithOverrides.moduleSet + referencedModuleSets.add(moduleSet.name) + collectModulesFromSet(moduleSet) + } + + // Add additional individual modules + for (module in spec.additionalModules) { + allModules.add(module.name) + } + + // Build product-scoped reachability: each module can see all other modules in the product + // This is different from module set validation where modules can only see within their set + val moduleToReachableModules = allModules.associateWith { allModules } + + return ProductModuleIndex( + productName = productName, + allModules = allModules, + moduleToReachableModules = moduleToReachableModules, + referencedModuleSets = referencedModuleSets, + ) +} + + diff --git a/platform/build-scripts/product-dsl/src/analysis/ModuleDependencyAnalysis.kt b/platform/build-scripts/product-dsl/src/analysis/ModuleDependencyAnalysis.kt new file mode 100644 index 000000000000..e3a15b984a3f --- /dev/null +++ b/platform/build-scripts/product-dsl/src/analysis/ModuleDependencyAnalysis.kt @@ -0,0 +1,336 @@ +// 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.productLayout.analysis + +import kotlinx.serialization.Serializable +import org.jetbrains.intellij.build.ModuleOutputProvider +import org.jetbrains.intellij.build.productLayout.buildModuleSetIndex +import org.jetbrains.intellij.build.productLayout.getProductionModuleDependencies + +/** + * Result of getting module dependencies from JPS model. + */ +@Serializable +internal data class ModuleDependenciesResult( + val moduleName: String, + val dependencies: List, + val transitiveDependencies: List? = null, + val error: String? = null +) + +/** + * Result of checking module reachability within a module set. + */ +@Serializable +internal data class ModuleReachabilityResult( + val moduleName: String, + val moduleSetName: String, + val satisfied: List, + val missing: List, + val error: String? = null +) + +/** + * Information about a missing dependency. + */ +@Serializable +internal data class MissingDependency( + val dependencyName: String, + val existsGlobally: Boolean, + val foundInModuleSets: List +) + +/** + * Result of finding a dependency path between two modules. + */ +@Serializable +internal data class DependencyPathResult( + val fromModule: String, + val toModule: String, + val path: List?, + val pathExists: Boolean, + val error: String? = null +) + +/** + * Gets direct JPS module dependencies for a given module. + * This queries the JPS model to find production runtime dependencies. + * + * @param moduleName The module name to query + * @param moduleOutputProvider Provider for accessing JPS modules + * @param includeTransitive If true, collects ALL transitive dependencies (BFS traversal) + * @return Module dependencies result with direct deps, and optionally transitive deps + */ +internal fun getModuleDependencies( + moduleName: String, + moduleOutputProvider: ModuleOutputProvider, + includeTransitive: Boolean = false +): ModuleDependenciesResult { + try { + val jpsModule = moduleOutputProvider.findModule(moduleName) + if (jpsModule == null) { + return ModuleDependenciesResult( + moduleName = moduleName, + dependencies = emptyList(), + transitiveDependencies = null, + error = "Module '$moduleName' not found in JPS model" + ) + } + + val dependencies = jpsModule.getProductionModuleDependencies() + .map { it.moduleReference.moduleName } + .toList() + + val transitiveDeps = if (includeTransitive) { + collectTransitiveDependencies(moduleName, moduleOutputProvider) + } else null + + return ModuleDependenciesResult( + moduleName = moduleName, + dependencies = dependencies, + transitiveDependencies = transitiveDeps + ) + } + catch (e: Exception) { + return ModuleDependenciesResult( + moduleName = moduleName, + dependencies = emptyList(), + transitiveDependencies = null, + error = "Failed to get dependencies: ${e.message}" + ) + } +} + +/** + * Collects ALL transitive dependencies of a module using BFS traversal. + * Returns a sorted list of all reachable modules (excluding the start module itself). + * + * @param moduleName Starting module + * @param moduleOutputProvider Provider for accessing JPS modules + * @return Sorted list of all transitive dependencies + */ +private fun collectTransitiveDependencies( + moduleName: String, + moduleOutputProvider: ModuleOutputProvider +): List { + val queue = ArrayDeque() + val visited = mutableSetOf() + val allDeps = mutableSetOf() + + queue.add(moduleName) + visited.add(moduleName) + + while (queue.isNotEmpty()) { + val current = queue.removeFirst() + + // Get dependencies of current module + val jpsModule = moduleOutputProvider.findModule(current) ?: continue + val dependencies = jpsModule.getProductionModuleDependencies() + .map { it.moduleReference.moduleName } + .toList() + + for (dependency in dependencies) { + // Add to result (exclude the starting module) + if (dependency != moduleName) { + allDeps.add(dependency) + } + + // Continue BFS if not visited + if (dependency !in visited) { + visited.add(dependency) + queue.add(dependency) + } + } + } + + return allDeps.sorted() +} + +/** + * Checks which dependencies of a module are reachable within a module set hierarchy. + * Uses the same reachability logic as the dependency validator to show which dependencies + * are satisfied and which are missing. + * + * @param moduleName The module to check + * @param moduleSetName The module set context + * @param allModuleSets All available module sets + * @param moduleOutputProvider Provider for accessing JPS modules + * @return Module reachability result + */ +internal fun checkModuleReachability( + moduleName: String, + moduleSetName: String, + allModuleSets: List, + moduleOutputProvider: ModuleOutputProvider +): ModuleReachabilityResult { + try { + // Get JPS dependencies + val dependenciesResult = getModuleDependencies(moduleName, moduleOutputProvider) + if (dependenciesResult.error != null) { + return ModuleReachabilityResult( + moduleName = moduleName, + moduleSetName = moduleSetName, + satisfied = emptyList(), + missing = emptyList(), + error = dependenciesResult.error + ) + } + + // Find the module set + val moduleSetEntry = allModuleSets.find { it.moduleSet.name == moduleSetName } + if (moduleSetEntry == null) { + return ModuleReachabilityResult( + moduleName = moduleName, + moduleSetName = moduleSetName, + satisfied = emptyList(), + missing = emptyList(), + error = "Module set '$moduleSetName' not found" + ) + } + + // Build reachability index + val index = buildModuleSetIndex(allModuleSets.map { it.moduleSet }) + + // Check if module is in this module set + val moduleDirectSets = index.moduleToDirectSets[moduleName] ?: emptySet() + if (moduleSetName !in moduleDirectSets) { + return ModuleReachabilityResult( + moduleName = moduleName, + moduleSetName = moduleSetName, + satisfied = emptyList(), + missing = emptyList(), + error = "Module '$moduleName' is not directly in module set '$moduleSetName'" + ) + } + + // Get reachable modules for this module + val reachableModules = index.moduleToReachableModules[moduleName] ?: emptySet() + + // Classify dependencies + val satisfied = mutableListOf() + val missing = mutableListOf() + + for (dependency in dependenciesResult.dependencies) { + if (dependency in reachableModules) { + satisfied.add(dependency) + } + else { + // Find which module sets contain this dependency + val containingModuleSets = index.moduleToDirectSets[dependency] ?: emptySet() + + missing.add(MissingDependency( + dependencyName = dependency, + existsGlobally = dependency in index.allModules, + foundInModuleSets = containingModuleSets.toList() + )) + } + } + + return ModuleReachabilityResult( + moduleName = moduleName, + moduleSetName = moduleSetName, + satisfied = satisfied, + missing = missing + ) + } + catch (e: Exception) { + return ModuleReachabilityResult( + moduleName = moduleName, + moduleSetName = moduleSetName, + satisfied = emptyList(), + missing = emptyList(), + error = "Failed to check reachability: ${e.message}" + ) + } +} + +/** + * Finds a transitive dependency path from one module to another. + * Uses BFS to find the shortest path through JPS module dependencies. + * + * @param fromModule Starting module + * @param toModule Target module + * @param moduleOutputProvider Provider for accessing JPS modules + * @return Dependency path result + */ +internal fun findDependencyPath( + fromModule: String, + toModule: String, + moduleOutputProvider: ModuleOutputProvider +): DependencyPathResult { + try { + // Check if both modules exist + if (moduleOutputProvider.findModule(fromModule) == null) { + return DependencyPathResult( + fromModule = fromModule, + toModule = toModule, + path = null, + pathExists = false, + error = "Module '$fromModule' not found in JPS model" + ) + } + + if (moduleOutputProvider.findModule(toModule) == null) { + return DependencyPathResult( + fromModule = fromModule, + toModule = toModule, + path = null, + pathExists = false, + error = "Module '$toModule' not found in JPS model" + ) + } + + // BFS to find shortest path + val queue = ArrayDeque>>() + val visited = mutableSetOf() + + queue.add(Pair(fromModule, listOf(fromModule))) + visited.add(fromModule) + + while (queue.isNotEmpty()) { + val (current, path) = queue.removeFirst() + + // Found target + if (current == toModule) { + return DependencyPathResult( + fromModule = fromModule, + toModule = toModule, + path = path, + pathExists = true + ) + } + + // Get dependencies of current module + val jpsModule = moduleOutputProvider.findModule(current) ?: continue + val dependencies = jpsModule.getProductionModuleDependencies() + .map { it.moduleReference.moduleName } + .toList() + + for (dependency in dependencies) { + if (dependency !in visited) { + visited.add(dependency) + val newPath = path + dependency + queue.add(Pair(dependency, newPath)) + } + } + } + + // No path found + return DependencyPathResult( + fromModule = fromModule, + toModule = toModule, + path = null, + pathExists = false + ) + } + catch (e: Exception) { + return DependencyPathResult( + fromModule = fromModule, + toModule = toModule, + path = null, + pathExists = false, + error = "Failed to find path: ${e.message}" + ) + } +} + + diff --git a/platform/build-scripts/product-dsl/src/analysis/ModulePathAnalysis.kt b/platform/build-scripts/product-dsl/src/analysis/ModulePathAnalysis.kt index b4052d82db2b..3c3fceb1dea5 100644 --- a/platform/build-scripts/product-dsl/src/analysis/ModulePathAnalysis.kt +++ b/platform/build-scripts/product-dsl/src/analysis/ModulePathAnalysis.kt @@ -13,7 +13,7 @@ import java.nio.file.Path * @param projectRoot Project root path for constructing file paths * @return Module paths result with all discovered paths */ -fun findModulePaths( +internal fun findModulePaths( moduleName: String, allModuleSets: List, products: List, diff --git a/platform/build-scripts/product-dsl/src/analysis/ModuleSetTraversal.kt b/platform/build-scripts/product-dsl/src/analysis/ModuleSetTraversal.kt new file mode 100644 index 000000000000..00b3c5c67bb5 --- /dev/null +++ b/platform/build-scripts/product-dsl/src/analysis/ModuleSetTraversal.kt @@ -0,0 +1,226 @@ +// 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.productLayout.analysis + +import org.jetbrains.intellij.build.productLayout.ModuleSet + +/** + * Unified utilities for traversing module set hierarchies. + * + * Provides BFS/DFS traversal with: + * - Proper cycle detection (throws error instead of silent failure) + * - Optional caching for repeated queries + * - Consistent API across all analysis functions + */ +object ModuleSetTraversal { + /** + * Collects all nested set names (direct + transitive) from a module set. + * + * Example: if `essential` includes `libraries`, and `libraries` includes `libraries.core`, + * returns `{"libraries", "libraries.core", ...}` for `essential`. + * + * @param startSetName Name of the module set to start from + * @param allModuleSets All module sets to search in + * @param cache Optional cache for repeated queries (key = set name, value = nested set names) + * @return Set of all nested set names (direct and transitive) + * @throws IllegalStateException if a cycle is detected + */ + fun collectAllNestedSets( + startSetName: String, + allModuleSets: List, + cache: MutableMap>? = null + ): Set { + // Check cache first + cache?.get(startSetName)?.let { return it } + + val result = mutableSetOf() + collectNestedSetsRecursive( + setName = startSetName, + allModuleSets = allModuleSets, + result = result, + visited = mutableSetOf(), + chain = mutableListOf() + ) + + // Store in cache + cache?.put(startSetName, result) + return result + } + + private fun collectNestedSetsRecursive( + setName: String, + allModuleSets: List, + result: MutableSet, + visited: MutableSet, + chain: MutableList + ) { + // Cycle detection: if setName is already in current chain, we have a cycle + if (setName in chain) { + error("Cycle detected in module set hierarchy: ${chain.joinToString(" → ")} → $setName") + } + + // Skip already fully processed sets + if (setName in visited) return + + chain.add(setName) + + val moduleSet = allModuleSets.firstOrNull { it.name == setName } + if (moduleSet != null) { + for (nestedSet in moduleSet.nestedSets) { + result.add(nestedSet.name) + collectNestedSetsRecursive(nestedSet.name, allModuleSets, result, visited, chain) + } + } + + chain.removeLast() + visited.add(setName) + } + + /** + * Builds an inclusion chain from a top-level set to a target set. + * + * Example: `buildInclusionChain("essential", "libraries.core", sets)` might return + * `["essential", "libraries", "libraries.core"]` + * + * @param fromSet Starting module set name + * @param toSet Target module set name + * @param allModuleSets All module sets to search in + * @return List representing the chain, or null if no path exists + */ + fun buildInclusionChain( + fromSet: String, + toSet: String, + allModuleSets: List + ): List? { + return buildChainRecursive(fromSet, toSet, allModuleSets, mutableSetOf()) + } + + private fun buildChainRecursive( + currentSet: String, + targetSet: String, + allModuleSets: List, + visited: MutableSet + ): List? { + if (currentSet in visited) return null + visited.add(currentSet) + + val moduleSet = allModuleSets.firstOrNull { it.name == currentSet } ?: return null + + // Check if target is directly nested + if (moduleSet.nestedSets.any { it.name == targetSet }) { + return listOf(currentSet, targetSet) + } + + // Recursively search through nested sets + for (nestedSet in moduleSet.nestedSets) { + val chain = buildChainRecursive(nestedSet.name, targetSet, allModuleSets, visited) + if (chain != null) { + return listOf(currentSet) + chain + } + } + + return null + } + + /** + * Collects all module names from a module set and its nested sets. + * Returns a Set (deduplicated). + * + * @param moduleSet The module set to collect from + * @param cache Optional cache for repeated queries + * @return Set of all module names + */ + fun collectAllModuleNames( + moduleSet: ModuleSet, + cache: MutableMap>? = null + ): Set { + cache?.get(moduleSet.name)?.let { return it } + + val result = mutableSetOf() + collectModulesRecursive(moduleSet, result) + + cache?.put(moduleSet.name, result) + return result + } + + private fun collectModulesRecursive(moduleSet: ModuleSet, result: MutableSet) { + for (module in moduleSet.modules) { + result.add(module.name) + } + for (nestedSet in moduleSet.nestedSets) { + collectModulesRecursive(nestedSet, result) + } + } + + /** + * Collects all module names into a List (preserving duplicates). + * Useful for detecting duplicate module declarations. + * + * @param moduleSet The module set to collect from + * @return List of all module names (may contain duplicates) + */ + fun collectAllModuleNamesAsList(moduleSet: ModuleSet): List { + val result = mutableListOf() + collectModulesIntoList(moduleSet, result) + return result + } + + private fun collectModulesIntoList(moduleSet: ModuleSet, result: MutableList) { + for (module in moduleSet.modules) { + result.add(module.name) + } + for (nestedSet in moduleSet.nestedSets) { + collectModulesIntoList(nestedSet, result) + } + } + + /** + * Checks if a module set (recursively) contains a specific module. + * + * @param moduleSet The module set to check + * @param moduleName The module name to find + * @return true if the module is in the set or any nested set + */ + fun containsModule(moduleSet: ModuleSet, moduleName: String): Boolean { + if (moduleSet.modules.any { it.name == moduleName }) { + return true + } + return moduleSet.nestedSets.any { containsModule(it, moduleName) } + } +} + +/** + * Simple cache for module set traversal results. + * Thread-safe for read operations, not for concurrent modifications. + */ +class TraversalCache { + private val nestedSetsCache = mutableMapOf>() + private val allModulesCache = mutableMapOf>() + + /** + * Gets or computes nested sets for a module set. + */ + fun getNestedSets(setName: String, compute: () -> Set): Set { + return nestedSetsCache.getOrPut(setName, compute) + } + + /** + * Gets or computes all modules for a module set. + */ + fun getAllModules(setName: String, compute: () -> Set): Set { + return allModulesCache.getOrPut(setName, compute) + } + + /** + * Clears all cached data. + */ + fun invalidate() { + nestedSetsCache.clear() + allModulesCache.clear() + } + + /** + * Returns the underlying cache maps for use with ModuleSetTraversal functions. + */ + fun nestedSetsCacheMap(): MutableMap> = nestedSetsCache + fun allModulesCacheMap(): MutableMap> = allModulesCache +} diff --git a/platform/build-scripts/product-dsl/src/analysis/SimilarityAnalysis.kt b/platform/build-scripts/product-dsl/src/analysis/SimilarityAnalysis.kt index 90122518a0c0..854189c51e3a 100644 --- a/platform/build-scripts/product-dsl/src/analysis/SimilarityAnalysis.kt +++ b/platform/build-scripts/product-dsl/src/analysis/SimilarityAnalysis.kt @@ -1,39 +1,6 @@ // 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.productLayout.analysis -import org.jetbrains.intellij.build.productLayout.ModuleSet - -/** - * Recursively collects all nested set names (direct + transitive) from a module set. - * - * For example, if essential includes libraries, and libraries includes libraries.core, - * this returns {"libraries", "libraries.core", ...} for essential. - * - * @param allModuleSets All module sets to search in - * @param startSetName The module set to start collecting from - * @param visited Set of already visited module sets to prevent infinite recursion - * @return Set of all nested set names (direct and transitive) - */ -fun collectAllNestedSetNames( - allModuleSets: List, - startSetName: String, - visited: MutableSet = mutableSetOf() -): Set { - if (visited.contains(startSetName)) return emptySet() - visited.add(startSetName) - - val startSet = allModuleSets.firstOrNull { it.name == startSetName } ?: return emptySet() - val result = mutableSetOf() - - for (nestedSet in startSet.nestedSets) { - result.add(nestedSet.name) - // Recursively collect nested sets from this nested set - result.addAll(collectAllNestedSetNames(allModuleSets, nestedSet.name, visited)) - } - - return result -} - /** * Detects overlapping or redundant module sets. * CRITICAL FIX: Filters out intentional nested set inclusions (e.g., libraries ⊃ libraries.core). @@ -44,7 +11,7 @@ fun collectAllNestedSetNames( * @param minOverlapPercent Minimum overlap percentage (0-100) to include in results * @return List of overlapping module set pairs sorted by overlap percentage (descending) */ -fun detectModuleSetOverlap( +internal fun detectModuleSetOverlap( allModuleSets: List, minOverlapPercent: Int = 50 ): List { @@ -63,8 +30,8 @@ fun detectModuleSetOverlap( // ✅ ENHANCED: Now checks TRANSITIVE relationships too! // Example: essential → libraries → libraries.core // This prevents false positive for "essential overlaps with libraries.core" - val ms1AllNestedSetNames = collectAllNestedSetNames(moduleSetsList, ms1.moduleSet.name) - val ms2AllNestedSetNames = collectAllNestedSetNames(moduleSetsList, ms2.moduleSet.name) + val ms1AllNestedSetNames = ModuleSetTraversal.collectAllNestedSets(ms1.moduleSet.name, moduleSetsList) + val ms2AllNestedSetNames = ModuleSetTraversal.collectAllNestedSets(ms2.moduleSet.name, moduleSetsList) if (ms1AllNestedSetNames.contains(ms2.moduleSet.name) || ms2AllNestedSetNames.contains(ms1.moduleSet.name)) { @@ -135,7 +102,7 @@ private fun generateOverlapRecommendation( * @param similarityThreshold Minimum similarity (0.0 to 1.0) to include in results * @return List of similar product pairs sorted by similarity (descending) */ -fun analyzeProductSimilarity( +internal fun analyzeProductSimilarity( products: List, similarityThreshold: Double = 0.7 ): List { diff --git a/platform/build-scripts/product-dsl/src/analysis/UnificationAnalysis.kt b/platform/build-scripts/product-dsl/src/analysis/UnificationAnalysis.kt index fa654705ccbf..fa2ce99bafa8 100644 --- a/platform/build-scripts/product-dsl/src/analysis/UnificationAnalysis.kt +++ b/platform/build-scripts/product-dsl/src/analysis/UnificationAnalysis.kt @@ -1,8 +1,6 @@ // 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.productLayout.analysis -import org.jetbrains.intellij.build.productLayout.collectAllModuleNames - /** * Suggests module set unification opportunities based on overlap, similarity, and usage patterns. * @@ -20,7 +18,7 @@ import org.jetbrains.intellij.build.productLayout.collectAllModuleNames * @param strategy Filter by strategy: "merge", "inline", "factor", "split", or "all" * @return List of suggestions sorted by priority */ -fun suggestModuleSetUnification( +internal fun suggestModuleSetUnification( allModuleSets: List, products: List, overlaps: List, @@ -44,9 +42,9 @@ fun suggestModuleSetUnification( products = null, sharedModuleSets = null, reason = overlap.recommendation, - impact = mapOf( - "moduleSetsSaved" to 1, - "overlapPercent" to overlap.overlapPercent + impact = UnificationImpact( + moduleSetsSaved = 1, + overlapPercent = overlap.overlapPercent ) )) } else if (overlap.overlapPercent >= 80) { @@ -60,7 +58,7 @@ fun suggestModuleSetUnification( products = null, sharedModuleSets = null, reason = overlap.recommendation, - impact = mapOf("overlapPercent" to overlap.overlapPercent) + impact = UnificationImpact(overlapPercent = overlap.overlapPercent) )) } } @@ -74,7 +72,7 @@ fun suggestModuleSetUnification( } // Use total module count (including nested sets) for inline candidate detection - val totalModuleCount = collectAllModuleNames(msEntry.moduleSet).size + val totalModuleCount = ModuleSetTraversal.collectAllModuleNames(msEntry.moduleSet).size if (usedByProducts.size <= 1 && totalModuleCount <= 5) { suggestions.add(UnificationSuggestion( priority = "low", @@ -86,10 +84,10 @@ fun suggestModuleSetUnification( products = null, sharedModuleSets = null, reason = "Used by only ${usedByProducts.size} product(s) and contains only $totalModuleCount modules. Consider inlining into the product directly.", - impact = mapOf( - "moduleSetsSaved" to 1, - "moduleCount" to totalModuleCount, - "affectedProducts" to usedByProducts.map { it.name } + impact = UnificationImpact( + moduleSetsSaved = 1, + moduleCount = totalModuleCount, + affectedProducts = usedByProducts.map { it.name } ) )) } @@ -110,9 +108,9 @@ fun suggestModuleSetUnification( products = listOf(pair.product1, pair.product2), sharedModuleSets = pair.sharedModuleSets, reason = "Products ${pair.product1} and ${pair.product2} share ${pair.sharedModuleSets.size} module sets (${(pair.similarity * 100).toInt()}% similarity). Consider creating a common base.", - impact = mapOf( - "similarity" to pair.similarity, - "sharedModuleSets" to pair.sharedModuleSets.size + impact = UnificationImpact( + similarity = pair.similarity, + sharedModuleSets = pair.sharedModuleSets.size ) )) } @@ -123,7 +121,7 @@ fun suggestModuleSetUnification( if (strategy == "split" || strategy == "all") { for (msEntry in allModuleSets) { // Use total module count (including nested sets) for split suggestions - val totalModuleCount = collectAllModuleNames(msEntry.moduleSet).size + val totalModuleCount = ModuleSetTraversal.collectAllModuleNames(msEntry.moduleSet).size if (totalModuleCount > 200) { suggestions.add(UnificationSuggestion( priority = "low", @@ -135,7 +133,7 @@ fun suggestModuleSetUnification( products = null, sharedModuleSets = null, reason = "Module set contains $totalModuleCount modules. Consider splitting into smaller, more focused sets for better maintainability.", - impact = mapOf("moduleCount" to totalModuleCount) + impact = UnificationImpact(moduleCount = totalModuleCount) )) } } @@ -175,42 +173,104 @@ fun findProductsUsingModuleSet( } } +/** + * Analyzes which products use a specific module set, distinguishing direct from indirect usage. + * Direct usage = product directly references the module set in its top-level configuration + * Indirect usage = product includes another module set that nests the target module set + * + * @param moduleSetName Name of the module set to analyze + * @param products All products + * @param allModuleSets All module sets with metadata (includes directNestedSets) + * @return Analysis result with direct/indirect usage and inclusion chains + */ +internal fun analyzeProductUsage( + moduleSetName: String, + products: List, + allModuleSets: List +): ProductUsageAnalysis { + val moduleSetsList = allModuleSets.map { it.moduleSet } + val directUsage = mutableListOf() + val indirectUsage = mutableListOf() + + // Cache for nested sets to avoid repeated traversals + val nestedSetsCache = mutableMapOf>() + + for (product in products) { + val topLevelSets = product.contentSpec?.moduleSets?.map { it.moduleSet.name } ?: emptyList() + + // Check if product directly references the target module set + if (topLevelSets.contains(moduleSetName)) { + directUsage.add(ProductUsageEntry( + product = product.name, + usageType = "direct", + inclusionChain = null + )) + } else { + // Check if any top-level set transitively includes the target + for (topLevelSet in topLevelSets) { + val allNested = ModuleSetTraversal.collectAllNestedSets(topLevelSet, moduleSetsList, nestedSetsCache) + if (allNested.contains(moduleSetName)) { + // Build the inclusion chain + val chain = ModuleSetTraversal.buildInclusionChain(topLevelSet, moduleSetName, moduleSetsList) + indirectUsage.add(ProductUsageEntry( + product = product.name, + usageType = "indirect", + inclusionChain = chain + )) + break // Only record once per product + } + } + } + } + + return ProductUsageAnalysis( + moduleSet = moduleSetName, + directUsage = directUsage, + indirectUsage = indirectUsage, + totalProducts = directUsage.size + indirectUsage.size + ) +} + /** * Analyzes the impact of merging, moving, or inlining module sets. * Checks for violations, calculates size impact, and provides recommendations. * * @param sourceSet Source module set name * @param targetSet Target module set name (null for inline operation) - * @param operation Operation type: - * - "merge" or "move": Combine source modules into target (treated identically - - * both validate architectural constraints and analyze impact of combining sets) - * - "inline": Remove module set, add modules directly to products + * @param operation Operation type (MERGE, MOVE, or INLINE) * @param allModuleSets All module sets with metadata * @param products All products * @return Impact analysis result with violations, size metrics, and recommendation */ -fun analyzeMergeImpact( +internal fun analyzeMergeImpact( sourceSet: String, targetSet: String?, - operation: String, + operation: MergeOperation, allModuleSets: List, products: List ): MergeImpactResult { + val operationStr = operation.name.lowercase() // Find source module set val sourceEntry = allModuleSets.firstOrNull { it.moduleSet.name == sourceSet } if (sourceEntry == null) { return MergeImpactResult( - operation = operation, + operation = operationStr, sourceSet = sourceSet, targetSet = targetSet, productsUsingSource = emptyList(), productsUsingTarget = emptyList(), productsThatWouldChange = emptyList(), - sizeImpact = emptyMap(), - violations = listOf(mapOf( - "type" to "notFound", - "severity" to "error", - "message" to "Source module set '$sourceSet' not found" + sizeImpact = SizeImpact( + sourceModuleCount = 0, + targetModuleCount = 0, + newModulesToTarget = 0, + duplicateModules = 0, + resultingModuleCount = 0 + ), + violations = listOf(MergeViolation( + type = "notFound", + severity = "error", + message = "Source module set '$sourceSet' not found" )), recommendation = "ERROR: Cannot analyze - source module set not found", safe = false @@ -223,17 +283,23 @@ fun analyzeMergeImpact( targetEntry = allModuleSets.firstOrNull { it.moduleSet.name == targetSet } if (targetEntry == null) { return MergeImpactResult( - operation = operation, + operation = operationStr, sourceSet = sourceSet, targetSet = targetSet, productsUsingSource = emptyList(), productsUsingTarget = emptyList(), productsThatWouldChange = emptyList(), - sizeImpact = emptyMap(), - violations = listOf(mapOf( - "type" to "notFound", - "severity" to "error", - "message" to "Target module set '$targetSet' not found" + sizeImpact = SizeImpact( + sourceModuleCount = 0, + targetModuleCount = 0, + newModulesToTarget = 0, + duplicateModules = 0, + resultingModuleCount = 0 + ), + violations = listOf(MergeViolation( + type = "notFound", + severity = "error", + message = "Target module set '$targetSet' not found" )), recommendation = "ERROR: Cannot analyze - target module set not found", safe = false @@ -244,17 +310,23 @@ fun analyzeMergeImpact( // Validate that source and target are different if (targetSet != null && sourceSet == targetSet) { return MergeImpactResult( - operation = operation, + operation = operationStr, sourceSet = sourceSet, targetSet = targetSet, productsUsingSource = emptyList(), productsUsingTarget = emptyList(), productsThatWouldChange = emptyList(), - sizeImpact = emptyMap(), - violations = listOf(mapOf( - "type" to "validation", - "severity" to "error", - "message" to "Source and target cannot be the same module set: '$sourceSet'" + sizeImpact = SizeImpact( + sourceModuleCount = 0, + targetModuleCount = 0, + newModulesToTarget = 0, + duplicateModules = 0, + resultingModuleCount = 0 + ), + violations = listOf(MergeViolation( + type = "validation", + severity = "error", + message = "Source and target cannot be the same module set: '$sourceSet'" )), recommendation = "ERROR: Source and target must be different module sets", safe = false @@ -271,10 +343,10 @@ fun analyzeMergeImpact( emptyList() } - // Calculate module changes (use collectAllModuleNames to include nested sets) - val sourceModules = collectAllModuleNames(sourceEntry.moduleSet) + // Calculate module changes (use ModuleSetTraversal.collectAllModuleNames to include nested sets) + val sourceModules = ModuleSetTraversal.collectAllModuleNames(sourceEntry.moduleSet) val targetModules = if (targetEntry != null) { - collectAllModuleNames(targetEntry.moduleSet) + ModuleSetTraversal.collectAllModuleNames(targetEntry.moduleSet) } else { emptySet() } @@ -283,17 +355,17 @@ fun analyzeMergeImpact( val duplicateModules = sourceModules.intersect(targetModules) // Check for community/ultimate violations - val violations = mutableListOf>() - if (operation == "merge" && targetEntry != null) { + val violations = mutableListOf() + if (operation == MergeOperation.MERGE && targetEntry != null) { val sourceLocation = sourceEntry.location val targetLocation = targetEntry.location if (sourceLocation == "ultimate" && targetLocation == "community") { - violations.add(mapOf( - "type" to "location", - "severity" to "error", - "message" to "Cannot merge ultimate module set \"$sourceSet\" into community module set \"$targetSet\"", - "fix" to "Move \"$targetSet\" to ultimate directory, or extract community modules from \"$sourceSet\"" + violations.add(MergeViolation( + type = "location", + severity = "error", + message = "Cannot merge ultimate module set \"$sourceSet\" into community module set \"$targetSet\"", + fix = "Move \"$targetSet\" to ultimate directory, or extract community modules from \"$sourceSet\"" )) } @@ -304,44 +376,44 @@ fun analyzeMergeImpact( } if (sourceLocation == "ultimate" && communityProductsUsingTarget.isNotEmpty()) { - violations.add(mapOf( - "type" to "community-uses-ultimate", - "severity" to "error", - "message" to "Merging ultimate set \"$sourceSet\" into \"$targetSet\" would expose ultimate modules to ${communityProductsUsingTarget.size} community products", - "affectedProducts" to communityProductsUsingTarget.map { it.name }, - "fix" to "Remove \"$targetSet\" from community products, or split ultimate modules from \"$sourceSet\"" + violations.add(MergeViolation( + type = "community-uses-ultimate", + severity = "error", + message = "Merging ultimate set \"$sourceSet\" into \"$targetSet\" would expose ultimate modules to ${communityProductsUsingTarget.size} community products", + affectedProducts = communityProductsUsingTarget.map { it.name }, + fix = "Remove \"$targetSet\" from community products, or split ultimate modules from \"$sourceSet\"" )) } } // Calculate size impact - val sizeImpact = mapOf( - "sourceModuleCount" to sourceModules.size, - "targetModuleCount" to targetModules.size, - "newModulesToTarget" to newModules.size, - "duplicateModules" to duplicateModules.size, - "resultingModuleCount" to targetModules.size + newModules.size + val sizeImpact = SizeImpact( + sourceModuleCount = sourceModules.size, + targetModuleCount = targetModules.size, + newModulesToTarget = newModules.size, + duplicateModules = duplicateModules.size, + resultingModuleCount = targetModules.size + newModules.size ) // Generate recommendation val recommendation = when { violations.isNotEmpty() -> "NOT RECOMMENDED: Operation would introduce violations. See violations for details." - operation == "merge" && duplicateModules.isNotEmpty() -> + operation == MergeOperation.MERGE && duplicateModules.isNotEmpty() -> "CAUTION: ${duplicateModules.size} modules already exist in target. Merge would create no duplicates, but review if modules serve the same purpose." - operation == "merge" && newModules.isNotEmpty() -> + operation == MergeOperation.MERGE && newModules.isNotEmpty() -> "SAFE TO MERGE: Would add ${newModules.size} new modules to \"$targetSet\". ${productsUsingTarget.size} products using target would gain these modules." - operation == "inline" -> + operation == MergeOperation.INLINE -> "SAFE TO INLINE: ${productsUsingSource.size} products using \"$sourceSet\" would directly include ${sourceModules.size} modules instead." else -> "Operation appears safe based on current analysis." } return MergeImpactResult( - operation = operation, + operation = operationStr, sourceSet = sourceSet, targetSet = targetSet, productsUsingSource = productsUsingSource.map { it.name }, productsUsingTarget = productsUsingTarget.map { it.name }, - productsThatWouldChange = if (operation == "merge") { + productsThatWouldChange = if (operation == MergeOperation.MERGE) { productsUsingTarget.map { it.name } } else { productsUsingSource.map { it.name } diff --git a/platform/build-scripts/product-dsl/src/analysis/ValidationAnalysis.kt b/platform/build-scripts/product-dsl/src/analysis/ValidationAnalysis.kt index 5823c3279e32..5122ae45178e 100644 --- a/platform/build-scripts/product-dsl/src/analysis/ValidationAnalysis.kt +++ b/platform/build-scripts/product-dsl/src/analysis/ValidationAnalysis.kt @@ -11,13 +11,16 @@ import java.nio.file.Path * This information is used by validation functions to ensure architectural constraints. * * @param projectRoot Absolute path to the project root directory - * @return Map of module name to ModuleLocationInfo + * @return ParseResult containing map of module name to ModuleLocationInfo, or failure with error details */ -fun parseModulesXml(projectRoot: Path): Map { +internal fun parseModulesXml(projectRoot: Path): ParseResult> { val modulesXmlPath = projectRoot.resolve(".idea/modules.xml") if (!Files.exists(modulesXmlPath)) { - return emptyMap() + return ParseResult.Failure( + error = "File not found: $modulesXmlPath", + partial = emptyMap() + ) } val modules = mutableMapOf() @@ -29,7 +32,13 @@ fun parseModulesXml(projectRoot: Path): Map { val projectModuleManager = document.getChildren("component") .find { it.getAttributeValue("name") == "ProjectModuleManager" } - val modulesParent = projectModuleManager?.getChild("modules") ?: return emptyMap() + val modulesParent = projectModuleManager?.getChild("modules") + if (modulesParent == null) { + return ParseResult.Failure( + error = "No element found in .idea/modules.xml", + partial = emptyMap() + ) + } for (moduleElement in modulesParent.getChildren("module")) { var filepath = moduleElement.getAttributeValue("filepath") ?: continue @@ -49,13 +58,16 @@ fun parseModulesXml(projectRoot: Path): Map { modules[moduleName] = ModuleLocationInfo(location, filepath) } + + return ParseResult.Success(modules) } catch (e: Exception) { - // If parsing fails, return empty map (validation will report as unknown) System.err.println("Warning: Failed to parse .idea/modules.xml: ${e.message}") + return ParseResult.Failure( + error = "Failed to parse .idea/modules.xml: ${e.message}", + partial = modules // Return any modules parsed before failure + ) } - - return modules } /** @@ -68,7 +80,7 @@ fun parseModulesXml(projectRoot: Path): Map { * @param projectRoot Project root path for constructing file paths * @return List of violations */ -fun validateCommunityProducts( +internal fun validateCommunityProducts( products: List, allModuleSets: List, moduleLocations: Map, @@ -136,7 +148,7 @@ fun validateCommunityProducts( * @param projectRoot Project root path for constructing file paths * @return List of violations */ -fun validateModuleSetLocations( +internal fun validateModuleSetLocations( allModuleSets: List, moduleLocations: Map, projectRoot: Path diff --git a/platform/build-scripts/product-dsl/src/generator.kt b/platform/build-scripts/product-dsl/src/generator.kt index 3ca9a258dac8..9bda5b487501 100644 --- a/platform/build-scripts/product-dsl/src/generator.kt +++ b/platform/build-scripts/product-dsl/src/generator.kt @@ -29,7 +29,7 @@ import java.nio.file.Path * @param label Description label ("community" or "ultimate") for header generation * @return Result containing file status and statistics */ -fun generateModuleSetXml(moduleSet: ModuleSet, outputDir: Path, label: String): ModuleSetFileResult { +internal fun generateModuleSetXml(moduleSet: ModuleSet, outputDir: Path, label: String): ModuleSetFileResult { val fileName = "${MODULE_SET_PREFIX}${moduleSet.name}.xml" val outputPath = outputDir.resolve(fileName) @@ -44,6 +44,7 @@ fun generateModuleSetXml(moduleSet: ModuleSet, outputDir: Path, label: String): // Only write if changed if (status != FileChangeStatus.UNCHANGED) { + Files.createDirectories(outputPath.parent) Files.writeString(outputPath, buildResult.xml) } @@ -84,14 +85,7 @@ fun generateProductXml( // Compare with existing file if it exists val originalContent = Files.readString(pluginXmlPath) - val status = if (originalContent == buildResult.xml) { - FileChangeStatus.UNCHANGED - } - else { - FileChangeStatus.MODIFIED - } - - // Only write if changed + val status = if (originalContent == buildResult.xml) FileChangeStatus.UNCHANGED else FileChangeStatus.MODIFIED if (status != FileChangeStatus.UNCHANGED) { Files.writeString(pluginXmlPath, buildResult.xml) } diff --git a/platform/build-scripts/product-dsl/src/generatorStats.kt b/platform/build-scripts/product-dsl/src/generatorStats.kt index 57abaa0b0818..14512e8a4b5f 100644 --- a/platform/build-scripts/product-dsl/src/generatorStats.kt +++ b/platform/build-scripts/product-dsl/src/generatorStats.kt @@ -12,7 +12,9 @@ enum class FileChangeStatus { /** File content was modified */ MODIFIED, /** File content unchanged */ - UNCHANGED + UNCHANGED, + /** File was deleted (obsolete) */ + DELETED } /** @@ -20,11 +22,11 @@ enum class FileChangeStatus { */ data class ModuleSetFileResult( /** File name (e.g., "intellij.moduleSets.essential.xml") */ - val fileName: String, + @JvmField val fileName: String, /** Change status of the file */ - val status: FileChangeStatus, + @JvmField val status: FileChangeStatus, /** Number of direct modules in this set (excluding nested) */ - val moduleCount: Int, + @JvmField val moduleCount: Int, ) /** @@ -37,10 +39,13 @@ data class ModuleSetGenerationResult( val outputDir: Path, /** Results for individual files */ val files: List, + /** Tracking map: directory -> set of generated file names (used for cleanup aggregation) */ + val trackingMap: Map> = emptyMap(), ) { val createdCount: Int get() = files.count { it.status == FileChangeStatus.CREATED } val modifiedCount: Int get() = files.count { it.status == FileChangeStatus.MODIFIED } val unchangedCount: Int get() = files.count { it.status == FileChangeStatus.UNCHANGED } + val deletedCount: Int get() = files.count { it.status == FileChangeStatus.DELETED } val totalModules: Int get() = files.sumOf { it.moduleCount } } @@ -66,124 +71,217 @@ data class ProductFileResult( * Result of generating all product XML files. */ data class ProductGenerationResult( - val products: List, + @JvmField val products: List, ) { - val createdCount: Int get() = products.count { it.status == FileChangeStatus.CREATED } - val modifiedCount: Int get() = products.count { it.status == FileChangeStatus.MODIFIED } - val unchangedCount: Int get() = products.count { it.status == FileChangeStatus.UNCHANGED } + val createdCount: Int + get() = products.count { it.status == FileChangeStatus.CREATED } + val modifiedCount: Int + get() = products.count { it.status == FileChangeStatus.MODIFIED } + val unchangedCount: Int + get() = products.count { it.status == FileChangeStatus.UNCHANGED } } -// ANSI color codes -private const val RESET = "\u001B[0m" -private const val BOLD = "\u001B[1m" -private const val GREEN = "\u001B[32m" -private const val YELLOW = "\u001B[33m" -private const val BLUE = "\u001B[34m" -private const val CYAN = "\u001B[36m" -private const val GRAY = "\u001B[90m" +/** + * Result of generating a single module descriptor dependency file. + */ +data class DependencyFileResult( + /** Module name (e.g., "intellij.platform.core.ui") */ + val moduleName: String, + /** Absolute path to the descriptor file */ + val descriptorPath: Path, + /** Change status of the file */ + val status: FileChangeStatus, + /** Number of dependencies added */ + val dependencyCount: Int, +) +/** + * Result of generating all module descriptor dependencies. + */ +data class DependencyGenerationResult( + val files: List, +) { + val createdCount: Int get() = files.count { it.status == FileChangeStatus.CREATED } + val modifiedCount: Int get() = files.count { it.status == FileChangeStatus.MODIFIED } + val unchangedCount: Int get() = files.count { it.status == FileChangeStatus.UNCHANGED } + val totalDependencies: Int get() = files.sumOf { it.dependencyCount } +} + +/** + * Combined results from all generation operations. + * Used to collect parallel generation results before printing summary. + */ +data class GenerationResults( + val moduleSetResults: List, + val dependencyResult: DependencyGenerationResult, + val productResult: ProductGenerationResult +) + +// ANSI color codes /** * Formats file change status to colored icon and text representation. * @return Pair of (coloredStatusIcon, statusText) */ private fun formatFileStatus(status: FileChangeStatus): Pair { return when (status) { - FileChangeStatus.CREATED -> "${YELLOW}+${RESET}" to "${YELLOW}created${RESET}" - FileChangeStatus.MODIFIED -> "${BLUE}✓${RESET}" to "${BLUE}modified${RESET}" - FileChangeStatus.UNCHANGED -> "${GRAY}•${RESET}" to "${GRAY}unchanged${RESET}" + FileChangeStatus.CREATED -> "${AnsiColors.YELLOW}+${AnsiColors.RESET}" to "${AnsiColors.YELLOW}created${AnsiColors.RESET}" + FileChangeStatus.MODIFIED -> "${AnsiColors.BLUE}✓${AnsiColors.RESET}" to "${AnsiColors.BLUE}modified${AnsiColors.RESET}" + FileChangeStatus.UNCHANGED -> "${AnsiColors.GRAY}•${AnsiColors.RESET}" to "${AnsiColors.GRAY}unchanged${AnsiColors.RESET}" + FileChangeStatus.DELETED -> "${AnsiColors.RED}-${AnsiColors.RESET}" to "${AnsiColors.RED}deleted${AnsiColors.RESET}" } } /** * Builds a colored summary string showing file change counts. - * @return Formatted string like "2 created, 5 modified, 10 unchanged" + * @return Formatted string like "2 created, 5 modified, 10 unchanged, 1 deleted" */ -private fun buildChangesSummary(createdCount: Int, modifiedCount: Int, unchangedCount: Int): String { +private fun buildChangesSummary(createdCount: Int, modifiedCount: Int, unchangedCount: Int, deletedCount: Int = 0): String { return buildList { - if (createdCount > 0) add("${YELLOW}$createdCount created${RESET}") - if (modifiedCount > 0) add("${BLUE}$modifiedCount modified${RESET}") - if (unchangedCount > 0) add("${GRAY}$unchangedCount unchanged${RESET}") + if (createdCount > 0) add("${AnsiColors.YELLOW}$createdCount created${AnsiColors.RESET}") + if (modifiedCount > 0) add("${AnsiColors.BLUE}$modifiedCount modified${AnsiColors.RESET}") + if (unchangedCount > 0) add("${AnsiColors.GRAY}$unchangedCount unchanged${AnsiColors.RESET}") + if (deletedCount > 0) add("${AnsiColors.RED}$deletedCount deleted${AnsiColors.RESET}") }.joinToString(", ") } +private const val SEPARATOR = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +/** + * Prints a section header with separator lines. + */ +private fun printSectionHeader(title: String) { + println("${AnsiColors.CYAN}${AnsiColors.BOLD}$SEPARATOR${AnsiColors.RESET}") + println("${AnsiColors.CYAN}${AnsiColors.BOLD}$title${AnsiColors.RESET}") + println("${AnsiColors.CYAN}${AnsiColors.BOLD}$SEPARATOR${AnsiColors.RESET}") +} + +/** + * Pluralizes "file" based on count. + */ +private fun fileWord(count: Int): String = if (count == 1) "file" else "files" + /** * Prints a formatted summary of generation results with colors. */ fun printGenerationSummary( moduleSetResults: List, + dependencyResult: DependencyGenerationResult?, productResult: ProductGenerationResult?, + projectRoot: Path, durationMs: Long ) { println() - println("${CYAN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}") - println("${CYAN}${BOLD}Module Sets${RESET}") - println("${CYAN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}") + printModuleSetsSummary(moduleSetResults) + printDependenciesSummary(dependencyResult, projectRoot) + printProductsSummary(productResult) + printOverallSummary(moduleSetResults, dependencyResult, productResult, durationMs) +} + +/** + * Prints module sets section with per-label breakdown. + */ +private fun printModuleSetsSummary(moduleSetResults: List) { + printSectionHeader("Module Sets") for (result in moduleSetResults) { val relativeDir = result.outputDir.toString().replace(System.getProperty("user.home"), "~") - println("${BOLD}${result.label.replaceFirstChar { it.uppercase() }}${RESET} ${GRAY}($relativeDir)${RESET}") + println("${AnsiColors.BOLD}${result.label.replaceFirstChar { it.uppercase() }}${AnsiColors.RESET} ${AnsiColors.GRAY}($relativeDir)${AnsiColors.RESET}") - // Show changed files + // Show changed files (up to 5) val changedFiles = result.files.filter { it.status != FileChangeStatus.UNCHANGED } for (file in changedFiles.take(5)) { val (statusIcon, statusText) = formatFileStatus(file.status) - println(" $statusIcon ${file.fileName} ($statusText, ${BOLD}${file.moduleCount}${RESET} modules)") + println(" $statusIcon ${file.fileName} ($statusText, ${AnsiColors.BOLD}${file.moduleCount}${AnsiColors.RESET} modules)") } - // Show summary if there are more files - val unchangedCount = result.unchangedCount - if (unchangedCount > 0) { - val fileWord = if (unchangedCount == 1) "file" else "files" - println(" ${GRAY}• $unchangedCount $fileWord unchanged${RESET}") + if (result.unchangedCount > 0) { + println(" ${AnsiColors.GRAY}• ${result.unchangedCount} ${fileWord(result.unchangedCount)} unchanged${AnsiColors.RESET}") } - val totalFiles = result.files.size - val changesSummary = buildChangesSummary(result.createdCount, result.modifiedCount, unchangedCount) - - println(" ${BOLD}Total:${RESET} $totalFiles files ($changesSummary), ${BOLD}${result.totalModules}${RESET} modules") + val changesSummary = buildChangesSummary(result.createdCount, result.modifiedCount, result.unchangedCount, result.deletedCount) + println(" ${AnsiColors.BOLD}Total:${AnsiColors.RESET} ${result.files.size} files ($changesSummary), ${AnsiColors.BOLD}${result.totalModules}${AnsiColors.RESET} modules") println() } +} - if (productResult != null && productResult.products.isNotEmpty()) { - println("${CYAN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}") - println("${CYAN}${BOLD}Products${RESET}") - println("${CYAN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}") +/** + * Prints module dependencies section. + */ +private fun printDependenciesSummary(dependencyResult: DependencyGenerationResult?, projectRoot: Path) { + if (dependencyResult == null || dependencyResult.files.isEmpty()) return - for (product in productResult.products) { - val (statusIcon, statusText) = formatFileStatus(product.status) - println("$statusIcon ${BOLD}${product.productName}${RESET} ${GRAY}(${product.relativePath})${RESET}") - println(" Status: $statusText") - println(" Content: ${BOLD}${product.includeCount}${RESET} xi:includes, ${BOLD}${product.contentBlockCount}${RESET} content blocks, ${BOLD}${product.totalModules}${RESET} modules") - } + printSectionHeader("Module Dependencies") - // Show summary with breakdown - val changesSummary = buildChangesSummary(productResult.createdCount, productResult.modifiedCount, productResult.unchangedCount) - - val productFileWord = if (productResult.products.size == 1) "file" else "files" - println(" ${BOLD}Total:${RESET} ${productResult.products.size} $productFileWord ($changesSummary)") - println() + // Show changed files (up to 10) + val changedFiles = dependencyResult.files.filter { it.status != FileChangeStatus.UNCHANGED } + for (file in changedFiles.take(10)) { + val (statusIcon, statusText) = formatFileStatus(file.status) + val relativePath = projectRoot.relativize(file.descriptorPath) + println(" $statusIcon ${AnsiColors.BOLD}${file.moduleName}${AnsiColors.RESET} ${AnsiColors.GRAY}($relativePath)${AnsiColors.RESET}") + println(" Status: $statusText, ${AnsiColors.BOLD}${file.dependencyCount}${AnsiColors.RESET} dependencies") } - // Overall summary - val totalModuleSetFiles = moduleSetResults.sumOf { it.files.size } - val totalModuleSetCreated = moduleSetResults.sumOf { it.createdCount } - val totalModuleSetModified = moduleSetResults.sumOf { it.modifiedCount } - val totalModuleSetUnchanged = moduleSetResults.sumOf { it.unchangedCount } + if (dependencyResult.unchangedCount > 0) { + println(" ${AnsiColors.GRAY}• ${dependencyResult.unchangedCount} ${fileWord(dependencyResult.unchangedCount)} unchanged${AnsiColors.RESET}") + } - println("${CYAN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}") - println("${CYAN}${BOLD}Summary${RESET}") - println("${CYAN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}") + val changesSummary = buildChangesSummary(dependencyResult.createdCount, dependencyResult.modifiedCount, dependencyResult.unchangedCount) + println(" ${AnsiColors.BOLD}Total:${AnsiColors.RESET} ${dependencyResult.files.size} ${fileWord(dependencyResult.files.size)} ($changesSummary), ${AnsiColors.BOLD}${dependencyResult.totalDependencies}${AnsiColors.RESET} dependencies") + println() +} - val moduleSetFileWord = if (totalModuleSetFiles == 1) "file" else "files" - val moduleSetSummary = buildChangesSummary(totalModuleSetCreated, totalModuleSetModified, totalModuleSetUnchanged) - println("${GREEN}✓${RESET} ${BOLD}$totalModuleSetFiles${RESET} module set $moduleSetFileWord ($moduleSetSummary)") +/** + * Prints products section. + */ +private fun printProductsSummary(productResult: ProductGenerationResult?) { + if (productResult == null || productResult.products.isEmpty()) return + printSectionHeader("Products") + + for (product in productResult.products) { + val (statusIcon, statusText) = formatFileStatus(product.status) + println("$statusIcon ${AnsiColors.BOLD}${product.productName}${AnsiColors.RESET} ${AnsiColors.GRAY}(${product.relativePath})${AnsiColors.RESET}") + println(" Status: $statusText") + println(" Content: ${AnsiColors.BOLD}${product.includeCount}${AnsiColors.RESET} xi:includes, ${AnsiColors.BOLD}${product.contentBlockCount}${AnsiColors.RESET} content blocks, ${AnsiColors.BOLD}${product.totalModules}${AnsiColors.RESET} modules") + } + + val changesSummary = buildChangesSummary(productResult.createdCount, productResult.modifiedCount, productResult.unchangedCount) + println(" ${AnsiColors.BOLD}Total:${AnsiColors.RESET} ${productResult.products.size} ${fileWord(productResult.products.size)} ($changesSummary)") + println() +} + +/** + * Prints overall summary with totals and timing. + */ +private fun printOverallSummary( + moduleSetResults: List, + dependencyResult: DependencyGenerationResult?, + productResult: ProductGenerationResult?, + durationMs: Long +) { + printSectionHeader("Summary") + + // Module sets total + val totalFiles = moduleSetResults.sumOf { it.files.size } + val totalCreated = moduleSetResults.sumOf { it.createdCount } + val totalModified = moduleSetResults.sumOf { it.modifiedCount } + val totalUnchanged = moduleSetResults.sumOf { it.unchangedCount } + val totalDeleted = moduleSetResults.sumOf { it.deletedCount } + val moduleSetSummary = buildChangesSummary(totalCreated, totalModified, totalUnchanged, totalDeleted) + println("${AnsiColors.GREEN}✓${AnsiColors.RESET} ${AnsiColors.BOLD}$totalFiles${AnsiColors.RESET} module set ${fileWord(totalFiles)} ($moduleSetSummary)") + + // Dependencies total + if (dependencyResult != null && dependencyResult.files.isNotEmpty()) { + val depSummary = buildChangesSummary(dependencyResult.createdCount, dependencyResult.modifiedCount, dependencyResult.unchangedCount) + println("${AnsiColors.GREEN}✓${AnsiColors.RESET} ${AnsiColors.BOLD}${dependencyResult.files.size}${AnsiColors.RESET} dependency ${fileWord(dependencyResult.files.size)} ($depSummary)") + } + + // Products total if (productResult != null) { - val productFileWord = if (productResult.products.size == 1) "file" else "files" - val productSummary = buildChangesSummary(productResult.createdCount, productResult.modifiedCount, productResult.unchangedCount) - println("${GREEN}✓${RESET} ${BOLD}${productResult.products.size}${RESET} product $productFileWord ($productSummary)") + val prodSummary = buildChangesSummary(productResult.createdCount, productResult.modifiedCount, productResult.unchangedCount) + println("${AnsiColors.GREEN}✓${AnsiColors.RESET} ${AnsiColors.BOLD}${productResult.products.size}${AnsiColors.RESET} product ${fileWord(productResult.products.size)} ($prodSummary)") } - println("${GREEN}⏱${RESET} Completed in ${BOLD}${durationMs / 1000.0}s${RESET}") - println("${CYAN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}") + println("${AnsiColors.GREEN}⏱${AnsiColors.RESET} Completed in ${AnsiColors.BOLD}${durationMs / 1000.0}s${AnsiColors.RESET}") + println("${AnsiColors.CYAN}${AnsiColors.BOLD}$SEPARATOR${AnsiColors.RESET}") } \ No newline at end of file diff --git a/platform/build-scripts/product-dsl/src/json/AnalysisJsonWriter.kt b/platform/build-scripts/product-dsl/src/json/AnalysisJsonWriter.kt index c066c4460294..8c3db8622d28 100644 --- a/platform/build-scripts/product-dsl/src/json/AnalysisJsonWriter.kt +++ b/platform/build-scripts/product-dsl/src/json/AnalysisJsonWriter.kt @@ -2,54 +2,41 @@ package org.jetbrains.intellij.build.productLayout.json import com.fasterxml.jackson.core.JsonGenerator +import kotlinx.serialization.Serializable +import org.jetbrains.intellij.build.productLayout.analysis.DependencyPathResult import org.jetbrains.intellij.build.productLayout.analysis.MergeImpactResult +import org.jetbrains.intellij.build.productLayout.analysis.ModuleDependenciesResult import org.jetbrains.intellij.build.productLayout.analysis.ModulePathsResult +import org.jetbrains.intellij.build.productLayout.analysis.ModuleReachabilityResult import org.jetbrains.intellij.build.productLayout.analysis.ModuleSetOverlap import org.jetbrains.intellij.build.productLayout.analysis.ProductSimilarityPair +import org.jetbrains.intellij.build.productLayout.analysis.ProductUsageAnalysis import org.jetbrains.intellij.build.productLayout.analysis.UnificationSuggestion /** * Writes product similarity analysis to JSON. * Includes similar product pairs and summary statistics. */ -fun writeProductSimilarityAnalysis( +internal fun writeProductSimilarityAnalysis( gen: JsonGenerator, pairs: List, threshold: Double ) { - gen.writeArrayFieldStart("pairs") - for (pair in pairs) { - gen.writeStartObject() - gen.writeStringField("product1", pair.product1) - gen.writeStringField("product2", pair.product2) - gen.writeNumberField("similarity", pair.similarity) - gen.writeNumberField("moduleSetSimilarity", pair.moduleSetSimilarity) - - gen.writeArrayFieldStart("sharedModuleSets") - for (setName in pair.sharedModuleSets) { - gen.writeString(setName) - } - gen.writeEndArray() - - gen.writeArrayFieldStart("uniqueToProduct1") - for (setName in pair.uniqueToProduct1) { - gen.writeString(setName) - } - gen.writeEndArray() - - gen.writeArrayFieldStart("uniqueToProduct2") - for (setName in pair.uniqueToProduct2) { - gen.writeString(setName) - } - gen.writeEndArray() - - gen.writeEndObject() - } - gen.writeEndArray() + @Serializable + data class Wrapper( + val pairs: List, + val totalPairs: Int, + val threshold: Double, + val summary: String + ) - gen.writeNumberField("totalPairs", pairs.size) - gen.writeNumberField("threshold", threshold) - gen.writeStringField("summary", "Found ${pairs.size} product pairs with ≥${(threshold * 100).toInt()}% similarity") + val wrapper = Wrapper( + pairs = pairs, + totalPairs = pairs.size, + threshold = threshold, + summary = "Found ${pairs.size} product pairs with ≥${(threshold * 100).toInt()}% similarity" + ) + gen.writeRawValue(kotlinxJson.encodeToString(wrapper)) } /** @@ -57,203 +44,111 @@ fun writeProductSimilarityAnalysis( * Includes overlapping module set pairs and summary statistics. * Note: Intentional nested set inclusions are already filtered out during analysis. */ -fun writeModuleSetOverlapAnalysis( +internal fun writeModuleSetOverlapAnalysis( gen: JsonGenerator, overlaps: List, minPercent: Int ) { - gen.writeArrayFieldStart("overlaps") - for (overlap in overlaps) { - gen.writeStartObject() - gen.writeStringField("moduleSet1", overlap.moduleSet1) - gen.writeStringField("moduleSet2", overlap.moduleSet2) - gen.writeStringField("location1", overlap.location1) - gen.writeStringField("location2", overlap.location2) - gen.writeStringField("relationship", overlap.relationship) - gen.writeNumberField("overlapPercent", overlap.overlapPercent) - gen.writeNumberField("sharedModules", overlap.sharedModules) - gen.writeNumberField("totalModules1", overlap.totalModules1) - gen.writeNumberField("totalModules2", overlap.totalModules2) - gen.writeStringField("recommendation", overlap.recommendation) - gen.writeEndObject() - } - gen.writeEndArray() + @Serializable + data class Wrapper( + val overlaps: List, + val count: Int, + val summary: String + ) - gen.writeNumberField("count", overlaps.size) - gen.writeStringField("summary", "Found ${overlaps.size} module set pairs with ≥$minPercent% overlap (excluding intentional nesting)") + val wrapper = Wrapper( + overlaps = overlaps, + count = overlaps.size, + summary = "Found ${overlaps.size} module set pairs with ≥$minPercent% overlap (excluding intentional nesting)" + ) + gen.writeRawValue(kotlinxJson.encodeToString(wrapper)) } /** * Writes module set unification suggestions to JSON. * Includes suggestions for merge, inline, factor, and split strategies. */ -fun writeUnificationSuggestions( +internal fun writeUnificationSuggestions( gen: JsonGenerator, suggestions: List ) { - gen.writeArrayFieldStart("suggestions") - for (suggestion in suggestions) { - gen.writeStartObject() - gen.writeStringField("priority", suggestion.priority) - gen.writeStringField("strategy", suggestion.strategy) - - if (suggestion.type != null) { - gen.writeStringField("type", suggestion.type) - } - if (suggestion.moduleSet != null) { - gen.writeStringField("moduleSet", suggestion.moduleSet) - } - if (suggestion.moduleSet1 != null) { - gen.writeStringField("moduleSet1", suggestion.moduleSet1) - } - if (suggestion.moduleSet2 != null) { - gen.writeStringField("moduleSet2", suggestion.moduleSet2) - } - if (suggestion.products != null) { - gen.writeArrayFieldStart("products") - for (product in suggestion.products) { - gen.writeString(product) - } - gen.writeEndArray() - } - if (suggestion.sharedModuleSets != null) { - gen.writeArrayFieldStart("sharedModuleSets") - for (setName in suggestion.sharedModuleSets) { - gen.writeString(setName) - } - gen.writeEndArray() - } - - gen.writeStringField("reason", suggestion.reason) - - gen.writeObjectFieldStart("impact") - for ((key, value) in suggestion.impact) { - when (value) { - is Number -> gen.writeNumberField(key, value.toDouble()) - is String -> gen.writeStringField(key, value) - is List<*> -> { - gen.writeArrayFieldStart(key) - for (item in value) { - gen.writeString(item.toString()) - } - gen.writeEndArray() - } - } - } - gen.writeEndObject() - - gen.writeEndObject() - } - gen.writeEndArray() + @Serializable + data class Wrapper( + val suggestions: List, + val totalSuggestions: Int, + val summary: String + ) - gen.writeNumberField("totalSuggestions", suggestions.size) - gen.writeStringField("summary", "Found ${suggestions.size} unification opportunities") + val wrapper = Wrapper( + suggestions = suggestions, + totalSuggestions = suggestions.size, + summary = "Found ${suggestions.size} unification opportunities" + ) + gen.writeRawValue(kotlinxJson.encodeToString(wrapper)) } /** * Writes merge impact analysis to JSON. * Includes products affected, size impact, violations, and recommendation. */ -fun writeMergeImpactAnalysis( +internal fun writeMergeImpactAnalysis( gen: JsonGenerator, impact: MergeImpactResult ) { - gen.writeStringField("operation", impact.operation) - gen.writeStringField("sourceSet", impact.sourceSet) - if (impact.targetSet != null) { - gen.writeStringField("targetSet", impact.targetSet) - } - - gen.writeArrayFieldStart("productsUsingSource") - for (product in impact.productsUsingSource) { - gen.writeString(product) - } - gen.writeEndArray() - - gen.writeArrayFieldStart("productsUsingTarget") - for (product in impact.productsUsingTarget) { - gen.writeString(product) - } - gen.writeEndArray() - - gen.writeArrayFieldStart("productsThatWouldChange") - for (product in impact.productsThatWouldChange) { - gen.writeString(product) - } - gen.writeEndArray() - - gen.writeObjectFieldStart("sizeImpact") - for ((key, value) in impact.sizeImpact) { - gen.writeNumberField(key, value) - } - gen.writeEndObject() - - gen.writeArrayFieldStart("violations") - for (violation in impact.violations) { - gen.writeStartObject() - for ((key, value) in violation) { - when (value) { - is String -> gen.writeStringField(key, value) - is Number -> gen.writeNumberField(key, value.toDouble()) - is List<*> -> { - gen.writeArrayFieldStart(key) - for (item in value) { - gen.writeString(item.toString()) - } - gen.writeEndArray() - } - } - } - gen.writeEndObject() - } - gen.writeEndArray() - - gen.writeStringField("recommendation", impact.recommendation) - gen.writeBooleanField("safe", impact.safe) + gen.writeRawValue(kotlinxJson.encodeToString(impact)) } /** * Writes module paths result to JSON. * Includes all paths from module to products and summary information. */ -fun writeModulePathsResult( +internal fun writeModulePathsResult( gen: JsonGenerator, result: ModulePathsResult ) { - gen.writeStringField("module", result.module) - - gen.writeArrayFieldStart("paths") - for (path in result.paths) { - gen.writeStartObject() - gen.writeStringField("type", path.type) - gen.writeStringField("path", path.path) - - gen.writeArrayFieldStart("files") - for (file in path.files) { - gen.writeStartObject() - gen.writeStringField("type", file.type) - if (file.path != null) { - gen.writeStringField("path", file.path) - } - gen.writeStringField("name", file.name) - gen.writeStringField("note", file.note) - gen.writeEndObject() - } - gen.writeEndArray() - - gen.writeEndObject() - } - gen.writeEndArray() - - gen.writeArrayFieldStart("moduleSets") - for (moduleSet in result.moduleSets) { - gen.writeString(moduleSet) - } - gen.writeEndArray() - - gen.writeArrayFieldStart("products") - for (product in result.products) { - gen.writeString(product) - } - gen.writeEndArray() + gen.writeRawValue(kotlinxJson.encodeToString(result)) +} + +/** + * Writes module dependencies result to JSON. + * Includes JPS module dependencies for a given module. + */ +internal fun writeModuleDependenciesResult( + gen: JsonGenerator, + result: ModuleDependenciesResult +) { + gen.writeRawValue(kotlinxJson.encodeToString(result)) +} + +/** + * Writes module reachability result to JSON. + * Includes satisfied and missing dependencies within a module set context. + */ +internal fun writeModuleReachabilityResult( + gen: JsonGenerator, + result: ModuleReachabilityResult +) { + gen.writeRawValue(kotlinxJson.encodeToString(result)) +} + +/** + * Writes dependency path result to JSON. + * Includes the transitive dependency path from one module to another. + */ +internal fun writeDependencyPathResult( + gen: JsonGenerator, + result: DependencyPathResult +) { + gen.writeRawValue(kotlinxJson.encodeToString(result)) +} + +/** + * Writes product usage analysis to JSON. + * Includes direct and indirect usage with inclusion chains. + */ +internal fun writeProductUsageAnalysis( + gen: JsonGenerator, + result: ProductUsageAnalysis +) { + gen.writeRawValue(kotlinxJson.encodeToString(result)) } diff --git a/platform/build-scripts/product-dsl/src/json/JsonWriterUtils.kt b/platform/build-scripts/product-dsl/src/json/JsonWriterUtils.kt new file mode 100644 index 000000000000..c09526da7239 --- /dev/null +++ b/platform/build-scripts/product-dsl/src/json/JsonWriterUtils.kt @@ -0,0 +1,68 @@ +// 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.productLayout.json + +import com.fasterxml.jackson.core.JsonGenerator + +/** + * Helper utilities for common JSON writing patterns. + * Reduces boilerplate in JSON writer functions. + */ + +/** + * Writes an array of strings to JSON. + * + * @param fieldName Name of the array field + * @param items Collection of strings to write + */ +internal fun JsonGenerator.writeStringArray(fieldName: String, items: Collection) { + writeArrayFieldStart(fieldName) + for (item in items) { + writeString(item) + } + writeEndArray() +} + +/** + * Writes a map with mixed value types to JSON. + * Handles String, Number, Boolean, and List<*> values. + * + * @param fieldName Name of the object field + * @param map Map with mixed value types + */ +internal fun JsonGenerator.writeMapField(fieldName: String, map: Map) { + writeObjectFieldStart(fieldName) + for ((key, value) in map) { + when (value) { + is Number -> writeNumberField(key, value.toDouble()) + is String -> writeStringField(key, value) + is Boolean -> writeBooleanField(key, value) + is List<*> -> { + writeArrayFieldStart(key) + for (item in value) { + writeString(item.toString()) + } + writeEndArray() + } + } + } + writeEndObject() +} + +/** + * Writes an array of objects to JSON, using a custom writer function for each element. + * + * @param fieldName Name of the array field + * @param items List of items to write + * @param elementWriter Function to write each element + */ +internal inline fun JsonGenerator.writeObjectArray( + fieldName: String, + items: List, + elementWriter: JsonGenerator.(T) -> Unit +) { + writeArrayFieldStart(fieldName) + for (item in items) { + elementWriter(item) + } + writeEndArray() +} diff --git a/platform/build-scripts/product-dsl/src/json/ModuleSetJsonWriter.kt b/platform/build-scripts/product-dsl/src/json/ModuleSetJsonWriter.kt index 92f7c96a18d4..a0b6f627602d 100644 --- a/platform/build-scripts/product-dsl/src/json/ModuleSetJsonWriter.kt +++ b/platform/build-scripts/product-dsl/src/json/ModuleSetJsonWriter.kt @@ -5,8 +5,8 @@ import com.fasterxml.jackson.core.JsonGenerator import org.jetbrains.intellij.build.productLayout.DuplicateIncludeDetector import org.jetbrains.intellij.build.productLayout.ModuleSet import org.jetbrains.intellij.build.productLayout.analysis.ModuleSetMetadata +import org.jetbrains.intellij.build.productLayout.analysis.ModuleSetTraversal import org.jetbrains.intellij.build.productLayout.analysis.ProductSpec -import org.jetbrains.intellij.build.productLayout.collectAllModuleNames import org.jetbrains.intellij.build.productLayout.collectAllModuleNamesFromSet import java.nio.file.Path import kotlin.io.path.exists @@ -17,7 +17,7 @@ import kotlin.io.path.isRegularFile * Uses kotlinx.serialization to serialize the ModuleSet structure directly, * then embeds the raw JSON using writeRawValue(). */ -fun writeModuleSet( +internal fun writeModuleSet( gen: JsonGenerator, moduleSet: ModuleSet, location: String, @@ -59,7 +59,7 @@ fun writeDuplicateAnalysis( // Find modules that appear in multiple module sets val moduleToSets = mutableMapOf>() for ((moduleSet, _, _) in allModuleSets) { - val allModules = collectAllModuleNames(moduleSet) + val allModules = ModuleSetTraversal.collectAllModuleNames(moduleSet) for (moduleName in allModules) { moduleToSets.computeIfAbsent(moduleName) { mutableListOf() }.add(moduleSet.name) } @@ -89,8 +89,8 @@ fun writeDuplicateAnalysis( val (set1, _, _) = allModuleSets[i] val (set2, _, _) = allModuleSets[j] - val modules1 = collectAllModuleNames(set1) - val modules2 = collectAllModuleNames(set2) + val modules1 = ModuleSetTraversal.collectAllModuleNames(set1) + val modules2 = ModuleSetTraversal.collectAllModuleNames(set2) val overlap = modules1.intersect(modules2) if (overlap.size > 5) { // Only report significant overlaps diff --git a/platform/build-scripts/product-dsl/src/json/ProductJsonWriter.kt b/platform/build-scripts/product-dsl/src/json/ProductJsonWriter.kt index 04f8216b6bc5..7bdc39518343 100644 --- a/platform/build-scripts/product-dsl/src/json/ProductJsonWriter.kt +++ b/platform/build-scripts/product-dsl/src/json/ProductJsonWriter.kt @@ -36,6 +36,9 @@ fun writeProduct( gen.writeString(buildModule) } gen.writeEndArray() + + // Product category (ULTIMATE, COMMUNITY, BACKEND) + gen.writeStringField("category", product.category.name) // Metrics gen.writeNumberField("totalModuleCount", product.totalModuleCount) diff --git a/platform/build-scripts/product-dsl/src/json/ValidationJsonWriter.kt b/platform/build-scripts/product-dsl/src/json/ValidationJsonWriter.kt index b3c37670be45..aceb69df100b 100644 --- a/platform/build-scripts/product-dsl/src/json/ValidationJsonWriter.kt +++ b/platform/build-scripts/product-dsl/src/json/ValidationJsonWriter.kt @@ -20,11 +20,7 @@ fun writeCommunityProductViolations( gen.writeStringField("moduleSet", violation.moduleSet) gen.writeStringField("moduleSetFile", violation.moduleSetFile) - gen.writeArrayFieldStart("ultimateModules") - for (module in violation.ultimateModules) { - gen.writeString(module) - } - gen.writeEndArray() + gen.writeStringArray("ultimateModules", violation.ultimateModules) gen.writeNumberField("communityModulesCount", violation.communityModulesCount) gen.writeNumberField("unknownModulesCount", violation.unknownModulesCount) @@ -37,17 +33,8 @@ fun writeCommunityProductViolations( gen.writeObjectFieldStart("summary") gen.writeNumberField("totalViolations", violations.size) - gen.writeArrayFieldStart("affectedProducts") - for (product in violations.map { it.product }.distinct().sorted()) { - gen.writeString(product) - } - gen.writeEndArray() - - gen.writeArrayFieldStart("affectedModuleSets") - for (moduleSet in violations.map { it.moduleSet }.distinct().sorted()) { - gen.writeString(moduleSet) - } - gen.writeEndArray() + gen.writeStringArray("affectedProducts", violations.map { it.product }.distinct().sorted()) + gen.writeStringArray("affectedModuleSets", violations.map { it.moduleSet }.distinct().sorted()) gen.writeEndObject() } @@ -67,19 +54,11 @@ fun writeModuleSetLocationViolations( gen.writeStringField("issue", violation.issue) if (violation.ultimateModules != null) { - gen.writeArrayFieldStart("ultimateModules") - for (module in violation.ultimateModules) { - gen.writeString(module) - } - gen.writeEndArray() + gen.writeStringArray("ultimateModules", violation.ultimateModules) } - + if (violation.communityModules != null) { - gen.writeArrayFieldStart("communityModules") - for (module in violation.communityModules) { - gen.writeString(module) - } - gen.writeEndArray() + gen.writeStringArray("communityModules", violation.communityModules) } if (violation.communityModulesCount != null) { diff --git a/platform/build-scripts/product-dsl/src/productDiscovery.kt b/platform/build-scripts/product-dsl/src/productDiscovery.kt new file mode 100644 index 000000000000..f37e4b8826b5 --- /dev/null +++ b/platform/build-scripts/product-dsl/src/productDiscovery.kt @@ -0,0 +1,314 @@ +// 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.productLayout + +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.jetbrains.intellij.build.ModuleOutputProvider +import org.jetbrains.jps.model.java.JavaSourceRootType +import org.jetbrains.jps.model.java.JpsJavaExtensionService +import java.nio.file.Files +import java.nio.file.Path + +/** + * Path to the product registry JSON file. + */ +const val PRODUCT_REGISTRY_PATH: String = "build/dev-build.json" + +/** + * Product registry containing all product configurations from dev-build.json. + */ +@Serializable +data class ProductConfigurationRegistry(@JvmField val products: Map) + +/** + * Product configuration from dev-build.json. + */ +@Serializable +data class ProductConfiguration( + @JvmField val modules: List, + @JvmField @SerialName("class") val className: String, + @JvmField val pluginXmlPath: String? = null +) + +/** + * Represents a discovered product with all its metadata. + * Used by both XML and JSON generators to avoid code duplication. + * Test products have properties = null (they don't have ProductProperties classes). + * + * Note: The `properties` field uses type `Any?` instead of `ProductProperties?` to avoid + * depending on the full build-scripts module (would create circular dependency). + * In practice, it holds ProductProperties instances or null for test products. + */ +data class DiscoveredProduct( + @JvmField val name: String, + @JvmField val config: ProductConfiguration, + @JvmField val properties: Any?, // ProductProperties or null + @JvmField val spec: ProductModulesContentSpec?, + @JvmField val pluginXmlPath: String?, +) + +/** + * Map of class FQN to actual file name for cases where class name != file name. + * Example: DotnetExternalProductProperties class is in ReSharperExternalProductProperties.kt file. + */ +private val CLASS_TO_FILE_NAME_OVERRIDES = mapOf( + "com.jetbrains.rider.build.product.DotnetExternalProductProperties" to "ReSharperExternalProductProperties.kt" +) + +/** + * Finds the actual source file for a ProductProperties class by searching JPS module source roots. + * This replaces hardcoded path mapping with actual file system lookup. + * + * @param buildModules List of build module names from dev-build.json (e.g., ["intellij.goland.build"]) + * @param productPropertiesClass The ProductProperties class to find + * @param moduleOutputProvider Provider for accessing JPS modules + * @param projectRoot Project root path for making paths relative + * @return Relative path to the source file + */ +fun findProductPropertiesSourceFile( + buildModules: List, + productPropertiesClass: Class<*>, + moduleOutputProvider: ModuleOutputProvider, + projectRoot: Path +): String { + val className = productPropertiesClass.name + + // Handle special cases where class name != file name + val fileName = CLASS_TO_FILE_NAME_OVERRIDES[className] ?: "${className.substringAfterLast('.')}.kt" + val packagePath = className.substringBeforeLast('.').replace('.', '/') + val relativePath = "$packagePath/$fileName" + + // Search each build module's source roots + for (buildModuleName in buildModules) { + val jpsModule = moduleOutputProvider.findModule(buildModuleName) ?: continue + + // Search production source roots (not test roots) + val sourceFile = jpsModule.sourceRoots + .asSequence() + .filter { it.rootType == JavaSourceRootType.SOURCE } + .firstNotNullOfOrNull { sourceRoot -> + JpsJavaExtensionService.getInstance().findSourceFile(sourceRoot, relativePath) + } + + if (sourceFile != null) { + return projectRoot.relativize(sourceFile).toString() + } + } + + throw IllegalStateException("Cannot find source file for $productPropertiesClass (searched for $relativePath in modules: $buildModules)") +} + +/** + * Extracts product validation data from discovered products. + * Returns list of (productName, ProductModulesContentSpec) pairs for validation. + * + * @param discoveredProducts List of discovered products + * @return List of product name and spec pairs for validation + */ +fun extractProductsForValidation(discoveredProducts: List): List> { + return discoveredProducts.map { it.name to it.spec } +} + +/** + * Generates product XMLs for all products using programmatic content. + * Takes discovered products and test product specs, then generates complete plugin.xml files. + * + * @param discoveredProducts Regular products discovered from dev-build.json + * @param testProductSpecs Test product specifications (name to ProductModulesContentSpec pairs) + * @param projectRoot The project root path + * @param moduleOutputProvider Module output provider for resolving module dependencies + * @return Result containing generation statistics + */ +suspend fun generateAllProductXmlFiles( + discoveredProducts: List, + testProductSpecs: List> = emptyList(), + projectRoot: Path, + moduleOutputProvider: ModuleOutputProvider, +): ProductGenerationResult { + // Convert test product specs to DiscoveredProduct instances + val testProducts = testProductSpecs.mapNotNull { (name, spec) -> + val xmlPath = "ultimate/platform-ultimate/testResources/META-INF/${name}Plugin.xml" + val xmlFile = projectRoot.resolve(xmlPath) + if (Files.notExists(xmlFile)) { + return@mapNotNull null + } + + DiscoveredProduct( + name = name, + config = ProductConfiguration( + className = "test-product", + modules = emptyList(), + pluginXmlPath = xmlPath, + ), + properties = null, + spec = spec, + pluginXmlPath = xmlPath, + ) + } + + val allProducts = discoveredProducts + testProducts + + // Detect if this is an Ultimate build by checking if community directory is a subdirectory + val isUltimateBuild = Files.exists(projectRoot.resolve("community")) + + val productResults = coroutineScope { + allProducts.map { discovered -> + async { + // Skip products without pluginXmlPath or spec configured + val pluginXmlRelativePath = discovered.pluginXmlPath ?: return@async null + val spec = discovered.spec ?: return@async null + + val pluginXmlPath = projectRoot.resolve(pluginXmlRelativePath) + + // Extract ProductProperties class name (works with both ProductProperties and null) + val productPropertiesClass = when (val props = discovered.properties) { + null -> "test-product" + else -> props.javaClass.name + } + + generateProductXml( + pluginXmlPath = pluginXmlPath, + spec = spec, + productName = discovered.name, + moduleOutputProvider = moduleOutputProvider, + productPropertiesClass = productPropertiesClass, + projectRoot = projectRoot, + isUltimateBuild = isUltimateBuild, + ) + } + }.awaitAll().filterNotNull() + } + + return ProductGenerationResult(productResults) +} + +/** + * Configuration for generating all module sets and products. + * Used by [generateAllModuleSetsWithProducts] to orchestrate the full generation process. + * + * @param moduleSetSources Map of label to (source object, output directory). The source object should contain ModuleSet functions (e.g., CommunityModuleSets, UltimateModuleSets). + * @param discoveredProducts Products discovered from dev-build.json (must be provided by caller) + * @param testProductSpecs List of test product specifications to generate alongside regular products + * @param projectRoot Project root path + * @param moduleOutputProvider Module output provider for resolving module dependencies and output directories + */ +data class ModuleSetGenerationConfig( + @JvmField val moduleSetSources: Map>, + @JvmField val discoveredProducts: List, + @JvmField val testProductSpecs: List> = emptyList(), + @JvmField val projectRoot: Path, + @JvmField val moduleOutputProvider: ModuleOutputProvider, +) + +/** + * Discovers all module sets from configured sources. + */ +private fun discoverAllModuleSets(moduleSetSources: Map>): List { + val allModuleSets = mutableListOf() + for ((_, source) in moduleSetSources) { + val (sourceObj, _) = source + allModuleSets.addAll(discoverModuleSets(sourceObj)) + } + return allModuleSets +} + +/** + * Aggregates tracking maps from multiple generation results and cleans up orphaned files. + * Returns the list of deleted file results. + */ +private fun aggregateAndCleanupOrphanedFiles(moduleSetResults: List): List { + val aggregatedTrackingMap = mutableMapOf>() + for (result in moduleSetResults) { + for ((dir, files) in result.trackingMap) { + aggregatedTrackingMap.computeIfAbsent(dir) { mutableSetOf() }.addAll(files) + } + } + + val deletedFiles = cleanupOrphanedModuleSetFiles(aggregatedTrackingMap) + if (deletedFiles.isNotEmpty()) { + println("\nDeleted ${deletedFiles.size} orphaned files") + } + return deletedFiles +} + +/** + * Generates all module sets and products with validation. + * Base implementation that orchestrates the full generation process. + * + * This function: + * 1. Discovers all module sets from configured sources + * 2. Validates all products (using pre-discovered products from config) + * 3. Generates module set XMLs in parallel + * 4. Generates module dependencies and product XMLs + * 5. Prints a comprehensive summary + * + * @param config Configuration specifying module set sources, discovered products, test products, and other parameters + */ +suspend fun generateAllModuleSetsWithProducts(config: ModuleSetGenerationConfig) { + val startTime = System.currentTimeMillis() + + // Discover all module sets and validate products + val allModuleSets = discoverAllModuleSets(config.moduleSetSources) + val products = extractProductsForValidation(config.discoveredProducts) + validateNoRedundantModuleSets(allModuleSets = allModuleSets, productSpecs = products) + + // Execute all generation operations in parallel + val (moduleSetResults, dependencyResult, productResult) = coroutineScope { + // TIER 1: Parallel module set generation for all configured sources + val moduleSetJobs = config.moduleSetSources.map { (label, source) -> + val (sourceObj, outputDir) = source + async { + doGenerateAllModuleSetsInternal( + obj = sourceObj, + outputDir = outputDir, + label = label, + moduleOutputProvider = config.moduleOutputProvider + ) + } + } + + // TIER 2: Parallel dependency and product generation (can run concurrently with TIER 1) + val dependencyJob = async { + val moduleSetsByLabel = config.moduleSetSources.mapValues { (_, source) -> + val (sourceObj, _) = source + discoverModuleSets(sourceObj) + } + + generateModuleDescriptorDependencies( + communityModuleSets = moduleSetsByLabel["community"] ?: emptyList(), + ultimateModuleSets = moduleSetsByLabel["ultimate"] ?: emptyList(), + coreModuleSets = moduleSetsByLabel["core"] ?: emptyList(), + moduleOutputProvider = config.moduleOutputProvider, + productSpecs = products + ) + } + + val productJob = async { + generateAllProductXmlFiles( + discoveredProducts = config.discoveredProducts, + testProductSpecs = config.testProductSpecs, + projectRoot = config.projectRoot, + moduleOutputProvider = config.moduleOutputProvider + ) + } + + GenerationResults( + moduleSetResults = moduleSetJobs.awaitAll(), + dependencyResult = dependencyJob.await(), + productResult = productJob.await() + ) + } + + aggregateAndCleanupOrphanedFiles(moduleSetResults) + + printGenerationSummary( + moduleSetResults = moduleSetResults, + dependencyResult = dependencyResult, + productResult = productResult, + projectRoot = config.projectRoot, + durationMs = System.currentTimeMillis() - startTime + ) +} diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/JarPackagerDependencyHelper.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/JarPackagerDependencyHelper.kt index be1b5479eddf..afe59bf80e11 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/JarPackagerDependencyHelper.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/JarPackagerDependencyHelper.kt @@ -7,23 +7,20 @@ import com.intellij.util.xml.dom.XmlElement import com.intellij.util.xml.dom.readXmlAsModel import org.jetbrains.intellij.build.impl.ModuleItem import org.jetbrains.intellij.build.impl.PluginLayout -import org.jetbrains.jps.model.java.JpsJavaClasspathKind +import org.jetbrains.intellij.build.productLayout.getProductionModuleDependencies +import org.jetbrains.intellij.build.productLayout.isProductionRuntimeDependency import org.jetbrains.jps.model.java.JpsJavaExtensionService -import org.jetbrains.jps.model.module.JpsDependencyElement import org.jetbrains.jps.model.module.JpsLibraryDependency import org.jetbrains.jps.model.module.JpsModule -import org.jetbrains.jps.model.module.JpsModuleDependency import org.jetbrains.jps.model.module.JpsModuleReference import java.util.concurrent.ConcurrentHashMap // production-only - JpsJavaClasspathKind.PRODUCTION_RUNTIME -internal class JarPackagerDependencyHelper(private val context: CompilationContext) { - private val javaExtensionService = JpsJavaExtensionService.getInstance() - +internal class JarPackagerDependencyHelper(private val moduleOutputProvider: ModuleOutputProvider) { private val libraryCache = ConcurrentHashMap>() fun getModuleDependencies(moduleName: String): Sequence { - return getModuleDependencies(context.findRequiredModule(moduleName)).map { it.moduleReference.moduleName } + return moduleOutputProvider.findRequiredModule(moduleName).getProductionModuleDependencies(withTests = false).map { it.moduleReference.moduleName } } fun isPluginModulePackedIntoSeparateJar(module: JpsModule, layout: PluginLayout?, frontendModuleFilter: FrontendModuleFilter): Boolean { @@ -71,7 +68,7 @@ internal class JarPackagerDependencyHelper(private val context: CompilationConte fun getPluginIdByModule(pluginModule: JpsModule): String { // it is ok to read the plugin descriptor with unresolved x-include as the ID should be specified at the root - val root = readXmlAsModel(getUnprocessedPluginXmlContent(module = pluginModule, context = context)) + val root = readXmlAsModel(getUnprocessedPluginXmlContent(module = pluginModule, context = moduleOutputProvider)) val element = root.getChild("id") ?: root.getChild("name") ?: throw IllegalStateException("Cannot find attribute id or name (module=$pluginModule)") return element.content!! } @@ -87,25 +84,16 @@ internal class JarPackagerDependencyHelper(private val context: CompilationConte } } - private fun getModuleDependencies(module: JpsModule): Sequence { - return sequence { - for (element in module.dependenciesList.dependencies) { - if (element is JpsModuleDependency && isProductionRuntime(element = element, withTests = false)) { - yield(element) - } - } - } - } - fun getLibraryDependencies(module: JpsModule, withTests: Boolean): List { //TODO Please write some sane code here, caching is broken, a proper caching crashes dev build if (module.name == "intellij.python.pyproject" && withTests) { return java.util.List.of() } return libraryCache.computeIfAbsent(module) { + val javaExtensionService = JpsJavaExtensionService.getInstance() val result = mutableListOf() for (element in module.dependenciesList.dependencies) { - if (isProductionRuntime(element = element, withTests = withTests) && element is JpsLibraryDependency) { + if (element is JpsLibraryDependency && isProductionRuntimeDependency(element = element, javaExtensionService = javaExtensionService, withTests = withTests)) { result.add(element) } } @@ -123,7 +111,7 @@ internal class JarPackagerDependencyHelper(private val context: CompilationConte fun hasLibraryInDependencyChainOfModuleDependencies(dependentModule: JpsModule, libraryName: String, siblings: Collection, withTests: Boolean): Boolean { val parentGroup = dependentModule.name.let { it.substring(0, it.lastIndexOf('.')) } val prefix = "$parentGroup." - for (dependency in getModuleDependencies(dependentModule)) { + for (dependency in dependentModule.getProductionModuleDependencies(withTests = false)) { val moduleName = dependency.moduleReference.moduleName if (moduleName == parentGroup) { if (getLibraryDependencies(dependency.module ?: continue, withTests).any { it.libraryReference.libraryName == libraryName }) { @@ -138,14 +126,6 @@ internal class JarPackagerDependencyHelper(private val context: CompilationConte } return false } - - private fun isProductionRuntime(element: JpsDependencyElement, withTests: Boolean): Boolean { - val scope = javaExtensionService.getDependencyExtension(element)?.scope ?: return false - if (withTests && scope.isIncludedIn(JpsJavaClasspathKind.TEST_RUNTIME)) { - return true - } - return scope.isIncludedIn(JpsJavaClasspathKind.PRODUCTION_RUNTIME) - } } internal fun readPluginContentFromDescriptor(pluginDescriptor: XmlElement): Sequence> { diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/ProductProperties.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/ProductProperties.kt index 1463cc9625c0..72f72a1dba6c 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/ProductProperties.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/ProductProperties.kt @@ -16,8 +16,6 @@ import kotlinx.collections.immutable.persistentMapOf import org.jetbrains.annotations.ApiStatus import org.jetbrains.intellij.build.impl.PlatformLayout import org.jetbrains.intellij.build.impl.qodana.QodanaProductProperties -import org.jetbrains.intellij.build.productLayout.CommunityModuleSets -import org.jetbrains.intellij.build.productLayout.ModuleSetProvider import org.jetbrains.intellij.build.productLayout.ProductModulesContentSpec import org.jetbrains.intellij.build.productLayout.ProductModulesLayout import org.jetbrains.jps.model.JpsProject @@ -512,15 +510,6 @@ abstract class ProductProperties { is PluginCreationFail -> result.errorsAndWarnings } } - - /** - * List of module sets providers used to discover and resolve module set relationships. - * Used only for packaging tests to group modules by their module sets. - * - * For community builds: [CommunityModuleSets] - * For ultimate builds: both UltimateModuleSets and [CommunityModuleSets] - */ - abstract val moduleSetsProviders: List } /** diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/classPath/contentModuleEmbedding.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/classPath/contentModuleEmbedding.kt index 966c751d1bc1..6c5736f70992 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/classPath/contentModuleEmbedding.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/classPath/contentModuleEmbedding.kt @@ -39,8 +39,6 @@ import org.jetbrains.intellij.build.impl.XIncludeElementResolver import org.jetbrains.intellij.build.impl.contentModuleNameToDescriptorFileName import org.jetbrains.intellij.build.impl.resolveIncludes import org.jetbrains.intellij.build.impl.toLoadPath -import java.io.IOException -import java.nio.file.Files /** * Defines a search scope for resolving XInclude references in plugin descriptors. @@ -359,23 +357,6 @@ internal class XIncludeElementResolverImpl( return JDOMUtil.load(it) } - // resolve module set files directly from generated directories - if (descriptorCache.isModuleSetOwner && loadPath.startsWith("META-INF/intellij.moduleSets.")) { - for (provider in context.productProperties.moduleSetsProviders) { - val file = provider.getOutputDirectory(context.paths).resolve(loadPath) - val data = try { - Files.readAllBytes(file) - } - catch (_: IOException) { - continue - } - - // if someone else has resolved this file before, use their result - descriptorCache.putIfAbsent(loadPath, data) - return JDOMUtil.load(data) - } - } - for (module in searchPath.modules) { findUnprocessedDescriptorContent(context.findRequiredModule(module), loadPath, context)?.let { data -> descriptorCache.putIfAbsent(loadPath, data) 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 2feef2867c1a..cf1a7e6f68ea 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 @@ -11,6 +11,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.intellij.build.BuildContext +import org.jetbrains.intellij.build.ModuleOutputProvider import org.jetbrains.intellij.build.PLATFORM_LOADER_JAR import org.jetbrains.intellij.build.UTIL_8_JAR import org.jetbrains.intellij.build.UTIL_JAR @@ -22,6 +23,7 @@ import org.jetbrains.intellij.build.productLayout.ProductModulesLayout import org.jetbrains.jps.model.java.JpsJavaClasspathKind import org.jetbrains.jps.model.java.JpsJavaExtensionService import org.jetbrains.jps.model.module.JpsLibraryDependency +import org.jetbrains.jps.model.module.JpsModuleDependency import org.jetbrains.jps.model.module.JpsModuleReference import java.util.SortedSet @@ -35,26 +37,18 @@ import java.util.SortedSet */ @Suppress("RemoveRedundantQualifierName") internal val PLATFORM_CORE_MODULES = java.util.List.of( - "intellij.platform.builtInServer", - "intellij.platform.diff", "intellij.platform.editor.ui", - "intellij.platform.externalSystem", - "intellij.platform.externalSystem.dependencyUpdater", "intellij.platform.codeStyle", - "intellij.platform.lang.core", "intellij.platform.ml", "intellij.platform.remote.core", "intellij.platform.remoteServers.agent.rt", "intellij.platform.usageView", "intellij.platform.execution", - "intellij.platform.analysis.impl", "intellij.platform.editor.ex", - "intellij.platform.externalProcessAuthHelper", "intellij.platform.lvcs", "intellij.platform.macro", "intellij.platform.remoteServers.impl", - "intellij.platform.smRunner", "intellij.platform.structureView.impl", "intellij.platform.testRunner", "intellij.platform.rd.community", @@ -69,9 +63,6 @@ internal val PLATFORM_CORE_MODULES = java.util.List.of( "intellij.platform.markdown.utils", "intellij.platform.util.commonsLangV2Shim", - "intellij.platform.externalSystem.impl", - "intellij.platform.credentialStore.ui", - // do we need it? "intellij.platform.sqlite", // todo not used by platform - move to plugin @@ -86,7 +77,7 @@ internal val PLATFORM_CORE_MODULES = java.util.List.of( ) @Suppress("RemoveRedundantQualifierName") -internal val PLATFORM_CUSTOM_PACK_MODE: Map = java.util.Map.of( +private val PLATFORM_CUSTOM_PACK_MODE: Map = java.util.Map.of( "jetbrains-annotations", LibraryPackMode.STANDALONE_SEPARATE_WITHOUT_VERSION_NAME, ) @@ -145,7 +136,6 @@ internal suspend fun createPlatformLayout(projectLibrariesUsedByPlugins: SortedS "intellij.platform.diagnostic.telemetry.rt", "intellij.platform.util", "intellij.platform.util.multiplatform", - "intellij.platform.core", // it has package `kotlin.coroutines.jvm.internal` - should be packed into the same JAR as coroutine lib, // to ensure that package index will not report one more JAR in a search path "intellij.platform.bootstrap.coroutine", @@ -397,7 +387,7 @@ fun collectExportedLibrariesFromLibraryModules( context: BuildContext, ): Map { val javaExtensionService = JpsJavaExtensionService.getInstance() - val result = mutableMapOf() + val result = LinkedHashMap() val includedModuleNames = layout.includedModules.map { it.moduleName } val corePluginsContentModuleNames = computeContentModulesPluginsWhichUseIdeaClassloader(context) @@ -472,6 +462,61 @@ private fun toModuleItemSequence(list: Collection, productLayout: Produc .map { ModuleItem(moduleName = it, relativeOutputFile = PlatformJarNames.getPlatformModuleJarName(it, frontendModuleFilter), reason = reason) } } +/** + * Sorts embedded modules topologically so dependencies are processed before dependents. + * This ensures that when computing transitive dependencies, modules don't incorrectly + * include dependencies that should belong to their own dependencies. + * + * @param embeddedModules modules to sort + * @param context build context + * @return list of modules in dependency order (dependencies before dependents) + */ +private fun sortEmbeddedModulesTopologically( + embeddedModules: Collection, + context: ModuleOutputProvider, +): List { + val moduleByName = embeddedModules.associateByTo(HashMap(embeddedModules.size)) { it.moduleName } + val moduleNames = moduleByName.keys + + val result = mutableListOf() + val visited = HashSet() + val visiting = HashSet() + + fun visit(moduleName: String) { + if (moduleName in visited) { + return + } + + if (moduleName in visiting) { + throw IllegalStateException("Circular dependency: ${(visiting - moduleName).joinToString(" -> ")}") + } + + visiting.add(moduleName) + + // visit dependencies first (only those in our processing set) + val jpsModule = context.findRequiredModule(moduleName) + for (dep in jpsModule.dependenciesList.dependencies) { + if (dep is JpsModuleDependency) { + val depName = dep.moduleReference.moduleName + if (moduleByName.containsKey(depName)) { + visit(depName) + } + } + } + + visiting.remove(moduleName) + visited.add(moduleName) + moduleByName.get(moduleName)?.let { result.add(it) } + } + + // Visit all modules + for (moduleName in moduleNames) { + visit(moduleName) + } + + return result +} + /** * Computes transitive dependencies for embedded modules that have `includeDependencies=true`. * Dependencies are packaged into the same JAR as their parent embedded module. @@ -490,8 +535,11 @@ private fun computeEmbeddedModuleDependencies( val result = LinkedHashSet() val rootChain = persistentListOf() + // Sort modules topologically to ensure dependencies are processed before dependents + val sortedEmbeddedModules = sortEmbeddedModulesTopologically(embeddedModules, context) + // For each embedded module, compute its transitive dependencies - for (embeddedModule in embeddedModules) { + for (embeddedModule in sortedEmbeddedModules) { val moduleName = embeddedModule.moduleName val relativeOutputFile = embeddedModule.relativeOutputFile val moduleSet = embeddedModule.moduleSet diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/productModuleLayout.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/productModuleLayout.kt index 1dac9ef0c1eb..ba7dd5f64e57 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/productModuleLayout.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/productModuleLayout.kt @@ -196,6 +196,11 @@ private fun isModuleCloseSource(moduleName: String, context: BuildContext): Bool return false } + // todo will be removed on the next stage + if (moduleName == "fleet.protocol" || moduleName.startsWith("fleet.rpc.")) { + return false + } + val sourceRoots = context.findRequiredModule(moduleName).sourceRoots.filter { it.rootType == JavaSourceRootType.SOURCE } if (sourceRoots.isEmpty()) { return false diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/CommunityModuleSets.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/CommunityModuleSets.kt index feb88a94a33e..143b43967d81 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/CommunityModuleSets.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/CommunityModuleSets.kt @@ -1,8 +1,10 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@file:Suppress("GrazieInspection") + package org.jetbrains.intellij.build.productLayout import com.intellij.openapi.application.PathManager -import org.jetbrains.intellij.build.BuildPaths +import org.jetbrains.intellij.build.productLayout.CommunityModuleSets.librariesIde import java.nio.file.Path /** @@ -11,12 +13,11 @@ import java.nio.file.Path * * Products call these functions directly in ProductProperties.getProductContentModules() to create * ModuleSet instances that will be automatically injected into the product plugin.xml during build. + * + * @see Module Sets Documentation - How module sets work and best practices + * @see Programmatic Content Documentation - How to use module sets in products */ -object CommunityModuleSets : ModuleSetProvider { - override fun getOutputDirectory(paths: BuildPaths): Path { - return paths.communityHomeDir.resolve("platform/platform-resources/generated") - } - +object CommunityModuleSets { /** * Main method to regenerate all community module set XML files from Kotlin definitions. * Run this whenever module sets are modified to keep XML files in sync. @@ -29,170 +30,26 @@ object CommunityModuleSets : ModuleSetProvider { generateAllModuleSets( obj = CommunityModuleSets, outputDir = projectRoot.resolve("community/platform/platform-resources/generated/META-INF"), - label = "community" + label = "community", + projectRoot = projectRoot, + moduleOutputProvider = createModuleOutputProvider(projectRoot) ) } - /** - * Minimal essential platform modules required by lightweight IDE products like GitClient. - * Contains only core backend/frontend split, editor, search, and basic infrastructure. - * Nested by essential() to avoid duplication. - */ - fun essentialMinimal(): ModuleSet = moduleSet("essential.minimal") { - // Include libraries first (they are xi:included in essential.minimal.xml) - moduleSet(libraries()) - - embeddedModule("intellij.platform.projectModel.impl", includeDependencies = true) - embeddedModule("intellij.platform.ide.impl", includeDependencies = true) - embeddedModule("intellij.platform.lang.impl", includeDependencies = true) - - // RPC is used by core IDE functionality - moduleSet(rpc()) - - // Core platform backend/frontend split - module("intellij.platform.settings.local") - module("intellij.platform.backend") - module("intellij.platform.project.backend") - module("intellij.platform.progress.backend") - module("intellij.platform.lang.impl.backend") - - // Frontend/monolith - module("intellij.platform.frontend") - module("intellij.platform.monolith") - - // Editor - module("intellij.platform.editor") - module("intellij.platform.editor.backend") - - // Search - module("intellij.platform.searchEverywhere") - module("intellij.platform.searchEverywhere.backend") - module("intellij.platform.searchEverywhere.frontend") - - // Completion - module("intellij.platform.inline.completion") - - // EEL (execution environment layer) - referenced from core classloader - embeddedModule("intellij.platform.eel.impl") - - // Concurrency utilities - referenced from core classloader - embeddedModule("intellij.platform.ide.concurrency") - } - - /** - * Essential platform modules required by most IDE products. - */ - fun essential(): ModuleSet = moduleSet("essential") { - // Include minimal essential modules (core backend/frontend, editor, search) - moduleSet(essentialMinimal()) - - // TODO: may be debugger shouldn't be essential? E.g. gateway doesn't need it. - moduleSet(debugger()) - - // The loading="embedded" attribute is required here because the intellij.platform.find module (which is loaded - // in embedded mode) has a compile dependency on intellij.platform.scopes. Without marking scopes as embedded, - // this would cause NoClassDefFoundError at runtime when classes from find try to use classes from scopes. - // This ensures proper classloader hierarchy is maintained for modules that depend on intellij.platform.scopes. - // This attribute should be removed once the find module no longer needs to be embedded. - embeddedModule("intellij.platform.scopes") - module("intellij.platform.scopes.backend") - - // todo navbar is not essential - module("intellij.platform.navbar") - module("intellij.platform.navbar.backend") - module("intellij.platform.navbar.frontend") - module("intellij.platform.navbar.monolith") - module("intellij.platform.clouds") - - module("intellij.platform.execution.serviceView") - module("intellij.platform.execution.serviceView.frontend") - module("intellij.platform.execution.serviceView.backend") - module("intellij.platform.execution.dashboard") - module("intellij.platform.execution.dashboard.frontend") - module("intellij.platform.execution.dashboard.backend") - - // The loading="embedded" attribute is required here for module synchronization with CWM's ThinClientFindAndReplaceExecutor. - // Since intellij.platform.frontend.split module loads in embedded mode, and it needs to override the default FindAndReplaceExecutor, - // the find module must also be marked as embedded to maintain proper dependency loading order. - // This attribute can be removed once ThinClientFindAndReplaceExecutor is removed. - embeddedModule("intellij.platform.find") - module("intellij.platform.find.backend") - module("intellij.platform.editor.frontend") - embeddedModule("intellij.platform.managed.cache") - module("intellij.platform.managed.cache.backend") - - module("intellij.platform.bookmarks.backend") - module("intellij.platform.bookmarks.frontend") - - module("intellij.platform.recentFiles") - module("intellij.platform.recentFiles.frontend") - module("intellij.platform.recentFiles.backend") - - module("intellij.platform.pluginManager.shared") - module("intellij.platform.pluginManager.backend") - module("intellij.platform.pluginManager.frontend") - - module("intellij.platform.execution.impl.frontend") - module("intellij.platform.execution.impl.backend") - module("intellij.platform.eel.tcp") - - - module("intellij.platform.completion.common") - module("intellij.platform.completion.frontend") - module("intellij.platform.completion.backend") - - embeddedModule("intellij.platform.analysis") - embeddedModule("intellij.platform.polySymbols") - } - - /** - * Provides the platform for implementing Debugger functionality. - * Corresponds to intellij.moduleSets.debugger.xml - */ - fun debugger(): ModuleSet = moduleSet("debugger") { - module("intellij.platform.debugger.impl.frontend") - module("intellij.platform.debugger.impl.backend") - embeddedModule("intellij.platform.debugger.impl.shared") - embeddedModule("intellij.platform.debugger.impl.rpc") - embeddedModule("intellij.platform.debugger.impl.ui") - embeddedModule("intellij.platform.debugger.impl") - embeddedModule("intellij.platform.debugger") - } - - /** - * Provides RPC functionality. - * Corresponds to intellij.moduleSets.rpc.xml - */ - fun rpc(): ModuleSet = moduleSet("rpc") { - // Fleet libraries are required for RPC - moduleSet(fleet()) - - embeddedModule("intellij.platform.rpc") - embeddedModule("intellij.platform.kernel") - module("intellij.platform.rpc.backend") - - module("intellij.platform.kernel.impl") - module("intellij.platform.kernel.backend") - - embeddedModule("intellij.platform.rpc.topics") - module("intellij.platform.rpc.topics.backend") - module("intellij.platform.rpc.topics.frontend") - } - - /** - * All library module sets combined (meta-set that includes core, ktor, misc, temporaryBundled). - */ - fun libraries(): ModuleSet = moduleSet("libraries") { - moduleSet(librariesCore()) - moduleSet(librariesKtor()) - moduleSet(librariesMisc()) - moduleSet(librariesTemporaryBundled()) - } - /** * Core library modules. */ - fun librariesCore(): ModuleSet = moduleSet("libraries.core") { + /** + * Core platform library modules required by ALL products including analysis tools. + * Contains universal utilities: serialization, compression, collections, parsing, networking. + * + * **Typical users:** All products (CodeServer, IDEA, PyCharm, etc.) + * + * **Note:** UI/IDE-specific libraries (JCEF, Jediterm, PTY4J, SSH) have been moved to `librariesIde()` + * + * @see librariesIde for UI and IDE-specific libraries + */ + fun librariesPlatform(): ModuleSet = moduleSet("libraries.platform") { embeddedModule("intellij.libraries.kotlin.reflect") // intellij.platform.wsl.impl and intellij.platform.util.http uses it embeddedModule("intellij.libraries.kotlinx.io") @@ -202,6 +59,7 @@ object CommunityModuleSets : ModuleSetProvider { embeddedModule("intellij.libraries.kotlinx.collections.immutable") embeddedModule("intellij.libraries.kotlinx.datetime") embeddedModule("intellij.libraries.kotlinx.html") + @Suppress("GrazieInspection") // Space plugin uses it and bundles into IntelliJ IDEA, but not bundles into DataGrip, so, or Space plugin should bundle this lib, // or IJ Platform. As it is a small library and consistency is important across other coroutine libs, bundle to IJ Platform. // note 2: despite what we use as "used by", AIA tests broken — @@ -247,10 +105,7 @@ object CommunityModuleSets : ModuleSetProvider { // used by intellij.platform.util.jdom, so, embedded embeddedModule("intellij.libraries.jaxen") embeddedModule("intellij.libraries.jbr") - embeddedModule("intellij.libraries.jcef") embeddedModule("intellij.libraries.jcip") - embeddedModule("intellij.libraries.jediterm.core") - embeddedModule("intellij.libraries.jediterm.ui") embeddedModule("intellij.libraries.jsoup") embeddedModule("intellij.libraries.jsonpath") embeddedModule("intellij.libraries.jsvg") @@ -259,25 +114,43 @@ object CommunityModuleSets : ModuleSetProvider { embeddedModule("intellij.libraries.kryo5") embeddedModule("intellij.libraries.lz4") embeddedModule("intellij.libraries.markdown") - embeddedModule("intellij.libraries.miglayout.swing") embeddedModule("intellij.libraries.mvstore") embeddedModule("intellij.libraries.oro.matcher") embeddedModule("intellij.libraries.proxy.vole") - embeddedModule("intellij.libraries.pty4j") - embeddedModule("intellij.libraries.rd.text") embeddedModule("intellij.libraries.rhino") embeddedModule("intellij.libraries.snakeyaml") embeddedModule("intellij.libraries.snakeyaml.engine") - embeddedModule("intellij.libraries.sshj") embeddedModule("intellij.libraries.stream") embeddedModule("intellij.libraries.velocity") - embeddedModule("intellij.libraries.winp") embeddedModule("intellij.libraries.xtext.xbase") embeddedModule("intellij.libraries.xz") } /** - * Ktor library modules. + * UI and IDE-specific library modules. + * Contains libraries for browser embedding, terminal UI, SSH, and other IDE features. + * + * **Typical use cases:** Full IDEs with user interface (IDEA, PyCharm, WebStorm, etc.) + * **Typical NON-users:** CodeServer (analysis-only tool), headless tools, pure analysis products + * + * **Note:** Image libraries (imgscalr, jsvg) are in `librariesPlatform()` as they're needed by `platform.util.ui` + */ + fun librariesIde(): ModuleSet = moduleSet("libraries.ide") { + embeddedModule("intellij.libraries.jcef") + embeddedModule("intellij.libraries.jediterm.core") + embeddedModule("intellij.libraries.jediterm.ui") + embeddedModule("intellij.libraries.miglayout.swing") + embeddedModule("intellij.libraries.pty4j") + embeddedModule("intellij.libraries.sshj") + embeddedModule("intellij.libraries.winp") + embeddedModule("intellij.libraries.rd.text") + } + + /** + * Ktor library modules for HTTP client communication. + * + * **Typical use cases:** RPC infrastructure, Remote Dev, Fleet backend, HTTP-based integrations + * **Typical NON-users:** CodeServer (analysis-only, no RPC), minimal IDEs without remote features */ fun librariesKtor(): ModuleSet = moduleSet("libraries.ktor") { embeddedModule("intellij.libraries.ktor.io") @@ -288,7 +161,12 @@ object CommunityModuleSets : ModuleSetProvider { } /** - * Miscellaneous library modules. + * Miscellaneous library modules for specialized use cases. + * + * **Note:** All libs here must NOT be embedded. If embedded, move to `librariesPlatform()` or `librariesIde()`. + * + * **Typical use cases:** XML-RPC communication, CSV parsing, document storage + * **Usage pattern:** Product-specific, not universally needed by all products */ fun librariesMisc(): ModuleSet = moduleSet("libraries.misc") { // all libs here must not be embedded, if it is embedded, it should be moved to libs-core.xml @@ -296,10 +174,17 @@ object CommunityModuleSets : ModuleSetProvider { module("intellij.libraries.xml.rpc") module("intellij.libraries.kotlinx.document.store.mvstore") module("intellij.libraries.opencsv") + module("intellij.libraries.lucene.common") } /** * Temporarily bundled library modules (planned to be removed). + * + * **⚠️ WARNING:** These are product-specific dependencies that should NOT be in core platform. + * + * **Current users:** Only DBE (DataGrip) - see jettison/xstream comments below + * **Goal:** Remove from `corePlatform` and move to specific products that need them + * **Typical NON-users:** Most products don't need these legacy libraries */ fun librariesTemporaryBundled(): ModuleSet = moduleSet("libraries.temporaryBundled") { // Currently used only by DBE (see https://youtrack.jetbrains.com/issue/IJPL-211789/CNFE-org.codehaus.jettison.mapped.Configuration). @@ -327,7 +212,6 @@ object CommunityModuleSets : ModuleSetProvider { module("intellij.platform.vcs.dvcs") module("intellij.platform.vcs.dvcs.impl") embeddedModule("intellij.platform.vcs") - embeddedModule("intellij.platform.diff.impl") moduleSet(vcsShared()) moduleSet(vcsFrontend()) @@ -442,47 +326,12 @@ object CommunityModuleSets : ModuleSetProvider { module("intellij.rd.ui") } - /** - * Fleet libraries used in IntelliJ Platform. They are built from sources and put in the distribution. - * Corresponds to intellij.moduleSets.fleet.xml - */ - fun fleet(): ModuleSet = moduleSet("libraries.fleet") { - // These modules are embedded because some of them are used by V1 platform modules - embeddedModule("fleet.andel") - embeddedModule("fleet.bifurcan") - embeddedModule("fleet.fastutil") - embeddedModule("fleet.kernel") - embeddedModule("fleet.multiplatform.shims") - embeddedModule("fleet.reporting.api") - embeddedModule("fleet.reporting.shared") - embeddedModule("fleet.rhizomedb") - embeddedModule("fleet.rpc") - embeddedModule("fleet.util.codepoints") - embeddedModule("fleet.util.core") - embeddedModule("fleet.util.logging.api") - embeddedModule("fleet.util.serialization") - - embeddedModule("fleet.rpc.server") - } - - /** - * Platform language base modules required by PlatformLangPlugin.xml. - * Contains core platform modules that are xi:included in PlatformLangPlugin.xml. - */ - fun platformLangBase(): ModuleSet = moduleSet("platformLangBase") { - embeddedModule("intellij.platform.builtInServer.impl") - embeddedModule("intellij.platform.smRunner") - embeddedModule("intellij.platform.externalSystem.dependencyUpdater") - embeddedModule("intellij.platform.externalSystem.impl") - embeddedModule("intellij.platform.externalProcessAuthHelper") - } - /** * IDE common modules (includes essential, compose, grid.core, vcs, xml, duplicates). */ fun ideCommon(): ModuleSet = moduleSet("ide.common") { // Include essential first (which includes libraries) - moduleSet(essential()) + moduleSet(CoreModuleSets.essential()) moduleSet(compose()) // Additional IDE-specific modules @@ -503,7 +352,6 @@ object CommunityModuleSets : ModuleSetProvider { module("intellij.platform.warmup") module("intellij.platform.inspect") module("intellij.settingsSync.core") - module("intellij.libraries.lucene.common") module("intellij.spellchecker") module("intellij.spellchecker.xml") module("intellij.platform.buildView") diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/CommunityProductFragments.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/CommunityProductFragments.kt index 3c0e2aa9bdb4..a1bbd8b246da 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/CommunityProductFragments.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/CommunityProductFragments.kt @@ -1,31 +1,16 @@ // 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.productLayout -import org.jetbrains.intellij.build.productLayout.CommunityModuleSets.platformLangBase - /** * Registry of reusable product content fragments for community products. * These fragments bundle commonly repeated patterns into reusable units. */ object CommunityProductFragments { - /** - * Platform Lang base fragment: includes PlatformLangPlugin.xml. - * - * PlatformLangPlugin.xml provides core platform language support including: - * - Platform language components and extensions - * - Core actions, refactoring, and editor support - * - * Use this fragment instead of manually calling deprecatedInclude("intellij.platform.resources", "META-INF/PlatformLangPlugin.xml"). - */ - fun platformLangBaseFragment(): ProductModulesContentSpec = productModules { - deprecatedInclude("intellij.platform.resources", "META-INF/PlatformLangPlugin.xml") - moduleSet(platformLangBase()) - } - /** * Java IDE base fragment: provides Java IDE module aliases and optional plugin includes. * * Includes: + * - PlatformLangPlugin.xml for platform language support * - Module aliases for Java IDE capability detection * - Optional remote servers support * - Optional UI Designer support @@ -34,7 +19,7 @@ object CommunityProductFragments { * Use this fragment for products that include Java IDE functionality. */ fun javaIdeBaseFragment(): ProductModulesContentSpec = productModules { - include(platformLangBaseFragment()) + deprecatedInclude("intellij.platform.resources", "META-INF/PlatformLangPlugin.xml") // Module capability aliases alias("com.intellij.modules.all") @@ -52,7 +37,7 @@ object CommunityProductFragments { * PyCharm Core fragment: provides PyCharm-specific platform extensions. * * Includes: - * - Platform language support (via platformLangBaseFragment) + * - PlatformLangPlugin.xml for platform language support * - Module capability alias for PyCharm * - Optional remote servers support * - PyCharm-specific extensions and actions (via pycharm-core.xml) @@ -62,7 +47,7 @@ object CommunityProductFragments { */ fun pycharmCoreFragment(): ProductModulesContentSpec = productModules { // Include platform lang base (PyCharm requires platform language support) - include(platformLangBaseFragment()) + deprecatedInclude("intellij.platform.resources", "META-INF/PlatformLangPlugin.xml") // Module capability alias alias("com.intellij.modules.pycharm") diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/CoreModuleSets.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/CoreModuleSets.kt new file mode 100644 index 000000000000..9fdd7db9f4b8 --- /dev/null +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/CoreModuleSets.kt @@ -0,0 +1,394 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@file:Suppress("GrazieInspection") + +package org.jetbrains.intellij.build.productLayout + +import org.jetbrains.intellij.build.productLayout.CommunityModuleSets.librariesIde +import org.jetbrains.intellij.build.productLayout.CommunityModuleSets.librariesKtor +import org.jetbrains.intellij.build.productLayout.CommunityModuleSets.librariesMisc +import org.jetbrains.intellij.build.productLayout.CommunityModuleSets.librariesPlatform +import org.jetbrains.intellij.build.productLayout.CommunityModuleSets.librariesTemporaryBundled +import org.jetbrains.intellij.build.productLayout.CoreModuleSets.coreIde +import org.jetbrains.intellij.build.productLayout.CoreModuleSets.coreLang +import org.jetbrains.intellij.build.productLayout.CoreModuleSets.corePlatform +import org.jetbrains.intellij.build.productLayout.CoreModuleSets.essential +import org.jetbrains.intellij.build.productLayout.CoreModuleSets.essentialMinimal +import org.jetbrains.intellij.build.productLayout.CoreModuleSets.fleet +import org.jetbrains.intellij.build.productLayout.CoreModuleSets.rpcBackend +import org.jetbrains.intellij.build.productLayout.CoreModuleSets.rpcMinimal + +/** + * Core platform module sets forming the foundation of IntelliJ products. + * + * This file contains the base module sets that provide the platform infrastructure: + * - **corePlatform**: Base platform without IDE (for analysis tools) + * - **coreIde**: Platform + basic IDE functionality + * - **coreLang**: Platform + IDE + language support + * - **essentialMinimal**: Lightweight IDE with editing + * - **essential**: Full essential IDE with debugging and navigation + * - **debugger**: Debugger platform + * - **rpc/rpcMinimal**: RPC infrastructure + * + * **Separated from CommunityModuleSets** to reduce file size and improve organization. + * Library module sets remain in CommunityModuleSets. + */ +object CoreModuleSets { + /** + * Core platform modules without IDE or language support. + * Contains base infrastructure for analysis and inspection tools. + * + * **Use when:** Building analysis/inspection tools that only need platform APIs and extension points + * + * **Example products:** + * - **CodeServer**: Analysis and inspection tool without IDE features + * - Other code analysis tools that only need platform APIs + * + * **Don't use for:** + * - Products needing IDE functionality → Use `coreIde()` instead + * - Products needing language support → Use `coreLang()` or `essentialMinimal()` + * - IDE products with editing capabilities → Use `essentialMinimal()` instead + * + * @see coreIde for platform with basic IDE functionality + * @see coreLang for platform with IDE and language support + * @see essentialMinimal for lightweight IDE with editing (most IDE products should use this) + */ + fun corePlatform(): ModuleSet = moduleSet("core.platform", selfContained = true, outputModule = "intellij.platform.ide.core") { + // Include only core libraries (universal infrastructure) + moduleSet(librariesPlatform()) + + embeddedModule("intellij.platform.util.ex", includeDependencies = true) + embeddedModule("intellij.platform.util.ui", includeDependencies = true) + + embeddedModule("intellij.platform.core", includeDependencies = true) + embeddedModule("intellij.platform.core.ui", includeDependencies = true) + embeddedModule("intellij.platform.core.impl", includeDependencies = true) + + embeddedModule("intellij.platform.projectModel", includeDependencies = true) + embeddedModule("intellij.platform.projectModel.impl", includeDependencies = true) + + // Analysis modules needed by core platform modules + embeddedModule("intellij.platform.analysis", includeDependencies = true) + embeddedModule("intellij.platform.analysis.impl", includeDependencies = true) + + // Include minimal RPC infrastructure AFTER core platform modules + // (kernel depends on platform.core, so core must be available first) + moduleSet(rpcMinimal()) + + embeddedModule("intellij.platform.ide.core", includeDependencies = true) + + // Note: intellij.platform.kernel now comes from rpcMinimal() above + } + + /** + * Core platform with basic IDE functionality. + * Adds IDE modules on top of platform infrastructure without language support. + * + * **Contents:** + * - `corePlatform()` (nested) - Base platform infrastructure + * - IDE module: intellij.platform.ide + * + * **Use when:** Building products that need IDE features but not language support + * + * **Architecture note:** This bridges the gap between pure platform (corePlatform) and + * full language-enabled IDE (coreLang). Most analysis tools won't need this. + * + * @see corePlatform for platform without IDE functionality + * @see coreLang for IDE with language support + */ + fun coreIde(): ModuleSet = moduleSet("core.ide") { + // Include core platform (util, core, projectModel, analysis, ide.core, kernel) + moduleSet(corePlatform()) + + // Add IDE-specific libraries (UI, terminal, browser, SSH) + moduleSet(librariesIde()) + + // Add basic IDE functionality on top of platform + embeddedModule("intellij.platform.ide", includeDependencies = true) + } + + /** + * Language support and IDE implementation modules for IntelliJ Platform. + * Builds on top of `coreIde()` to provide language features and IDE implementation. + * + * **Contents:** + * - `coreIde()` (nested) - Includes corePlatform + intellij.platform.ide + * - Language modules: lang.core, lang, lang.impl + * - IDE implementation: ide.impl (placed here because it depends on lang.core) + * - Additional dependencies: eel.impl, diff.impl, fleet.andel + * + * **Architecture note:** `ide.impl` is in this module set (not in coreIde) because + * it depends on `lang.core`. This resolves the circular dependency: + * coreIde → lang.core → ide.impl (all in proper order). + * + * **Use when:** Building products that need language support and IDE features but not + * the full essentialMinimal infrastructure (editor, search, RPC, backend/frontend split). + * + * **⚠️ WARNING:** Most products should use `essentialMinimal()` instead, which includes + * this module set plus essential IDE infrastructure (editor, search, RPC). + * + * Only use this directly if you need language features but want to exclude editor/search/RPC modules. + * + * **Products using this:** All products via `essentialMinimal()` which nests this module set + * + * @see coreIde for IDE functionality without language support + * @see corePlatform for base platform without IDE or language support + * @see essentialMinimal for full minimal IDE (includes this + RPC + editor + search) - RECOMMENDED + */ + fun coreLang(): ModuleSet = moduleSet("core.lang") { + // Include core IDE (corePlatform + intellij.platform.ide) + moduleSet(coreIde()) + + embeddedModule("intellij.platform.lang.core", includeDependencies = true) + embeddedModule("intellij.platform.lang", includeDependencies = true) + embeddedModule("intellij.platform.lang.impl", includeDependencies = true) + + // IDE implementation (depends on lang.core, so must come after) + embeddedModule("intellij.platform.ide.impl", includeDependencies = true) + + // Additional dependencies specific to lang.impl and ide.impl + embeddedModule("intellij.platform.ide.concurrency") + embeddedModule("intellij.platform.builtInServer") + embeddedModule("intellij.platform.externalSystem") + embeddedModule("intellij.platform.eel.impl") + embeddedModule("intellij.platform.diff") + embeddedModule("intellij.platform.diff.impl") + embeddedModule("fleet.andel", includeDependencies = true) + + // Temporary: lang.impl incorrectly depends on xstream (should be removed) + moduleSet(librariesTemporaryBundled()) + } + + /** + * Minimal essential platform modules required by lightweight IDE products WITH editing capabilities. + * + * **Contents:** + * - `coreLang()` (nested) - Includes corePlatform + language support + ide.impl + * - `rpcBackend()` - RPC backend/frontend split and topics (base RPC from corePlatform) + * - Backend/frontend split modules (settings, backend, project.backend, etc.) + * - Editor modules (editor, editor.backend) + * - Search modules (searchEverywhere with backend/frontend) + * - Inline completion + * + * **Total:** ~162 modules including all dependencies + * + * **Use when:** Building lightweight IDE products that provide code editing functionality + * + * **Example products:** + * - **GitClient**: Lightweight VCS IDE with editing - uses `essentialMinimal()` + `vcs()` + * - **Gateway**: Remote development gateway - uses `essentialMinimal()` + `vcs()` + `ssh()` + * + * **Don't use for:** + * - Analysis-only tools without editing (e.g., CodeServer) → Use `corePlatform()` instead + * + * **Hierarchy:** + * ``` + * essentialMinimal + * └─ coreLang + * └─ corePlatform + * └─ libraries + * ``` + * + * **Note:** Most IDE products should start with this module set or `essential()` (which includes this). + * Nested by `essential()` to avoid duplication. + * + * @see essential for full IDE with navigation, debugging, and more features + * @see coreLang for just language support without editor/search/RPC + * @see corePlatform for analysis tools without editing + */ + fun essentialMinimal(): ModuleSet = moduleSet("essential.minimal") { + // Lang includes corePlatform (which includes librariesPlatform) as nested set + moduleSet(coreLang()) + + // RPC backend functionality (base RPC/kernel already in corePlatform via rpcMinimal) + moduleSet(rpcBackend()) + + // Additional library sets not in corePlatform but needed by essentialMinimal+ + moduleSet(librariesKtor()) // For RPC/Remote Dev + moduleSet(librariesMisc()) // For specialized uses (XML-RPC, CSV, document store) + + // Credential store (needed by 36 products) + embeddedModule("intellij.platform.credentialStore.ui", includeDependencies = true) + embeddedModule("intellij.platform.credentialStore.impl", includeDependencies = true) + + // Core platform backend/frontend split + module("intellij.platform.settings.local") + module("intellij.platform.backend") + module("intellij.platform.project.backend") + module("intellij.platform.progress.backend") + module("intellij.platform.lang.impl.backend") + + // Frontend/monolith + module("intellij.platform.frontend") + module("intellij.platform.monolith") + + // Editor + module("intellij.platform.editor") + module("intellij.platform.editor.backend") + + // Search + module("intellij.platform.searchEverywhere") + module("intellij.platform.searchEverywhere.backend") + module("intellij.platform.searchEverywhere.frontend") + + // Completion + module("intellij.platform.inline.completion") + } + + /** + * Recent files support (both backend and frontend). + * Provides recently opened files UI and persistence. + */ + fun recentFiles(): ModuleSet = moduleSet("recentFiles") { + module("intellij.platform.recentFiles") + module("intellij.platform.recentFiles.frontend") + module("intellij.platform.recentFiles.backend") + } + + /** + * Essential platform modules required by most IDE products. + */ + fun essential(): ModuleSet = moduleSet("essential") { + // Include minimal essential modules (core backend/frontend, editor, search) + moduleSet(essentialMinimal()) + + // TODO: may be debugger shouldn't be essential? E.g. gateway doesn't need it. + moduleSet(debugger()) + + // The loading="embedded" attribute is required here because the intellij.platform.find module (which is loaded + // in embedded mode) has a compile dependency on intellij.platform.scopes. Without marking scopes as embedded, + // this would cause NoClassDefFoundError at runtime when classes from find try to use classes from scopes. + // This ensures proper classloader hierarchy is maintained for modules that depend on intellij.platform.scopes. + // This attribute should be removed once the find module no longer needs to be embedded. + embeddedModule("intellij.platform.scopes") + module("intellij.platform.scopes.backend") + + // todo navbar is not essential + module("intellij.platform.navbar") + module("intellij.platform.navbar.backend") + module("intellij.platform.navbar.frontend") + module("intellij.platform.navbar.monolith") + module("intellij.platform.clouds") + + module("intellij.platform.execution.serviceView") + module("intellij.platform.execution.serviceView.frontend") + module("intellij.platform.execution.serviceView.backend") + module("intellij.platform.execution.dashboard") + module("intellij.platform.execution.dashboard.frontend") + module("intellij.platform.execution.dashboard.backend") + + // The loading="embedded" attribute is required here for module synchronization with CWM's ThinClientFindAndReplaceExecutor. + // Since intellij.platform.frontend.split module loads in embedded mode, and it needs to override the default FindAndReplaceExecutor, + // the find module must also be marked as embedded to maintain proper dependency loading order. + // This attribute can be removed once ThinClientFindAndReplaceExecutor is removed. + embeddedModule("intellij.platform.find") + module("intellij.platform.find.backend") + module("intellij.platform.editor.frontend") + embeddedModule("intellij.platform.managed.cache") + module("intellij.platform.managed.cache.backend") + + module("intellij.platform.bookmarks.backend") + module("intellij.platform.bookmarks.frontend") + + moduleSet(recentFiles()) + + module("intellij.platform.pluginManager.shared") + module("intellij.platform.pluginManager.backend") + module("intellij.platform.pluginManager.frontend") + + module("intellij.platform.execution.impl.frontend") + module("intellij.platform.execution.impl.backend") + module("intellij.platform.eel.tcp") + + + module("intellij.platform.completion.common") + module("intellij.platform.completion.frontend") + module("intellij.platform.completion.backend") + + embeddedModule("intellij.platform.polySymbols") + + // Platform language modules (moved from platformLangBase for consolidation) + // These provide core IDE functionality needed by all full IDE products + embeddedModule("intellij.platform.builtInServer.impl", includeDependencies = true) + embeddedModule("intellij.platform.smRunner", includeDependencies = true) + embeddedModule("intellij.platform.externalSystem.dependencyUpdater", includeDependencies = true) + embeddedModule("intellij.platform.externalSystem.impl", includeDependencies = true) + embeddedModule("intellij.platform.externalProcessAuthHelper", includeDependencies = true) + } + + /** + * Provides the platform for implementing Debugger functionality. + */ + fun debugger(): ModuleSet = moduleSet("debugger") { + module("intellij.platform.debugger.impl.frontend") + module("intellij.platform.debugger.impl.backend") + embeddedModule("intellij.platform.debugger.impl.shared") + embeddedModule("intellij.platform.debugger.impl.rpc") + embeddedModule("intellij.platform.debugger.impl.ui") + embeddedModule("intellij.platform.debugger", includeDependencies = true) + embeddedModule("intellij.platform.debugger.impl", includeDependencies = true) + } + + fun fleet(): ModuleSet = moduleSet("fleet") { + // Same modules as fleet() - all are required + embeddedModule("fleet.bifurcan") + embeddedModule("fleet.fastutil") + embeddedModule("fleet.kernel", includeDependencies = true) + embeddedModule("fleet.multiplatform.shims") + embeddedModule("fleet.reporting.api") + embeddedModule("fleet.reporting.shared") + embeddedModule("fleet.rhizomedb") + embeddedModule("fleet.rpc", includeDependencies = true) + embeddedModule("fleet.util.codepoints") + embeddedModule("fleet.util.core") + embeddedModule("fleet.util.logging.api") + embeddedModule("fleet.util.serialization") + embeddedModule("fleet.rpc.server", includeDependencies = true) + } + + /** + * Minimal RPC infrastructure required by kernel and core platform modules. + * Contains fleet libraries and base RPC/kernel modules without backend/frontend split. + * + * **Use when:** Need minimal RPC support for modules like intellij.platform.kernel + * without full RPC backend/frontend/topics infrastructure + * + * **Note:** Backend modules (`rpc.backend`, `kernel.backend`, `topics.backend`) are in `rpcBackend()`, not here. + * + * **Total:** ~15 modules (13 from fleet + 2 platform modules) + * + * @see rpcBackend for full RPC functionality with backend/frontend split (includes kernel.backend) + * @see fleet for the fleet module set definition + */ + fun rpcMinimal(): ModuleSet = moduleSet("rpc.minimal", outputModule = "intellij.platform.ide.core") { + // All fleet modules (13 total) including transitive content dependencies + // All modules are content modules with XML descriptors, so splitting is not practical + moduleSet(fleet()) + + // Base RPC and kernel modules (backend modules are in rpc(), not here) + embeddedModule("intellij.platform.rpc", includeDependencies = true) + embeddedModule("intellij.platform.kernel", includeDependencies = true) + } + + /** + * Provides RPC backend/frontend split and topics support. + * + * **Assumes base RPC already available:** This module set extends `rpcMinimal()` which is included + * in `corePlatform()`. It only adds the backend/frontend/topics modules on top of the base. + * + * **Use when:** Building products that need full RPC functionality with backend separation. + * Products using `essentialMinimal()` get both `rpcMinimal()` (via corePlatform) and this module set. + * + * @see rpcMinimal for base RPC and kernel modules (included in corePlatform) + */ + fun rpcBackend(): ModuleSet = moduleSet("rpc.backend.extended") { + // Base RPC (rpcMinimal) already available from corePlatform + // Only add backend/frontend/topics functionality + module("intellij.platform.rpc.backend") + module("intellij.platform.kernel.backend") + module("intellij.platform.kernel.impl") + + embeddedModule("intellij.platform.rpc.topics") + module("intellij.platform.rpc.topics.backend") + module("intellij.platform.rpc.topics.frontend") + } +} diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/ModuleSetRunner.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/ModuleSetRunner.kt index 19f61fee62cc..6658b756a75b 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/ModuleSetRunner.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/ModuleSetRunner.kt @@ -10,11 +10,29 @@ import org.jetbrains.intellij.build.impl.JpsModuleOutputProvider import org.jetbrains.intellij.build.impl.bazelOutputRoot import org.jetbrains.intellij.build.productLayout.analysis.JsonFilter import org.jetbrains.intellij.build.productLayout.analysis.ModuleSetMetadata +import org.jetbrains.intellij.build.productLayout.analysis.ProductCategory import org.jetbrains.intellij.build.productLayout.analysis.ProductSpec import org.jetbrains.jps.model.serialization.JpsMavenSettings import org.jetbrains.jps.model.serialization.JpsSerializationManager import java.nio.file.Path +/** + * Determines product category based on module sets included in the content spec. + * + * @param contentSpec Product's module content specification + * @return ProductCategory based on which core module sets are used + */ +private fun determineProductCategory(contentSpec: ProductModulesContentSpec?): ProductCategory { + if (contentSpec == null) return ProductCategory.BACKEND + + val moduleSetNames = contentSpec.moduleSets.map { it.moduleSet.name } + return when { + "ide.ultimate" in moduleSetNames -> ProductCategory.ULTIMATE + "ide.common" in moduleSetNames -> ProductCategory.COMMUNITY + else -> ProductCategory.BACKEND + } +} + /** * Parses JSON argument from command line in the format `--json` or `--json='{"filter":"...","value":"..."}'`. * Returns null for full JSON output, or JsonFilter for filtered output. @@ -100,14 +118,24 @@ private suspend fun jsonResponse( ) { // Prepare all module sets with metadata val communityModuleSetsWithMeta = communityModuleSets.map { - ModuleSetMetadata(it, "community", communitySourceFile) + ModuleSetMetadata( + moduleSet = it, + location = "community", + sourceFile = communitySourceFile, + directNestedSets = it.nestedSets.map { nested -> nested.name } + ) } val ultimateModuleSetsWithMeta = if (ultimateSourceFile == null) { emptyList() } else { ultimateModuleSets.map { - ModuleSetMetadata(moduleSet = it, location = "ultimate", sourceFile = ultimateSourceFile) + ModuleSetMetadata( + moduleSet = it, + location = "ultimate", + sourceFile = ultimateSourceFile, + directNestedSets = it.nestedSets.map { nested -> nested.name } + ) } } val allModuleSets = communityModuleSetsWithMeta + ultimateModuleSetsWithMeta @@ -115,14 +143,15 @@ private suspend fun jsonResponse( // Discover regular products and add passed test products val regularProducts = discoverAllProducts(projectRoot, moduleOutputProvider).asSequence().map { // For test products (properties = null), use "test-product" as source file - val sourceFile = if (it.properties == null) { + val props = it.properties // Store in local val to enable smart cast + val sourceFile = if (props == null) { "test-product" } else { // Use JPS-based lookup to find actual source file in module source roots findProductPropertiesSourceFile( buildModules = it.config.modules, - productPropertiesClass = it.properties.javaClass, + productPropertiesClass = props.javaClass, moduleOutputProvider = moduleOutputProvider, projectRoot = projectRoot ) @@ -134,6 +163,7 @@ private suspend fun jsonResponse( pluginXmlPath = it.pluginXmlPath, contentSpec = it.spec, // Pass full ProductModulesContentSpec for complete DSL serialization buildModules = it.config.modules, + category = determineProductCategory(it.spec), ) } val testProductSpecs = testProducts.asSequence().map { (name, spec) -> @@ -147,10 +177,16 @@ private suspend fun jsonResponse( ) } - streamModuleAnalysisJson(allModuleSets = allModuleSets, products = (regularProducts + testProductSpecs).toList(), projectRoot = projectRoot, filter = parseJsonArgument(jsonArg)) + streamModuleAnalysisJson( + allModuleSets = allModuleSets, + products = (regularProducts + testProductSpecs).toList(), + projectRoot = projectRoot, + filter = parseJsonArgument(jsonArg), + moduleOutputProvider = moduleOutputProvider + ) } -internal fun createModuleOutputProvider(projectRoot: Path): ModuleOutputProvider { +fun createModuleOutputProvider(projectRoot: Path): ModuleOutputProvider { val project = JpsSerializationManager.getInstance().loadProject( projectRoot.toString(), mapOf("MAVEN_REPOSITORY" to JpsMavenSettings.getMavenRepositoryPath()), diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/ProductConfigurationRegistry.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/ProductConfigurationRegistry.kt index 53b58c229161..a419380c5f5a 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/ProductConfigurationRegistry.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/ProductConfigurationRegistry.kt @@ -1,17 +1,5 @@ // 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.productLayout -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -internal const val PRODUCT_REGISTRY_PATH = "build/dev-build.json" - -@Serializable -internal data class ProductConfigurationRegistry(@JvmField val products: Map) - -@Serializable -internal data class ProductConfiguration( - @JvmField val modules: List, - @JvmField @SerialName("class") val className: String, - @JvmField val pluginXmlPath: String? = null -) \ No newline at end of file +// Note: ProductConfigurationRegistry and ProductConfiguration have been moved to product-dsl module +// to support product discovery in the DSL layer without depending on full build-scripts module \ No newline at end of file diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/productXmlFileGenerator.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/productXmlFileGenerator.kt index 7cec1e1fbf1f..af166c08c4d2 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/productXmlFileGenerator.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/productXmlFileGenerator.kt @@ -2,90 +2,25 @@ package org.jetbrains.intellij.build.productLayout import kotlinx.serialization.json.Json -import org.jetbrains.intellij.build.BuildPaths import org.jetbrains.intellij.build.ModuleOutputProvider -import org.jetbrains.intellij.build.ProductProperties import org.jetbrains.intellij.build.dev.createProductProperties -import org.jetbrains.jps.model.java.JavaSourceRootType -import org.jetbrains.jps.model.java.JpsJavaExtensionService import java.nio.file.Files import java.nio.file.Path /** - * Represents a discovered product with all its metadata. - * Used by both XML and JSON generators to avoid code duplication. - * Test products have properties = null (they don't have ProductProperties classes). - */ -internal data class DiscoveredProduct( - @JvmField val name: String, - @JvmField val config: ProductConfiguration, - @JvmField val properties: ProductProperties?, - @JvmField val spec: ProductModulesContentSpec?, - @JvmField val pluginXmlPath: String?, -) - -/** - * Map of class FQN to actual file name for cases where class name != file name. - * Example: DotnetExternalProductProperties class is in ReSharperExternalProductProperties.kt file. - */ -private val CLASS_TO_FILE_NAME_OVERRIDES = mapOf( - "com.jetbrains.rider.build.product.DotnetExternalProductProperties" to "ReSharperExternalProductProperties.kt" -) - -/** - * Finds the actual source file for a ProductProperties class by searching JPS module source roots. - * This replaces hardcoded path mapping with actual file system lookup. - * - * @param buildModules List of build module names from dev-build.json (e.g., ["intellij.goland.build"]) - * @param productPropertiesClass The ProductProperties class to find - * @param moduleOutputProvider Provider for accessing JPS modules - * @param projectRoot Project root path for making paths relative - * @return Relative path to the source file - */ -fun findProductPropertiesSourceFile( - buildModules: List, - productPropertiesClass: Class<*>, - moduleOutputProvider: ModuleOutputProvider, - projectRoot: Path -): String { - val className = productPropertiesClass.name - - // Handle special cases where class name != file name - val fileName = CLASS_TO_FILE_NAME_OVERRIDES[className] ?: "${className.substringAfterLast('.')}.kt" - val packagePath = className.substringBeforeLast('.').replace('.', '/') - val relativePath = "$packagePath/$fileName" - - // Search each build module's source roots - for (buildModuleName in buildModules) { - val jpsModule = moduleOutputProvider.findModule(buildModuleName) ?: continue - - // Search production source roots (not test roots) - val sourceFile = jpsModule.sourceRoots - .asSequence() - .filter { it.rootType == JavaSourceRootType.SOURCE } - .firstNotNullOfOrNull { sourceRoot -> - JpsJavaExtensionService.getInstance().findSourceFile(sourceRoot, relativePath) - } - - if (sourceFile != null) { - return projectRoot.relativize(sourceFile).toString() - } - } - - throw IllegalStateException("Cannot find source file for $productPropertiesClass (searched for $relativePath in modules: $buildModules)") -} - -/** - * Discovers all products from dev-build.json registry (internal representation). - * Returns DiscoveredProduct instances for internal use by XML generator. + * Discovers all products from dev-build.json registry with their ProductProperties instances. + * This is the only function in build-scripts that depends on ProductProperties. + * All other product-related generation logic has been moved to product-dsl module. * * @param projectRoot The project root path - * @return List of discovered products with all metadata + * @param moduleOutputProvider Module output provider for creating ProductProperties + * @return List of discovered products with ProductProperties instances */ -internal suspend fun discoverAllProducts(projectRoot: Path, moduleOutputProvider: ModuleOutputProvider): List { +suspend fun discoverAllProducts(projectRoot: Path, moduleOutputProvider: ModuleOutputProvider): List { val jsonContent = Files.readString(projectRoot.resolve(PRODUCT_REGISTRY_PATH)) val productToConfiguration = Json.decodeFromString(jsonContent).products val products = mutableListOf() + for ((productName, productConfig) in productToConfiguration) { val productProperties = createProductProperties( productConfiguration = productConfig, @@ -98,7 +33,7 @@ internal suspend fun discoverAllProducts(projectRoot: Path, moduleOutputProvider DiscoveredProduct( name = productName, config = productConfig, - properties = productProperties, + properties = productProperties, // ProductProperties instance spec = spec, pluginXmlPath = productConfig.pluginXmlPath ) @@ -108,24 +43,25 @@ internal suspend fun discoverAllProducts(projectRoot: Path, moduleOutputProvider return products } - /** * Discovers all products for validation purposes. - * Returns list of (productName, ProductModulesContentSpec) pairs. + * Convenience wrapper that calls discoverAllProducts() and extracts validation data. * * @param projectRoot The project root path + * @param moduleOutputProvider Module output provider * @return List of product name and spec pairs for validation */ suspend fun discoverAllProductsForValidation(projectRoot: Path, moduleOutputProvider: ModuleOutputProvider): List> { - return discoverAllProducts(projectRoot, moduleOutputProvider).map { it.name to it.spec } + return extractProductsForValidation(discoverAllProducts(projectRoot, moduleOutputProvider)) } /** * Generates product XMLs for all products using programmatic content. - * Discovers products from dev-build.json and accepts test products as parameter, then generates complete plugin.xml files. + * Convenience wrapper that discovers products then delegates to product-dsl generation logic. * * @param projectRoot The project root path * @param testProductSpecs Test product specifications (name to ProductModulesContentSpec pairs) + * @param moduleOutputProvider Module output provider * @return Result containing generation statistics */ suspend fun generateAllProductXmlFiles( @@ -133,50 +69,11 @@ suspend fun generateAllProductXmlFiles( testProductSpecs: List> = emptyList(), moduleOutputProvider: ModuleOutputProvider, ): ProductGenerationResult { - val regularProducts = discoverAllProducts(projectRoot, moduleOutputProvider) - - // Convert test product specs to DiscoveredProduct instances - val testProducts = testProductSpecs.mapNotNull { (name, spec) -> - val xmlPath = "ultimate/platform-ultimate/testResources/META-INF/${name}Plugin.xml" - val xmlFile = projectRoot.resolve(xmlPath) - if (Files.notExists(xmlFile)) { - return@mapNotNull null - } - - DiscoveredProduct( - name = name, - config = ProductConfiguration( - className = "test-product", - modules = emptyList(), - pluginXmlPath = xmlPath, - ), - properties = null, - spec = spec, - pluginXmlPath = xmlPath, - ) - } - - val products = regularProducts + testProducts - - // Detect if this is an Ultimate build (projectRoot != communityRoot) - val isUltimateBuild = projectRoot != BuildPaths.COMMUNITY_ROOT.communityRoot - - val productResults = products.mapNotNull { discovered -> - // Skip products without pluginXmlPath or spec configured - val pluginXmlRelativePath = discovered.pluginXmlPath ?: return@mapNotNull null - val spec = discovered.spec ?: return@mapNotNull null - - val pluginXmlPath = projectRoot.resolve(pluginXmlRelativePath) - generateProductXml( - pluginXmlPath = pluginXmlPath, - spec = spec, - productName = discovered.name, - moduleOutputProvider = moduleOutputProvider, - productPropertiesClass = discovered.properties?.javaClass?.name ?: "test-product", - projectRoot = projectRoot, - isUltimateBuild = isUltimateBuild, - ) - } - - return ProductGenerationResult(productResults) + val discoveredProducts = discoverAllProducts(projectRoot, moduleOutputProvider) + return generateAllProductXmlFiles( + discoveredProducts = discoveredProducts, + testProductSpecs = testProductSpecs, + projectRoot = projectRoot, + moduleOutputProvider = moduleOutputProvider + ) } \ No newline at end of file diff --git a/platform/build-scripts/testFramework/src/com/intellij/platform/buildScripts/testFramework/pluginModel/ModuleBasedPluginLayoutProvider.kt b/platform/build-scripts/testFramework/src/com/intellij/platform/buildScripts/testFramework/pluginModel/ModuleBasedPluginLayoutProvider.kt index 3c94a0bead9f..a45e8841d349 100644 --- a/platform/build-scripts/testFramework/src/com/intellij/platform/buildScripts/testFramework/pluginModel/ModuleBasedPluginLayoutProvider.kt +++ b/platform/build-scripts/testFramework/src/com/intellij/platform/buildScripts/testFramework/pluginModel/ModuleBasedPluginLayoutProvider.kt @@ -1,6 +1,7 @@ // 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.buildScripts.testFramework.pluginModel +import com.intellij.platform.distributionContent.testFramework.deserializeContentData import com.intellij.platform.runtime.product.ProductMode import com.intellij.platform.runtime.product.ProductModules import com.intellij.platform.runtime.product.RuntimeModuleLoadingRule @@ -14,22 +15,14 @@ import org.jetbrains.jps.model.java.JpsJavaExtensionService import org.jetbrains.jps.model.module.JpsModule import java.io.InputStream import java.nio.file.Path +import kotlin.io.path.exists import kotlin.io.path.inputStream import kotlin.io.path.pathString +import kotlin.io.path.readText -fun createLayoutProviderForProductWithModuleBasedLoader( - project: JpsProject, - runtimeModuleRepository: RuntimeModuleRepository, - productRootModuleName: String, - productMode: ProductMode, - corePluginDescriptorPath: String, -): PluginLayoutProvider { - return ModuleBasedPluginLayoutProvider(project, runtimeModuleRepository, productRootModuleName, productMode, corePluginDescriptorPath) -} - -private class ModuleBasedPluginLayoutProvider( +class ModuleBasedPluginLayoutProvider( private val project: JpsProject, - private val runtimeModuleRepository: RuntimeModuleRepository, + runtimeModuleRepository: RuntimeModuleRepository, private val productRootModuleName: String, productMode: ProductMode, private val corePluginDescriptorPath: String, @@ -38,9 +31,10 @@ private class ModuleBasedPluginLayoutProvider( private val mainModulesOfBundledPlugins: Set init { - val productRootModule = project.findModuleByName(productRootModuleName) ?: error("Cannot find module '$productRootModuleName'") - val productModulesPath = productRootModule.findProductionFile("META-INF/$productRootModuleName/product-modules.xml") - ?: error("Cannot find product-modules.xml in '$productRootModuleName' module") + val productRootModule = requireNotNull(project.findModuleByName(productRootModuleName)) { "Cannot find module '$productRootModuleName'" } + val productModulesPath = requireNotNull(productRootModule.findProductionFile("META-INF/$productRootModuleName/product-modules.xml")) { + "Cannot find product-modules.xml in '$productRootModuleName' module" + } val resourceFileResolver = object : ResourceFileResolver { override fun readResourceFile(moduleId: RuntimeModuleId, relativePath: String): InputStream? { val module = project.findModuleByName(moduleId.stringId) ?: return null @@ -48,7 +42,7 @@ private class ModuleBasedPluginLayoutProvider( } } productModules = ProductModulesSerialization.loadProductModules( - productModulesPath.inputStream(), + productModulesPath.inputStream(), productModulesPath.pathString, productMode, runtimeModuleRepository, @@ -81,9 +75,9 @@ private class ModuleBasedPluginLayoutProvider( .mapNotNull { project.findModuleByName(it) } - val mainModule = mainGroupModules.find { - it.findProductionFile(corePluginDescriptorPath) != null - } ?: error("Cannot find '$corePluginDescriptorPath' in the main module group of '$productRootModuleName'") + val mainModule = requireNotNull(mainGroupModules.find { it.findProductionFile(corePluginDescriptorPath) != null }) { + "Cannot find '$corePluginDescriptorPath' in the main module group of '$productRootModuleName'" + } return PluginLayoutDescription( mainJpsModule = mainModule.name, @@ -101,6 +95,20 @@ private class ModuleBasedPluginLayoutProvider( return null } + // Try to load plugin-content.yaml if it exists + val contentDataPath = mainModule.findProductionFile("plugin-content.yaml") + if (contentDataPath != null && contentDataPath.exists()) { + val contentData = deserializeContentData(contentDataPath.readText()) + return toPluginLayoutDescription( + entries = contentData, + mainModuleName = mainModule.name, + pluginDescriptorPath = "META-INF/plugin.xml", + mainLibDir = "lib", + jarsToIgnore = emptySet() + ) + } + + // Fallback: just the main module return PluginLayoutDescription( mainJpsModule = mainModule.name, pluginDescriptorPath = "META-INF/plugin.xml", diff --git a/platform/build-scripts/testFramework/src/com/intellij/platform/buildScripts/testFramework/pluginModel/PluginLayoutDescription.kt b/platform/build-scripts/testFramework/src/com/intellij/platform/buildScripts/testFramework/pluginModel/PluginLayoutDescription.kt index 28d4ecaddfa9..27d263e148bd 100644 --- a/platform/build-scripts/testFramework/src/com/intellij/platform/buildScripts/testFramework/pluginModel/PluginLayoutDescription.kt +++ b/platform/build-scripts/testFramework/src/com/intellij/platform/buildScripts/testFramework/pluginModel/PluginLayoutDescription.kt @@ -1,15 +1,14 @@ // 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.buildScripts.testFramework.pluginModel -import com.intellij.platform.plugins.testFramework.resolveModuleSet import com.intellij.platform.distributionContent.testFramework.FileEntry import com.intellij.platform.distributionContent.testFramework.deserializeContentData +import com.intellij.platform.plugins.testFramework.resolveModuleSet import org.jetbrains.jps.model.JpsProject import org.jetbrains.jps.model.java.JpsJavaExtensionService import org.jetbrains.jps.model.module.JpsModule import org.jetbrains.jps.util.JpsPathUtil import java.nio.file.Path -import kotlin.io.path.Path import kotlin.io.path.exists import kotlin.io.path.readText @@ -86,13 +85,13 @@ private class YamlFileBasedPluginLayoutProvider( .distinct() for (moduleName in (productModuleNames + productEmbeddedModuleNames)) { - loadAndMergeModuleContent(moduleName, "dist.all/lib/$moduleName.jar", baseEntries) + loadAndMergeModuleContent(moduleName, baseEntries) } return baseEntries } - private fun loadAndMergeModuleContent(moduleName: String, jarName: String, baseEntries: MutableList) { + private fun loadAndMergeModuleContent(moduleName: String, baseEntries: MutableList) { val module = project.findModuleByName(moduleName) ?: return val contentRootUrl = module.contentRootsList.urls.firstOrNull() ?: return val moduleContentPath = JpsPathUtil.urlToNioPath(contentRootUrl).resolve("module-content.yaml") @@ -106,7 +105,7 @@ private class YamlFileBasedPluginLayoutProvider( // replace placeholder with actual jar path for (entry in moduleEntries) { if (entry.name == "") { - baseEntries.add(entry.copy(name = jarName)) + baseEntries.add(entry.copy(name = "dist.all/lib/$moduleName.jar")) } else { baseEntries.add(entry) @@ -158,7 +157,7 @@ private class YamlFileBasedPluginLayoutProvider( get() = "Note that the test uses the data from *content.yaml files, so if you changed the layouts, run '$nameOfTestWhichGeneratesFiles' to make sure that they are up-to-date." } -private fun toPluginLayoutDescription( +internal fun toPluginLayoutDescription( entries: List, mainModuleName: String, pluginDescriptorPath: String, diff --git a/platform/built-in-server-api/module-content.yaml b/platform/built-in-server-api/module-content.yaml new file mode 100644 index 000000000000..3aef35398ebe --- /dev/null +++ b/platform/built-in-server-api/module-content.yaml @@ -0,0 +1,3 @@ +- name: dist.all/lib/intellij.platform.builtInServer.jar + modules: + - name: intellij.platform.builtInServer \ No newline at end of file diff --git a/platform/built-in-server-api/resources/intellij.platform.builtInServer.xml b/platform/built-in-server-api/resources/intellij.platform.builtInServer.xml new file mode 100644 index 000000000000..aa52f3dbdcd0 --- /dev/null +++ b/platform/built-in-server-api/resources/intellij.platform.builtInServer.xml @@ -0,0 +1,2 @@ + + diff --git a/platform/built-in-server/resources/intellij.platform.builtInServer.impl.xml b/platform/built-in-server/resources/intellij.platform.builtInServer.impl.xml index 44c2615229ab..08cb69a70fea 100644 --- a/platform/built-in-server/resources/intellij.platform.builtInServer.impl.xml +++ b/platform/built-in-server/resources/intellij.platform.builtInServer.impl.xml @@ -1,7 +1,24 @@ + + + + + + + + + + + + + + + + + diff --git a/platform/core-api/module-content.yaml b/platform/core-api/module-content.yaml new file mode 100644 index 000000000000..c72ac54d6f66 --- /dev/null +++ b/platform/core-api/module-content.yaml @@ -0,0 +1,5 @@ +- name: dist.all/lib/intellij.platform.core.jar + modules: + - name: intellij.platform.core + - name: intellij.platform.util.coroutines + - name: intellij.platform.util.progress \ No newline at end of file diff --git a/platform/core-api/resources/intellij.platform.core.xml b/platform/core-api/resources/intellij.platform.core.xml new file mode 100644 index 000000000000..c1ce65986a53 --- /dev/null +++ b/platform/core-api/resources/intellij.platform.core.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/platform/core-impl/module-content.yaml b/platform/core-impl/module-content.yaml new file mode 100644 index 000000000000..a94770f92790 --- /dev/null +++ b/platform/core-impl/module-content.yaml @@ -0,0 +1,5 @@ +- name: dist.all/lib/intellij.platform.core.impl.jar + modules: + - name: intellij.platform.core.impl + - name: intellij.platform.plugins.parser.impl + - name: intellij.platform.syntax \ No newline at end of file diff --git a/platform/core-impl/resources/intellij.platform.core.impl.xml b/platform/core-impl/resources/intellij.platform.core.impl.xml new file mode 100644 index 000000000000..02f5766bc149 --- /dev/null +++ b/platform/core-impl/resources/intellij.platform.core.impl.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/platform/core-impl/src/com/intellij/ide/plugins/PluginDescriptorLoader.kt b/platform/core-impl/src/com/intellij/ide/plugins/PluginDescriptorLoader.kt index 001b295a2743..9983e95a097c 100644 --- a/platform/core-impl/src/com/intellij/ide/plugins/PluginDescriptorLoader.kt +++ b/platform/core-impl/src/com/intellij/ide/plugins/PluginDescriptorLoader.kt @@ -847,6 +847,7 @@ fun isProductWithTheOnlyDescriptor(platformPrefix: String): Boolean { platformPrefix == PlatformUtils.DBE_PREFIX || platformPrefix == PlatformUtils.DATASPELL_PREFIX || platformPrefix == PlatformUtils.GATEWAY_PREFIX || + platformPrefix == "CodeServer" || platformPrefix == PlatformUtils.GIT_CLIENT_PREFIX } diff --git a/platform/core-ui/BUILD.bazel b/platform/core-ui/BUILD.bazel index 95ae7c27b0ea..46e30176d20b 100644 --- a/platform/core-ui/BUILD.bazel +++ b/platform/core-ui/BUILD.bazel @@ -1,11 +1,18 @@ ### auto-generated section `build intellij.platform.core.ui` start -load("@rules_jvm//:jvm.bzl", "jvm_library") +load("@rules_jvm//:jvm.bzl", "jvm_library", "resourcegroup") + +resourcegroup( + name = "core-ui_resources", + srcs = glob(["resources/**/*"]), + strip_prefix = "resources" +) jvm_library( name = "core-ui", module_name = "intellij.platform.core.ui", visibility = ["//visibility:public"], srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True), + resources = [":core-ui_resources"], deps = [ "//platform/util", "//platform/core-api:core", diff --git a/platform/core-ui/intellij.platform.core.ui.iml b/platform/core-ui/intellij.platform.core.ui.iml index 35e8e7b331ca..de297648caaa 100644 --- a/platform/core-ui/intellij.platform.core.ui.iml +++ b/platform/core-ui/intellij.platform.core.ui.iml @@ -3,6 +3,7 @@ + diff --git a/platform/core-ui/module-content.yaml b/platform/core-ui/module-content.yaml new file mode 100644 index 000000000000..c07b56e8df3f --- /dev/null +++ b/platform/core-ui/module-content.yaml @@ -0,0 +1,3 @@ +- name: dist.all/lib/intellij.platform.core.ui.jar + modules: + - name: intellij.platform.core.ui \ No newline at end of file diff --git a/platform/core-ui/resources/intellij.platform.core.ui.xml b/platform/core-ui/resources/intellij.platform.core.ui.xml new file mode 100644 index 000000000000..9a3dca761d73 --- /dev/null +++ b/platform/core-ui/resources/intellij.platform.core.ui.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/platform/credential-store-impl/module-content.yaml b/platform/credential-store-impl/module-content.yaml new file mode 100644 index 000000000000..9aba4173096c --- /dev/null +++ b/platform/credential-store-impl/module-content.yaml @@ -0,0 +1,7 @@ +- name: dist.all/lib/intellij.platform.credentialStore.impl.jar + modules: + - name: intellij.platform.credentialStore.impl + libraries: + dbus-java: + - name: $MAVEN_REPOSITORY$/com/github/hypfvieh/dbus-java-transport-native-unixsocket/4/dbus-java-transport-native-unixsocket-4.jar + - name: $MAVEN_REPOSITORY$/com/github/hypfvieh/dbus-java-core/4/dbus-java-core-4.jar \ No newline at end of file diff --git a/platform/credential-store-impl/resources/META-INF/credential-store.xml b/platform/credential-store-impl/resources/intellij.platform.credentialStore.impl.xml similarity index 56% rename from platform/credential-store-impl/resources/META-INF/credential-store.xml rename to platform/credential-store-impl/resources/intellij.platform.credentialStore.impl.xml index 92fb59bb5707..5e6a1b697373 100644 --- a/platform/credential-store-impl/resources/META-INF/credential-store.xml +++ b/platform/credential-store-impl/resources/intellij.platform.credentialStore.impl.xml @@ -1,4 +1,19 @@ + + + + + + + + + + + + + + + + diff --git a/platform/credential-store-ui/module-content.yaml b/platform/credential-store-ui/module-content.yaml new file mode 100644 index 000000000000..006d9ab9cf1e --- /dev/null +++ b/platform/credential-store-ui/module-content.yaml @@ -0,0 +1,3 @@ +- name: dist.all/lib/intellij.platform.credentialStore.ui.jar + modules: + - name: intellij.platform.credentialStore.ui \ No newline at end of file diff --git a/platform/credential-store-ui/resources/intellij.platform.credentialStore.ui.xml b/platform/credential-store-ui/resources/intellij.platform.credentialStore.ui.xml new file mode 100644 index 000000000000..11534da39d91 --- /dev/null +++ b/platform/credential-store-ui/resources/intellij.platform.credentialStore.ui.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/platform/diff-api/module-content.yaml b/platform/diff-api/module-content.yaml new file mode 100644 index 000000000000..4eb139fe2a2f --- /dev/null +++ b/platform/diff-api/module-content.yaml @@ -0,0 +1,3 @@ +- name: dist.all/lib/intellij.platform.diff.jar + modules: + - name: intellij.platform.diff \ No newline at end of file diff --git a/platform/diff-api/resources/intellij.platform.diff.xml b/platform/diff-api/resources/intellij.platform.diff.xml new file mode 100644 index 000000000000..bf03841e6f89 --- /dev/null +++ b/platform/diff-api/resources/intellij.platform.diff.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/platform/diff-impl/resources/intellij.platform.diff.impl.xml b/platform/diff-impl/resources/intellij.platform.diff.impl.xml index af25f0e80387..8deb59de5a70 100644 --- a/platform/diff-impl/resources/intellij.platform.diff.impl.xml +++ b/platform/diff-impl/resources/intellij.platform.diff.impl.xml @@ -1,4 +1,20 @@ + + + + + + + + + + + + + + + + diff --git a/platform/eel-impl/resources/intellij.platform.eel.impl.xml b/platform/eel-impl/resources/intellij.platform.eel.impl.xml index 8ada7a94e606..1252ff950c3f 100644 --- a/platform/eel-impl/resources/intellij.platform.eel.impl.xml +++ b/platform/eel-impl/resources/intellij.platform.eel.impl.xml @@ -1,4 +1,13 @@ + + + + + + + + + diff --git a/platform/external-process-auth-helper/resources/intellij.platform.externalProcessAuthHelper.xml b/platform/external-process-auth-helper/resources/intellij.platform.externalProcessAuthHelper.xml index bd8704d78bc1..551fe786512c 100644 --- a/platform/external-process-auth-helper/resources/intellij.platform.externalProcessAuthHelper.xml +++ b/platform/external-process-auth-helper/resources/intellij.platform.externalProcessAuthHelper.xml @@ -1,4 +1,13 @@ + + + + + + + + + diff --git a/platform/external-system-api/dependency-updater/resources/intellij.platform.externalSystem.dependencyUpdater.xml b/platform/external-system-api/dependency-updater/resources/intellij.platform.externalSystem.dependencyUpdater.xml index ab59c22dd7d2..1bea33141483 100644 --- a/platform/external-system-api/dependency-updater/resources/intellij.platform.externalSystem.dependencyUpdater.xml +++ b/platform/external-system-api/dependency-updater/resources/intellij.platform.externalSystem.dependencyUpdater.xml @@ -1,7 +1,13 @@ + + + + + + diff --git a/platform/external-system-api/module-content.yaml b/platform/external-system-api/module-content.yaml new file mode 100644 index 000000000000..1d7d99ccb168 --- /dev/null +++ b/platform/external-system-api/module-content.yaml @@ -0,0 +1,3 @@ +- name: dist.all/lib/intellij.platform.externalSystem.jar + modules: + - name: intellij.platform.externalSystem \ No newline at end of file diff --git a/platform/external-system-api/resources/intellij.platform.externalSystem.xml b/platform/external-system-api/resources/intellij.platform.externalSystem.xml new file mode 100644 index 000000000000..aa52f3dbdcd0 --- /dev/null +++ b/platform/external-system-api/resources/intellij.platform.externalSystem.xml @@ -0,0 +1,2 @@ + + diff --git a/platform/external-system-impl/resources/intellij.platform.externalSystem.impl.xml b/platform/external-system-impl/resources/intellij.platform.externalSystem.impl.xml index 00275f17134d..9f4282717d0f 100644 --- a/platform/external-system-impl/resources/intellij.platform.externalSystem.impl.xml +++ b/platform/external-system-impl/resources/intellij.platform.externalSystem.impl.xml @@ -1,7 +1,25 @@ + + + + + + + + + + + + + + + + + + diff --git a/platform/ide-core/module-content.yaml b/platform/ide-core/module-content.yaml new file mode 100644 index 000000000000..58199f6669c5 --- /dev/null +++ b/platform/ide-core/module-content.yaml @@ -0,0 +1,4 @@ +- name: dist.all/lib/intellij.platform.ide.core.jar + modules: + - name: intellij.platform.ide.core + - name: intellij.platform.settings \ No newline at end of file diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.xml b/platform/ide-core/resources/META-INF/intellij.moduleSets.core.platform.xml similarity index 71% rename from platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.xml rename to platform/ide-core/resources/META-INF/intellij.moduleSets.core.platform.xml index b939ad66fca9..a2dac60f301d 100644 --- a/platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.xml +++ b/platform/ide-core/resources/META-INF/intellij.moduleSets.core.platform.xml @@ -1,10 +1,10 @@ - - + + - + @@ -50,10 +50,7 @@ - - - @@ -62,43 +59,52 @@ - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - + + + + - - - - + + + + + + + + + + + - \ No newline at end of file diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.rpc.xml b/platform/ide-core/resources/META-INF/intellij.moduleSets.rpc.minimal.xml similarity index 67% rename from platform/platform-resources/generated/META-INF/intellij.moduleSets.rpc.xml rename to platform/ide-core/resources/META-INF/intellij.moduleSets.rpc.minimal.xml index 98d75fe06792..b633de8a943f 100644 --- a/platform/platform-resources/generated/META-INF/intellij.moduleSets.rpc.xml +++ b/platform/ide-core/resources/META-INF/intellij.moduleSets.rpc.minimal.xml @@ -1,11 +1,10 @@ - - + + - - + @@ -24,12 +23,6 @@ - - - - - - \ No newline at end of file diff --git a/platform/ide-core/resources/intellij.platform.ide.core.xml b/platform/ide-core/resources/intellij.platform.ide.core.xml new file mode 100644 index 000000000000..32e00e695b93 --- /dev/null +++ b/platform/ide-core/resources/intellij.platform.ide.core.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/kernel/backend/resources/intellij.platform.kernel.backend.xml b/platform/kernel/backend/resources/intellij.platform.kernel.backend.xml index b61e34e4e92b..4fdf4c8e02f8 100644 --- a/platform/kernel/backend/resources/intellij.platform.kernel.backend.xml +++ b/platform/kernel/backend/resources/intellij.platform.kernel.backend.xml @@ -1,7 +1,6 @@ - + - diff --git a/platform/kernel/rpc/resources/intellij.platform.rpc.xml b/platform/kernel/rpc/resources/intellij.platform.rpc.xml index 99fa362ad8cb..db319820a3ae 100644 --- a/platform/kernel/rpc/resources/intellij.platform.rpc.xml +++ b/platform/kernel/rpc/resources/intellij.platform.rpc.xml @@ -1,6 +1,9 @@ + - + + + diff --git a/platform/kernel/shared/module-content.yaml b/platform/kernel/shared/module-content.yaml index 04d553fb750a..06a0ed74d225 100644 --- a/platform/kernel/shared/module-content.yaml +++ b/platform/kernel/shared/module-content.yaml @@ -1,3 +1,4 @@ - name: dist.all/lib/intellij.platform.kernel.jar modules: - - name: intellij.platform.kernel \ No newline at end of file + - name: intellij.platform.kernel + - name: intellij.platform.pasta \ No newline at end of file diff --git a/platform/kernel/shared/resources/intellij.platform.kernel.xml b/platform/kernel/shared/resources/intellij.platform.kernel.xml index 4e04e5ff9970..dfc4bf48a64c 100644 --- a/platform/kernel/shared/resources/intellij.platform.kernel.xml +++ b/platform/kernel/shared/resources/intellij.platform.kernel.xml @@ -1,8 +1,12 @@ + - + + + + diff --git a/platform/lang-api/module-content.yaml b/platform/lang-api/module-content.yaml new file mode 100644 index 000000000000..91999db88dd0 --- /dev/null +++ b/platform/lang-api/module-content.yaml @@ -0,0 +1,7 @@ +- name: dist.all/lib/intellij.platform.lang.jar + modules: + - name: intellij.platform.backend.observation + - name: intellij.platform.ide.core.impl + - name: intellij.platform.indexing + - name: intellij.platform.lang + - name: intellij.platform.refactoring \ No newline at end of file diff --git a/platform/lang-api/resources/intellij.platform.lang.xml b/platform/lang-api/resources/intellij.platform.lang.xml new file mode 100644 index 000000000000..a2c06ba2e8a7 --- /dev/null +++ b/platform/lang-api/resources/intellij.platform.lang.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/lang-core/module-content.yaml b/platform/lang-core/module-content.yaml new file mode 100644 index 000000000000..27a2cd6b97a8 --- /dev/null +++ b/platform/lang-core/module-content.yaml @@ -0,0 +1,3 @@ +- name: dist.all/lib/intellij.platform.lang.core.jar + modules: + - name: intellij.platform.lang.core \ No newline at end of file diff --git a/platform/lang-core/resources/intellij.platform.lang.core.xml b/platform/lang-core/resources/intellij.platform.lang.core.xml new file mode 100644 index 000000000000..c5e291952375 --- /dev/null +++ b/platform/lang-core/resources/intellij.platform.lang.core.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/lang-impl/module-content.yaml b/platform/lang-impl/module-content.yaml index 378d75cefad0..042e844b3276 100644 --- a/platform/lang-impl/module-content.yaml +++ b/platform/lang-impl/module-content.yaml @@ -10,7 +10,6 @@ - name: intellij.platform.ide.initialConfigImport - name: intellij.platform.ide.internal - name: intellij.platform.ide.updateChecker - - name: intellij.platform.indexing.impl - name: intellij.platform.lang.impl - name: intellij.platform.wsl.impl library: googlecode.plist.dd diff --git a/platform/lang-impl/resources/intellij.platform.lang.impl.xml b/platform/lang-impl/resources/intellij.platform.lang.impl.xml index bf03841e6f89..792a8508b764 100644 --- a/platform/lang-impl/resources/intellij.platform.lang.impl.xml +++ b/platform/lang-impl/resources/intellij.platform.lang.impl.xml @@ -1,2 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/platform-api/module-content.yaml b/platform/platform-api/module-content.yaml new file mode 100644 index 000000000000..6d04cc823ba4 --- /dev/null +++ b/platform/platform-api/module-content.yaml @@ -0,0 +1,13 @@ +- name: dist.all/lib/intellij.platform.ide.jar + modules: + - name: intellij.platform.credentialStore + - name: intellij.platform.icons + - name: intellij.platform.ide + - name: intellij.platform.ide.observable + - name: intellij.platform.ide.progress + - name: intellij.platform.ide.rpc + - name: intellij.platform.instanceContainer + - name: intellij.platform.project + - name: intellij.platform.serviceContainer + - name: intellij.platform.util.diff + - name: intellij.platform.util.text.matching \ No newline at end of file diff --git a/platform/platform-api/resources/intellij.platform.ide.xml b/platform/platform-api/resources/intellij.platform.ide.xml new file mode 100644 index 000000000000..08314b1fe87a --- /dev/null +++ b/platform/platform-api/resources/intellij.platform.ide.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/platform-impl/concurrency/resources/intellij.platform.ide.concurrency.xml b/platform/platform-impl/concurrency/resources/intellij.platform.ide.concurrency.xml index 8daaab1f293f..53cc2c3a3d4a 100644 --- a/platform/platform-impl/concurrency/resources/intellij.platform.ide.concurrency.xml +++ b/platform/platform-impl/concurrency/resources/intellij.platform.ide.concurrency.xml @@ -1,4 +1,13 @@ + + + + + + + + + diff --git a/platform/platform-impl/module-content.yaml b/platform/platform-impl/module-content.yaml index cb2a3c6df8bc..7af30b495455 100644 --- a/platform/platform-impl/module-content.yaml +++ b/platform/platform-impl/module-content.yaml @@ -1,51 +1,29 @@ - name: dist.all/lib/intellij.platform.ide.impl.jar modules: - - name: intellij.platform.backend.observation - name: intellij.platform.buildData - name: intellij.platform.codeStyle.impl - - name: intellij.platform.core.ui - - name: intellij.platform.credentialStore - name: intellij.platform.diagnostic.startUpPerformanceReporter - - name: intellij.platform.icons - - name: intellij.platform.ide - - name: intellij.platform.ide.core - - name: intellij.platform.ide.core.impl - name: intellij.platform.ide.core.plugins - name: intellij.platform.ide.impl - - name: intellij.platform.ide.observable - - name: intellij.platform.ide.progress - name: intellij.platform.ide.recentProjects - - name: intellij.platform.ide.rpc - name: intellij.platform.ide.ui - name: intellij.platform.ide.util.io.impl - name: intellij.platform.ide.util.netty - name: intellij.platform.ijent - name: intellij.platform.ijent.community.buildConstants - name: intellij.platform.ijent.community.impl - - name: intellij.platform.indexing - - name: intellij.platform.instanceContainer - name: intellij.platform.jbr - - name: intellij.platform.lang - name: intellij.platform.locking.impl - name: intellij.platform.objectSerializer - - name: intellij.platform.pasta - name: intellij.platform.pratt - - name: intellij.platform.project - - name: intellij.platform.refactoring - name: intellij.platform.resources - name: intellij.platform.resources.en - - name: intellij.platform.serviceContainer - - name: intellij.platform.settings - name: intellij.platform.syntax.extensions - name: intellij.platform.syntax.i18n - name: intellij.platform.syntax.psi - name: intellij.platform.syntax.tree - name: intellij.platform.syntax.util - name: intellij.platform.ui.jcef - - name: intellij.platform.util.io.storages - - name: intellij.platform.util.progress - - name: intellij.platform.util.text.matching - - name: intellij.platform.util.ui library: jetbrains.intellij.deps.rwmutex.idea files: - name: $MAVEN_REPOSITORY$/org/jetbrains/intellij/deps/rwmutex-idea/0/rwmutex-idea-0.jar \ No newline at end of file diff --git a/platform/platform-impl/resources/intellij.platform.ide.impl.xml b/platform/platform-impl/resources/intellij.platform.ide.impl.xml index bf03841e6f89..671638209d7f 100644 --- a/platform/platform-impl/resources/intellij.platform.ide.impl.xml +++ b/platform/platform-impl/resources/intellij.platform.ide.impl.xml @@ -1,2 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.core.ide.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.core.ide.xml new file mode 100644 index 000000000000..97ff21cee846 --- /dev/null +++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.core.ide.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.core.lang.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.core.lang.xml new file mode 100644 index 000000000000..03eb12952a3d --- /dev/null +++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.core.lang.xml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.debugger.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.debugger.xml index 17bbda151170..491d78928e25 100644 --- a/platform/platform-resources/generated/META-INF/intellij.moduleSets.debugger.xml +++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.debugger.xml @@ -1,6 +1,6 @@ - - + + @@ -9,7 +9,7 @@ - + \ No newline at end of file diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.essential.minimal.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.essential.minimal.xml index 615cf32540ce..509c38a0a16a 100644 --- a/platform/platform-resources/generated/META-INF/intellij.moduleSets.essential.minimal.xml +++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.essential.minimal.xml @@ -1,11 +1,13 @@ - - + + - - + + + + @@ -51,10 +53,7 @@ - - - @@ -63,49 +62,20 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -124,19 +94,88 @@ - - - - - - + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -150,8 +189,6 @@ - - \ No newline at end of file diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.essential.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.essential.xml index e8c2fe3874e5..fe501abdcbb0 100644 --- a/platform/platform-resources/generated/META-INF/intellij.moduleSets.essential.xml +++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.essential.xml @@ -1,12 +1,14 @@ - - + + - - + + + + @@ -52,10 +54,7 @@ - - - @@ -64,49 +63,20 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -125,19 +95,88 @@ - - - - - - + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -151,8 +190,6 @@ - - @@ -162,8 +199,14 @@ - + + + + + + + @@ -187,9 +230,6 @@ - - - @@ -199,8 +239,12 @@ - + + + + + \ No newline at end of file diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.fleet.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.fleet.xml similarity index 83% rename from platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.fleet.xml rename to platform/platform-resources/generated/META-INF/intellij.moduleSets.fleet.xml index 446728fc89bc..cb87e3881da8 100644 --- a/platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.fleet.xml +++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.fleet.xml @@ -1,10 +1,9 @@ - - + + - diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.ide.common.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.ide.common.xml index a48d8d5d45e1..e417ae833dbd 100644 --- a/platform/platform-resources/generated/META-INF/intellij.moduleSets.ide.common.xml +++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.ide.common.xml @@ -8,8 +8,10 @@ - - + + + + @@ -55,10 +57,7 @@ - - - @@ -67,49 +66,20 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -128,19 +98,88 @@ - - - - - - + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -154,8 +193,6 @@ - - @@ -165,8 +202,14 @@ - + + + + + + + @@ -190,9 +233,6 @@ - - - @@ -202,8 +242,12 @@ - + + + + + @@ -258,7 +302,6 @@ - @@ -304,7 +347,6 @@ - diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.ide.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.ide.xml new file mode 100644 index 000000000000..df040dca5150 --- /dev/null +++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.ide.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.misc.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.misc.xml index a791f9dac11e..d1bbbad5c710 100644 --- a/platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.misc.xml +++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.misc.xml @@ -8,5 +8,6 @@ + \ No newline at end of file diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.core.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.platform.xml similarity index 88% rename from platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.core.xml rename to platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.platform.xml index 6c4a2314582b..caa3c13fb262 100644 --- a/platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.core.xml +++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.libraries.platform.xml @@ -1,6 +1,6 @@ - + @@ -49,10 +49,7 @@ - - - @@ -61,19 +58,14 @@ - - - - - diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.platformLangBase.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.platformLangBase.xml deleted file mode 100644 index a2329ded595c..000000000000 --- a/platform/platform-resources/generated/META-INF/intellij.moduleSets.platformLangBase.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.recentFiles.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.recentFiles.xml new file mode 100644 index 000000000000..05a3bc87c16b --- /dev/null +++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.recentFiles.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.rpc.backend.extended.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.rpc.backend.extended.xml new file mode 100644 index 000000000000..4abeee38476f --- /dev/null +++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.rpc.backend.extended.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.vcs.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.vcs.xml index 3e0015d7097a..948a0911c5bf 100644 --- a/platform/platform-resources/generated/META-INF/intellij.moduleSets.vcs.xml +++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.vcs.xml @@ -27,7 +27,6 @@ - \ No newline at end of file diff --git a/platform/platform-resources/src/META-INF/PlatformLangPlugin.xml b/platform/platform-resources/src/META-INF/PlatformLangPlugin.xml index d3fe91d8320b..923d5b4933aa 100644 --- a/platform/platform-resources/src/META-INF/PlatformLangPlugin.xml +++ b/platform/platform-resources/src/META-INF/PlatformLangPlugin.xml @@ -39,10 +39,6 @@ - - - - diff --git a/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginModelValidator.kt b/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginModelValidator.kt index 47e7977fa114..89dbc192f036 100644 --- a/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginModelValidator.kt +++ b/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginModelValidator.kt @@ -24,12 +24,16 @@ import org.jetbrains.jps.model.java.JpsJavaExtensionService import org.jetbrains.jps.model.module.JpsModule import org.jetbrains.jps.model.module.JpsModuleSourceRoot import java.io.StringWriter +import java.nio.file.Files import java.nio.file.Path -import kotlin.io.path.* +import kotlin.io.path.invariantSeparatorsPathString +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.name +import kotlin.io.path.nameWithoutExtension data class CorePluginDescription( val mainModuleName: String, - val rootPluginXmlName: String = "plugin.xml" + val rootPluginXmlName: String = "plugin.xml", ) val COMMUNITY_CORE_PLUGINS = listOf( @@ -147,17 +151,16 @@ class PluginValidationResult internal constructor( class PluginModelValidator( private val project: JpsProject, private val projectHomePath: Path, - private val validationOptions: PluginValidationOptions + private val validationOptions: PluginValidationOptions, ) { private val pluginIdToInfo = LinkedHashMap() private val pluginAliases = HashSet() private val _errors = mutableListOf() - private val xIncludeLoader = - LoadFromSourceXIncludeLoader( - prefixesOfPathsIncludedFromLibrariesViaXiInclude = validationOptions.prefixesOfPathsIncludedFromLibrariesViaXiInclude, - project = project, - parentDirectoriesPatterns = listOf("META-INF", "idea", "") + validationOptions.additionalPatternsOfDirectoriesContainingIncludedXmlFiles, - ) + private val xIncludeLoader = LoadFromSourceXIncludeLoader( + prefixesOfPathsIncludedFromLibrariesViaXiInclude = validationOptions.prefixesOfPathsIncludedFromLibrariesViaXiInclude, + project = project, + parentDirectoriesPatterns = listOf("META-INF", "idea", "") + validationOptions.additionalPatternsOfDirectoriesContainingIncludedXmlFiles, + ) fun validate(): PluginValidationResult { // 1. collect plugin and module file info set @@ -170,7 +173,7 @@ class PluginModelValidator( return@flatMap emptyList() } } - + descriptorFileInfos.flatMapTo(pluginAliases) { it.descriptor.pluginAliases } val moduleNameToInfo = HashMap() @@ -237,7 +240,7 @@ class PluginModelValidator( reportError("Failed to load descriptor for '${sourceModule.name}'", sourceModule) return@withSystemProperty } - + val pluginDescriptor = pluginModuleInfo.descriptor val pluginDescriptorFile = pluginModuleInfo.descriptorFile @@ -275,7 +278,7 @@ class PluginModelValidator( val descriptor = pluginInfo.descriptor for (incompatibleWithId in descriptor.incompatibleWith) { - if (incompatibleWithId !in pluginIdToInfo && incompatibleWithId !in pluginAliases + if (incompatibleWithId !in pluginIdToInfo && incompatibleWithId !in pluginAliases && incompatibleWithId !in validationOptions.referencedPluginIdsOfExternalPlugins) { reportError("'incompatible-with' refers to unknown plugin '$incompatibleWithId'", pluginInfo.sourceModule, mapOf("descriptorFile" to pluginInfo.descriptorFile)) @@ -380,10 +383,10 @@ class PluginModelValidator( isMainModule: Boolean, contentModuleNameFromThisPluginToLoadingRule: Map, ) { - val moduleDependenciesCount = dependenciesElements.count { + val moduleDependenciesCount = dependenciesElements.count { it is DependenciesElement.ModuleDependency || it is DependenciesElement.PluginDependency && it.pluginId.startsWith("com.intellij.modules.") } - + for (child in dependenciesElements) { fun registerError(message: String, fix: String? = null) { @@ -420,7 +423,7 @@ class PluginModelValidator( } val dependency = pluginIdToInfo[id] - if (dependency == null + if (dependency == null && id !in validationOptions.referencedPluginIdsOfExternalPlugins && id !in pluginAliases && IdeaPluginOsRequirement.fromModuleId(PluginId.getId(id)) == null) { @@ -480,7 +483,7 @@ class PluginModelValidator( } !isMainModule && loadingRule == ModuleLoadingRuleValue.OPTIONAL && moduleName != "intellij.platform.backend" -> { // remove this check when IJPL-201428 is fixed - + val thisModuleName = referencingModuleInfo.name ?: error("Module name is not specified for $referencingModuleInfo") val thisLoadingRule = contentModuleNameFromThisPluginToLoadingRule.getValue(thisModuleName) val problemDescription = when (thisLoadingRule) { @@ -503,7 +506,7 @@ class PluginModelValidator( } } } - + referencingModuleInfo.dependencies.add(Reference(moduleName, isPlugin = false, moduleInfo)) if (!pluginModuleVisibilityCheckDisabled) { when (moduleInfo.descriptor.moduleVisibility) { @@ -563,7 +566,7 @@ class PluginModelValidator( contentElements: List, referencingModuleInfo: ModuleInfo, contentModuleNameToFileInfo: Map, - moduleNameToInfo: MutableMap + moduleNameToInfo: MutableMap, ) { val nonPrivateModules = ArrayList() for (contentElement in contentElements) { @@ -779,7 +782,7 @@ class PluginModelValidator( private fun findPluginAndModuleDescriptors(module: JpsModule): List { if (module.name !in validationOptions.modulesWithIncorrectlyPlacedModuleDescriptor) { for (sourceRoot in module.sourceRoots) { - val moduleXml = sourceRoot.findFile("META-INF/${module.name}.xml") + val moduleXml = findFile(sourceRoot, "META-INF/${module.name}.xml") if (moduleXml != null) { reportError( "Module descriptor must be in the root of module root", @@ -798,11 +801,16 @@ class PluginModelValidator( val (productionPluginDescriptors, testPluginDescriptors) = module.sourceRoots.mapNotNull { sourceRoot -> - val pluginDescriptorFile = sourceRoot.findFile("META-INF/$pluginFileName") ?: return@mapNotNull null + val pluginDescriptorFile = findFile(sourceRoot, "META-INF/$pluginFileName") ?: return@mapNotNull null val descriptor = loadRawPluginDescriptor(pluginDescriptorFile) ?: return@mapNotNull null - PluginDescriptorFileInfo(module, pluginDescriptorFile, descriptor, sourceRoot.rootType.isForTests) + PluginDescriptorFileInfo( + sourceModule = module, + descriptorFile = pluginDescriptorFile, + descriptor = descriptor, + inTests = sourceRoot.rootType.isForTests, + ) } - .partition { !it.inTests } + .partition { !it.inTests } if (customRootPluginXmlFileName != null && productionPluginDescriptors.isEmpty()) { reportError( @@ -810,22 +818,26 @@ class PluginModelValidator( sourceModule = module, ) } - - val moduleDescriptors = - module.sourceRoots.flatMap { sourceRoot -> - sourceRoot.path.listDirectoryEntries("*.xml") - .filter { it.nameWithoutExtension == module.name || it.nameWithoutExtension.startsWith("${module.name}.") } - .mapNotNull { moduleDescriptorFile -> - val descriptor = loadRawPluginDescriptor(moduleDescriptorFile) ?: return@mapNotNull null - val contentModuleName = when { - moduleDescriptorFile.nameWithoutExtension.removeSuffix("._test") == module.name -> moduleDescriptorFile.nameWithoutExtension - else -> "${module.name}/${moduleDescriptorFile.nameWithoutExtension.removePrefix("${module.name}.")}" - } - ContentModuleDescriptorFileInfo(contentModuleName, module, moduleDescriptorFile, descriptor) - } - } - listOf(productionPluginDescriptors, testPluginDescriptors).forEach { pluginDescriptors -> + val moduleDescriptors = module.sourceRoots.flatMap { sourceRoot -> + sourceRoot.path.listDirectoryEntries("*.xml") + .filter { it.nameWithoutExtension == module.name || it.nameWithoutExtension.startsWith("${module.name}.") } + .mapNotNull { moduleDescriptorFile -> + val descriptor = loadRawPluginDescriptor(moduleDescriptorFile) ?: return@mapNotNull null + val contentModuleName = when { + moduleDescriptorFile.nameWithoutExtension.removeSuffix("._test") == module.name -> moduleDescriptorFile.nameWithoutExtension + else -> "${module.name}/${moduleDescriptorFile.nameWithoutExtension.removePrefix("${module.name}.")}" + } + ContentModuleDescriptorFileInfo( + contentModuleName = contentModuleName, + sourceModule = module, + descriptorFile = moduleDescriptorFile, + descriptor = descriptor, + ) + } + } + + for (pluginDescriptors in listOf(productionPluginDescriptors, testPluginDescriptors)) { if (pluginDescriptors.size > 1) { reportError( "Duplicated plugin.xml", @@ -843,8 +855,10 @@ class PluginModelValidator( } private fun loadRawPluginDescriptor(file: Path): RawPluginDescriptor? { - if (!file.exists()) return null - + if (Files.notExists(file)) { + return null + } + return loadRawPluginDescriptorInTest(file, xIncludeLoader) } @@ -885,6 +899,7 @@ internal data class ModuleInfo( ) { @JvmField val content = mutableListOf() + @JvmField val dependencies = mutableListOf() @@ -953,6 +968,6 @@ internal fun hasContentOrDependenciesInV2Format(descriptor: RawPluginDescriptor) return descriptor.contentModules.isNotEmpty() || descriptor.dependencies.isNotEmpty() } -private fun JpsModuleSourceRoot.findFile(relativePath: String): Path? { - return JpsJavaExtensionService.getInstance().findSourceFile(this, relativePath) +private fun findFile(root: JpsModuleSourceRoot, relativePath: String): Path? { + return JpsJavaExtensionService.getInstance().findSourceFile(root, relativePath) } \ No newline at end of file diff --git a/platform/projectModel-api/module-content.yaml b/platform/projectModel-api/module-content.yaml new file mode 100644 index 000000000000..78c0c1f0bb2e --- /dev/null +++ b/platform/projectModel-api/module-content.yaml @@ -0,0 +1,9 @@ +- name: dist.all/lib/intellij.platform.projectModel.jar + modules: + - name: intellij.platform.backend.workspace + - name: intellij.platform.concurrency + - name: intellij.platform.diagnostic.telemetry + - name: intellij.platform.eel.provider + - name: intellij.platform.projectModel + - name: intellij.platform.workspace.jps + - name: intellij.platform.workspace.storage \ No newline at end of file diff --git a/platform/projectModel-api/resources/intellij.platform.projectModel.xml b/platform/projectModel-api/resources/intellij.platform.projectModel.xml new file mode 100644 index 000000000000..7879fd86336a --- /dev/null +++ b/platform/projectModel-api/resources/intellij.platform.projectModel.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/platform/projectModel-impl/module-content.yaml b/platform/projectModel-impl/module-content.yaml index 0c74f6f7d856..ffaf96151d83 100644 --- a/platform/projectModel-impl/module-content.yaml +++ b/platform/projectModel-impl/module-content.yaml @@ -1,19 +1,6 @@ - name: dist.all/lib/intellij.platform.projectModel.impl.jar modules: - - name: intellij.platform.backend.workspace - - name: intellij.platform.concurrency - - name: intellij.platform.core.impl - - name: intellij.platform.diagnostic.telemetry - name: intellij.platform.diagnostic.telemetry.exporters - name: intellij.platform.diagnostic.telemetry.impl - - name: intellij.platform.eel.provider - - name: intellij.platform.plugins.parser.impl - - name: intellij.platform.projectModel - name: intellij.platform.projectModel.impl - - name: intellij.platform.syntax - - name: intellij.platform.util.coroutines - - name: intellij.platform.util.diff - - name: intellij.platform.util.ex - - name: intellij.platform.util.http - - name: intellij.platform.workspace.jps - - name: intellij.platform.workspace.storage \ No newline at end of file + - name: intellij.platform.util.http \ No newline at end of file diff --git a/platform/projectModel-impl/resources/intellij.platform.projectModel.impl.xml b/platform/projectModel-impl/resources/intellij.platform.projectModel.impl.xml index 7e5b68b24107..ab85db453813 100644 --- a/platform/projectModel-impl/resources/intellij.platform.projectModel.impl.xml +++ b/platform/projectModel-impl/resources/intellij.platform.projectModel.impl.xml @@ -1,4 +1,17 @@ + + + + + + + + + + + + + diff --git a/platform/smRunner/resources/intellij.platform.smRunner.xml b/platform/smRunner/resources/intellij.platform.smRunner.xml index 05b1dc2a8c56..3cb93a7ac6e1 100644 --- a/platform/smRunner/resources/intellij.platform.smRunner.xml +++ b/platform/smRunner/resources/intellij.platform.smRunner.xml @@ -1,7 +1,18 @@ + + + + + + + + + + + diff --git a/platform/util-ex/BUILD.bazel b/platform/util-ex/BUILD.bazel index 3701a58617bb..d251b1274a12 100644 --- a/platform/util-ex/BUILD.bazel +++ b/platform/util-ex/BUILD.bazel @@ -1,11 +1,18 @@ ### auto-generated section `build intellij.platform.util.ex` start -load("@rules_jvm//:jvm.bzl", "jvm_library") +load("@rules_jvm//:jvm.bzl", "jvm_library", "resourcegroup") + +resourcegroup( + name = "util-ex_resources", + srcs = glob(["resources/**/*"]), + strip_prefix = "resources" +) jvm_library( name = "util-ex", module_name = "intellij.platform.util.ex", visibility = ["//visibility:public"], srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True), + resources = [":util-ex_resources"], deps = [ "@lib//:kotlin-stdlib", "//libraries/kotlin/reflect", diff --git a/platform/util-ex/intellij.platform.util.ex.iml b/platform/util-ex/intellij.platform.util.ex.iml index c57d49d918ad..e290b651a84b 100644 --- a/platform/util-ex/intellij.platform.util.ex.iml +++ b/platform/util-ex/intellij.platform.util.ex.iml @@ -3,6 +3,7 @@ + diff --git a/platform/util-ex/module-content.yaml b/platform/util-ex/module-content.yaml new file mode 100644 index 000000000000..8eb002dd3597 --- /dev/null +++ b/platform/util-ex/module-content.yaml @@ -0,0 +1,3 @@ +- name: dist.all/lib/intellij.platform.util.ex.jar + modules: + - name: intellij.platform.util.ex \ No newline at end of file diff --git a/platform/util-ex/resources/intellij.platform.util.ex.xml b/platform/util-ex/resources/intellij.platform.util.ex.xml new file mode 100644 index 000000000000..fa221f007d86 --- /dev/null +++ b/platform/util-ex/resources/intellij.platform.util.ex.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/util/ui/module-content.yaml b/platform/util/ui/module-content.yaml new file mode 100644 index 000000000000..091b2a3434f9 --- /dev/null +++ b/platform/util/ui/module-content.yaml @@ -0,0 +1,3 @@ +- name: dist.all/lib/intellij.platform.util.ui.jar + modules: + - name: intellij.platform.util.ui \ No newline at end of file diff --git a/platform/util/ui/resources/intellij.platform.util.ui.xml b/platform/util/ui/resources/intellij.platform.util.ui.xml new file mode 100644 index 000000000000..6f21fba829f7 --- /dev/null +++ b/platform/util/ui/resources/intellij.platform.util.ui.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/xdebugger-api/resources/intellij.platform.debugger.xml b/platform/xdebugger-api/resources/intellij.platform.debugger.xml index 776c3860cde6..7141cc2baaf2 100644 --- a/platform/xdebugger-api/resources/intellij.platform.debugger.xml +++ b/platform/xdebugger-api/resources/intellij.platform.debugger.xml @@ -1,4 +1,16 @@ + + + + + + + + + + + + diff --git a/platform/xdebugger-impl/resources/intellij.platform.debugger.impl.xml b/platform/xdebugger-impl/resources/intellij.platform.debugger.impl.xml index 9524565e3928..76b980d1106c 100644 --- a/platform/xdebugger-impl/resources/intellij.platform.debugger.impl.xml +++ b/platform/xdebugger-impl/resources/intellij.platform.debugger.impl.xml @@ -1,4 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -8,6 +8,4 @@ - - diff --git a/plugins/stream-debugger-core/resources/intellij.debugger.streams.core.xml b/plugins/stream-debugger-core/resources/intellij.debugger.streams.core.xml index 0dade6ae9d03..a0e944d28f39 100644 --- a/plugins/stream-debugger-core/resources/intellij.debugger.streams.core.xml +++ b/plugins/stream-debugger-core/resources/intellij.debugger.streams.core.xml @@ -1,4 +1,4 @@ - + diff --git a/python/build/src/org/jetbrains/intellij/build/pycharm/PyCharmCommunityProperties.kt b/python/build/src/org/jetbrains/intellij/build/pycharm/PyCharmCommunityProperties.kt index e1b2774c47b1..56031a59807d 100644 --- a/python/build/src/org/jetbrains/intellij/build/pycharm/PyCharmCommunityProperties.kt +++ b/python/build/src/org/jetbrains/intellij/build/pycharm/PyCharmCommunityProperties.kt @@ -7,7 +7,10 @@ import kotlinx.collections.immutable.plus import org.jetbrains.intellij.build.* import org.jetbrains.intellij.build.impl.qodana.QodanaProductProperties import org.jetbrains.intellij.build.io.copyFileToDir -import org.jetbrains.intellij.build.productLayout.* +import org.jetbrains.intellij.build.productLayout.CommunityModuleSets +import org.jetbrains.intellij.build.productLayout.CommunityProductFragments +import org.jetbrains.intellij.build.productLayout.ProductModulesContentSpec +import org.jetbrains.intellij.build.productLayout.productModules import java.nio.file.Files import java.nio.file.Path @@ -47,9 +50,6 @@ open class PyCharmCommunityProperties(protected val communityHome: Path) : PyCha qodanaProductProperties = QodanaProductProperties(@Suppress("SpellCheckingInspection") "QDPYC", "Qodana Community for Python") } - override val moduleSetsProviders: List - get() = listOf(CommunityModuleSets) - override fun getProductContentDescriptor(): ProductModulesContentSpec = productModules { // Module capability aliases alias("com.intellij.modules.pycharm.community") diff --git a/python/ide-common/resources/META-INF/PyCharmCorePlugin.xml b/python/ide-common/resources/META-INF/PyCharmCorePlugin.xml index 30647469c917..41ebefd85c0c 100644 --- a/python/ide-common/resources/META-INF/PyCharmCorePlugin.xml +++ b/python/ide-common/resources/META-INF/PyCharmCorePlugin.xml @@ -18,7 +18,6 @@ -