Support embedding stamping info from bazel in `java_single_jar`

A new attribute `stamp` controls the Bazel build info included in the output:
   - `stamp = 1`: Always embed Bazel build information, even in `--nostamp` builds.
   - `stamp = 0`: Embed Bazel build information with constant values even in `--stamp` builds.
   - `stamp = -1`: Embedding of Bazel build information is controlled by the `--[no]stamp` flag.

The above only takes effect when `exclude_build_data = False` (default is `True`). It is an error to specify `stamp = 1` without `exclude_build_data = False`.

Fixes https://github.com/bazelbuild/rules_java/issues/352

PiperOrigin-RevId: 881412734
Change-Id: I876b3a3b328eb363ad112dfc0fdfe599de03b164
diff --git a/MODULE.bazel b/MODULE.bazel
index 204b8e8..e98ca3d 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -6,14 +6,7 @@
 )
 
 bazel_dep(name = "platforms", version = "0.0.11")
-bazel_dep(name = "rules_cc", version = "0.2.13")
-archive_override(
-    module_name = "rules_cc",
-    integrity = "sha256-y3RA9zEyB7HqBXVTgrlFmvfJARcEDzILe2ugNGC4ZrE=",
-    strip_prefix = "rules_cc-b5a65591334f74371f4d75003768957a740cd868",
-    urls = ["https://github.com/bazelbuild/rules_cc/archive/b5a65591334f74371f4d75003768957a740cd868.tar.gz"],
-)
-
+bazel_dep(name = "rules_cc", version = "0.2.17")
 bazel_dep(name = "bazel_features", version = "1.30.0")
 bazel_dep(name = "bazel_skylib", version = "1.6.1")
 bazel_dep(name = "protobuf", version = "32.1", repo_name = "com_google_protobuf")
diff --git a/java/common/BUILD b/java/common/BUILD
index 9958a88..9881236 100644
--- a/java/common/BUILD
+++ b/java/common/BUILD
@@ -36,9 +36,7 @@
     name = "semantics_bzl",
     srcs = ["java_semantics.bzl"],
     visibility = ["//visibility:public"],
