IJPL-217839 introduce javaIdeBaseFragment as a replacement for deprecated xi-include

GitOrigin-RevId: 7e02eec1ae4b27cfa318b780ce9e00c5dadee350
This commit is contained in:
Vladimir Krivosheev
2025-11-12 17:49:28 +00:00
committed by intellij-monorepo-bot
parent 9b7fc15cbb
commit bc38cfec45
9 changed files with 64 additions and 204 deletions
@@ -230,7 +230,7 @@ fun intellijCommunityBaseFragment(): ProductModulesContentSpec = productModules
alias("com.intellij.platform.ide.provisioner")
include(CommunityProductFragments.platformLangBaseFragment())
deprecatedInclude("intellij.java.ide.resources", "META-INF/JavaIdePlugin.xml")
include(CommunityProductFragments.javaIdeBaseFragment())
deprecatedInclude("intellij.idea.community.customization", "META-INF/tips-intellij-idea-community.xml")
moduleSet(CommunityModuleSets.debuggerStreams())
@@ -2,14 +2,22 @@
<!-- To regenerate, run 'Generate Product Layouts' or directly CommunityModuleSets.main() -->
<!-- Source: org.jetbrains.intellij.build.IdeaCommunityProperties -->
<idea-plugin xmlns:xi="http://www.w3.org/2001/XInclude">
<module value="com.intellij.modules.all"/>
<module value="com.intellij.modules.idea"/>
<module value="com.intellij.modules.idea.community"/>
<module value="com.intellij.modules.java-capable"/>
<module value="com.intellij.modules.jsp.base"/>
<module value="com.intellij.modules.python-core-capable"/>
<module value="com.intellij.modules.python-in-non-pycharm-ide-capable"/>
<module value="com.intellij.platform.ide.provisioner"/>
<xi:include href="/META-INF/PlatformLangPlugin.xml"/>
<xi:include href="intellij.platform.remoteServers.impl.xml">
<xi:fallback/>
</xi:include>
<xi:include href="/META-INF/DesignerCorePlugin.xml">
<xi:fallback/>
</xi:include>
<xi:include href="/META-INF/JavaIdePlugin.xml"/>
<xi:include href="/META-INF/tips-intellij-idea-community.xml"/>
<xi:include href="/META-INF/community-customization.xml"/>
@@ -1,29 +1,4 @@
<!--
~ Copyright 2000-2014 JetBrains s.r.o.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<idea-plugin xmlns:xi="http://www.w3.org/2001/XInclude">
<module value="com.intellij.modules.all"/>
<module value="com.intellij.modules.jsp.base"/>
<xi:include href="intellij.platform.remoteServers.impl.xml">
<xi:fallback/>
</xi:include>
<xi:include href="/META-INF/DesignerCorePlugin.xml">
<xi:fallback/>
</xi:include>
<idea-plugin>
<extensions defaultExtensionNs="com.intellij">
<applicationService serviceInterface="com.intellij.openapi.application.IdeUrlTrackingParametersProvider"
serviceImplementation="com.intellij.ide.customization.UtmIdeUrlTrackingParametersProvider"
@@ -3,9 +3,7 @@ package org.jetbrains.intellij.build.productLayout
import com.intellij.openapi.util.JDOMUtil
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.jdom.Element
import org.jetbrains.intellij.build.productLayout.analysis.ProductSpec
import java.nio.file.Path
import kotlin.io.path.exists
import kotlin.io.path.isRegularFile
@@ -201,7 +199,6 @@ object DuplicateIncludeDetector {
return fileName.endsWith("-customization.xml") ||
fileName == "ultimate.xml" ||
fileName == "PlatformLangPlugin.xml" ||
fileName == "JavaIdePlugin.xml" ||
fileName == "structuralsearch.xml" ||
fileName.endsWith("Plugin.xml")
}
@@ -210,33 +207,11 @@ object DuplicateIncludeDetector {
* Represents a single xi:include element.
*/
private data class XiInclude(
val href: String,
val lineNumber: Int?
@JvmField val href: String,
@JvmField val lineNumber: Int?
)
}
/**
* Detects and prints duplicate xi:include elements in product plugin.xml files.
* This is a convenience function that takes discovered products and outputs JSON report.
*
* @param products List of discovered products with their plugin XML paths
* @param projectRoot Project root directory for resolving relative paths
*/
fun detectAndPrintDuplicateIncludes(products: List<ProductSpec>, projectRoot: Path) {
// Convert ProductSpec plugin paths to actual file paths
val productFiles = products
.mapNotNull { it.pluginXmlPath }
.map { projectRoot.resolve(it) }
.filter { it.exists() && it.isRegularFile() }
// Run detection
val report = DuplicateIncludeDetector.detectDuplicates(productFiles, projectRoot)
// Output JSON
val json = Json { prettyPrint = true }
println(json.encodeToString(report))
}
/**
* Report containing all detected duplicate includes.
*/
@@ -113,7 +113,7 @@ fun streamModuleAnalysisJson(
}
filter.filter == "duplicates" -> {
gen.writeObjectFieldStart("duplicateAnalysis")
writeDuplicateAnalysis(gen, allModuleSets)
writeDuplicateAnalysis(gen, allModuleSets, enrichedProducts, projectRoot)
gen.writeEndObject()
}
filter.filter == "product" && filter.value != null -> {
@@ -219,7 +219,7 @@ private fun writeAllSections(
// Write duplicate analysis
gen.writeObjectFieldStart("duplicateAnalysis")
writeDuplicateAnalysis(gen, allModuleSets)
writeDuplicateAnalysis(gen, allModuleSets, products, projectRoot)
gen.writeEndObject()
// Write product composition analysis
@@ -29,8 +29,8 @@ internal fun StringBuilder.appendOpeningTag(
// Determine if xi:include namespace is needed
val hasXmlIncludes = !inlineXmlIncludes && spec.deprecatedXmlIncludes.isNotEmpty()
val hasModuleSetIncludes = !inlineModuleSets && spec.moduleSets.isNotEmpty()
val needsXiNamespace = hasXmlIncludes || hasModuleSetIncludes
// when we inline another some xi-include file, it can in turn have own xi-includes
val needsXiNamespace = inlineXmlIncludes || hasXmlIncludes || hasModuleSetIncludes
if (needsXiNamespace) {
append("<idea-plugin xmlns:xi=\"http://www.w3.org/2001/XInclude\">\n")
}
@@ -62,7 +62,7 @@ internal fun generateXIncludes(
isUltimateBuild: Boolean,
) {
for (include in spec.deprecatedXmlIncludes) {
// When inlining: skip ultimate-only xi-includes in Community builds
// When inlining: skip ultimate-only `xi-includes` in Community builds
if (inlineXmlIncludes && include.ultimateOnly && !isUltimateBuild) {
continue
}
@@ -111,7 +111,7 @@ internal fun generateXIncludes(
}
/**
* Converts a resource path to an xi:include href path.
* Converts a resource path to an `xi:include` href path.
*/
internal fun resourcePathToXIncludePath(resourcePath: String): String {
return if (isModuleNameLikeFilename(resourcePath)) resourcePath else "/$resourcePath"
@@ -2,10 +2,15 @@
package org.jetbrains.intellij.build.productLayout.json
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.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
import kotlin.io.path.isRegularFile
/**
* Writes a single module set to JSON.
@@ -43,11 +48,13 @@ fun writeModuleSet(
}
/**
* Writes duplicate analysis section.
* Writes duplicate analysis section including both module duplicates and xi:include duplicates.
*/
fun writeDuplicateAnalysis(
gen: JsonGenerator,
allModuleSets: List<ModuleSetMetadata>
allModuleSets: List<ModuleSetMetadata>,
products: List<ProductSpec>,
projectRoot: Path
) {
// Find modules that appear in multiple module sets
val moduleToSets = mutableMapOf<String, MutableList<String>>()
@@ -121,4 +128,17 @@ fun writeDuplicateAnalysis(
}
}
gen.writeEndArray()
// xi:include duplicate detection
val productFiles = products
.mapNotNull { it.pluginXmlPath }
.map { projectRoot.resolve(it) }
.filter { it.exists() && it.isRegularFile() }
val report = DuplicateIncludeDetector.detectDuplicates(productFiles, projectRoot)
// Serialize using kotlinx.serialization and write raw JSON (consistent with ModuleSet pattern)
val reportJson = kotlinxJson.encodeToString(report)
gen.writeFieldName("xiIncludeDuplicates")
gen.writeRawValue(reportJson)
}
@@ -1,142 +0,0 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.intellij.build
import com.intellij.openapi.application.PathManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import org.jetbrains.intellij.build.productLayout.DuplicateIncludeDetector
import java.nio.file.Path
import kotlin.io.path.exists
import kotlin.io.path.isDirectory
import kotlin.io.path.isRegularFile
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.readText
import kotlin.io.path.walk
/**
* Command-line tool to detect duplicate xi:include elements in product plugin.xml files.
*
* Usage:
* bazel run //community/platform/build-scripts:DuplicateIncludeAnalyzer
*
* Output:
* JSON report showing products with duplicate includes, where each duplicate comes from,
* and summary statistics.
*/
object DuplicateIncludeAnalyzer {
@JvmStatic
fun main(args: Array<String>) {
runBlocking(Dispatchers.Default) {
val projectRoot = Path.of(PathManager.getHomePathFor(DuplicateIncludeAnalyzer::class.java)!!)
// Discover all product plugin.xml files
val productFiles = discoverProductFiles(projectRoot)
// Run detection
val report = DuplicateIncludeDetector.detectDuplicates(productFiles, projectRoot)
// Output JSON
val json = Json { prettyPrint = true }
println(json.encodeToString(report))
}
}
/**
* Discovers all product plugin.xml files in the project.
* Searches in known product directories and looks for files ending with Plugin.xml or named plugin.xml.
*/
private fun discoverProductFiles(projectRoot: Path): List<Path> {
val productFiles = mutableListOf<Path>()
// Known product directories to search
val productDirs = listOf(
projectRoot.resolve("community"),
projectRoot.resolve("ultimate"),
projectRoot.resolve("CIDR"),
projectRoot.resolve("goland"),
projectRoot.resolve("ruby"),
projectRoot.resolve("WebStorm"),
projectRoot.resolve("dbe"),
projectRoot.resolve("aqua"),
projectRoot.resolve("rider"),
projectRoot.resolve("python"),
projectRoot.resolve("plugins"),
)
for (dir in productDirs) {
if (!dir.exists() || !dir.isDirectory()) {
continue
}
// Search for Plugin.xml files in resources/META-INF directories
dir.walk()
.filter { it.isRegularFile() }
.filter { it.parent?.fileName?.toString() == "META-INF" }
.filter {
val name = it.fileName.toString()
name.endsWith("Plugin.xml") || name == "plugin.xml"
}
.filter { isProductFile(it) }
.forEach { productFiles.add(it) }
}
return productFiles.distinct()
}
/**
* Checks if an XML file is a product descriptor (not a plugin descriptor).
* Products don't have their own <id> tag or have <id>com.intellij</id>.
* Also filters out test files and non-product descriptors.
*/
private fun isProductFile(file: Path): Boolean {
// Skip test resources
if (file.toString().contains("/testResources/") ||
file.toString().contains("/testSrc/") ||
file.toString().contains("/test/")) {
return false
}
// Skip toolbox
if (file.toString().contains("/toolbox/")) {
return false
}
val fileName = file.fileName.toString()
// Skip module descriptor files (have dots in the name like intellij.platform.jewel.detektPlugin.xml)
if (fileName != "plugin.xml" && fileName.contains(".") && !fileName.matches(Regex("^[A-Z][a-zA-Z]*Plugin\\.xml$"))) {
return false
}
try {
val content = file.readText()
// Check for <id> tag that's NOT com.intellij
val idMatch = Regex("""<id>([^<]+)</id>""").find(content)
if (idMatch != null && idMatch.groupValues[1] != "com.intellij") {
// This is a plugin with its own ID, not a product
return false
}
// Check if it has ApplicationInfo.xml nearby (strong indicator of a product)
val resourceRoot = file.parent?.parent // Go up from META-INF to resources
if (resourceRoot != null) {
val ideaDir = resourceRoot.resolve("idea")
if (ideaDir.exists() && ideaDir.isDirectory()) {
val hasAppInfo = ideaDir.listDirectoryEntries()
.any { it.fileName.toString().endsWith("ApplicationInfo.xml") }
if (hasAppInfo) {
return true
}
}
}
// If no <id> tag or has com.intellij, likely a product
return true
}
catch (e: Exception) {
return false
}
}
}
@@ -18,4 +18,28 @@ object CommunityProductFragments {
fun platformLangBaseFragment(): ProductModulesContentSpec = productModules {
deprecatedInclude("intellij.platform.resources", "META-INF/PlatformLangPlugin.xml")
}
/**
* Java IDE base fragment: provides Java IDE module aliases and optional plugin includes.
*
* Includes:
* - Module aliases for Java IDE capability detection
* - Optional remote servers support
* - Optional UI Designer support
* - Extensions for IDEA-specific customization (UTM tracking, new UI onboarding)
*
* Use this fragment for products that include Java IDE functionality.
*/
fun javaIdeBaseFragment(): ProductModulesContentSpec = productModules {
// Module capability aliases
alias("com.intellij.modules.all")
alias("com.intellij.modules.jsp.base")
// Optional plugin support (with fallback)
deprecatedInclude("intellij.platform.remoteServers.impl", "intellij.platform.remoteServers.impl.xml", optional = true)
deprecatedInclude("intellij.uiDesigner", "META-INF/DesignerCorePlugin.xml", optional = true)
// Extensions block (UTM tracking, new UI onboarding)
deprecatedInclude("intellij.java.ide.resources", "META-INF/JavaIdePlugin.xml")
}
}