platform/build-scripts/product-dsl/docs/dependency_generation.md
This document describes how IntelliJ's build system generates module dependencies in XML descriptor files.
Question: Which JPS (.iml) dependencies become <module> entries in XML?
Answer: Only those with PRODUCTION_RUNTIME scope.
| JPS Scope | In PRODUCTION_RUNTIME? | → XML? | Why |
|---|---|---|---|
| COMPILE | ✓ Yes | ✓ Yes | Code needed at compile AND runtime |
| RUNTIME | ✓ Yes | ✓ Yes | Runtime-only dependencies |
| PROVIDED | ✗ No | ✗ No | Provided by runtime environment |
| TEST | ✗ No | ✗ No | Test code only |
Data Flow:
.iml files → Graph (all edges + scope) → Filter (PRODUCTION_RUNTIME) → XML output
All dependency generation and validation must use PluginGraph as the single source of truth. Avoid re-parsing plugin.xml or reading module/product descriptors from disk to determine dependencies or "pseudo-core" plugins; product DSL and module sets already populate the graph.
ModuleSetGenerationConfig.projectLibraryToModuleMap (built from JPS library modules), not by scanning .idea libraries or module libraries.The graph is still incomplete when computePluginContentFromDslSpec runs:
To keep auto-add decisions graph-driven, model building pre-marks all JPS targets that have
{moduleName}.xml descriptors on disk. This descriptor-presence flag is stored on content module nodes
and used by DSL test plugin expansion to decide which JPS deps are eligible for auto-add, without
additional disk I/O during generation. These modules are registered in the graph later when the DSL test
plugin content is added via addPluginWithContent.
Invariant: markDescriptorModules() must run after the last graph mutation before
computePluginContentFromDslSpec executes. The graph snapshot carries a descriptorFlagsComplete flag,
and DSL test plugin expansion fails fast if the flag is not set.
Validation behavior is specified in docs/validators/README.md.
Key Functions:
PluginGraphBuilder.addJpsDependencies() - stores ALL deps with scope as graph edgesContentModuleDependencyPlanner.computeJpsDeps() - filters to production scopescollectPluginGraphDeps() + filterPluginDependencies() (PluginDependencyPlanner) - plugin.xml filtering based on graphPROVIDED means "provided by the runtime environment":
These are needed at compile time for type checking, but the classes come from the environment at runtime, not from the dependency module.
TEST scope dependencies are only for test code execution. Production runtime
doesn't need them. Test descriptors (._test.xml) handle test deps separately.
Source: JpsJavaDependencyScope.java:23-26 defines which scopes include PRODUCTION_RUNTIME.
See quick-start.md for running the generator.
The dependency generation system uses a 5-stage pipeline architecture with slot-based ComputeNode execution. For the complete architecture diagram and system context, see architecture-overview.md.
Key optimizations:
PluginContentCache (pre-warmed in Stage 2)DeferredFileUpdater batches all writes for atomic commitValidation behavior is specified in docs/validators/README.md. This section provides generation-facing context only.
Plugin dependency validation queries PluginGraph directly; no parallel resolution map is built.
PluginGraph
│
▼
createResolutionQuery() (PluginDependencyResolution.kt)
│
▼
PluginContentStructureValidator (PluginContentStructureValidator.kt):
└── Structural validation (loading-mode constraints within a plugin)
PluginContentDependencyValidator (PluginContentDependencyValidator.kt):
├── Availability validation (deps in bundling products)
├── Global existence (ON_DEMAND deps exist somewhere)
└── Filtered dependency validation (implicit deps not in XML)
| Plugin Type | Bundling Source | REQUIRED Deps Scope | ON_DEMAND Deps |
|---|---|---|---|
| Production bundled | Graph EDGE_BUNDLES | product + bundled plugin modules | Global existence |
| Test bundled | Graph EDGE_BUNDLES_TEST | product + bundled plugin modules | Global existence |
| Non-bundled | (none) | N/A | Global existence |
Key insight: Test plugins rely on graph flags and bundling edges, so validation doesn't need a separate product map.
| Component | File |
|---|---|
| Plugin validation model building | src/validator/rule/PluginDependencyResolution.kt |
| Plugin validation logic | src/validator/PluginContentDependencyValidator.kt |
| Plugin structural validation | src/validator/PluginContentStructureValidator.kt |
| Content module plugin dep validation | src/validator/ContentModulePluginDependencyValidator.kt |
| Plugin dependency planning + XML writing | src/generator/ContentModuleDependencyGenerator.kt, src/generator/ContentModuleXmlWriter.kt, src/generator/PluginXmlDependencyGenerator.kt, src/generator/PluginXmlWriter.kt |
| Data models | src/validation/ValidationModels.kt |
A specialized validation rule ensures content modules properly declare plugin dependencies.
Problem: When intellij.foo (content module) has IML dependency on intellij.python.community.plugin (plugin main module), it needs <plugin id="PythonCore"/> in its XML. Without this, runtime fails with NoClassDefFoundError.
Solution: ContentModulePluginDependencyValidator validates:
contentProductionSources in the graphThis handles many-to-many content module → plugin relationships directly from the graph.
Suppressing Known Issues:
allowedMissingPluginIds in test plugin DSL.suppressions.json (contentModules.<module>.suppressPlugins).testPlugin(
pluginId = "intellij.some.tests.plugin",
name = "Some Tests Plugin",
pluginXmlPath = "path/to/testResources/META-INF/plugin.xml",
) {
module("intellij.some.tests.module", allowedMissingPluginIds = listOf("com.intellij.css"))
}
{
"contentModules": {
"intellij.react.ultimate": {
"suppressPlugins": ["com.intellij.css"]
}
}
}
See docs/validators/plugin-content-dependency.md for details.
Generates dependencies for product modules (modules declared in module sets like essential(), ideCommon()).
Responsibilities:
includeDependencies=true, library modules (intellij.libraries.*), settings modulesFiles updated: {moduleName}.xml in META-INF/
Generates dependencies for plugin content modules (modules declared in plugin.xml <content> sections).
Responsibilities:
._test modules){moduleName}.xml and {moduleName}._test.xml descriptors._test (their .xml IS the test descriptor)Files updated: content module XMLs, test descriptor XMLs
Generates dependencies for plugin.xml of plugin main modules.
Responsibilities:
plugin.xml - main plugin descriptorFiles updated: plugin.xml
Async-safe caching layer for module descriptor information.
Features:
Orchestrates all generation via generateAllModuleSetsWithProducts().
Configuration via ModuleSetGenerationConfig:
data class ModuleSetGenerationConfig(
val moduleSetSources: Map<String, Pair<Any, Path>>, // label → (source object, output dir)
val discoveredProducts: List<DiscoveredProduct>,
val testProductSpecs: List<Pair<String, ProductModulesContentSpec>> = emptyList(),
val projectRoot: Path,
val outputProvider: ModuleOutputProvider,
val nonBundledPlugins: Map<String, Set<TargetName>> = emptyMap(),
val knownPlugins: Set<TargetName> = emptySet(),
val testPluginsByProduct: Map<String, Set<TargetName>> = emptyMap(),
val includeTestPluginDescriptorsFromSources: Boolean = false,
val skipXIncludePaths: Set<String> = emptySet(),
val xIncludePrefixFilter: (moduleName: String) -> String? = { null },
val testFrameworkContentModules: Set<ContentModuleName> = emptySet(), // Modules indicating test plugins
val testingLibraries: Set<String> = emptySet(),
val testLibraryAllowedInModule: Map<ContentModuleName, Set<String>> = emptyMap(),
val pluginAllowedMissingDependencies: Map<ContentModuleName, Set<ContentModuleName>> = emptyMap(),
val libraryModuleFilter: (libraryModuleName: String) -> Boolean = { true },
val projectLibraryToModuleMap: Map<String, String> = emptyMap(),
val suppressionConfigPath: Path? = null,
val validationFilter: Set<String>? = null,
val dslTestPluginAutoAddLoadingMode: ModuleLoadingRuleValue = ModuleLoadingRuleValue.OPTIONAL,
)
testFrameworkContentModules - Modules that indicate a plugin is a test plugin when declared as <content>. Plugins declaring any of these modules are excluded from production validation because they won't be present at runtime. See validation-rules.md for details.
The plugin model has no concept of TEST/COMPILE/RUNTIME scopes - it only knows about runtime dependencies. JPS .iml files DO have scopes, which affects dependency generation. See TL;DR for the scope filtering rules.
The generator computes both production and test dependencies for each content module:
| Edge Type | JPS Scopes Included | Written to XML | Use Case |
|---|---|---|---|
EDGE_CONTENT_MODULE_DEPENDS_ON | COMPILE, RUNTIME (and TEST for test-runtime-only modules) | Yes | Production validation |
EDGE_CONTENT_MODULE_DEPENDS_ON_TEST | COMPILE, RUNTIME, TEST | No | Test plugin validation |
For written XML, test scope is also included when a module runs only in test runtime (test descriptor ._test modules and modules that only have test-plugin content sources). For these test-runtime-only modules, libraryModuleFilter is bypassed for both written and test dependency sets, so required test libraries are preserved.
Key insight: Content modules are production code with intrinsic dependencies. Scope filtering is based on where the module is sourced (production vs test-only), not on ad-hoc XML state.
intellij.platform.lang.iml has:
- intellij.platform.core (COMPILE scope)
- intellij.libraries.hamcrest (TEST scope)
Graph edges created:
EDGE_CONTENT_MODULE_DEPENDS_ON: lang -> core
EDGE_CONTENT_MODULE_DEPENDS_ON_TEST: lang -> core, lang -> hamcrest
Production validation: Only checks EDGE_CONTENT_MODULE_DEPENDS_ON
→ Won't report hamcrest as missing
Test plugin validation: Checks EDGE_CONTENT_MODULE_DEPENDS_ON_TEST
→ Will validate hamcrest is available
| Aspect | Module Descriptor Dependencies | Plugin Dependencies |
|---|---|---|
| Source | Modules in module sets | Modules in plugin.xml <content> |
| Generator | ModuleDescriptorDependencyGenerator | PluginDependencyPlanner + PluginXmlWriter |
| Files updated | {moduleName}.xml | plugin.xml, content module XMLs |
| Validation | Full transitive validation | JPS dependencies with filtering |
| Configuration | includeDependencies=true flag | Automatic for all content modules |
| Filtering | None (use @skip-dependency-generation to skip) | Globally embedded module filtering + suppressions |
Plugin XML dependencies are generated only for plugins that have a main target in the graph
(real plugin modules extracted from disk or discovered via dependencies). Placeholder plugin-id
nodes created only to model <depends> edges are skipped.
DSL-defined plugins (testPlugin {}) are generated from Kotlin specs; PluginXmlWriter
does not update their plugin.xml files (handled by TestPluginXmlGenerator).
Dependencies are computed from the graph's JPS edges (production-runtime scopes only); plugin.xml content is read only to preserve manual entries and xi:include content.
Dependencies are generated from production-scope JPS edges. A dependency can be omitted from XML in two cases:
suppressions.json (or allowlists).Implicit dependencies are JPS production deps missing from XML (JPS deps - XML deps). Validators treat these as auto-inferred JPS deps and still validate them unless they are suppressed or allowlisted.
See errors.md for error handling details.
Dependencies to globally embedded modules are automatically skipped when generating dependencies, except for same-plugin sibling content-module deps. This reduces XML bloat without dropping structural dependencies that must stay explicit.
A dependency target is skipped only if ALL of these conditions are true:
EMBEDDED.Embedded-check scope depends on dependency origin:
"Discovered real products" means GenerationModel.discovery.products (synthetic test product specs are excluded).
Embedded-check scope excludes analysis-only products that do not define runtime embedding guarantees for plugin dependencies (currently CodeServer).
If a target is embedded in every product from the embedded-check scope, it is always loaded at runtime and doesn't need explicit XML dependency declarations because:
| Target module | Plugin source present? | Embedded in embedded-check scope? | Skip? |
|---|---|---|---|
intellij.platform.core | No | Yes | ✓ Yes |
intellij.platform.frontend.split | No | No (embedded only in JetBrainsClient while plugin is bundled in Idea + JetBrainsClient) | ✗ No |
intellij.platform.core | Yes | Yes | ✓ Yes |
intellij.sh.core from intellij.sh.markdown in intellij.sh.plugin | Yes (same plugin sibling) | Yes | ✗ No |
This filtering applies to:
<dependencies><module> in plugin.xml)Content modules directly in products (via module sets) do NOT skip embedded deps because they're not "inside a plugin" - they're at the product level where the embedding relationship is defined. Same-plugin sibling content-module deps also stay explicit so generated descriptors and validation graph edges preserve the plugin's internal loading relationships.
The filtering is implemented in:
EmbeddedModuleUtils.kt - shared utility functionscollectPluginGraphDeps() + filterPluginDependencies() (PluginDependencyPlanner) - plugin.xml filteringContentModuleDependencyPlanner.computeJpsDeps() - content module filteringTestPluginDependencyPlanner - DSL test plugin filteringKey functions:
// Check if module has any plugin as content source
fun GraphScope.hasPluginSource(moduleId: Int): Boolean
// Check if target is globally embedded across all real products
fun GraphScope.shouldSkipEmbeddedPluginDependency(depModuleId: Int, allRealProductNames: Set<String>): Boolean
For module set modules (including library modules like intellij.libraries.*), embedded-module filtering is not applied.
All JPS dependencies with descriptors are included automatically.
If a module requires manual dependency management (e.g., for specific ordering requirements),
add the @skip-dependency-generation comment to the module descriptor XML file:
<idea-plugin visibility="internal">
<!-- @skip-dependency-generation - reason for manual management -->
<dependencies>
<!-- manually managed dependencies -->
</dependencies>
</idea-plugin>
Use cases:
intellij.libraries.junit5.jupiter)When this marker is present, the module is completely skipped by ModuleDescriptorDependencyGenerator,
preserving all manually specified dependencies.
Test plugins are special plugins that provide test framework modules for running tests.
Unlike regular products, test plugins have plugin.xml in test resources (testResources/META-INF/).
Plugins extracted from plugin.xml are detected as test plugins based on their content modules. DSL-defined test plugins (testPlugin {}) are always treated as test plugins even if they don't declare test framework modules. A plugin is a test plugin if it declares any test framework module in its <content> block:
testFrameworkContentModules = setOf(
"intellij.libraries.junit4",
"intellij.libraries.junit5",
"intellij.libraries.junit5.jupiter",
"intellij.platform.testFramework",
"intellij.platform.testFramework.core",
"intellij.tools.testsBootstrap",
)
Key implications:
EDGE_BUNDLES_TEST) instead of a separate product mapforTestPlugin (module sets + all bundled plugins); DSL test plugins use forDslTestPlugin (module sets + bundled production plugins + self)| Type | Definition | Auto-fix behavior |
|---|---|---|
| DSL-defined | Created via testPlugin {} in getProductContentDescriptor() | Skipped - fix in Kotlin |
| Discovered | Manually created with handwritten plugin.xml | Auto-fixes can be applied |
For DSL-defined test plugins, the generator can automatically add JPS module dependencies (production runtime, test runtime, and PROVIDED scopes) that have module descriptors but weren't explicitly declared.
Key Principle: Only add unresolvable modules - those not available in the same product (module sets + bundled production plugins; other test plugins excluded).
DSL test plugins also support a test-descriptor fallback for JPS targets: when dependency target X has no X.xml descriptor but has X._test.xml, dependency planning treats it as content module X._test.
JPS Dependencies (.iml)
│
▼
Check: Has module descriptor?
│
├── NO → Skip (not a content module)
│
└── YES → Check: Is module resolvable?
│
├── YES (in product module set/bundled production plugin content) → Skip
│
└── NO (unresolvable) → Auto-add to test plugin content
Auto-add uses the graph to check resolvable modules, but traverses JPS dependencies of the
explicit content modules. Project library dependencies resolve via ModuleSetGenerationConfig.projectLibraryToModuleMap
(built from JPS library modules, not graph targets), and libraryModuleFilter is ignored for DSL test plugins
because the test plugin must be a complete container in dev mode. Auto-added modules are written into the
generated test plugin content (the <!-- region additional --> block), so repeat runs are clean.
This fallback is scoped to DSL test plugin auto-add and does not change global content-module dependency generation/classification.
Why this design: Module sets are just convenience for avoiding duplication - they're NOT special. The auto-add logic respects them naturally without special handling.
| Aspect | Products | Test Plugins |
|---|---|---|
| plugin.xml location | resources/META-INF/ | testResources/META-INF/ |
| Module set handling | xi:include or inline | Always inlined |
| Dependency resolution | forProductionPlugin predicate | forTestPlugin predicate |
| Content modules | Satisfy other plugin deps | Don't satisfy production deps |
For DSL reference, see dsl-api-reference.md.
._test.xml)Test descriptors provide dependencies for test code.
Two cases:
Regular content modules (e.g., intellij.foo):
intellij.foo.xmlintellij.foo._test.xmlwithTests=true for JPS dependenciesTest content modules (e.g., intellij.foo._test):
.xml file IS the test descriptor._test._test.xml fileisTestDescriptor=true (production deps include TEST scope)Code logic:
val isTestDescriptor = contentModuleName.endsWith("._test")
val plan = planContentModuleDependenciesWithBothSets(
contentModuleName = contentModuleName,
isTestDescriptor = isTestDescriptor,
libraryModuleFilter = config.libraryModuleFilter,
)
// plan.moduleDependencies -> main descriptor
// plan.testDependencies -> moduleName._test.xml for non-test descriptor modules
Dependencies are written within region markers:
<dependencies>
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<module name="intellij.libraries.grpc"/>
<module name="intellij.platform.kernel"/>
<!-- endregion -->
</dependencies>
Region types:
WRAPS_ENTIRE_SECTION - module descriptors (region wraps whole <dependencies>)INSIDE_SECTION - real META-INF/plugin.xml descriptors only (region inside, preserves manual entries)NONE - legacy files without markersIf a non-plugin descriptor has region markers inside <dependencies>, generation normalizes it to WRAPS_ENTIRE_SECTION
behavior (manual entries outside the generated region are not implicitly preserved unless covered by suppression rules).
Validation is implemented by pipeline validators under src/validator/. See docs/validators/README.md for the authoritative specs. This document focuses on dependency generation only.
The suppression config system provides a JSON-based single source of truth for dependency suppressions.
Location: platform/buildScripts/suppressions.json
Purpose: When JPS dependencies shouldn't be written to XML descriptors (e.g., legacy deps that were manually managed), the suppression config tracks these exclusions. This is an explicit contract for incremental cleanup: suppressed deps are intentionally omitted from XML and must not trigger validation errors.
Validation is graph-based: only dependencies represented after filtering/suppression are validated; suppressed JPS deps are excluded by design.
For detailed implementation documentation, see SuppressionConfigGenerator.kt.
{
"contentModules": {
"intellij.foo": {
"suppressModules": ["intellij.bar"],
"suppressPlugins": ["com.intellij.java"]
}
},
"plugins": {
"intellij.cidr.clangd": {
"suppressModules": ["intellij.platform.core"],
"suppressPluginModules": []
}
}
}
| Field | Purpose |
|---|---|
contentModules[].suppressModules | Module deps to suppress from content module XMLs |
contentModules[].suppressPlugins | Plugin deps to suppress from content module <depends> |
plugins[].suppressModules | Module deps to suppress from plugin.xml <dependencies> |
plugins[].suppressPluginModules | Plugin module deps to suppress from plugin.xml |
# Normal generation: updates XML only
bazel run //platform/buildScripts:plugin-model-tool
# Update suppressions without touching XML (captures current XML state)
bazel run //platform/buildScripts:plugin-model-tool -- --update-suppressions
# Review and commit suppressions changes
git diff platform/buildScripts/suppressions.json
--update-suppressions reports non-DSL cases as warnings and updates their suppression entries in suppressions.json.
DSL test plugin allowlists (allowedMissingPluginIds) remain in code and are not serialized into suppressions.json.
Key principle: The generator should produce ZERO changes when run twice.
See errors.md for details on direct error suppression.
src/pipeline/src/pipeline/generators/src/validator/src/validator/rule/, src/model/src/discovery/src/traversal/src/tooling/platform/buildScripts/src/productLayout/ and src/ (CommunityModuleSets)All paths in src/ are relative to community/platform/build-scripts/product-dsl/.