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 @@
+
+
+
+
+
+
+
+
+
+
+