-    deps = [
-        "@rules_cc//cc/common",
-    ],
+    deps = ["@rules_cc//cc/common:cc_helper_bzl"],
 )
 
 bzl_library(
diff --git a/java/common/rules/BUILD b/java/common/rules/BUILD
index f6fde9d..4c24d07 100644
--- a/java/common/rules/BUILD
+++ b/java/common/rules/BUILD
@@ -35,7 +35,11 @@
     name = "java_single_jar_bzl",
     srcs = ["java_single_jar.bzl"],
     visibility = ["//java:__subpackages__"],
-    deps = ["//java/common"],
+    deps = [
+        "//java/common",
+        "//java/common:semantics_bzl",
+        "//java/common/rules/impl:java_helper_bzl",
+    ],
 )
 
 bzl_library(
diff --git a/java/common/rules/impl/BUILD b/java/common/rules/impl/BUILD
index 6ab4137..70adecc 100644
--- a/java/common/rules/impl/BUILD
+++ b/java/common/rules/impl/BUILD
@@ -29,7 +29,7 @@
 bzl_library(
     name = "java_helper_bzl",
     srcs = ["java_helper.bzl"],
-    visibility = ["//visibility:private"],
+    visibility = ["//java:__subpackages__"],
     deps = [
         "//java/common:semantics_bzl",
         "//java/common/rules:java_helper_bzl",
diff --git a/java/common/rules/impl/java_binary_deploy_jar.bzl b/java/common/rules/impl/java_binary_deploy_jar.bzl
index 7b7d69d..d6add14 100644
--- a/java/common/rules/impl/java_binary_deploy_jar.bzl
+++ b/java/common/rules/impl/java_binary_deploy_jar.bzl
@@ -19,15 +19,6 @@
 
 # copybara: default visibility
 
-def _get_build_info(ctx, stamp):
-    if helper.is_stamping_enabled(ctx, stamp):
-        # Makes the target depend on BUILD_INFO_KEY, which helps to discover stamped targets
-        # See b/326620485 for more details.
-        ctx.version_file  # buildifier: disable=no-effect
-        return ctx.attr._build_info_translator[OutputGroupInfo].non_redacted_build_info_files.to_list()
-    else:
-        return ctx.attr._build_info_translator[OutputGroupInfo].redacted_build_info_files.to_list()
-
 def create_deploy_archives(
         ctx,
         java_attrs,
@@ -70,7 +61,7 @@
         order = "preorder",
     )
     multi_release = ctx.fragments.java.multi_release_deploy_jars
-    build_info_files = _get_build_info(ctx, ctx.attr.stamp)
+    build_info_files = helper.get_build_info(ctx, ctx.attr.stamp)
     build_target = str(ctx.label)
     manifest_lines = ctx.attr.deploy_manifest_lines + extra_manifest_lines
     create_deploy_archive(
diff --git a/java/common/rules/impl/java_helper.bzl b/java/common/rules/impl/java_helper.bzl
index e45fac9..638878b 100644
--- a/java/common/rules/impl/java_helper.bzl
+++ b/java/common/rules/impl/java_helper.bzl
@@ -241,6 +241,15 @@
     # stamp == -1 / auto
     return int(ctx.configuration.stamp_binaries())
 
+def _get_build_info(ctx, stamp):
+    if helper.is_stamping_enabled(ctx, stamp):
+        # Makes the target depend on BUILD_INFO_KEY, which helps to discover stamped targets
+        # See b/326620485 for more details.
+        ctx.version_file  # buildifier: disable=no-effect
+        return ctx.attr._build_info_translator[OutputGroupInfo].non_redacted_build_info_files.to_list()
+    else:
+        return ctx.attr._build_info_translator[OutputGroupInfo].redacted_build_info_files.to_list()
+
 helper = struct(
     collect_all_targets_as_deps = _collect_all_targets_as_deps,
     filter_launcher_for_target = _filter_launcher_for_target,
@@ -265,6 +274,7 @@
     detokenize_javacopts = _loading_phase_helper.detokenize_javacopts,
     tokenize_javacopts = _loading_phase_helper.tokenize_javacopts,
     is_stamping_enabled = _is_stamping_enabled,
+    get_build_info = _get_build_info,
     get_relative = _loading_phase_helper.get_relative,
     has_target_constraints = _loading_phase_helper.has_target_constraints,
 )
diff --git a/java/common/rules/java_single_jar.bzl b/java/common/rules/java_single_jar.bzl
index 95b7ca2..c955275 100644
--- a/java/common/rules/java_single_jar.bzl
+++ b/java/common/rules/java_single_jar.bzl
@@ -15,6 +15,8 @@
 
 load("//java/common:java_common.bzl", "java_common")
 load("//java/common:java_info.bzl", "JavaInfo")
+load("//java/common:java_semantics.bzl", "semantics")
+load("//java/common/rules/impl:java_helper.bzl", "helper")
 
 # copybara: default visibility
 
@@ -69,8 +71,15 @@
     else:
         fail("\"compress\" attribute (%s) must be: yes, no, preserve." % ctx.attr.compress)
 
+    if ctx.attr.exclude_build_data and ctx.attr.stamp == 1:
+        fail("Enabling stamping has not effect with exclude_build_data enabled")
+
+    build_info_files = []
     if ctx.attr.exclude_build_data:
         args.add("--exclude_build_data")
+    else:
+        build_info_files = helper.get_build_info(ctx, ctx.attr.stamp)
+        args.add_all(build_info_files, before_each = "--build_info_file")
     if ctx.attr.multi_release:
         args.add("--multi_release")
 
@@ -78,7 +87,7 @@
         args.add("--exclude_pattern", ctx.attr.exclude_pattern)
 
     ctx.actions.run(
-        inputs = inputs,
+        inputs = depset(build_info_files, transitive = [inputs]),
         outputs = [ctx.outputs.output],
         arguments = [args],
         progress_message = "Merging into %s" % ctx.outputs.output.short_path,
@@ -138,6 +147,20 @@
             executable = True,
         ),
         "output": attr.output(),
+        "stamp": attr.int(
+            doc = """
+              Whether to embed extra Bazel build information into the build_data.properties file:
+                * `stamp = 1`: Always embed Bazel build information, even in `--nostamp` builds.
+                * `stamp = 0`: Embed Bazel build information with constant values, even in `--stamp` builds.
+                * `stamp = -1`: Embedding of Bazel build information is controlled by the `--[no]stamp` flag.
+
+                Note: whether the output contains the build_data.properties file is controlled
+                 by the `exclude_build_data` attribute.
+                """,
+            default = 0,
+            values = [-1, 0, 1],
+        ),
+        "_build_info_translator": attr.label(default = semantics.BUILD_INFO_TRANSLATOR_LABEL),
     },
     implementation = _bazel_java_single_jar_impl,
     doc = """
diff --git a/test/java/common/rules/BUILD b/test/java/common/rules/BUILD
index 32870da..8e74a2a 100644
--- a/test/java/common/rules/BUILD
+++ b/test/java/common/rules/BUILD
@@ -4,6 +4,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_single_jar_tests.bzl", "java_single_jar_tests")
 load(":java_test_tests.bzl", "java_test_tests")
 load(":merge_attrs_tests.bzl", "merge_attrs_test_suite")
 
@@ -21,6 +22,8 @@
 
 java_import_tests(name = "java_import_tests")
 
+java_single_jar_tests(name = "java_single_jar_tests")
+
 java_test_tests(name = "java_test_tests")
 
 add_exports_tests(name = "add_exports_tests")
diff --git a/test/java/common/rules/java_single_jar_tests.bzl b/test/java/common/rules/java_single_jar_tests.bzl
new file mode 100644
index 0000000..8ea43da
--- /dev/null
+++ b/test/java/common/rules/java_single_jar_tests.bzl
@@ -0,0 +1,164 @@
+"""Tests for the java_single_jar rule"""
+
+load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite")
+load("@rules_testing//lib:truth.bzl", "matching")
+load("@rules_testing//lib:util.bzl", "util")
+load("//java:java_single_jar.bzl", "java_single_jar")
+load("//java/common:java_semantics.bzl", "semantics")
+
+def _label_to_bin_path(label):
+    segments = ["{bindir}"]
+    if label.repo_name:
+        segments.extend(["external", label.repo_name])
+    segments.append(label.package)
+    return "/".join(segments)
+
+_BUILD_INFO_PATH = _label_to_bin_path(Label(semantics.BUILD_INFO_TRANSLATOR_LABEL))
+
+def _test_java_single_jar_basic(name):
+    util.helper_target(
+        java_single_jar,
+        name = name + "/jar",
+        deps = ["1.jar", "2.jar"],
+    )
+
+    analysis_test(
+        name = name,
+        impl = _test_java_single_jar_basic_impl,
+        target = name + "/jar",
+    )
+
+def _test_java_single_jar_basic_impl(env, target):
+    assert_that_action = env.expect.that_target(target).action_named("JavaSingleJar")
+    assert_that_action.argv().contains_at_least([
+        "--sources",
+        "{package}/1.jar",
+        "{package}/2.jar",
+        "--output",
+        "{bindir}/{package}/{name}.jar",
+        "--normalize",
+        "--dont_change_compression",
+        "--exclude_build_data",
+        "--multi_release",
+    ]).in_order()
+
+def _test_java_single_jar_force_enable_stamping(name):
+    util.helper_target(
+        java_single_jar,
+        name = name + "/jar",
+        stamp = 1,
+        exclude_build_data = False,
+    )
+
+    analysis_test(
+        name = name,
+        impl = _test_java_single_jar_force_enable_stamping_impl,
+        target = name + "/jar",
+    )
+
+def _test_java_single_jar_force_enable_stamping_impl(env, target):
+    assert_that_action = env.expect.that_target(target).action_named("JavaSingleJar")
+    assert_that_action.contains_flag_values([
+        ("--build_info_file", _BUILD_INFO_PATH + "/non_volatile_file.properties"),
+        ("--build_info_file", _BUILD_INFO_PATH + "/volatile_file.properties"),
+    ])
+
+def _test_java_single_jar_force_disable_stamping(name):
+    util.helper_target(
+        java_single_jar,
+        name = name + "/jar",
+        stamp = 0,
+        exclude_build_data = False,
+    )
+
+    analysis_test(
+        name = name,
+        impl = _test_java_single_jar_force_disable_stamping_impl,
+        target = name + "/jar",
+    )
+
+def _test_java_single_jar_force_disable_stamping_impl(env, target):
+    assert_that_action = env.expect.that_target(target).action_named("JavaSingleJar")
+    assert_that_action.contains_flag_values([
+        ("--build_info_file", _BUILD_INFO_PATH + "/redacted_file.properties"),
+    ])
+
+def _test_java_single_jar_stamping_enabled_build_data_excluded_fails(name):
+    util.helper_target(
+        java_single_jar,
+        name = name + "/jar",
+        stamp = 1,
+        exclude_build_data = True,
+    )
+
+    analysis_test(
+        name = name,
+        impl = _test_java_single_jar_stamping_enabled_build_data_excluded_fails_impl,
+        target = name + "/jar",
+        expect_failure = True,
+    )
+
+def _test_java_single_jar_stamping_enabled_build_data_excluded_fails_impl(env, target):
+    env.expect.that_target(target).failures().contains_predicate(
+        matching.str_matches("Enabling stamping has not effect with exclude_build_data enabled"),
+    )
+
+def _test_java_single_jar_stamp_attr_auto_stamp_flag_enabled(name):
+    util.helper_target(
+        java_single_jar,
+        name = name + "/jar",
+        stamp = -1,
+        exclude_build_data = False,
+    )
+
+    analysis_test(
+        name = name,
+        impl = _test_java_single_jar_stamp_attr_auto_stamp_flag_enabled_impl,
+        target = name + "/jar",
+        config_settings = {
+            "//command_line_option:stamp": True,
+        },
+    )
+
+def _test_java_single_jar_stamp_attr_auto_stamp_flag_enabled_impl(env, target):
+    assert_that_action = env.expect.that_target(target).action_named("JavaSingleJar")
+    assert_that_action.contains_flag_values([
+        ("--build_info_file", _BUILD_INFO_PATH + "/non_volatile_file.properties"),
+        ("--build_info_file", _BUILD_INFO_PATH + "/volatile_file.properties"),
+    ])
+
+def _test_java_single_jar_stamp_attr_auto_stamp_flag_disabled(name):
+    util.helper_target(
+        java_single_jar,
+        name = name + "/jar",
+        stamp = -1,
+        exclude_build_data = False,
+    )
+
+    analysis_test(
+        name = name,
+        impl = _test_java_single_jar_stamp_attr_auto_stamp_flag_disabled_impl,
+        target = name + "/jar",
+        config_settings = {
+            "//command_line_option:stamp": False,
+        },
+    )
+
+def _test_java_single_jar_stamp_attr_auto_stamp_flag_disabled_impl(env, target):
+    assert_that_action = env.expect.that_target(target).action_named("JavaSingleJar")
+    assert_that_action.contains_flag_values([
+        ("--build_info_file", _BUILD_INFO_PATH + "/redacted_file.properties"),
+    ])
+
+def java_single_jar_tests(name):
+    test_suite(
+        name = name,
+        tests = [
+            _test_java_single_jar_basic,
+            _test_java_single_jar_force_enable_stamping,
+            _test_java_single_jar_force_disable_stamping,
+            _test_java_single_jar_stamping_enabled_build_data_excluded_fails,
+            _test_java_single_jar_stamp_attr_auto_stamp_flag_enabled,
+            _test_java_single_jar_stamp_attr_auto_stamp_flag_disabled,
+        ],
+    )