diff --git a/.idea/modules.xml b/.idea/modules.xml index a1e93f5c5ad6..6ec9d4dd4026 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -636,6 +636,7 @@ + diff --git a/build/bazel-generated-file-list.txt b/build/bazel-generated-file-list.txt index 066450342957..5399e595ff0a 100644 --- a/build/bazel-generated-file-list.txt +++ b/build/bazel-generated-file-list.txt @@ -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 diff --git a/libraries/README.md b/libraries/README.md new file mode 100644 index 000000000000..e010f094c2ed --- /dev/null +++ b/libraries/README.md @@ -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 + + + + + + + + + + + + +``` + +**Key points:** +- The `name` attribute in `` must match the library name defined in `.idea/libraries/ExampleLibrary.xml` +- If the module depends on other embedded modules, add them as additional `` entries (see `junit4` example) + +### 3. Create Plugin Descriptor + +File: `resources/intellij.libraries.example.lib.xml` + +```xml + + +``` + +**Note:** If your module depends on other library modules, add them: +```xml + + + + + +``` + +### 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 + +``` + +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 + +``` + +#### Community project file: `community/.idea/modules.xml` +```xml + +``` + +**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: `` → `` + - TEST scope: `` → `` + - 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 +``` diff --git a/libraries/guava/BUILD.bazel b/libraries/guava/BUILD.bazel new file mode 100644 index 000000000000..02a3c25f77ba --- /dev/null +++ b/libraries/guava/BUILD.bazel @@ -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 \ No newline at end of file diff --git a/libraries/guava/intellij.libraries.guava.iml b/libraries/guava/intellij.libraries.guava.iml new file mode 100644 index 000000000000..c76176264e6b --- /dev/null +++ b/libraries/guava/intellij.libraries.guava.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/libraries/guava/resources/intellij.libraries.guava.xml b/libraries/guava/resources/intellij.libraries.guava.xml new file mode 100644 index 000000000000..6fb76a66265d --- /dev/null +++ b/libraries/guava/resources/intellij.libraries.guava.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/libraries/replace-lib-with-module.js b/libraries/replace-lib-with-module.js new file mode 100644 index 000000000000..b1601902814c --- /dev/null +++ b/libraries/replace-lib-with-module.js @@ -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', + // + pattern: new RegExp( + ``, + 'g' + ), + replacement: `` + }, + { + name: 'testScope', + // + pattern: new RegExp( + ``, + 'g' + ), + replacement: `` + }, + { + name: 'testScopeExported', + // + pattern: new RegExp( + ``, + 'g' + ), + replacement: `` + } + ]; +} + +/** + * 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); +} diff --git a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/JarPackager.kt b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/JarPackager.kt index f67b2497d895..1b650ab3263a 100644 --- a/platform/build-scripts/src/org/jetbrains/intellij/build/impl/JarPackager.kt +++ b/platform/build-scripts/src/org/jetbrains/intellij/build/impl/JarPackager.kt @@ -117,8 +117,6 @@ private val predefinedMergeRules = listOf 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) }, diff --git a/platform/platform-resources/src/META-INF/essential-libs.xml b/platform/platform-resources/src/META-INF/essential-libs.xml index ee76cfbe7dc0..540a1e7ced8b 100644 --- a/platform/platform-resources/src/META-INF/essential-libs.xml +++ b/platform/platform-resources/src/META-INF/essential-libs.xml @@ -5,5 +5,7 @@ + + \ No newline at end of file