platform/build-scripts/CONTENT_MODULE_EMBEDDING.md
This document explains how the IntelliJ Platform build system handles content module descriptor embedding in a scrambling-aware manner, solving two critical issues with obfuscated code.
Related Issue: IJPL-215077
Related Files:
classPath/contentModuleEmbedding.kt - Core embedding functionsclassPath/classpath.kt - plugin-classpath.txt generation & scrambled embeddingimpl/productModuleLayout.kt - Product module handlingimpl/PluginXmlPatcher.kt - Plugin XML patching & non-scrambled embeddingimpl/CachedDescriptorContainer.kt - Descriptor cache implementationzkm/ZkmScrambleTool.kt - Scrambling & cache updatescore-impl/src/com/intellij/ide/plugins/PluginDescriptorLoader.ktContent modules are a modular way to organize plugin/product code. They allow:
<content>
<module name="my.module.core"/>
<module name="my.module.optional" loading="on-demand"/>
</content>
| Aspect | Product Modules | Plugin Content Modules |
|---|---|---|
| Definition | Content modules of the core product/platform | Content modules within individual plugins |
| Descriptor | Main product plugin.xml (e.g., IdeaCorePlugin.xml) | Plugin's plugin.xml file |
| Layout Type | PlatformLayout | PluginLayout |
| Handled In | productModuleLayout.kt::processProductModule() | PluginXmlPatcher.kt::patchPluginXml() |
| Examples | intellij.platform.coverage, intellij.platform.debugger.impl | Plugin-specific modules |
| Scrambling Check | isInScrambledFile (embedded + closed-source) | pathsToScramble.isEmpty() |
Key Insight: Product modules are essentially "content modules of the core product" and follow the same XML structure but with different embedding logic.
When content module descriptors were embedded as CDATA sections in XML at build time, the scrambler (obfuscator) couldn't modify class names inside those CDATA sections because XML scramblers don't process CDATA content.
<!-- Scrambler CANNOT modify class names here ❌ -->
<module name="some.module"><![CDATA[
<extensions>
<implementation class="com.example.OriginalClassName"/>
</extensions>
]]></module>
Previously, descriptor content was inlined too early in the build process (before scrambling), so scrambled class names weren't reflected in the final descriptors.
Old Flow:
Compilation → Layout (inline descriptors) → Scrambling → Distribution
↑
Problem: Uses unscrambled names!
CDATA (Character Data) sections tell XML parsers: "treat this as raw text, don't parse it."
Example:
<module name="my.module"><![CDATA[
<extensions>
<implementation class="com.example.OriginalClassName"/>
</extensions>
]]></module>
Inside the CDATA:
<extensions> is NOT parsed as an XML element (it's text)class="com.example.OriginalClassName" is NOT parsed as an attribute (it's text)Scrambling tools that work on XML typically:
class="..." attributesProblem: Inside CDATA, there IS no element tree. It's all text.
Regular XML: CDATA:
<class>Foo</class> <![CDATA[<class>Foo</class>]]>
↓ ↓
Element with text content Single text node: "<class>Foo</class>"
(scrambler can parse) (scrambler sees raw string)
Content module descriptors must be embedded as CDATA because:
The Catch-22:
CachedDescriptorContainer is a thread-safe, in-memory cache that stores plugin descriptor file contents (XML) as byte arrays throughout the build process. It acts as a shared state container that spans multiple build phases.
Key Characteristics:
ConcurrentHashMap<Key, Map<String, ByteArray>>┌─────────────────────────────────────────────────────┐
│ DESCRIPTOR CACHE LIFECYCLE │
└─────────────────────────────────────────────────────┘
LAYOUT PHASE (Populate Cache):
├─ JAR Packaging (JarPackager.kt)
│ └─ Caches META-INF/*.xml from source JARs
├─ XML Patching (PluginXmlPatcher.kt)
│ └─ Caches plugin.xml after patching
└─ xi:include Resolution (XIncludeElementResolverImpl)
└─ Caches included descriptor files
SCRAMBLING PHASE (Update Cache):
└─ ZkmScrambleTool.scramble()
└─ updatePackageIndexUsingTempFile()
└─ Reads scrambled JARs
└─ Extracts META-INF/*.xml files
└─ Updates cache with scrambled content
(Class names now obfuscated!)
CLASSPATH GENERATION (Read Cache):
└─ classpath.kt::generatePluginClassPath()
└─ Reads from cache (now contains scrambled names)
└─ Embeds in plugin-classpath.txt
Without the cache, the build system would read descriptors from JARs multiple times:
The cache solves this by:
Location: ZkmScrambleTool.kt::updatePackageIndexUsingTempFile() (lines 513-535)
After scrambling modifies JAR files, this function:
META-INF/// Simplified code from ZkmScrambleTool.kt
readZipFile(originalFile) { name, data ->
if ((name.startsWith("META-INF/") || !name.contains('/')) && name.endsWith(".xml")) {
cacheWriter.put(name, dataBuffer.toByteArray())
}
}
cacheWriter.apply() // Atomic update
Critical: This is why embedContentModules() in classpath.kt must run AFTER scrambling - it reads from this updated cache.
The cache is scoped to prevent conflicts:
// Platform scope - all platform modules
val platformCache = cachedDescriptorContainer.forPlatform(platformLayout)
// Plugin scope - specific plugin directory (includes OS variant)
val pluginCache = cachedDescriptorContainer.forPlugin(pluginDir)
This ensures:
The solution uses two different code paths depending on whether code is scrambled:
pathsToScramble.isEmpty() == truepathsToScramble.isEmpty() == falseImportant: There are two distinct levels of xi:include elements that are handled differently:
Always resolved - These are xi:includes in the main plugin.xml that define the plugin structure.
<!-- These ARE resolved to find all <content> declarations -->
<idea-plugin>
<xi:include href="META-INF/PlatformExtensionPoints.xml"/>
<xi:include href="META-INF/SomeFileWith ContentTag.xml"/>
</idea-plugin>
Why resolved?
From productModuleLayout.kt:
// Scrambling isn't an issue: the scrambler can modify XML.
// If a file is included, we assume—and it should be the case—that both the including
// module and the module containing the included file are scrambled together.
// Note: CDATA isn't processed, so embedded content modules use different logic.
// We must resolve includes to collect all content modules, since the <content> tag may
// be specified in an included file. This is done not only for performance but for correctness.
Key point: We must resolve these xi:includes because:
<content> tags might be in included files<content><module>)Conditionally embedded - These are the actual content module descriptor files.
<content>
<!-- This module's descriptor is conditionally embedded as CDATA -->
<module name="my.module.core"/>
</content>
Handling depends on scrambling:
Why the distinction?
┌─────────────────────────────────────────────────────────────────────────┐
│ COMPLETE BUILD PIPELINE WITH CACHE STATE │
└─────────────────────────────────────────────────────────────────────────┘
┌──────────────┐
│ Compilation │ Compile source code to classes
│ Phase │ Cache State: Empty
└──────┬───────┘
│
v
┌──────────────┐
│ Layout │ Organize modules into JARs
│ Phase │ • Create CachedDescriptorContainer
│ │ - forPlatform() for platform descriptors
│ │ - forPlugin() for plugin descriptors
│ │ • Cache descriptors DURING:
│ │ - JAR packaging (JarPackager.kt)
│ │ - Plugin XML patching (PluginXmlPatcher.kt)
│ │ - xi:include resolution (XIncludeElementResolverImpl)
│ │ Cache State: Original descriptors ✓
└──────┬───────┘
│
v
┌──────────────┐
│ Scrambling │ Obfuscate class names (if configured)
│ Phase │ • Modify JAR files
│ │ • Update cache via updatePackageIndexUsingTempFile()
│ │ • Read scrambled JARs, extract XML, update cache
│ │ Cache State: Scrambled descriptors ✓
└──────┬───────┘
│
├─────────────────────────┬─────────────────────────────────┐
│ │ │
v v v
┌────────────────┐ ┌──────────────────┐ ┌──────────────────────┐
│ PRODUCT │ │ PLUGIN │ │ PLUGIN │
│ MODULES │ │ (Non-Scrambled) │ │ (Scrambled) │
│ │ │ │ │ │
│ Check: │ │ PluginXmlPatcher │ │ Resolve structural │
│ isInScrambled │ │ .kt (Layout) │ │ xi:includes │
│ File? │ │ │ │ but DON'T embed │
│ │ │ if (pathsTo │ │ content modules │
│ if (!isIn │ │ Scramble │ │ │
│ Scrambled │ │ .isEmpty()) { │ │ if (!pathsTo │
│ File) { │ │ embed now │ │ Scramble │
│ embed now │ │ (reads cache: │ │ .isEmpty()) { │
│ (reads cache:│ │ original) │ │ defer to │
│ original) │ │ } │ │ classpath.kt │
│ (non- │ │ │ │ } │
│ embedded: │ │ Embeds content │ │ │
│ separate │ │ modules in │ │ │
│ classloader) │ │ plugin.xml │ │ │
└────────────────┘ └──────────────────┘ └──────────────────────┘
│ │ │
│ │ │
v v v
┌──────────────────────────────────────────────────────────────┐
│ Generate plugin-classpath.txt (classpath.kt) │
│ │
│ For scrambled plugins: │
│ if (!pluginLayout.pathsToScramble.isEmpty()) { │
│ embedContentModules(...) // Read from UPDATED cache! │
│ } // Cache contains scrambled │
│ // class names after Phase 3 │
│ Result: Binary file with pre-computed metadata │
└──────────────────────────────────────────────────────────────┘
│
v
┌──────────────┐
│Distribution │ Generate OS-specific distributions
│ Phase │ Cache State: No longer needed
└──────────────┘
| Type | Scrambling Check | Condition | Action | Location |
|---|---|---|---|---|
| Product Module | isInScrambledFile | |||
| (embedded + closed-source) | !isInScrambledFile | Embed now in product XML | ||
| (for xi:include resolution) | productModuleLayout.kt | |||
| Product Module | isInScrambledFile | isInScrambledFile | Don't embed | |
| (use plugin-classpath.txt) | productModuleLayout.kt | |||
| Plugin Content | pathsToScramble.isEmpty() | .isEmpty() | Embed now in plugin.xml | PluginXmlPatcher.kt |
| Plugin Content | pathsToScramble.isEmpty() | !.isEmpty() | Defer to plugin-classpath.txt | classpath.kt |
// We do not embed the module descriptor because scrambling can rename classes.
//
// However, we cannot rely solely on the `PLUGIN_CLASSPATH` descriptor: for non-embedded modules,
// xi:included files (e.g., META-INF/VcsExtensionPoints.xml) are not resolvable from the core
// classpath, since a non-embedded module uses a separate classloader.
//
// Because scrambling applies only (by policy) to embedded modules, we embed the module descriptor
// for non-embedded modules to address this.
//
// Note: We could implement runtime loading via the module's classloader, but that would
// significantly complicate the runtime code.
if (!isInScrambledFile) {
resolveAndEmbedContentModuleDescriptor(...)
}
Why the distinction?
A binary optimization file containing pre-computed plugin metadata:
Location: lib/plugin-classpath.txt in the distribution
Functions:
writePluginClassPathHeader() - Writes format version, product descriptor, plugin countgeneratePluginClassPath() - Writes plugin entries with descriptors and file listsCritical Code (classpath.kt):
// ONLY embed if scrambling is enabled
if (!pluginLayout.pathsToScramble.isEmpty()) {
val xIncludeResolver = createXIncludeElementResolver(
searchPath = listOf(
pluginLayout.includedModules.mapTo(LinkedHashSet()) { it.moduleName }
to pluginDescriptorContainer,
platformLayout.includedModules.mapTo(LinkedHashSet()) { it.moduleName }
to platformDescriptorContainer,
),
context = context,
)
embedContentModules(
rootElement = rootElement,
pluginLayout = pluginLayout,
pluginDescriptorContainer = pluginDescriptorContainer,
xIncludeResolver = xIncludeResolver,
context = context,
)
}
// else: Don't embed content modules; defer to plugin-classpath.txt
// (structural xi:includes ARE still resolved to find <content> tags)
Process:
plugin-classpath.txt (binary format)descriptorContent != null: Parse embedded CDATA (scrambled names!)descriptorContent == null: Load from file/JARKey Code (PluginDescriptorLoader.kt):
for (module in descriptor.content.modules) {
if (module.descriptorContent == null) {
// Not embedded - load from separate file/JAR
// This happens for non-scrambled plugins
val jarFile = pluginDir.resolve("lib/modules/${module.moduleId.name}.jar")
classPath = Collections.singletonList(jarFile)
loadModuleFromSeparateJar(...)
}
else {
// Embedded as CDATA - parse directly from memory!
// This happens for scrambled plugins
// Class names are already scrambled!
val subRaw = PluginDescriptorFromXmlStreamConsumer(...).let {
it.consume(createXmlStreamReader(module.descriptorContent))
it.getBuilder()
}
}
}
┌─────────────────────────────────────────────────────────────┐
│ plugin-classpath.txt │
├─────────────────────────────────────────────────────────────┤
│ HEADER │
├─────────────────────────────────────────────────────────────┤
│ [1 byte] Format Version (2) │
│ [1 byte] jarOnly Flag (0/1) │
│ [4 bytes] Product Descriptor Size │
│ [N bytes] Product Descriptor Content (XML with CDATA) │
│ [2 bytes] Plugin Count │
├─────────────────────────────────────────────────────────────┤
│ PLUGIN ENTRIES (repeated for each plugin) │
├─────────────────────────────────────────────────────────────┤
│ [2 bytes] File Count │
│ [UTF] Plugin Directory Name │
│ [4 bytes] Plugin Descriptor Size │
│ [N bytes] Plugin Descriptor Content (XML, possibly CDATA) │
│ [UTF] File Path 1 (relative to plugin dir) │
│ [UTF] File Path 2 │
│ ... │
│ [UTF] File Path N │
└─────────────────────────────────────────────────────────────┘
| Without plugin-classpath.txt | With plugin-classpath.txt |
|---|---|
| Scan plugin directories | Read single binary file |
| Open each JAR to find plugin.xml | Descriptors already in memory |
| Parse XML for each plugin | Parse pre-cached bytes |
| Resolve xi:includes at runtime | Embedded as CDATA (if scrambled) |
┌─────────────────────────────────────────────────────────────────┐
│ BUILD TIME → RUNTIME CONNECTION │
└─────────────────────────────────────────────────────────────────┘
BUILD TIME RUNTIME
┌──────────────────────┐ ┌────────────────────────────┐
│ 1. Compilation │ │ 1. Read plugin-classpath │
│ └─> Source to │ │ .txt │
│ classes │ │ │
├──────────────────────┤ ├────────────────────────────┤
│ 2. Layout │ │ 2. Parse header │
│ └─> Organize JARs │ │ • Version │
│ └─> Cache │ │ • Product descriptor │
│ descriptors │ │ • Plugin count │
│ (during JAR │ ├────────────────────────────┤
│ packaging, │ │ 3. For each plugin: │
│ XML patching) │ │ • Read descriptor bytes │
├──────────────────────┤ │ • Parse from memory │
│ 3. Scrambling │ writes │ (no disk I/O!) │
│ └─> Obfuscate │ ═══════> ├────────────────────────────┤
│ class names │ to file │ 4. Handle content modules: │
│ └─> Modifies │ │ if (descriptorContent │
│ cached │ │ != null) { │
│ descriptors │ │ // Embedded CDATA │
├──────────────────────┤ │ // Scrambled names! │
│ 4a. PluginXmlPatcher │ │ parse from memory │
│ if (pathsTo │ │ } else { │
│ Scramble │ │ // Load from file │
│ .isEmpty()) { │ │ load via DataLoader │
│ embed in │ │ } │
│ plugin.xml │ ├────────────────────────────┤
│ } │ │ 5. Create classloaders │
│ │ │ with pre-sorted JARs │
│ 4b. classpath.kt │ └────────────────────────────┘
│ if (!pathsTo │
│ Scramble │
│ .isEmpty()) { │
│ embed in │
│ PLUGIN_ │
│ CLASSPATH.txt │
│ (read from │
│ cache) │
│ } │
└──────────────────────┘
│
└──── Content modules with scrambled class names ────────┘
embedContentModules()Location: classPath/contentModuleEmbedding.kt
Purpose: Embeds content module descriptors as CDATA in a plugin's root descriptor.
Called From:
classpath.kt - For scrambled plugins (plugin-classpath.txt generation)PluginXmlPatcher.kt - For non-scrambled plugins (plugin.xml patching)Process:
<content>/<module> elementsresolveAndEmbedContentModuleDescriptor()separate-jar attribute)resolveAndEmbedContentModuleDescriptor()Location: classPath/contentModuleEmbedding.kt
Purpose: Helper function that resolves a content module descriptor and embeds it as CDATA.
Key Features:
Critical: Reads from cache after scrambling, ensuring scrambled class names are used.
resolveContentModuleDescriptor()Location: classPath/contentModuleEmbedding.kt
Purpose: Resolves and loads a content module descriptor from cache or source.
Cache Strategy:
cachedDescriptorContainer for already-processed descriptorLocation: impl/productModuleLayout.kt::processProductModule()
Logic:
val isEmbedded = moduleElement.getAttributeValue("loading") == "embedded"
val isInScrambledFile = isEmbedded && isModuleCloseSource(moduleName, context)
if (!isInScrambledFile) {
// Non-scrambled OR non-embedded: Embed now for xi:include resolution
resolveAndEmbedContentModuleDescriptor(
moduleElement = moduleElement,
cachedDescriptorContainer = cachedDescriptorContainer,
xIncludeResolver = xIncludeResolver,
context = context,
)
}
// else: Scrambled file - will be handled via plugin-classpath.txt at runtimeĶ
Why?
PluginXmlPatcher runs during the Layout Phase, BEFORE scrambling occurs. At this point:
Note: Before this logic runs, structural xi:includes in plugin.xml are ALREADY resolved to find all <content> tags.
When: Layout Phase (before scrambling)
Location: impl/PluginXmlPatcher.kt::patchPluginXml() (lines 94-108)
Condition: pluginLayout.pathsToScramble.isEmpty() == true
For non-scrambled plugins, it's safe to embed immediately:
// Structural xi:includes already resolved by this point
filterAndProcessContentModules(rootElement, pluginMainModuleName, context) { moduleElement, moduleName, _ ->
if (pluginLayout.pathsToScramble.isEmpty()) {
// NOT scrambled → embed content module descriptors now in plugin.xml
// Safe because: no scrambling = no class name changes
embedContentModules(
moduleElement = moduleElement,
pluginDescriptorContainer = descriptorContainer,
xIncludeResolver = xIncludeResolver,
context = context,
moduleName = moduleName,
)
}
// else: Scrambling enabled → skip embedding
// Will be handled later in classpath.kt after scrambling
}
What happens to scrambled plugins here?
<content> tags)<module> elements remain emptyWhen: Classpath Generation Phase (after scrambling)
Location: classPath/classpath.kt::generatePluginClassPath() (lines 193-209)
Condition: pluginLayout.pathsToScramble.isEmpty() == false
For scrambled plugins, embedding must wait until after scrambling:
if (!pluginLayout.pathsToScramble.isEmpty()) {
// IS scrambled → embed in plugin-classpath.txt
// Reads from UPDATED cache (post-scrambling)
embedContentModules(
rootElement = rootElement,
pluginLayout = pluginLayout,
pluginDescriptorContainer = pluginDescriptorContainer, // Contains scrambled content!
xIncludeResolver = xIncludeResolver,
context = context,
)
}
Key Insight: The same embedContentModules() function is called in both paths, but:
<!-- CLionPlugin.xml: Example of problematic inlined content -->
<idea-plugin>
<!-- <editor-fold desc="Inlined from PlatformLangPlugin.xml"> -->
<id>com.intellij</id>
<name>IDEA CORE</name>
<module value="com.intellij.modules.platform" />
<xi:include href="/META-INF/PlatformLangComponents.xml" />
<!-- ... 300+ more lines ... -->
<!-- </editor-fold> -->
</idea-plugin>
Problems:
<!-- CLionPlugin.xml: Clean xi:include references -->
<idea-plugin>
<xi:include href="META-INF/PlatformLangPlugin.xml"/>
<xi:include href="intellij.platform.remoteServers.impl.xml"/>
<xi:include href="META-INF/ultimate.xml"/>
</idea-plugin>
Benefits:
Location: build/testSrc/.../IdeaUltimateBuildTest.kt
The test verifies both issues are resolved:
// 1. Verify product descriptor doesn't contain unscrambled class names
val xmlContent = cachedXml.decodeToString()
assertThat(xmlContent).doesNotContain("com.intellij.ide.todo.TodoConfiguration")
assertThat(xmlContent).doesNotContain("com.intellij.ide.bookmarks.Bookmark")
// 2. Verify plugin descriptor doesn't contain unscrambled class names
val pluginContent = (distFiles.first().content as InMemoryDistFileContent).data.decodeToString()
assertThat(pluginContent).doesNotContain(
"com.intellij.cwm.connection.backend.license.OpenLicenseSettingsAction"
)
Check 1: Does your plugin configure scrambling?
Look for pathsToScramble in your plugin's layout configuration:
// In your PluginLayout builder:
pluginLayout {
mainModule = "your.plugin.main"
pathsToScramble = listOf("lib/your-plugin.jar")
// If pathsToScramble is non-empty, you need scrambling-aware embedding
}
Check 2: Does your plugin have content modules?
Look for <content> tags in your plugin.xml:
<!-- In your plugin.xml -->
<content>
<module name="your.plugin.core"/>
<module name="your.plugin.optional"/>
</content>
Result: If BOTH checks are true, your plugin uses Path 2 (scrambled embedding via plugin-classpath.txt).
Step 1: Build with scrambling enabled
# Full distribution build
./gradlew buildDistribution
# Or plugin-specific build
./gradlew :intellij.idea.ultimate.build:buildPlugin
Step 2: Check the build output
cd build/dist/plugins/YourPlugin/lib
# plugin-classpath.txt should exist
ls -la plugin-classpath.txt
Step 3: Inspect the descriptor cache (for debugging)
Add temporary logging in embedContentModules():
// In classPath/contentModuleEmbedding.kt
val cachedData = pluginDescriptorContainer.getCachedFileData(descriptorFilename)
println("Module: $moduleName")
println("Descriptor content: ${cachedData?.decodeToString()?.take(500)}")
// Should see scrambled class names like "com.a.b.c" instead of "com.example.Foo"
Step 4: Verify at runtime
Run the IDE with the scrambled plugin and check:
ClassNotFoundException errorsWrong: Manually embedding before scrambling
// DON'T DO THIS:
fun layout() {
embedContentModules(...) // Uses original class names!
scramble(...) // Too late - already embedded
}
Right: Let the build system handle timing
The build system automatically:
Wrong: Added content module but forgot to scramble its JAR
pluginLayout {
pathsToScramble = listOf("lib/main.jar")
// Added new content module:
content {
module("new.module") // Will be in lib/modules/new.module.jar
}
// Problem: new.module.jar is NOT in pathsToScramble!
}
Right: Scramble all JARs with scrambled code
pluginLayout {
pathsToScramble = listOf(
"lib/main.jar",
"lib/modules/new.module.jar" // Don't forget this!
)
}
Wrong: Caching descriptor content across phases
// DON'T DO THIS:
val descriptorContent = cache.get("plugin.xml") // During Layout
// ... scrambling happens ...
useDescriptor(descriptorContent) // Uses OLD content!
Right: Always read from cache when needed
The cache is updated by scrambling, so always read fresh:
// After scrambling, read from cache again
val currentDescriptor = cache.get("plugin.xml") // Gets scrambled version
# For Gradle builds
./gradlew buildDistribution -Dintellij.build.verbose=true
# For tests
./tests.cmd -Dintellij.build.verbose=true \
-Dintellij.build.test.patterns=ProductModulesXmlConsistencyTest
Scrambling logs contain useful information about what was processed:
# Find scrambling logs
find build/artifacts -name "scramble-logs" -type d
# Extract and view
cd build/artifacts/scramble-logs
unzip -l your-plugin.zip
Add breakpoint or logging in:
ZkmScrambleTool.kt::updatePackageIndexUsingTempFile() (line 513)# Extract unscrambled JAR (before scrambling)
cd build/dist-unscrambled/plugins/YourPlugin
unzip -p lib/your-plugin.jar META-INF/plugin.xml > before.xml
# Extract scrambled JAR (after scrambling)
cd build/dist/plugins/YourPlugin
unzip -p lib/your-plugin.jar META-INF/plugin.xml > after.xml
# Compare class names
grep -o 'class="[^"]*"' before.xml | head -10 # Original names
grep -o 'class="[^"]*"' after.xml | head -10 # Scrambled names
Run the consistency test to verify no unscrambled names leak:
./tests.cmd \
-Dintellij.build.clean.output.root=false \
-Dintellij.build.incremental.compilation=true \
-Dintellij.build.test.patterns=ProductModulesXmlConsistencyTest
This test automatically checks:
Symptom: Plugin fails to load with ClassNotFoundException: com.example.OriginalClassName
Cause: Content module was embedded before scrambling with original class names, but the actual class was renamed.
Solution:
pathsToScramble includes the module's JARSymptom: Long startup time, many disk I/O operations
Cause: Content modules not embedded in plugin-classpath.txt, falling back to runtime loading.
Solution:
lib/ directorypathsToScramble is configured correctlyembedContentModules() is being called in classpath.ktSymptom: Build error: "Could not find descriptor for module X in cache"
Cause: Descriptor wasn't cached during Layout phase.
Solution:
META-INF/plugin.xml or the expected descriptorAlways configure pathsToScramble for closed-source plugins
Test both scrambled and non-scrambled builds
Monitor cache updates during development
Use incremental builds for faster iteration
./tests.cmd \
-Dintellij.build.clean.output.root=false \
-Dintellij.build.incremental.compilation=true
Document scrambling requirements
The scrambling-aware architecture solves both issues through a dual-path approach:
!pathsToScramble.isEmpty()): Embed in plugin-classpath.txt after scramblingpathsToScramble.isEmpty()): Embed in plugin.xml during patchingBuild Pipeline Order: Compilation → Layout (with caching) → Scrambling → Conditionally Embed (dual path) → Distribution
This architecture ensures correctness (scrambled names preserved) while maintaining optimal performance (pre-computed metadata).