Skip to content

Decouple rules_java from java_tools and JDK catalog release cycles #395

Open

Description

@davido

Could rules_java consume independently versioned Java-tool and JDK catalogs,
while retaining its rule implementations and repository/toolchain machinery?

I have tested this split with Gerrit as a consumer. It uses java_tools v22.0
and JDK 25.0.5
from separate catalog modules, without requiring a new upstream
rules_java release. An initial fork implements the catalog interface; after
that, the rules implementation stays fixed while either catalog is replaced.

Three module boundaries

Module Responsibility
rules_java Rules, repository creation, toolchain machinery and compatible default catalog dependencies.
java_tools URLs and checksums for the official Bazel-built Java tools archives. The existing Bazel pipeline continues producing the binaries.
java_toolchains Vendor JDK catalog, release/URL source, pin generation and synchronization checks. The prototype retains this repository's existing JDK 27 API.

Proposed name: jdk_catalog (currently implemented as java_toolchains).

Implementation

The fork is based on upstream rules_java commit
575fa89e24d253947eb08353542154f5af05d005 and preserves that revision's default
v22 tools and JDK pins, including fmeum's JDK 25.0.5 refresh.

java/repositories.bzl imports JAVA_TOOLS_CONFIG from java_tools and the
remote-JDK pin list from java_toolchains. The existing rules_java extension
still creates the repositories. Its identity, repository names, tool labels and
repository mappings stay in place. The BUILD files inside the official tools
archives therefore continue resolving their dependencies through rules_java's
existing mapping.

The JDK source-of-truth, pin generator and synchronization check move together
to java_toolchains. The former rules_java entry points forward there, so a
selected catalog is checked against its own generator configuration rather
than against a stale copy in rules_java.

The existing JDK 27 adapter still uses rules_java's toolchain helpers. Bzlmod
supports this module dependency cycle; the catalog file itself contains no
loads back into rules_java, avoiding a Starlark load cycle.

Gerrit as the consumer

A working consumer prototype is available as
Gerrit change 653343: Consume independent Java catalogs from GitHub.
It is posted as WIP for design review, with the verification results below.

Gerrit's MODULE.bazel declares:

bazel_dep(name = "rules_java", version = "9.9.0")
bazel_dep(name = "java_tools", version = "22.0")
bazel_dep(name = "java_toolchains", version = "0.0.2-catalog.1")

The consumer pins all three prototype modules to GitHub commits:

git_override(
    module_name = "rules_java",
    commit = "35fb6bd2c43e83483da1c16d23051a0c314bc1a4",
    remote = "https://github.com/davido/rules_java.git",
)

git_override(
    module_name = "java_tools",
    commit = "c52c8de5b652c86439f0d265efc9fafdc6f2a72f",
    remote = "https://github.com/davido/java_tools.git",
)

git_override(
    module_name = "java_toolchains",
    commit = "d4de6da36f73dcc0cff2db9fd73a633c4426083d",
    remote = "https://github.com/davido/java_toolchains.git",
)

These replace Gerrit's existing single_version_override for rules_java.
No sibling checkouts or command-line module overrides are required:

bazelisk test //javatests/com/google/gerrit/json:json_tests \
  --lockfile_mode=update --cache_test_results=no

The rules_java override selects the modified fork, not unmodified released
9.9.0. Adding the other two dependencies to vanilla 9.9.0 alone would not make
it read those catalogs. The initial interface change is necessary.

The Gerrit consumer change adds the two catalog dependencies, pins the three
GitHub sources and aligns rules_cc to 0.2.25 for the HEAD-based fork's dependency
floor. Existing JDK25 language/runtime selectors remain unchanged. The JSON
test target passes with both vanilla 9.9.0 and the fork plus catalogs; the
GitHub-backed run passes all 12 JUnit cases with zero failures or errors.
A second run disabled the local action cache, disk cache, remote cache and
test-result cache: 207 actions completed in approximately 69 seconds, including
Java compilation, and all 12 JUnit cases passed again. No local module overrides
were used. The tool and JDK archives were reused from the download cache.

The fetched module files were compared with all three pinned commits. The
selected JavaBuilder matches the official v22 archive, and the selected JDK
executable reports 25.0.5. The JDK catalog exactly matches upstream's pin list,
including Fabian's latest-patch refresh.

This initial adoption includes upstream HEAD's rule changes relative to 9.9.0;
it is not a data-only update of released 9.9.0. The independence demonstrated
below starts after the initial interface extraction.

All three prototype branches are available on GitHub. Their module versions
are not BCR releases; the pinned git overrides above provide the reproducible
source selection. Registry publication remains a separate release task.

Execution evidence

On macOS x86-64 with Bazel 9.3.0, one unchanged rules implementation compiles a
Java library, executes a test and runs an application in all four combinations:

Actual tools release Actual compiler and test JVM Result
v21.0 JDK 25.0.4 PASS
v22.0 JDK 25.0.4 PASS
v21.0 JDK 25.0.5 PASS
v22.0 JDK 25.0.5 PASS

