IJ-MR-175479 IJ-CR-146078 IJPL-209476 guava as product module

GitOrigin-RevId: 695080bfc70661c5755dae20202ced5e7fa58652
This commit is contained in:
Vladimir Krivosheev
2025-10-04 19:03:47 +00:00
committed by intellij-monorepo-bot
parent 195269d7bd
commit e0831b8cf3
9 changed files with 493 additions and 2 deletions
+1
View File
@@ -636,6 +636,7 @@
<module fileurl="file://$PROJECT_DIR$/libraries/dokka/intellij.libraries.dokka.iml" filepath="$PROJECT_DIR$/libraries/dokka/intellij.libraries.dokka.iml" />
<module fileurl="file://$PROJECT_DIR$/libraries/grpc/intellij.libraries.grpc.iml" filepath="$PROJECT_DIR$/libraries/grpc/intellij.libraries.grpc.iml" />
<module fileurl="file://$PROJECT_DIR$/libraries/grpc-netty-shaded/intellij.libraries.grpc.netty.shaded.iml" filepath="$PROJECT_DIR$/libraries/grpc-netty-shaded/intellij.libraries.grpc.netty.shaded.iml" />
<module fileurl="file://$PROJECT_DIR$/libraries/guava/intellij.libraries.guava.iml" filepath="$PROJECT_DIR$/libraries/guava/intellij.libraries.guava.iml" />
<module fileurl="file://$PROJECT_DIR$/libraries/hamcrest/intellij.libraries.hamcrest.iml" filepath="$PROJECT_DIR$/libraries/hamcrest/intellij.libraries.hamcrest.iml" />
<module fileurl="file://$PROJECT_DIR$/libraries/hamcrest-more-matchers/intellij.libraries.hamcrest.more.matchers.iml" filepath="$PROJECT_DIR$/libraries/hamcrest-more-matchers/intellij.libraries.hamcrest.more.matchers.iml" />
<module fileurl="file://$PROJECT_DIR$/libraries/junit4/intellij.libraries.junit4.iml" filepath="$PROJECT_DIR$/libraries/junit4/intellij.libraries.junit4.iml" />
+1
View File
@@ -347,6 +347,7 @@ libraries/detekt-compose-rules
libraries/dokka
libraries/grpc
libraries/grpc-netty-shaded
libraries/guava
libraries/hamcrest
libraries/hamcrest-more-matchers
libraries/io
+190
View File
@@ -0,0 +1,190 @@
# Product Embedded Library Modules
This directory contains product-embedded library modules that make third-party libraries available throughout the IntelliJ platform.
## Creating a New Library Module
To create a new product-embedded module for a library (e.g., for a library named "example-lib"):
### 1. Create Module Structure
Create the following directory structure:
```
community/libraries/example-lib/
├── intellij.libraries.example.lib.iml
└── resources/
└── intellij.libraries.example.lib.xml
```
### 2. Create Module Definition File (`.iml`)
File: `intellij.libraries.example.lib.iml`
```xml
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="ExampleLibrary" level="project" />
</component>
</module>
```
**Key points:**
- The `name` attribute in `<orderEntry type="library">` must match the library name defined in `.idea/libraries/ExampleLibrary.xml`
- If the module depends on other embedded modules, add them as additional `<orderEntry type="module">` entries (see `junit4` example)
### 3. Create Plugin Descriptor
File: `resources/intellij.libraries.example.lib.xml`
```xml
<idea-plugin>
</idea-plugin>
```
**Note:** If your module depends on other library modules, add them:
```xml
<idea-plugin>
<dependencies>
<module name="intellij.libraries.hamcrest"/>
</dependencies>
</idea-plugin>
```
### 4. Register in Essential Libraries (if needed)
If the library should be loaded as an essential embedded module, add it to:
File: `community/platform/platform-resources/src/META-INF/essential-libs.xml`
```xml
<module name="intellij.libraries.example.lib" loading="embedded"/>
```
Add the entry in alphabetical order.
### 5. Register in Project Modules
Add the module to both `modules.xml` files in alphabetical order:
#### Root project file: `.idea/modules.xml`
```xml
<module fileurl="file://$PROJECT_DIR$/community/libraries/example-lib/intellij.libraries.example.lib.iml"
filepath="$PROJECT_DIR$/community/libraries/example-lib/intellij.libraries.example.lib.iml" />
```
#### Community project file: `community/.idea/modules.xml`
```xml
<module fileurl="file://$PROJECT_DIR$/libraries/example-lib/intellij.libraries.example.lib.iml"
filepath="$PROJECT_DIR$/libraries/example-lib/intellij.libraries.example.lib.iml" />
```
**Important:**
- Root `modules.xml` uses full path with `community/` prefix
- Community `modules.xml` uses relative path without `community/` prefix
### 6. Build File
**Do NOT create `BUILD.bazel` manually** - it will be auto-generated by the build system.
### 7. Verify Changes
After creating the module, run the following tests to ensure project structure consistency:
- **`IdeaUltimatePluginStructureTest`** - Verifies the plugin structure for IDEA Ultimate
- **`CommunityProjectConsistencyTest`** - Verifies community project consistency
Run tests using:
```bash
./tests.cmd -Dintellij.build.test.patterns=IdeaUltimatePluginStructureTest
./tests.cmd -Dintellij.build.test.patterns=CommunityProjectConsistencyTest
```
## Examples
- **Simple library:** `hamcrest/` - single library dependency
- **Library with dependencies:** `junit4/` - depends on hamcrest module
- **Multiple libraries:** `grpc/` - exports multiple related libraries
## Naming Conventions
- **Directory name:** Use kebab-case (e.g., `example-lib`, `grpc-netty-shaded`)
- **Module name:** Use dot-separated format (e.g., `intellij.libraries.example.lib`, `intellij.libraries.grpc.netty.shaded`)
- **IML filename:** Should match the module name (e.g., `intellij.libraries.example.lib.iml`)
- **Plugin XML filename:** Should match the module name (e.g., `intellij.libraries.example.lib.xml`)
## Prerequisites
Before creating a library module, ensure:
1. The library is already defined in `.idea/libraries/` as a project library
2. The library artifacts are available in the Maven repository or bundled appropriately
## Migrating Existing Library References to Library Modules
When converting a project-level library to an embedded library module, you need to update all `.iml` files that reference the library.
### Automated Migration Script
A generic Node.js script is available to automate the migration of library references across the entire project:
**Location:** `community/libraries/replace-lib-with-module.js`
**Usage:**
```bash
# Dry run to preview changes for Guava
node community/libraries/replace-lib-with-module.js --old-lib=Guava --new-module=intellij.libraries.guava --dry-run
# Apply Guava replacement
node community/libraries/replace-lib-with-module.js --old-lib=Guava --new-module=intellij.libraries.guava
# Replace a different library
node community/libraries/replace-lib-with-module.js --old-lib=Jackson --new-module=intellij.libraries.jackson
# Add custom exclude directories
node community/libraries/replace-lib-with-module.js --old-lib=Guava --new-module=intellij.libraries.guava --exclude=custom-dir
# Show help
node community/libraries/replace-lib-with-module.js --help
```
**Script Features:**
- Parses `.idea/modules.xml` to find all IML files
- Supports multiple replacement patterns:
- Regular scope: `<orderEntry type="library" name="Guava" ... />` → `<orderEntry type="module" module-name="intellij.libraries.guava" />`
- TEST scope: `<orderEntry type="library" scope="TEST" name="Guava" ... />` → `<orderEntry type="module" ... scope="TEST" />`
- TEST scope with exported attribute
- Excludes specified directories (default: `fleet/`, `toolbox/`)
- Provides detailed statistics of files modified and replacements made
- Dry-run mode to preview changes without modifying files
**Example Output:**
```
Configuration:
Old library: Guava
New module: intellij.libraries.guava
Excluded dirs: fleet, toolbox
Dry run: false
Found 15000 module files in modules.xml
Processing IML files:
✓ platform/ssh/intellij.platform.ssh.iml (1 regular)
✓ community/android/testartifacts/intellij.android.testartifacts.iml (1 regular)
...
Summary:
Files scanned: 14500
Files modified: 450
Files skipped: 550
Replacements by type:
Regular scope: 396
TEST scope: 109
TEST scope (exported): 3
Total: 508
```
+18
View File
@@ -0,0 +1,18 @@
### auto-generated section `build intellij.libraries.guava` start
load("@rules_jvm//:jvm.bzl", "jvm_library", "resourcegroup")
resourcegroup(
name = "guava_resources",
srcs = glob(["resources/**/*"]),
strip_prefix = "resources"
)
jvm_library(
name = "guava",
visibility = ["//visibility:public"],
srcs = glob([], allow_empty = True),
resources = [":guava_resources"],
exports = ["@lib//:guava"],
runtime_deps = ["@lib//:guava"]
)
### auto-generated section `build intellij.libraries.guava` end
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="Guava" level="project" />
</component>
</module>
@@ -0,0 +1,2 @@
<idea-plugin>
</idea-plugin>
+267
View File
@@ -0,0 +1,267 @@
#!/usr/bin/env node
/**
* Generic script to replace library references with module references in IntelliJ project IML files.
*
* Usage:
* node replace-lib-with-module.js --old-lib=LibName --new-module=intellij.libraries.libname [options]
*
* Options:
* --old-lib=NAME Library name to replace (required)
* --new-module=NAME Target module name (required)
* --dry-run Show what would be changed without modifying files
* --exclude=DIR Additional directory to exclude (can be used multiple times, default: fleet, toolbox)
* --help Show this help message
*
* Examples:
* # Dry run for Guava
* node replace-lib-with-module.js --old-lib=Guava --new-module=intellij.libraries.guava --dry-run
*
* # Apply Guava replacement
* node replace-lib-with-module.js --old-lib=Guava --new-module=intellij.libraries.guava
*
* # Replace another library with custom exclusions
* node replace-lib-with-module.js --old-lib=Jackson --new-module=intellij.libraries.jackson --exclude=custom-dir
*/
const fs = require('fs');
const path = require('path');
// Parse command line arguments
const args = process.argv.slice(2);
const config = {
dryRun: args.includes('--dry-run'),
oldLib: null,
newModule: null,
excludeDirs: ['fleet', 'toolbox'],
showHelp: args.includes('--help')
};
// Parse custom arguments
for (const arg of args) {
if (arg.startsWith('--old-lib=')) {
config.oldLib = arg.split('=')[1];
} else if (arg.startsWith('--new-module=')) {
config.newModule = arg.split('=')[1];
} else if (arg.startsWith('--exclude=')) {
const dir = arg.split('=')[1];
if (!config.excludeDirs.includes(dir)) {
config.excludeDirs.push(dir);
}
}
}
if (config.showHelp) {
console.log(`
Generic script to replace library references with module references in IntelliJ project IML files.
Usage:
node replace-lib-with-module.js --old-lib=LibName --new-module=intellij.libraries.libname [options]
Options:
--old-lib=NAME Library name to replace (required)
--new-module=NAME Target module name (required)
--dry-run Show what would be changed without modifying files
--exclude=DIR Additional directory to exclude (can be used multiple times, default: fleet, toolbox)
--help Show this help message
Examples:
# Dry run for Guava
node replace-lib-with-module.js --old-lib=Guava --new-module=intellij.libraries.guava --dry-run
# Apply Guava replacement
node replace-lib-with-module.js --old-lib=Guava --new-module=intellij.libraries.guava
# Replace another library with custom exclusions
node replace-lib-with-module.js --old-lib=Jackson --new-module=intellij.libraries.jackson --exclude=custom-dir
`);
process.exit(0);
}
// Validate required arguments
if (!config.oldLib || !config.newModule) {
console.error('Error: Both --old-lib and --new-module are required arguments.\n');
console.log('Usage: node replace-lib-with-module.js --old-lib=LibName --new-module=intellij.libraries.libname [options]');
console.log('Run with --help for more information.');
process.exit(1);
}
const PROJECT_ROOT = path.join(__dirname, '..', '..');
const MODULES_XML = path.join(PROJECT_ROOT, '.idea', 'modules.xml');
console.log('Configuration:');
console.log(` Old library: ${config.oldLib}`);
console.log(` New module: ${config.newModule}`);
console.log(` Excluded dirs: ${config.excludeDirs.join(', ')}`);
console.log(` Dry run: ${config.dryRun}`);
console.log('');
// Statistics
const stats = {
filesScanned: 0,
filesModified: 0,
filesSkipped: 0,
replacements: {
regular: 0,
testScope: 0,
testScopeExported: 0
}
};
/**
* Extract IML file paths from modules.xml
*/
function extractImlPaths(modulesXmlPath) {
const content = fs.readFileSync(modulesXmlPath, 'utf-8');
const filepathRegex = /filepath="\$PROJECT_DIR\$\/([^"]+)"/g;
const paths = [];
let match;
while ((match = filepathRegex.exec(content)) !== null) {
paths.push(match[1]);
}
return paths;
}
/**
* Check if path should be excluded
*/
function shouldExclude(filePath) {
for (const excludeDir of config.excludeDirs) {
if (filePath.startsWith(excludeDir + '/') || filePath.includes('/' + excludeDir + '/')) {
return true;
}
}
return false;
}
/**
* Create replacement patterns based on library and module names
*/
function createReplacementPatterns(oldLib, newModule) {
return [
{
name: 'regular',
// <orderEntry type="library" name="Guava" level="project" />
pattern: new RegExp(
`<orderEntry type="library" name="${oldLib}" level="project" />`,
'g'
),
replacement: `<orderEntry type="module" module-name="${newModule}" />`
},
{
name: 'testScope',
// <orderEntry type="library" scope="TEST" name="Guava" level="project" />
pattern: new RegExp(
`<orderEntry type="library" scope="TEST" name="${oldLib}" level="project" />`,
'g'
),
replacement: `<orderEntry type="module" module-name="${newModule}" scope="TEST" />`
},
{
name: 'testScopeExported',
// <orderEntry type="library" exported="" scope="TEST" name="Guava" level="project" />
pattern: new RegExp(
`<orderEntry type="library" exported="" scope="TEST" name="${oldLib}" level="project" />`,
'g'
),
replacement: `<orderEntry type="module" module-name="${newModule}" exported="" scope="TEST" />`
}
];
}
/**
* Process a single IML file
*/
function processImlFile(filePath) {
const absolutePath = path.join(PROJECT_ROOT, filePath);
if (!fs.existsSync(absolutePath)) {
console.log(` ⚠ File not found: ${filePath}`);
stats.filesSkipped++;
return;
}
let content = fs.readFileSync(absolutePath, 'utf-8');
let modified = false;
const fileReplacements = {};
const patterns = createReplacementPatterns(config.oldLib, config.newModule);
for (const { name, pattern, replacement } of patterns) {
const matches = content.match(pattern);
if (matches && matches.length > 0) {
fileReplacements[name] = matches.length;
stats.replacements[name] += matches.length;
content = content.replace(pattern, replacement);
modified = true;
}
}
stats.filesScanned++;
if (modified) {
stats.filesModified++;
const replacementSummary = Object.entries(fileReplacements)
.map(([type, count]) => `${count} ${type}`)
.join(', ');
console.log(` ✓ ${filePath} (${replacementSummary})`);
if (!config.dryRun) {
fs.writeFileSync(absolutePath, content, 'utf-8');
}
}
}
/**
* Main execution
*/
function main() {
console.log('Reading modules from .idea/modules.xml...\n');
const imlPaths = extractImlPaths(MODULES_XML);
console.log(`Found ${imlPaths.length} module files in modules.xml\n`);
console.log('Processing IML files:\n');
for (const imlPath of imlPaths) {
if (shouldExclude(imlPath)) {
stats.filesSkipped++;
continue;
}
processImlFile(imlPath);
}
// Print summary
console.log('\n' + '='.repeat(60));
console.log('Summary:');
console.log('='.repeat(60));
console.log(`Files scanned: ${stats.filesScanned}`);
console.log(`Files modified: ${stats.filesModified}`);
console.log(`Files skipped: ${stats.filesSkipped}`);
console.log('');
console.log('Replacements by type:');
console.log(` Regular scope: ${stats.replacements.regular}`);
console.log(` TEST scope: ${stats.replacements.testScope}`);
console.log(` TEST scope (exported): ${stats.replacements.testScopeExported}`);
console.log(` Total: ${stats.replacements.regular + stats.replacements.testScope + stats.replacements.testScopeExported}`);
if (config.dryRun) {
console.log('\n⚠ DRY RUN MODE - No files were modified');
console.log('Run without --dry-run to apply changes');
} else {
console.log('\n✓ All changes applied successfully');
}
}
// Run the script
try {
main();
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
}
@@ -117,8 +117,6 @@ private val predefinedMergeRules = listOf<Pair<String, (String, FrontendModuleFi
"groovy.jar" to { it, _ -> it.startsWith("org.codehaus.groovy:") },
"jsch-agent.jar" to { it, _ -> it.startsWith("jsch-agent") },
rdJarName to { it, _ -> it.startsWith("rd-") },
// separate file to use in Gradle Daemon classpath
"guava.jar" to { it, _ -> it == "Guava" },
"opentelemetry.jar" to { it, _ -> it == "opentelemetry" || it == "opentelemetry-semconv" || it.startsWith("opentelemetry-exporter-otlp") },
"bouncy-castle.jar" to { it, _ -> it.startsWith("bouncy-castle-") },
PRODUCT_BACKEND_JAR to { name, filter -> (name.startsWith("License") || name.startsWith("jetbrains.codeWithMe.lobby.server.")) && filter.isBackendProjectLibrary(name) },
@@ -5,5 +5,7 @@
<module name="intellij.libraries.kotlinx.serialization.core" loading="embedded"/>
<module name="intellij.libraries.kotlinx.serialization.json" loading="embedded"/>
<module name="intellij.libraries.guava" loading="embedded"/>
</content>
</idea-plugin>