platform/build-scripts/bazel/README.md
A build tool that converts IntelliJ's JPS (JetBrains Project System) module definitions into Bazel BUILD files.
The JPS to Bazel compiler reads .iml files and JPS project configuration, then generates Bazel targets that produce equivalent build outputs. This enables building IntelliJ Platform modules with Bazel while maintaining the existing JPS project structure.
JPS Project Model (XML/IML files)
│
▼
┌─────────────────────────────────┐
│ JpsModuleToBazel.main() │ Entry point
│ ├─ Load JPS project │
│ ├─ Detect community/ultimate │
│ └─ Initialize generator │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ BazelBuildFileGenerator │ Core generator
│ ├─ Enumerate modules │
│ ├─ Resolve dependencies │
│ └─ Generate targets │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Output Files │
│ ├─ BUILD.bazel (per module) │
│ ├─ lib/MODULE.bazel │
│ ├─ lib/BUILD.bazel │
│ └─ build/bazel-targets.json │
└─────────────────────────────────┘
| File | Lines | Purpose |
|---|---|---|
JpsModuleToBazel.kt | ~400 | Entry point; orchestrates generation; handles workspace detection and two-pass execution |
BazelBuildFileGenerator.kt | ~1200 | Core generator; processes modules, computes dependencies, generates jvm_library targets |
dependency.kt | ~680 | Dependency analysis; converts JPS dependencies to Bazel labels; handles scope mapping |
lib.kt | ~350 | Library handling; generates jvm_import/java_import targets for Maven and local libraries |
dsl.kt | ~160 | Bazel DSL model; BuildFile/Target/LoadStatement classes for code generation |
BazelFileUpdater.kt | ~70 | Incremental file updates; preserves manual edits via section markers |
ModuleDescriptor.kt | ~60 | Data class representing a parsed JPS module with all its metadata |
UrlCache.kt | ~310 | HTTP/Maven resolution; caches JAR URLs and SHA256 checksums |
CompareJpsWithBazel.kt | ~180 | Verification tool; compares JPS vs Bazel compilation output |
BazelProjectStructure.kt | ~50 | Utility for traversing BUILD file structure |
PrepareLogForJaeger.kt | ~40 | Tracing/profiling utilities |
Represents a parsed JPS module:
ModuleDescriptor(
imlFile: Path, // Location of .iml file
module: JpsModule, // JPS model object
contentRoots: List<Path>, // Module content directories
sources: List<SourceDirDescriptor>, // Production source roots
resources: List<ResourceDescriptor>, // Production resources
testSources: List<SourceDirDescriptor>,// Test source roots
testResources: List<ResourceDescriptor>,// Test resources
isCommunity: Boolean, // In community/ or ultimate/
bazelBuildFileDir: Path, // Where to write BUILD.bazel
targetName: String // Bazel target name
)
Container grouping all modules:
ModuleList(
community: List<ModuleDescriptor>, // Community modules
ultimate: List<ModuleDescriptor>, // Ultimate modules
skippedModules: List<String>, // Modules not converted
deps: Map<ModuleDescriptor, ModuleDeps>, // Production dependencies
testDeps: Map<ModuleDescriptor, ModuleDeps> // Test dependencies
)
Resolved dependencies for a module:
ModuleDeps(
deps: List<BazelLabel>, // Regular compile dependencies
provided: List<BazelLabel>, // Provided scope (compile-only, neverlink)
runtimeDeps: List<BazelLabel>, // Runtime-only dependencies
exports: List<BazelLabel>, // Re-exported dependencies
associates: List<BazelLabel>, // Test friend modules
plugins: List<String> // Kotlin compiler plugins
)
sealed interface Library
MavenLibrary(
mavenCoordinates: String, // "group:artifact:version"
jars: List<MavenFileDescription>, // Compiled JARs
sourceJars: List<MavenFileDescription>,
target: LibraryTarget
)
LocalLibrary(
files: List<Path>, // Direct file references
bazelBuildFileDir: Path,
target: LibraryTarget
)
// JpsModuleToBazel.kt
JpsSerializationManager.getInstance().loadProject(
projectPath = communityRoot,
pathVariables = mapOf("MAVEN_REPOSITORY" to m2Repo),
loadUnloadedModules = true
)
Loads the complete JPS model including:
.iml module files.idea/libraries/*.xml library definitions.idea/jarRepositories.xml for Maven repository URLsFor each JPS module:
.iml directory until finding a parent containing all content rootscommunityRoot**/*.kt, **/*.java, **/*.form)intellij.platform.core → core)For each module dependency:
// dependency.kt - generateDeps()
when (element) {
is JpsModuleDependency -> {
// Resolve to ModuleDescriptor
// Get scope: COMPILE, PROVIDED, RUNTIME, TEST
// Create BazelLabel
}
is JpsLibraryDependency -> {
// Check if Maven or local library
// Extract coordinates or file paths
// Generate library target
}
}
The generator processes modules in two passes:
This separation ensures community libraries are defined in @lib// and can be reused by ultimate modules.
For each module:
# Production target
jvm_library(
name = "platform-core",
srcs = glob(["src/**/*.kt", "src/**/*.java"]),
resources = [":platform-core_resources"],
deps = [":util", "@lib//:annotations"],
exports = [":exported-dep"],
visibility = ["//visibility:public"],
)
# Test target (if test sources exist)
jvm_library(
name = "platform-core_test_lib",
srcs = glob(["test/**/*.kt", "test/**/*.java"]),
associates = [":platform-core"], # Test friend access
deps = [...],
)
jps_test(
name = "platform-core_test",
runtime_deps = [":platform-core_test_lib"],
)
# Resource target
resourcegroup(
name = "platform-core_resources",
srcs = glob(["resources/**/*"]),
strip_prefix = "resources",
)
Maven Libraries:
# lib/MODULE.bazel
http_file(
name = "maven_org_jetbrains_annotations__file",
url = "https://repo1.maven.org/.../annotations-24.0.0.jar",
sha256 = "abc123...",
)
# lib/BUILD.bazel
jvm_import(
name = "annotations",
jar = "@maven_org_jetbrains_annotations__file//file",
source_jar = "@maven_org_jetbrains_annotations_sources__file//file",
)
Local Libraries:
java_import(
name = "local-lib",
jars = ["snapshots/lib-1.0-abc123.jar"],
)
build/bazel-targets.json manifestbuild/bazel-generated-file-list.txt for tracking| JPS Scope | Bazel Attribute | Notes |
|---|---|---|
| COMPILE | deps | Regular compile+runtime dependency |
| PROVIDED | provided → jvm_provided_library | Compile-only, neverlink=true |
| RUNTIME | runtime_deps | Runtime-only, not on compile classpath |
| TEST | Test-specific | Uses associates for test friend access |
| From | To | Label Format |
|---|---|---|
| Community | Community | //path:target |
| Ultimate | Community | @community//path:target |
| Ultimate | Ultimate | //path:target |
| Community | Ultimate | Not allowed (enforced) |
Test modules get special associates attribute for internal access:
jvm_library(
name = "platform-core_test_lib",
associates = [":platform-core"], # Can access internal members
)
group:artifact:version.idea/jarRepositories.xml)http_file rule in lib/MODULE.bazeljvm_import target in lib/BUILD.bazelLibraries with -SNAPSHOT versions or outside the project tree:
lib/snapshots/ with content hash in filenamejava_import targetLibraries tracked in VCS under lib/ directory:
java_import target with relative paths--workspace_directory=<path> # Override workspace root
--run_without_ultimate_root=true # Community-only mode
--default-custom-modules=true # Include predefined custom modules
--m2-repo=<path> # Maven repository location
--assert-all-outputs-exist-with-output-base=<path> # Verify output JARs
| Variable | Description |
|---|---|
BUILD_WORKSPACE_DIRECTORY | Override workspace detection |
RUN_WITHOUT_ULTIMATE_ROOT | Force community-only mode |
JPS_TO_BAZEL_TREAT_KOTLIN_DEV_VERSION_AS_SNAPSHOT | Treat Kotlin dev versions as snapshots |
MAVEN_REPOSITORY | Maven local repository path |
The generator searches for marker files to find project roots:
.community.root.marker → Community root.ultimate.root.marker → Ultimate root (in parent of community)| File | Description |
|---|---|
BUILD.bazel | Per-module target definitions (next to module content) |
lib/MODULE.bazel | http_file rules for Maven dependencies with SHA256 |
lib/BUILD.bazel | jvm_import and java_import library targets |
build/bazel-targets.json | Module→JAR mapping for IDE integration |
build/bazel-generated-file-list.txt | List of generated files for cleanup |
{
"modules": {
"intellij.platform.core": {
"productionTargets": ["@community//platform/core:core"],
"productionJars": ["bazel-out/.../core.jar"],
"testTargets": ["@community//platform/core:core_test_lib"],
"testJars": ["bazel-out/.../core_test_lib.jar"],
"exports": ["@lib//:annotations"],
"moduleLibraries": {}
}
},
"projectLibraries": {
"annotations": {
"target": "@lib//:annotations",
"jars": ["external/..."],
"sourceJars": ["external/..."]
}
}
}
BazelFileUpdater manages incremental updates while preserving manual edits:
# Manual code here is preserved
### auto-generated section `build modulename` start
jvm_library(
name = "modulename",
...
)
### auto-generated section `build modulename` end
# More manual code preserved
To disable generation for a specific section:
### skip generation section `build modulename`
# Your custom target here
Define target generation in BazelBuildFileGenerator.kt:
private fun generateCustomTarget(module: ModuleDescriptor): Target {
return target("custom_rule") {
option("name", module.targetName)
// Add options
}
}
Add load statement in the build file:
buildFile.load("@rules//:defs.bzl", "custom_rule")
Call from appropriate generation phase (production or test targets)
Extend scope handling in dependency.kt:
// In addDep() function
JpsJavaDependencyScope.NEW_SCOPE -> {
moduleDeps.newScopeDeps.add(label)
}
Update ModuleDeps data class with new list
Generate attribute in target:
option("new_scope_deps", moduleDeps.newScopeDeps)
Detect plugin in BazelBuildFileGenerator.computeKotlincOptions():
val pluginJar = pluginClasspath.find { it.name.startsWith("my-plugin-") }
if (pluginJar != null) {
moduleDeps.plugins.add("@lib//:my-plugin")
}
Define library target for the plugin in lib/BUILD.bazel
Add new library type in lib.kt:
class CustomLibrary(
val customData: String,
override val target: LibraryTarget
) : Library
Add detection logic in dependency.kt:
private fun getLibrary(library: JpsLibrary): Library? {
if (isCustomLibrary(library)) {
return CustomLibrary(...)
}
// existing logic
}
Generate targets in lib.kt:
fun generateCustomLib(lib: CustomLibrary): Target { ... }
Located in test/org/jetbrains/intellij/build/bazel/BazelGeneratorIntegrationTests.kt
Test cases use projects in testData/integration/:
kotlin-snapshot-librarysnapshot-repository-librarysnapshot-librarysnapshot-library-in-treeFrom community/platform/build-scripts/bazel:
bazel test //:bazel-generator-integration-tests --test_output=all
CompareJpsWithBazel.kt compares JPS and Bazel compilation outputs:
Extracted from JpsKotlinFacetModuleExtension:
api_version, language_versionopt_in annotationsplugin_options for compiler plugins-X flagsGenerates kt_kotlinc_options target when options are present.
From JpsJavaExtensionService.projectCompilerConfiguration:
--add-exports module directivesGenerates kt_javac_options target.
Predefined in DEFAULT_CUSTOM_MODULES:
| JPS Module | Bazel Label |
|---|---|
intellij.idea.community.build.zip | @community//build:zip |
intellij.platform.jps.build.dependencyGraph | @community//build:dependency-graph |
intellij.platform.jps.build.javac.rt | @community//build:build-javac-rt |
intellij.platform.buildScripts.bazel (the generator itself)intellij.tools.build.bazel.jvmIncBuilderintellij.tools.build.bazel.jvmIncBuilderTestsWhen a library is marked PROVIDED scope:
ProvidedLibraries multimapjvm_provided_library wrapper with neverlink=true