The older cases explicitly select older catalog versions through root version
overrides. The fork's default dependencies and rule code are unchanged between
cases; its defaults remain the current HEAD pins.

The test checks the actual JVM patch version. The compiler executable is also
checked with java -version. The selected JavaBuilder and Turbine JAR SHA-256
values are compared against the JARs inside the checksum-verified official
release archives. A separate audit records the expected and selected hashes.
For example, JavaBuilder changes from e8701b67... in the v21 cases to
5a6be1a5... in the v22 cases.

This demonstrates independent selection and use of the tools and JDK
catalogs, rather than only resolving different constants. No further rules_java
release or change to its implementation is needed between those consumers.

These results are Bzlmod-only and cover the tested combinations. MVS is not a
binary compatibility solver; passing this matrix does not establish that
arbitrary tools, compiler JDKs and Bazel versions are compatible.

Remaining design and release questions

  • Compatibility policy: v22's Turbine needs a compiler runtime of at least
    JDK25. Catalog versions need documented requirements and appropriate CI;
    compatibility_level alone does not describe the full compatibility matrix.
  • Platform coverage: the current execution evidence is macOS x86-64.
    Linux, Windows and cross-platform RBE remain to be validated.
  • New JDK majors: patch updates for already-supported majors are catalog
    changes. Supporting new majors still needs registration and compiler policy.
  • Ownership/publication: who owns the catalog APIs, release automation,
    defaults and registry submissions? The prototype does not create a competing
    java_tools binary build pipeline.
  • Scope: legacy WORKSPACE support is outside this experiment. Upstream
    acceptance needs an explicit supported-scope decision.

Root consumers already have override_repo and custom runtime registration as
escape hatches. The additional benefit here is a shared, independently
versioned catalog interface that can compose through the module graph. More
frequent combined rules_java releases remain a reasonable alternative if that
benefit does not justify maintaining the additional boundaries.

Additional context:

#180
#385
bazel-contrib/SIG-rules-authors#127

Would maintainers find this direction useful?

