From 5b64cc622188ef4cd27845dac72f56bfe85d1a60 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Sat, 1 Nov 2025 23:27:45 +0100 Subject: [PATCH] IJPL-215077 migrate CE/DS/PCC products to DSL to fix xi-include resolve without complicating build scripts GitOrigin-RevId: cc9030aad002c6034c0ebb5e001cb85ac5420302 --- .../intellij/build/IdeaCommunityProperties.kt | 41 ++++ .../resources/META-INF/IdeaPlugin.xml | 39 ++-- .../META-INF/community-customization.xml | 8 + .../productLayout/programmatic-content.md | 196 ++++++++++++------ .../pycharm/PyCharmCommunityProperties.kt | 58 ++++-- .../resources/META-INF/PyCharmCorePlugin.xml | 44 ++-- .../META-INF/pycharm-core-customization.xml | 16 ++ 7 files changed, 271 insertions(+), 131 deletions(-) create mode 100644 community-resources/resources/META-INF/community-customization.xml create mode 100644 python/ide-common/resources/META-INF/pycharm-core-customization.xml diff --git a/build/src/org/jetbrains/intellij/build/IdeaCommunityProperties.kt b/build/src/org/jetbrains/intellij/build/IdeaCommunityProperties.kt index f1ac81604286..b5a27ae28987 100644 --- a/build/src/org/jetbrains/intellij/build/IdeaCommunityProperties.kt +++ b/build/src/org/jetbrains/intellij/build/IdeaCommunityProperties.kt @@ -12,6 +12,8 @@ import org.jetbrains.intellij.build.io.copyFileToDir import org.jetbrains.intellij.build.kotlin.KotlinBinaries 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.productModules import java.nio.file.Path internal suspend fun createCommunityBuildContext( @@ -120,6 +122,45 @@ open class IdeaCommunityProperties(private val communityHomeDir: Path) : BaseIde additionalVmOptions = persistentListOf("-Dllm.show.ai.promotion.window.on.start=false") } + override fun getProductContentDescriptor(): ProductModulesContentSpec { + return getIntelliJCommunityProductContentDescriptor(includeCommunityExtensions = true) + } + + protected fun getIntelliJCommunityProductContentDescriptor( + includeCommunityExtensions: Boolean, + ): ProductModulesContentSpec = productModules { + alias("com.intellij.modules.idea") + alias("com.intellij.modules.idea.community") + alias("com.intellij.modules.java-capable") + alias("com.intellij.modules.python-core-capable") + alias("com.intellij.modules.python-in-non-pycharm-ide-capable") + alias("com.intellij.platform.ide.provisioner") + + deprecatedInclude("intellij.java.ide.resources", "META-INF/JavaIdePlugin.xml") + deprecatedInclude("intellij.idea.community.customization", "META-INF/tips-intellij-idea-community.xml") + + moduleSet(CommunityModuleSets.debuggerStreams()) + + module("intellij.platform.coverage") + module("intellij.platform.coverage.agent") + module("intellij.xml.xmlbeans") + module("intellij.platform.ide.newUiOnboarding") + module("intellij.platform.ide.newUsersOnboarding") + module("intellij.ide.startup.importSettings") + module("intellij.platform.customization.min") + module("intellij.idea.customization.base") + module("intellij.idea.customization.backend") + module("intellij.platform.tips") + + moduleSet(CommunityModuleSets.ideCommon()) + moduleSet(CommunityModuleSets.rdCommon()) + + if (includeCommunityExtensions) { + deprecatedInclude("intellij.platform.extended.community.impl", "META-INF/community-extensions.xml", ultimateOnly = true) + } + deprecatedInclude("intellij.idea.community.customization", "META-INF/community-customization.xml") + } + override suspend fun copyAdditionalFiles(context: BuildContext, targetDir: Path) { super.copyAdditionalFiles(context, targetDir) diff --git a/community-resources/resources/META-INF/IdeaPlugin.xml b/community-resources/resources/META-INF/IdeaPlugin.xml index 7d5799ed540b..8dcc33330e24 100644 --- a/community-resources/resources/META-INF/IdeaPlugin.xml +++ b/community-resources/resources/META-INF/IdeaPlugin.xml @@ -1,18 +1,25 @@ + + + - - - - - + + + + + + + + + + + - - - + @@ -23,20 +30,6 @@ + - - - - - - - - - - - - - diff --git a/community-resources/resources/META-INF/community-customization.xml b/community-resources/resources/META-INF/community-customization.xml new file mode 100644 index 000000000000..0fac3f325c5b --- /dev/null +++ b/community-resources/resources/META-INF/community-customization.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/programmatic-content.md b/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/programmatic-content.md index c70b35d33805..4a682ff95b73 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/programmatic-content.md +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/programmatic-content.md @@ -55,8 +55,13 @@ override fun getProductContentModules(): ProductModulesContentSpec { return productModules { // XML includes (optional - can also be defined in plugin.xml manually) // Specify module name and resource path within that module - include("intellij.platform.resources", "META-INF/PlatformLangPlugin.xml") - include("intellij.gateway", "META-INF/Gateway.xml") + deprecatedInclude("intellij.platform.resources", "META-INF/PlatformLangPlugin.xml") + deprecatedInclude("intellij.gateway", "META-INF/Gateway.xml") + + // Ultimate-only includes (only included in Ultimate builds) + // When inlining: Skipped in Community builds + // When NOT inlining: Generates xi:include with xi:fallback for graceful handling + deprecatedInclude("intellij.platform.extended.community.impl", "META-INF/community-extensions.xml", ultimateOnly = true) // Include module sets moduleSet(CommunityModuleSets.essential()) @@ -76,63 +81,69 @@ override fun getProductContentModules(): ProductModulesContentSpec { } ``` -### 2. Add Marker Tags to plugin.xml +### 2. Add pluginXmlPath to dev-build.json -Add the marker tags where you want the content injected: +Register the product's plugin.xml file path in `build/dev-build.json`: -```xml - - - - - - - - - - +```json +"DataSpell": { + "modules": [...], + "class": "com.intellij.dataspell.build.DataSpellProperties", + "pluginXmlPath": "dataspell/ide/resources/META-INF/DataSpellPlugin.xml" +} ``` -### 3. Generate Static Content +This tells the generator which file to regenerate for this product. -Run the generator to populate the content between markers: +### 3. Generate Plugin.xml + +Run the generator to create the complete plugin.xml file: ```bash -./gradlew :platform.buildScripts:run +UltimateModuleSets.main() # for ultimate + community + products +CommunityModuleSets.main() # for community products only ``` -This will generate content like: +Or use the IDE's "Generate Product Layouts" run configuration. + +This will generate a complete plugin.xml file like: ```xml - - - - + + + + + + + - - - - - - + + + + + + + + + + + + - - - - - + ``` ## How It Works -### Deduplication Strategy +### File Generation Strategy -The marker-based approach prevents duplicate content: +The system generates complete plugin.xml files from Kotlin code: -1. **Dev mode**: Build reads markers, removes old content, injects fresh content from Kotlin code -2. **Non-dev mode**: Static XML already contains the content (generated once, committed to VCS) -3. **No conflicts**: Build always replaces whatever is between markers +1. **Static generation**: The entire plugin.xml is generated from `getProductContentDescriptor()` +2. **Auto-generated header**: Each file includes a "DO NOT EDIT" comment indicating it's generated +3. **VCS-committed**: Generated files are committed to version control +4. **Build-time injection**: At runtime, `buildProductContentXml()` is also called during build for validation ### Generated Content Structure @@ -162,11 +173,28 @@ Each module set generates a separate `` block with a `source` attribute To migrate a product to programmatic content: -1. **Implement `getProductContentModules()`** in your `ProductProperties` class -2. **Add marker tags** to your product's plugin.xml -3. **Run the generator** to populate initial content -4. **Commit the generated content** to VCS -5. **Verify** both dev mode (build injects fresh) and non-dev mode (uses static) work +1. **Implement `getProductContentDescriptor()`** in your `ProductProperties` class + - Define module aliases with `alias()` + - Add xi:includes with `deprecatedInclude()` + - Include module sets with `moduleSet()` + - Add individual modules with `module()` or `embeddedModule()` + +2. **Extract extensions** to separate XML files (e.g., `*-customization.xml`) + - Move `` blocks from plugin.xml to dedicated files + - Reference them via `deprecatedInclude()` + +3. **Add pluginXmlPath** to `build/dev-build.json` for your product + +4. **Run the generator** to create the complete plugin.xml: + ```bash + UltimateModuleSets.main() # or CommunityModuleSets.main() + ``` + +5. **Verify generated file** matches expected structure + +6. **Commit all changes** to VCS (Kotlin code, generated XML, extracted extensions) + +7. **Test compilation** to ensure product builds correctly ## Example: Gateway @@ -189,29 +217,71 @@ override fun getProductContentModules(): ProductModulesContentSpec { The content is generated into `/remote-dev/gateway/resources/META-INF/plugin.xml`. +## Ultimate-Only Includes + +The `ultimateOnly` flag on `deprecatedInclude()` enables conditional inclusion of resources that only exist in Ultimate builds. + +### Behavior + +**When inlining** (`inlineXmlIncludes = true`): +- Community builds: Skip the include entirely +- Ultimate builds: Inline the content normally + +**When NOT inlining** (`inlineXmlIncludes = false`): +- Generates `` with `` wrapper for graceful handling: + ```xml + + + + ``` +- Community builds: XInclude processor skips gracefully (file not found, fallback used) +- Ultimate builds: XInclude processor includes the file normally + +### Example + +```kotlin +override fun getProductContentModules(): ProductModulesContentSpec { + return productModules { + // Regular include - always processed + deprecatedInclude("intellij.pycharm.community", "META-INF/pycharm-core.xml") + + // Ultimate-only - conditionally processed + deprecatedInclude("intellij.platform.extended.community.impl", + "META-INF/community-extensions.xml", + ultimateOnly = true) + } +} +``` + +**Generated XML (Community build)**: +```xml + + + + +``` + +**Generated XML (Ultimate build)**: +```xml + + +``` + +### Use Cases + +Use `ultimateOnly = true` when: +1. The included XML file exists only in Ultimate repository +2. Multiple products (both Community and Ultimate variants) share the same descriptor +3. You need backward compatibility during migration (xi:fallback allows runtime resolution) + ## Implementation Details ### Key Functions -- **`processProgrammaticModules()`** (PlatformModules.kt:610): Build-time injection -- **`buildProductContentXml()`** (ModuleSetBuilder.kt:231): Static XML generation -- **`generateProductXml()`** (ModuleSetBuilder.kt:299): Marker replacement -- **`generateGatewayProductXml()`** (ModuleSetBuilder.kt:346): Gateway-specific helper - -### Marker Tags - -- ``: Start marker (self-closing tag) -- ``: End marker (self-closing tag) - -These tags are preserved in the XML and used by both static generation and build-time injection. - -### Backward Compatibility - -If marker tags are not found: -- **Build time**: Content is appended at the end (existing behavior) -- **Static generation**: File is skipped (no changes) - -This ensures products can migrate incrementally without breaking existing builds. +- **`buildProductContentXml()`** (generator.kt): Generates complete XML from ProductModulesContentSpec +- **`generateProductXml()`** (generator.kt): Writes generated XML to plugin.xml file +- **`generateAllProductXmlFiles()`** (generator.kt): Batch generation for all registered products +- **`collectAndValidateAliases()`** (generator.kt): Validates module aliases for duplicates ## Benefits 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 9e28417a3ae7..72d188f5b197 100644 --- a/python/build/src/org/jetbrains/intellij/build/pycharm/PyCharmCommunityProperties.kt +++ b/python/build/src/org/jetbrains/intellij/build/pycharm/PyCharmCommunityProperties.kt @@ -9,6 +9,8 @@ import org.jetbrains.intellij.build.impl.qodana.QodanaProductProperties import org.jetbrains.intellij.build.io.copyFileToDir 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.productModules import java.nio.file.Files import java.nio.file.Path @@ -51,6 +53,27 @@ open class PyCharmCommunityProperties(protected val communityHome: Path) : PyCha override val moduleSetsProviders: List get() = listOf(CommunityModuleSets) + override fun getProductContentDescriptor(): ProductModulesContentSpec = productModules { + // Module capability aliases + alias("com.intellij.modules.pycharm.community") + alias("com.intellij.modules.python-core-capable") + alias("com.intellij.platform.ide.provisioner") + + // Content modules + module("intellij.platform.ide.newUiOnboarding") + module("intellij.ide.startup.importSettings") + module("intellij.platform.tips") + + // Module sets + moduleSet(CommunityModuleSets.ideCommon()) + moduleSet(CommunityModuleSets.rdCommon()) + + // Static includes + deprecatedInclude("intellij.platform.extended.community.impl", "META-INF/community-extensions.xml", ultimateOnly = true) + deprecatedInclude("intellij.pycharm.community", "META-INF/pycharm-core.xml") + deprecatedInclude("intellij.pycharm.community", "META-INF/pycharm-core-customization.xml") + } + override suspend fun copyAdditionalFiles(context: BuildContext, targetDir: Path) { super.copyAdditionalFiles(context, targetDir) @@ -59,8 +82,9 @@ open class PyCharmCommunityProperties(protected val communityHome: Path) : PyCha copyFileToDir(context.paths.communityHomeDir.resolve("NOTICE.txt"), licenseTargetDir) } - override fun getSystemSelector(appInfo: ApplicationInfoProperties, buildNumber: String): String = - "PyCharmCE${appInfo.majorVersion}.${appInfo.minorVersionMainPart}" + override fun getSystemSelector(appInfo: ApplicationInfoProperties, buildNumber: String): String { + return "PyCharmCE${appInfo.majorVersion}.${appInfo.minorVersionMainPart}" + } override fun getBaseArtifactName(appInfo: ApplicationInfoProperties, buildNumber: String): String = "pycharmPC-$buildNumber" @@ -79,24 +103,28 @@ open class PyCharmCommunityProperties(protected val communityHome: Path) : PyCha PyCharmBuildUtils.copySkeletons(context, targetDir, "skeletons-win*.zip") } - override fun getUninstallFeedbackPageUrl(appInfo: ApplicationInfoProperties): String = - "https://www.jetbrains.com/pycharm/uninstall/?version=${appInfo.productCode}-${appInfo.majorVersion}.${appInfo.minorVersion}" + override fun getUninstallFeedbackPageUrl(appInfo: ApplicationInfoProperties): String { + return "https://www.jetbrains.com/pycharm/uninstall/?version=${appInfo.productCode}-${appInfo.majorVersion}.${appInfo.minorVersion}" + } } override fun createMacCustomizer(projectHome: String): MacDistributionCustomizer = PyCharmMacDistributionCustomizer(communityHome) - override fun createLinuxCustomizer(projectHome: String): LinuxDistributionCustomizer = object : LinuxDistributionCustomizer() { - init { - iconPngPath = "${communityHome}/python/build/resources/PyCharmCore128.png" - iconPngPathForEAP = "${communityHome}/python/build/resources/PyCharmCore128_EAP.png" - snapName = "pycharm-community" - snapDescription = - "Python IDE for professional developers. Save time while PyCharm takes care of the routine. " + - "Focus on bigger things and embrace the keyboard-centric approach to get the most of PyCharm’s many productivity features." - } + override fun createLinuxCustomizer(projectHome: String): LinuxDistributionCustomizer { + return object : LinuxDistributionCustomizer() { + init { + iconPngPath = "${communityHome}/python/build/resources/PyCharmCore128.png" + iconPngPathForEAP = "${communityHome}/python/build/resources/PyCharmCore128_EAP.png" + snapName = "pycharm-community" + snapDescription = + "Python IDE for professional developers. Save time while PyCharm takes care of the routine. " + + "Focus on bigger things and embrace the keyboard-centric approach to get the most of PyCharm’s many productivity features." + } - override fun getRootDirectoryName(appInfo: ApplicationInfoProperties, buildNumber: String): String = - "pycharm-community-${if (appInfo.isEAP) buildNumber else appInfo.fullVersion}" + override fun getRootDirectoryName(appInfo: ApplicationInfoProperties, buildNumber: String): String { + return "pycharm-community-${if (appInfo.isEAP) buildNumber else appInfo.fullVersion}" + } + } } override fun getOutputDirectoryName(appInfo: ApplicationInfoProperties): String = "pycharm-ce" diff --git a/python/ide-common/resources/META-INF/PyCharmCorePlugin.xml b/python/ide-common/resources/META-INF/PyCharmCorePlugin.xml index b7ffc27f01bf..6e4c4912c929 100644 --- a/python/ide-common/resources/META-INF/PyCharmCorePlugin.xml +++ b/python/ide-common/resources/META-INF/PyCharmCorePlugin.xml @@ -1,39 +1,23 @@ - + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + diff --git a/python/ide-common/resources/META-INF/pycharm-core-customization.xml b/python/ide-common/resources/META-INF/pycharm-core-customization.xml new file mode 100644 index 000000000000..e26810db68ca --- /dev/null +++ b/python/ide-common/resources/META-INF/pycharm-core-customization.xml @@ -0,0 +1,16 @@ + + + + + + + + + + +