From ef1292d387b943473707131d963100fc2fc62dbc Mon Sep 17 00:00:00 2001 From: Malte Poll <1780588+malt3@users.noreply.github.com> Date: Mon, 4 May 2026 11:56:26 +0100 Subject: [PATCH 1/2] Support runfiles groups Teach the Java rules to describe their runfiles as named, ordered groups so that downstream packaging rules can build more efficient artifacts. `java_binary`, `java_test`, `java_library`, and `java_import` now describe their runfiles for the `rules_runfiles_group` ruleset. Instead of seeing a single flat runfiles tree, a packaging rule (e.g. a container-image or archive rule) can split a binary's runfiles into layers and order them so that the content that changes least often lands in the most cacheable layers: * the JDK / java_runtime at the foundation tier (never merged), * `java_import` targets (typically third-party jars) at the shared-deps tier, * `java_library` code and the binary itself at the executable tier, marked `first_party` only for targets in the main repository The rules don't return any new provider. A packager walks a binary's graph with an aspect from `rules_runfiles_group` and asks each target's describer -- named by the rule's private `_runfiles_group_describer` attribute -- which group it adds and which attributes (`_runfiles_group_attrs`) it merges runfiles in from. A `java_library` or `java_import` adds one group holding its runtime jars; a `neverlink` target is skipped. A `java_binary` or `java_test` adds one group holding the executable and its own outputs, and marks it as the executable group. The JDK is described by a single `java_runtime_runfiles` target that resolves the same Java runtime toolchain; every binary merges it in through its private `_runfiles_group_java_runtime` attribute, so all binaries in a configuration share the same `rules_java#java_runtime` group. Every group carries the `rules_java` merge affinity so that JVM-shaped groups stay together when a packager has to merge groups to fit a layer limit. A dependency that returns `JavaInfo` but has no describer (a custom rule) gets a synthesized group covering its transitive runtime jars and its default runfiles, so a packager never silently loses its files. A dependency without `JavaInfo` (a genrule or a plain `.jar` file) gets a group covering the legacy jars the rules put on the runtime classpath. `java_import` gains a `runfiles_weight` attribute so dependency-management rulesets can hint at relative sizes to guide those merge decisions. Adds a dependency on `rules_runfiles_group` for both Bzlmod and WORKSPACE setups. --- MODULE.bazel | 1 + distro/relnotes.bzl | 5 +- java/bazel/rules/BUILD.bazel | 1 + java/bazel/rules/bazel_java_binary.bzl | 15 + java/bazel/rules/bazel_java_import.bzl | 10 +- java/bazel/rules/bazel_java_library.bzl | 10 +- java/common/rules/impl/BUILD | 22 ++ .../rules/impl/runfiles_group_support.bzl | 228 +++++++++++ java/common/rules/java_import.bzl | 9 + java/rules_java_deps.bzl | 10 + test/java/bazel/rules/BUILD.bazel | 3 + .../java/bazel/rules/runfiles_group_tests.bzl | 358 ++++++++++++++++++ test/repo/.bazelrc | 6 + 13 files changed, 674 insertions(+), 4 deletions(-) create mode 100644 java/common/rules/impl/runfiles_group_support.bzl create mode 100644 test/java/bazel/rules/runfiles_group_tests.bzl diff --git a/MODULE.bazel b/MODULE.bazel index 13d44ed5..68206afa 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -8,6 +8,7 @@ bazel_dep(name = "platforms", version = "0.0.11") bazel_dep(name = "rules_cc", version = "0.2.25") bazel_dep(name = "bazel_features", version = "1.30.0") bazel_dep(name = "bazel_skylib", version = "1.6.1") +bazel_dep(name = "rules_runfiles_group", version = "0.1.0") bazel_dep(name = "protobuf", version = "32.1", repo_name = "com_google_protobuf") bazel_dep(name = "zlib", version = "1.3.1.bcr.5") diff --git a/distro/relnotes.bzl b/distro/relnotes.bzl index e47c3c95..3c88e8da 100644 --- a/distro/relnotes.bzl +++ b/distro/relnotes.bzl @@ -20,11 +20,12 @@ bazel_dep(name = "rules_java", version = "{VERSION}") **WORKSPACE setup** -With Bazel 8.0.0 and before 8.3.0, add the following to your `.bazelrc` file: +With Bazel 8.x, add the following to your `.bazelrc` file: ~~~ +# https://github.com/bazelbuild/bazel/issues/23043 # https://github.com/bazelbuild/bazel/pull/26119 -common --repositories_without_autoloads=bazel_features_version,bazel_features_globals +common --repositories_without_autoloads=rules_runfiles_group,bazel_features_version,bazel_features_globals ~~~ In all cases, add the following to your `WORKSPACE` file: diff --git a/java/bazel/rules/BUILD.bazel b/java/bazel/rules/BUILD.bazel index 108a3f9c..6f8bf17e 100644 --- a/java/bazel/rules/BUILD.bazel +++ b/java/bazel/rules/BUILD.bazel @@ -36,6 +36,7 @@ bzl_library( "@bazel_skylib//lib:paths", "@rules_cc//cc:find_cc_toolchain_bzl", "@rules_cc//cc/common", + "@rules_runfiles_group//runfiles_group:lib", ], ) diff --git a/java/bazel/rules/bazel_java_binary.bzl b/java/bazel/rules/bazel_java_binary.bzl index 998ac898..2f64daaa 100644 --- a/java/bazel/rules/bazel_java_binary.bzl +++ b/java/bazel/rules/bazel_java_binary.bzl @@ -26,6 +26,11 @@ load("//java/common/rules:rule_util.bzl", "merge_attrs") load("//java/common/rules/impl:java_binary_deploy_jar.bzl", "create_deploy_archives") load("//java/common/rules/impl:java_binary_impl.bzl", "basic_java_binary", "binary_provider_helper") load("//java/common/rules/impl:java_helper.bzl", "helper") +load( + "//java/common/rules/impl:runfiles_group_support.bzl", + "BINARY_RUNFILES_GROUP_ATTRS", + "describer_attrs", +) load("//java/private:java_info.bzl", "JavaInfo") def _bazel_java_binary_impl(ctx): @@ -378,6 +383,16 @@ logic as the Java package of source files. For example, a source file at executable = True, ), } if not bazel_features.rules._has_launcher_maker_toolchain else {}, + describer_attrs( + Label("//java/common/rules/impl:java_binary_runfiles_group_describer"), + BINARY_RUNFILES_GROUP_ATTRS, + ), + { + # The JDK as one runfiles group, shared by every binary in the configuration. + "_runfiles_group_java_runtime": attr.label( + default = Label("//java/common/rules/impl:runfiles_group_java_runtime"), + ), + }, ) def make_java_binary(executable): diff --git a/java/bazel/rules/bazel_java_import.bzl b/java/bazel/rules/bazel_java_import.bzl index 855420fe..13801d48 100644 --- a/java/bazel/rules/bazel_java_import.bzl +++ b/java/bazel/rules/bazel_java_import.bzl @@ -18,7 +18,9 @@ Definition of java_import rule. load("//java/common:java_semantics.bzl", "semantics") load("//java/common/rules:java_import.bzl", "JAVA_IMPORT_ATTRS") +load("//java/common/rules:rule_util.bzl", "merge_attrs") load("//java/common/rules/impl:bazel_java_import_impl.bzl", "bazel_java_import_rule") +load("//java/common/rules/impl:runfiles_group_support.bzl", "LIBRARY_RUNFILES_GROUP_ATTRS", "describer_attrs") load("//java/private:java_info.bzl", "JavaInfo") def _proxy(ctx): @@ -59,7 +61,13 @@ java_import = rule( """, - attrs = JAVA_IMPORT_ATTRS, + attrs = merge_attrs( + JAVA_IMPORT_ATTRS, + describer_attrs( + Label("//java/common/rules/impl:java_import_runfiles_group_describer"), + LIBRARY_RUNFILES_GROUP_ATTRS, + ), + ), provides = [JavaInfo], fragments = ["java", "cpp"], toolchains = [semantics.JAVA_TOOLCHAIN], diff --git a/java/bazel/rules/bazel_java_library.bzl b/java/bazel/rules/bazel_java_library.bzl index 2c923946..c90c1ecc 100644 --- a/java/bazel/rules/bazel_java_library.bzl +++ b/java/bazel/rules/bazel_java_library.bzl @@ -19,7 +19,9 @@ Definition of java_library rule. load("//java/common:java_semantics.bzl", "semantics") load("//java/common/rules:android_lint.bzl", "android_lint_subrule") load("//java/common/rules:java_library.bzl", "JAVA_LIBRARY_ATTRS") +load("//java/common/rules:rule_util.bzl", "merge_attrs") load("//java/common/rules/impl:bazel_java_library_impl.bzl", "bazel_java_library_rule") +load("//java/common/rules/impl:runfiles_group_support.bzl", "LIBRARY_RUNFILES_GROUP_ATTRS", "describer_attrs") load("//java/private:java_info.bzl", "JavaInfo") def _proxy(ctx): @@ -53,7 +55,13 @@ java_library = rule( jar"). """, - attrs = JAVA_LIBRARY_ATTRS, + attrs = merge_attrs( + JAVA_LIBRARY_ATTRS, + describer_attrs( + Label("//java/common/rules/impl:java_library_runfiles_group_describer"), + LIBRARY_RUNFILES_GROUP_ATTRS, + ), + ), provides = [JavaInfo], outputs = { "classjar": "lib%{name}.jar", diff --git a/java/common/rules/impl/BUILD b/java/common/rules/impl/BUILD index ed1eb2f7..40a94f70 100644 --- a/java/common/rules/impl/BUILD +++ b/java/common/rules/impl/BUILD @@ -1,4 +1,12 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load( + ":runfiles_group_support.bzl", + "java_binary_runfiles_group_describer", + "java_import_runfiles_group_describer", + "java_library_runfiles_group_describer", + "java_runtime_runfiles", + "java_runtime_runfiles_group_describer", +) package( default_applicable_licenses = ["@rules_java//:license"], @@ -23,6 +31,7 @@ bzl_library( "//java/common:proguard_spec_info_bzl", "@rules_cc//cc:action_names_bzl", # TODO: remove after https://github.com/bazelbuild/rules_cc/issues/913 is fixed "@rules_cc//cc/common:cc_helper_bzl", + "@rules_runfiles_group//runfiles_group:lib", ], ) @@ -48,3 +57,16 @@ filegroup( ], visibility = ["//java/common/rules:__pkg__"], ) + +# Runfiles group describers of the Java rules (see runfiles_group_support.bzl), +# the defaults of their private _runfiles_group_describer attributes. +java_library_runfiles_group_describer(name = "java_library_runfiles_group_describer") + +java_import_runfiles_group_describer(name = "java_import_runfiles_group_describer") + +java_binary_runfiles_group_describer(name = "java_binary_runfiles_group_describer") + +java_runtime_runfiles_group_describer(name = "java_runtime_runfiles_group_describer") + +# The JDK as one runfiles group, merged in by every java_binary and java_test. +java_runtime_runfiles(name = "runfiles_group_java_runtime") diff --git a/java/common/rules/impl/runfiles_group_support.bzl b/java/common/rules/impl/runfiles_group_support.bzl new file mode 100644 index 00000000..f9cc4a12 --- /dev/null +++ b/java/common/rules/impl/runfiles_group_support.bzl @@ -0,0 +1,228 @@ +# Copyright 2026 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Describes the runfiles groups of the Java rules for rules_runfiles_group. + +A packaging rule (a container image or archive rule) walks a binary's graph with +an aspect and asks each target's runfiles group describer what it adds to the +binary's runfiles and which attributes it merges runfiles in from. The Java rules +point their `_runfiles_group_describer` attribute at the describers below. + +It uses information visible to the aspect: +JavaInfo, DefaultInfo and the rule's attributes. The JDK, which a binary takes from +the Java runtime toolchain, is described by a singleton target that resolves the +same toolchain; every binary merges it in, so all of them share one group for it. +""" + +load("@rules_runfiles_group//runfiles_group:lib.bzl", "runfiles_groups") +load("//java/common:java_semantics.bzl", "semantics") +load("//java/private:java_info.bzl", "JavaInfo") + +# copybara: default visibility + +# Stamped on every group the Java rules produce, so that JVM-shaped groups stay +# together when a packager has to merge groups to fit a layer limit. +MERGE_AFFINITY = "rules_java" + +# The named group the java_binary / java_test rules merge in for the JDK. It is +# shared by every binary, so it is named with a string rather than with a Label. +JAVA_RUNTIME_GROUP = "rules_java#java_runtime" + +# The attributes a packager's aspect visits. +LIBRARY_RUNFILES_GROUP_ATTRS = ["data", "deps", "exports", "runtime_deps"] +BINARY_RUNFILES_GROUP_ATTRS = ["_runfiles_group_java_runtime", "_test_support", "data", "deps", "runtime_deps"] + +def own_kind(label): + """Returns the `kind` of the group holding a target's own outputs. + + Only a target in the main repository is first-party. Plenty of Java targets + Bazel builds from source belong to somebody else -- the protobuf runtime, + say -- and a packaging rule that selects on `kind` should not have to treat + those as the user's own code. + + Args: + label: (Label) The label of the target owning the group. + + Returns: + (str) One of runfiles_groups.KINDS. + """ + return "first_party" if label.repo_name == "" else "third_party" + +def java_fallback_entry(_ctx, dep): + """Synthesizes the group of a dependency that does not describe its groups. + + A Java target's dependency may well be a custom rule that returns JavaInfo + and lands in a binary's runtime classpath without a runfiles group describer. + Not covering those would silently drop their runfiles. + + The group covers the two sources a Java target draws a dependency's runfiles + from: the runtime classpath, built from the dependency's transitive runtime + jars, and the dependency's own default runfiles, which a target collects + implicitly. Both are empty for a neverlink dependency. + + Deliberately not all of DefaultInfo.files, which rules_runfiles_group's + generic fallback would include: a neverlink java_library includes its jars + there while not adding them to a binary's runfiles. Only a dependency + without JavaInfo -- a genrule or a plain .jar file -- has the .jar files + among them put on the runtime classpath, as "legacy jars" (see collect_deps). + + Args: + _ctx: (ctx) The aspect context of the merging target. + dep: (Target) A dependency without a runfiles group describer. + + Returns: + (RunfilesGroupInfo|None) The dependency's group, if one should exist. + """ + contents = [] + if JavaInfo in dep: + contents.append(dep[JavaInfo].transitive_runtime_jars) + else: + legacy_jars = [f for f in dep[DefaultInfo].files.to_list() if f.extension == "jar"] + if legacy_jars: + contents.append(depset(legacy_jars)) + default_runfiles = dep[DefaultInfo].default_runfiles + if default_runfiles != None: + contents.append(default_runfiles) + if not contents: + return None + + return runfiles_groups.entry( + name = dep.label, + content = runfiles_groups.union(_ctx, contents), + merge_affinity = MERGE_AFFINITY, + ) + +def _java_dep_specs(attrs): + return [runfiles_groups.merge_from(attr, fallback = java_fallback_entry) for attr in attrs] + [ + runfiles_groups.merge_from("data", fallback = "synthesize"), + ] + +def _describe_java_library_runfiles(target, ctx): + if ctx.rule.attr.neverlink: + # neverlink libraries do not contribute runfiles to the final binary. + return runfiles_groups.node(add = [], merge_from = []) + + # The library's own runfiles are its output class jar. + return runfiles_groups.node( + add = [runfiles_groups.entry( + name = ctx.label, + content = depset(target[JavaInfo].runtime_output_jars), + kind = own_kind(ctx.label), + merge_affinity = MERGE_AFFINITY, + )], + merge_from = _java_dep_specs(["deps", "exports", "runtime_deps"]), + ) + +java_library_runfiles_group_describer = runfiles_groups.make_describer_rule( + describe = _describe_java_library_runfiles, +) + +def _describe_java_import_runfiles(target, ctx): + if ctx.rule.attr.neverlink: + return runfiles_groups.node(add = [], merge_from = []) + + weight = getattr(ctx.rule.attr, "runfiles_weight", 0) + return runfiles_groups.node( + add = [runfiles_groups.entry( + name = ctx.label, + content = depset(target[JavaInfo].runtime_output_jars), + kind = "third_party", + rank = runfiles_groups.RANK_SHARED_DEPS, + weight = weight if weight > 0 else None, + merge_affinity = MERGE_AFFINITY, + )], + merge_from = _java_dep_specs(["deps", "exports", "runtime_deps"]), + ) + +java_import_runfiles_group_describer = runfiles_groups.make_describer_rule( + describe = _describe_java_import_runfiles, +) + +def _describe_java_binary_runfiles(target, ctx): + default_info = target[DefaultInfo] + executable = default_info.files_to_run.executable + merge_from = _java_dep_specs(["deps", "runtime_deps", "_test_support"]) + executable_group = None + if executable: + executable_group = ctx.label + + # Only a binary with an executable puts the JDK into its runfiles. + merge_from.append("_runfiles_group_java_runtime") + + return runfiles_groups.node( + add = [runfiles_groups.entry( + name = ctx.label, + content = depset([executable] if executable else [], transitive = [default_info.files]), + kind = own_kind(ctx.label), + rank = runfiles_groups.RANK_EXECUTABLE, + merge_affinity = MERGE_AFFINITY, + )], + merge_from = merge_from, + executable_group = executable_group, + ) + +java_binary_runfiles_group_describer = runfiles_groups.make_describer_rule( + describe = _describe_java_binary_runfiles, +) + +def _describe_java_runtime_runfiles(target, _ctx): + return runfiles_groups.node(add = [runfiles_groups.entry( + name = JAVA_RUNTIME_GROUP, + content = target[DefaultInfo].files, + kind = "foundation", + rank = runfiles_groups.RANK_FOUNDATION, + do_not_merge = True, + merge_affinity = MERGE_AFFINITY, + )]) + +java_runtime_runfiles_group_describer = runfiles_groups.make_describer_rule( + describe = _describe_java_runtime_runfiles, +) + +def _java_runtime_runfiles_impl(ctx): + # The same toolchain, in the same configuration, that java_binary takes its + # runtime from, so its files are exactly what a binary puts into its runfiles. + return [DefaultInfo(files = semantics.find_java_runtime_toolchain(ctx).files)] + +java_runtime_runfiles = rule( + implementation = _java_runtime_runfiles_impl, + doc = """\ +The files of the resolved Java runtime, as one runfiles group. + +Instantiated once, as the default of java_binary's and java_test's private +`_runfiles_group_java_runtime` attribute, so that every binary in a configuration +merges in the same group for the JDK and a packager builds it once. +""", + attrs = { + "_runfiles_group_describer": attr.label(default = Label("//java/common/rules/impl:java_runtime_runfiles_group_describer")), + "_runfiles_group_attrs": attr.string_list(default = []), + }, + toolchains = [semantics.JAVA_RUNTIME_TOOLCHAIN], +) + +def describer_attrs(describer, walked_attrs): + """The private attributes that make a rule describe its runfiles groups. + + Args: + describer: (Label) The describer target of the rule. + walked_attrs: (list[str]) The attributes a packager's aspect walks into. + + Returns: + (dict[str, Attribute]) Attributes to merge into the rule's attrs. + """ + return { + "_runfiles_group_describer": attr.label(default = describer), + "_runfiles_group_attrs": attr.string_list(default = walked_attrs), + } diff --git a/java/common/rules/java_import.bzl b/java/common/rules/java_import.bzl index a6e9712f..126cf63c 100644 --- a/java/common/rules/java_import.bzl +++ b/java/common/rules/java_import.bzl @@ -124,5 +124,14 @@ This corresponds to the javac and JVM --add-opens= flags. """, ), "licenses": attr.license() if hasattr(attr, "license") else attr.string_list(), + "runfiles_weight": attr.int( + default = 0, + doc = """ +Weight hint for the target's runfiles group. If set to a value greater +than 0, this weight is attached to the target's runfiles group to help packaging +rules make informed merge decisions. Intended to be set by dependency management +rulesets (e.g. rules_jvm_external) using actual jar byte sizes. + """, + ), "_java_toolchain_type": attr.label(default = semantics.JAVA_TOOLCHAIN_TYPE), } diff --git a/java/rules_java_deps.bzl b/java/rules_java_deps.bzl index c9742f94..8e1e5936 100644 --- a/java/rules_java_deps.bzl +++ b/java/rules_java_deps.bzl @@ -159,6 +159,15 @@ def bazel_skylib_repo(): ], ) +def rules_runfiles_group_repo(): + maybe( + http_archive, + name = "rules_runfiles_group", + sha256 = "a2e1d6121419ce57f3a22a12a71cbfc985fbdd983242f680801510ba2a482650", + strip_prefix = "rules_runfiles_group-0.1.0", + urls = ["https://github.com/bazel-contrib/rules_runfiles_group/releases/download/v0.1.0/rules_runfiles_group-v0.1.0.tar.gz"], + ) + def platforms_repo(): maybe( http_archive, @@ -231,6 +240,7 @@ def rules_java_dependencies(): """ compatibility_proxy_repo() bazel_skylib_repo() + rules_runfiles_group_repo() rules_cc_repo() protobuf_repo() platforms_repo() diff --git a/test/java/bazel/rules/BUILD.bazel b/test/java/bazel/rules/BUILD.bazel index 09c1f1a0..87c6da4b 100644 --- a/test/java/bazel/rules/BUILD.bazel +++ b/test/java/bazel/rules/BUILD.bazel @@ -3,6 +3,7 @@ load(":java_import_tests.bzl", "java_import_tests") load(":java_library_tests.bzl", "java_library_tests") load(":java_plugin_tests.bzl", "java_plugin_tests") load(":java_test_tests.bzl", "java_test_tests") +load(":runfiles_group_tests.bzl", "runfiles_group_tests") java_binary_tests(name = "java_binary_tests") @@ -13,3 +14,5 @@ java_library_tests(name = "java_library_tests") java_plugin_tests(name = "java_plugin_tests") java_test_tests(name = "java_test_tests") + +runfiles_group_tests(name = "runfiles_group_tests") diff --git a/test/java/bazel/rules/runfiles_group_tests.bzl b/test/java/bazel/rules/runfiles_group_tests.bzl new file mode 100644 index 00000000..d7c073cf --- /dev/null +++ b/test/java/bazel/rules/runfiles_group_tests.bzl @@ -0,0 +1,358 @@ +"""Tests for the runfiles group describers of the java rules.""" + +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") +load("@rules_cc//cc:cc_binary.bzl", "cc_binary") +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_runfiles_group//runfiles_group:runfiles_group_analysis_test.bzl", "runfiles_group_analysis_test") +load("//java:java_binary.bzl", "java_binary") +load("//java:java_import.bzl", "java_import") +load("//java:java_library.bzl", "java_library") +load("//java:java_test.bzl", "java_test") +load("//java/common:java_info.bzl", "JavaInfo") +load("//java/common/rules/impl:runfiles_group_support.bzl", "own_kind") + +def _fake_jvm_import_impl(ctx): + jar = ctx.file.jar + return [ + DefaultInfo(files = depset([jar]), runfiles = ctx.runfiles(files = [jar])), + JavaInfo(output_jar = jar, compile_jar = jar), + ] + +# A stand-in for a custom rule that returns the rules_java providers but has no +# runfiles group describer, e.g. rules_jvm_external's jvm_import. The java rules have to +# synthesize a group for such a dependency, or a packager would find no group +# holding its jars. +_fake_jvm_import = rule( + implementation = _fake_jvm_import_impl, + attrs = {"jar": attr.label(allow_single_file = True, mandatory = True)}, +) + +def _helper(rule_fn, name, **kwargs): + rule_fn(name = name, tags = ["manual"], **kwargs) + +def _own_kind_impl(ctx): + env = unittest.begin(ctx) + + asserts.equals(env, "first_party", own_kind(Label("//java/com/example:lib"))) + + # Java code Bazel builds from source in another module -- the protobuf + # runtime, say -- is not the user's own code. + asserts.equals(env, "third_party", own_kind(Label("@other_repo//:lib"))) + + return unittest.end(env) + +_own_kind_test = unittest.make(_own_kind_impl) + +def _label_str(name): + """The canonical string form of a target's label, as the test reports it.""" + return str(native.package_relative_label(":" + name)) + +def runfiles_group_tests(name): + """Registers the runfiles group tests and a test_suite that bundles them. + + Args: + name: Name of the generated test_suite; also used as a prefix for the + individual analysis test targets. + """ + tests = [ + "own_kind", + "simple_binary", + "transitive_deps", + "runtime_deps", + "java_import", + "java_import_weight", + "with_data", + "library_with_data", + "foreign_java_dep", + "genrule_runtime_dep", + "jar_file_runtime_dep", + "native_libraries", + "transitive_data", + "java_test", + "group_ordering", + "merge_to_limit", + ] + + _own_kind_test(name = name + "/own_kind") + _test_simple_binary(name + "/simple_binary") + _test_binary_with_transitive_deps(name + "/transitive_deps") + _test_binary_with_runtime_deps(name + "/runtime_deps") + _test_binary_with_java_import(name + "/java_import") + _test_binary_with_java_import_weight(name + "/java_import_weight") + _test_binary_with_data(name + "/with_data") + _test_library_with_data(name + "/library_with_data") + _test_binary_with_foreign_java_dep(name + "/foreign_java_dep") + _test_binary_with_genrule_runtime_dep(name + "/genrule_runtime_dep") + _test_binary_with_jar_file_runtime_dep(name + "/jar_file_runtime_dep") + _test_binary_with_native_libraries(name + "/native_libraries") + _test_binary_with_transitive_data(name + "/transitive_data") + _test_java_test_rule(name + "/java_test") + _test_binary_group_ordering(name + "/group_ordering") + _test_merge_to_limit(name + "/merge_to_limit") + + native.test_suite( + name = name, + tests = [name + "/" + test for test in tests], + ) + +def _test_simple_binary(name): + _helper(java_library, name + "_lib", srcs = ["java/A.java"]) + _helper( + java_binary, + name + "_bin", + main_class = "A", + runtime_deps = [name + "_lib"], + ) + + runfiles_group_analysis_test( + name = name, + binaries = [name + "_bin"], + overlapping_group_behavior = "error", + expected_executable_group = _label_str(name + "_bin"), + ) + +def _test_binary_with_transitive_deps(name): + _helper(java_library, name + "_leaf", srcs = ["java/A.java"]) + _helper(java_library, name + "_mid", srcs = ["java/B.java"], deps = [name + "_leaf"]) + _helper( + java_binary, + name + "_bin", + main_class = "B", + runtime_deps = [name + "_mid"], + ) + runfiles_group_analysis_test( + name = name, + binaries = [name + "_bin"], + overlapping_group_behavior = "error", + ) + +def _test_binary_with_runtime_deps(name): + _helper(java_library, name + "_compile_dep", srcs = ["java/A.java"]) + _helper(java_library, name + "_runtime_dep", srcs = ["java/B.java"]) + _helper( + java_binary, + name + "_bin", + srcs = ["java/Main.java"], + main_class = "Main", + deps = [name + "_compile_dep"], + runtime_deps = [name + "_runtime_dep"], + ) + runfiles_group_analysis_test( + name = name, + binaries = [name + "_bin"], + overlapping_group_behavior = "error", + ) + +def _test_binary_with_java_import(name): + _helper( + java_import, + name + "_imported", + jars = ["B.jar"], + ) + _helper( + java_binary, + name + "_bin", + main_class = "A", + runtime_deps = [name + "_imported"], + ) + runfiles_group_analysis_test( + name = name, + binaries = [name + "_bin"], + overlapping_group_behavior = "error", + ) + +def _test_binary_with_java_import_weight(name): + _helper( + java_import, + name + "_imported", + jars = ["B.jar"], + runfiles_weight = 42, + ) + _helper( + java_binary, + name + "_bin", + main_class = "A", + runtime_deps = [name + "_imported"], + ) + runfiles_group_analysis_test( + name = name, + binaries = [name + "_bin"], + overlapping_group_behavior = "error", + ) + +def _test_binary_with_data(name): + _helper(java_library, name + "_lib", srcs = ["java/A.java"]) + _helper( + java_binary, + name + "_bin", + main_class = "A", + runtime_deps = [name + "_lib"], + data = ["java/A.java"], + ) + runfiles_group_analysis_test( + name = name, + binaries = [name + "_bin"], + overlapping_group_behavior = "error", + ) + +def _test_library_with_data(name): + # A library's data files land in the binary's runfiles through + # collect_default, so the library has to put them in a group of their own. + _helper(java_library, name + "_lib", srcs = ["java/A.java"], data = ["java/A.java"]) + _helper( + java_binary, + name + "_bin", + main_class = "A", + runtime_deps = [name + "_lib"], + ) + runfiles_group_analysis_test( + name = name, + binaries = [name + "_bin"], + overlapping_group_behavior = "error", + ) + +def _test_binary_with_foreign_java_dep(name): + _helper(_fake_jvm_import, name + "_foreign", jar = "B.jar") + _helper( + java_binary, + name + "_bin", + main_class = "A", + runtime_deps = [name + "_foreign"], + ) + runfiles_group_analysis_test( + name = name, + binaries = [name + "_bin"], + overlapping_group_behavior = "error", + ) + +def _test_binary_with_genrule_runtime_dep(name): + # A runtime_deps entry without JavaInfo has its .jar outputs put on the + # runtime classpath as legacy jars. + native.genrule( + name = name + "_gen", + outs = [name + "_gen.jar"], + cmd = "", + tags = ["manual"], + ) + _helper( + java_binary, + name + "_bin", + srcs = ["java/Main.java"], + main_class = "Main", + runtime_deps = [name + "_gen"], + ) + runfiles_group_analysis_test( + name = name, + binaries = [name + "_bin"], + overlapping_group_behavior = "error", + ) + +def _test_binary_with_jar_file_runtime_dep(name): + _helper( + java_binary, + name + "_bin", + srcs = ["java/Main.java"], + main_class = "Main", + runtime_deps = ["B.jar"], + ) + runfiles_group_analysis_test( + name = name, + binaries = [name + "_bin"], + overlapping_group_behavior = "error", + ) + +def _test_binary_with_native_libraries(name): + _helper(cc_library, name + "_helper", srcs = ["lib2.so"]) + _helper(cc_binary, name + "_jni.so", srcs = ["lib1.so"], deps = [name + "_helper"]) + _helper(java_library, name + "_lib", srcs = ["java/A.java"], deps = [name + "_jni.so"]) + _helper( + java_binary, + name + "_bin", + srcs = ["java/Main.java"], + main_class = "Main", + runtime_deps = [name + "_lib"], + ) + runfiles_group_analysis_test( + name = name, + binaries = [name + "_bin"], + overlapping_group_behavior = "error", + ) + +def _test_binary_with_transitive_data(name): + # Data of a library reached through deps and exports lands in the binary's + # runfiles. + _helper(native.filegroup, name + "_data", srcs = ["some.js"]) + _helper(native.filegroup, name + "_extra", srcs = ["some.gss"]) + _helper(java_library, name + "_bundle2", data = [name + "_extra"]) + _helper(java_library, name + "_bundle", data = [name + "_data"], exports = [name + "_bundle2"]) + _helper(java_library, name + "_lib", srcs = ["java/A.java"], deps = [name + "_bundle"]) + _helper( + java_binary, + name + "_bin", + srcs = ["java/Main.java"], + main_class = "Main", + runtime_deps = [name + "_lib"], + ) + runfiles_group_analysis_test( + name = name, + binaries = [name + "_bin"], + overlapping_group_behavior = "error", + ) + +def _test_java_test_rule(name): + _helper(java_library, name + "_lib", srcs = ["java/A.java"]) + _helper( + java_test, + name + "_test_bin", + srcs = ["java/A.java"], + test_class = "A", + runtime_deps = [name + "_lib"], + ) + runfiles_group_analysis_test( + name = name, + binaries = [name + "_test_bin"], + overlapping_group_behavior = "error", + ) + +def _test_binary_group_ordering(name): + _helper(java_library, name + "_lib", srcs = ["java/A.java"]) + _helper( + java_binary, + name + "_bin", + main_class = "A", + runtime_deps = [name + "_lib"], + ) + runfiles_group_analysis_test( + name = name, + binaries = [name + "_bin"], + overlapping_group_behavior = "error", + # The java_runtime is at the foundation rank, everything else at the + # executable rank. + expected_group_names = [ + "rules_java#java_runtime", + _label_str(name + "_lib"), + _label_str(name + "_bin"), + ], + ) + +def _test_merge_to_limit(name): + _helper(java_library, name + "_lib_a", srcs = ["java/A.java"]) + _helper(java_library, name + "_lib_b", srcs = ["java/B.java"]) + _helper(java_library, name + "_lib_c", srcs = ["java/C.java"]) + _helper( + java_binary, + name + "_bin", + main_class = "A", + runtime_deps = [ + name + "_lib_a", + name + "_lib_b", + name + "_lib_c", + ], + ) + runfiles_group_analysis_test( + name = name, + binaries = [name + "_bin"], + overlapping_group_behavior = "error", + max_groups = 4, + expected_group_count = 4, + ) diff --git a/test/repo/.bazelrc b/test/repo/.bazelrc index 8d0ffca7..b2136d11 100644 --- a/test/repo/.bazelrc +++ b/test/repo/.bazelrc @@ -2,6 +2,12 @@ build:bzlmod --experimental_enable_bzlmod common --incompatible_disallow_empty_glob +# rules_runfiles_group (loaded by the java rule implementations) must be excluded +# from autoloads to avoid a load cycle in WORKSPACE mode on Bazel 8. On Bazel +# 8.0.0-8.3.0 the same applies to bazel_features' generated repos. +# https://github.com/bazelbuild/bazel/issues/23043 +common --repositories_without_autoloads=rules_runfiles_group,bazel_features_version,bazel_features_globals + # Enable modern C++ features, for compiling java_tools from source build --cxxopt=-std=c++17 build --host_cxxopt=-std=c++17 From 09343bdb3e92fe0656f9f91556e56d3fc9ee8e2e Mon Sep 17 00:00:00 2001 From: Malte Poll <1780588+malt3@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:34:24 +0100 Subject: [PATCH 2/2] temporary commit: use pre-release version of rules_runfiles_group Drop before merging the PR. This updates rrg to a pre-release, so we can test the behaviour in rules_java before commiting to the final, aspect-based API. --- .bazelrc | 3 +++ MODULE.bazel | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.bazelrc b/.bazelrc index 12d684fc..d7819a18 100644 --- a/.bazelrc +++ b/.bazelrc @@ -20,3 +20,6 @@ test --nointerface_shared_objects # For testing add_exports/add_opens module flags behavior. test --incompatible_java_info_merge_runtime_module_flags + +# Delete this before merging the PR +common --registry=https://bazel-contrib.github.io/rules_runfiles_group --registry=https://bcr.bazel.build diff --git a/MODULE.bazel b/MODULE.bazel index 68206afa..4269f876 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -8,7 +8,7 @@ bazel_dep(name = "platforms", version = "0.0.11") bazel_dep(name = "rules_cc", version = "0.2.25") bazel_dep(name = "bazel_features", version = "1.30.0") bazel_dep(name = "bazel_skylib", version = "1.6.1") -bazel_dep(name = "rules_runfiles_group", version = "0.1.0") +bazel_dep(name = "rules_runfiles_group", version = "99.0.0-dev.20261009.1.f6acddf") bazel_dep(name = "protobuf", version = "32.1", repo_name = "com_google_protobuf") bazel_dep(name = "zlib", version = "1.3.1.bcr.5")