Activity

  1. meteorcloudy commented on Nov 10, 2026

    @meteorcloudy
    Member

    @hvadehra Does this proposal make sense?

  2. davido commented on Nov 12, 2026

    @davido
    ContributorAuthor

    I have extended the prototype with another practical use case: consuming the newly packaged prebuilt ProGuard allowlister.

    I published a personal java_tools snapshot, 23.0-davido.1, including bazelbuild/bazel#32318. This lets me test the new tool without waiting for the official java_tools release requested in bazelbuild/java_tools#104, followed by a combined rules_java release.

    The Gerrit consumer prototype selects:

    bazel_dep(name = "rules_java", version = "9.9.0")
    bazel_dep(name = "java_tools", version = "23.0-davido.1")
    bazel_dep(name = "java_toolchains", version = "0.0.2-catalog.1")
    
    git_override(
        module_name = "rules_java",
        commit = "a6ac7cbd7160104dcb3b7e974ce7d087fdbaeda1",
        remote = "https://github.com/davido/rules_java.git",
    )
    
    git_override(
        module_name = "java_tools",
        commit = "34d71142ce6b94f4434ea42d5fdaeba699e386b8",
        remote = "https://github.com/davido/java_tools.git",
    )
    
    git_override(
        module_name = "java_toolchains",
        commit = "d4de6da36f73dcc0cff2db9fd73a633c4426083d",
        remote = "https://github.com/davido/java_toolchains.git",
    )

    The prebuilt allowlister removes the Java toolchain’s dependency on the Python allowlister and its native zipper. That dependency previously required a Linux C++ toolchain when submitting builds from macOS to Linux RBE workers.

    I rebased the consumer directly onto upstream Gerrit master, excluding the placeholder C++ toolchain workaround, and verified //javatests/com/google/gerrit/json:json_tests:

    • All 12 tests pass locally and on Linux GCP RBE, using JDK 25.0.5.
    • The fresh-output-base RBE run records 136 remote spawn actions, zero action-cache hits, and zero C++ compilation actions.
    • Disk caching, remote action-cache reuse, and local fallback were disabled.
    • All three modules resolve from their published GitHub commits; no local module overrides are used.

    This extension required a one-time rules_java wiring change to select the newly packaged allowlister. After that, compatible tools archive updates can be selected through the independently versioned catalog, without another rules implementation change or combined rules_java release. The JDK catalog remains unchanged.

    That is the release boundary I am proposing: rule/API changes still belong in rules_java, while compatible tools-version updates should be independently releasable.

    While the upstream design remains under discussion, these forks also let me publish and validate experimental snapshots on my own cadence. They are personal GitHub prereleases, not official releases or BCR publications.

    Update

    Another practical result from testing the catalog split: consuming my personal
    java_tools snapshot exposed a previously hidden annotation dependency in
    Gerrit's server_tests. Bazel's intentional JUnit runner dependency cleanup
    removed annotations that the test had been obtaining implicitly. Adding the
    direct dependency fixes it; the affected test class passes with the snapshot.

    I have requested migration-note coverage in
    java_tools#104.

    This supports making tools updates easier to test independently: smaller,
    more frequent updates can help downstream projects discover migration work
    earlier. It is also an argument for more frequent combined rules_java releases;
    this finding alone does not establish that a module split is necessary.

    The consumer prototype remains
    Gerrit change 653343.
    The prebuilt allowlister needs an initial rules_java wiring change; after that
    interface exists, compatible catalog updates can be consumed independently.

  3. hvadehra commented on Nov 13, 2026

    @hvadehra
    Member

    Decoupling the toolchains/JDK catalog from rules_java makes sense to me (as I said at #180 (comment)).

    But for java_tools not so much. The java rules are tightly coupled with those so ideally we would have the sources for those in rules_java itself (instead of in Bazel), so we could make changes to both the tools and the rules atomically without worrying about version skew. This is currently blocked on the fact that the compilation actions are still in native Bazel. But there is hope that will change ~soon.

  4. davido commented on Nov 13, 2026

    @davido
    ContributorAuthor

    @hvadehra — three questions to scope this:

    1. Name. The SIG meeting suggested jdk_catalog for the extracted module — it's a JDK pin catalog, not a toolchain ruleset (no rules, no load cycle back into rules_java). Any objection from the rules_java side?

    2. java_tools as a first-class, overridable module. Source-coupling aside (agreed tool and rule sources should co-locate eventually), the pain is release latency: a compatible tools bump today needs two serialized releases — a java_tools release, then a combined rules_java release — before anyone downstream can use it. Concretely: the prebuilt ProGuard allowlister drops the Linux C++ toolchain requirement for macOS→Linux RBE, but I could only test it by publishing a personal java_tools snapshot and overriding it — and that same snapshot surfaced a hidden JUnit-annotation dependency in a downstream test earlier than a combined release would have. Would you be OK exposing java_tools as a first-class, overridable BCR module so a bump like that skips the two-release wait? It's already a separate repo on Bazel's cadence — this just makes that boundary a swappable bazel_dep, e.g.:

    bazel_dep(name = "rules_java", version = "10.0.0")
    bazel_dep(name = "java_tools", version = "23.0")   # bump independently, no rules_java release
    bazel_dep(name = "jdk_catalog", version = "1.2.3")
    1. LTS pins in the catalog. OK to move the full JDK pin list — LTS (11/17/21/25) as well as non-LTS (27) — into bazel-contrib/jdk_catalog, with the default catalog still vendored in rules_java so a no-override build is unchanged and external catalogs are purely opt-in?
  5. hvadehra commented on Nov 13, 2026

    @hvadehra
    Member
    1. SGTM
    2. The timeframe we're currently targeting for having the native compilation code in Starlark is 2027Q1. We can revisit this if that does not pan out, but for now I don't think it makes sense to make java_tools a BCR module. If the main issue we're trying to solve is the extra rules_java release, ISTM https://bazel.build/external/extension#overriding-and-injecting-module-extension-repos would let (root modules) achieve the same with just a java_tools release.
    3. If the jdk_catalog does not define toolchains, what would the opt-in mechanism would look like for someone wishing to use a non-LTS java_toolchain?
  6. davido commented on Nov 13, 2026

    @davido
    ContributorAuthor

    If the jdk_catalog does not define toolchains, what would the opt-in mechanism would look like for someone wishing to use a non-LTS java_toolchain?

    jdk_catalog defines no new toolchain rules — it loads rules_java's own rules and just instantiates the non-LTS toolchain with them:

    https://github.com/davido/jdk_catalog/blob/main/toolchains/BUILD.bazel#L1-L29

    load("@rules_java//toolchains:default_java_toolchain.bzl",
          "DEFAULT_TOOLCHAIN_CONFIGURATION", "default_java_toolchain")
    load("@rules_java//toolchains:java_toolchain_alias.bzl",
         "java_runtime_version_alias")

    So "not a toolchain ruleset" holds (no new rule types, no fork, reuses rules_java's machinery) — "defines no toolchains" was my imprecision. It defines the non-LTS toolchain instances: a java_runtime_version_alias (remotejdk_27 runtime) and a default_java_toolchain (toolchain_jdk_27, javac on JDK 27 — the LTS toolchains' javac tops out at 25 and rejects source/target 27). The non_lts_jdks module extension registers both.

    Opt-in is just bazel_dep(name = "jdk_catalog") + --java_runtime_version=remotejdk_27 (or --java_language_version=27); no manual register_toolchains. A consumer that already wires its own toolchain (Gerrit, for Error Prone) can instead build its own default_java_toolchain on top of @jdk_catalog//toolchains:remotejdk_27.

    For the working end to end example you can see:

    https://gerrit-review.googlesource.com/c/gerrit/+/637823/16/tools/BUILD#26

    I hope that clarifies it?

  7. fmeum commented on Nov 13, 2026

    @fmeum
    Contributor

    Please take note of bazelbuild/bazel#32428, which will make it so that jdk_catalog can't register its Java toolchains directly, the root module will have to do that explicitly. This could be as simple as register_toolchains("@jdk_catalog//:all").

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions