Use the `launcher_maker` toolchain if available (#294)

This avoids an unnecessary dependency on a C++ toolchain matching the target platform when not building for Windows.

Also fix the configuration of the launcher, which should be built for the target platform.

Closes #294

COPYBARA_INTEGRATE_REVIEW=https://github.com/bazelbuild/rules_java/pull/294 from fmeum:cross-compile-to-unix 13f097a3c541f603e091c139a4543b632382c9aa
PiperOrigin-RevId: 762307907
Change-Id: I1e172a81bd198b02032a705c459ead48b0012aea
diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml
index 6dc8cf3..18088a2 100644
--- a/.bazelci/presubmit.yml
+++ b/.bazelci/presubmit.yml
@@ -42,16 +42,33 @@
 flags_workspace_integration: &flags_workspace_integration
   - "--noenable_bzlmod"
   - "--enable_workspace"
+  # TODO(hvd): remove after Bazel 8.3.0
+  - "--repositories_without_autoloads=bazel_features_version,bazel_features_globals"
 
 buildifier: latest
 
 matrix:
   all_platforms: ["ubuntu2004", "macos", "macos_arm64", "windows"]
-  integration_platforms: ["ubuntu2004", "macos", "macos_arm64", "windows"]
   bazel: ["7.6.1", "8.2.1", "last_green"] # Bazel 6 tested separately, needs different flags
-  modern_bazel: ["8.2.1", "last_green"] # Fully supported Bazel versions
+  modern_bazel: ["last_green"] # Fully supported Bazel versions
 
 tasks:
+# Bazel 9+
+  build_and_test:
+    name: "Bazel {modern_bazel}"
+    bazel: ${{ modern_bazel }}
+    platform: ${{ all_platforms }}
+    build_targets: *build_targets
+    test_targets: *test_targets
+# Bazel 8.x
+  build_and_test_bazel8:
+    name: "Bazel 8.2.1"
+    bazel: "8.2.1"
+    platform: ${{ all_platforms }}
+    build_targets: *build_targets
+    test_targets: *test_targets
+    test_flags:
+    - "--test_tag_filters=-min_bazel_9"
 # Bazel 7.x
   build_and_test_bazel7:
     name: "Bazel 7.6.1"
@@ -60,14 +77,7 @@
     build_targets: *build_targets
     test_targets: *test_targets
     test_flags:
-      - "--test_tag_filters=-min_bazel_8"
-# Bazel 8+
-  build_and_test:
-    name: "Bazel {modern_bazel}"
-    bazel: ${{ modern_bazel }}
-    platform: ${{ all_platforms }}
-    build_targets: *build_targets
-    test_targets: *test_targets
+      - "--test_tag_filters=-min_bazel_8,-min_bazel_9"
 # Bazel 6.x
   build_and_test_bazel6:
     name: "Bazel 6.5.0"
@@ -76,11 +86,11 @@
     build_targets: *build_targets_bazel6
     test_targets: *test_targets_bazel6
     test_flags:
-      - "--test_tag_filters=-min_bazel_7,-min_bazel_8"
+      - "--test_tag_filters=-min_bazel_7,-min_bazel_8,-min_bazel_9"
   ubuntu2004_integration_bazel6:
     name: "Integration w/ Bazel 6.5.0"
     bazel: 6.5.0
-    platform: ${{ integration_platforms }}
+    platform: ${{ all_platforms }}
     working_directory: "test/repo"
     shell_commands:
     - sh setup.sh
@@ -93,7 +103,7 @@
   integration_build_and_test:
     name: "Integration w/ Bazel {bazel}"
     bazel: ${{ bazel }}
-    platform: ${{ integration_platforms }}
+    platform: ${{ all_platforms }}
     working_directory: "test/repo"
     shell_commands:
     - sh setup.sh
@@ -104,7 +114,7 @@
   integration_build_and_test_workspace:
     name: "Integration (WORKSPACE) w/ Bazel {bazel}"
     bazel: ${{ bazel }}
-    platform: ${{ integration_platforms }}
+    platform: ${{ all_platforms }}
     working_directory: "test/repo"
     shell_commands:
     - sh setup.sh
diff --git a/MODULE.bazel b/MODULE.bazel
index 4b70044..bcfd03a 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -7,14 +7,7 @@
 
 bazel_dep(name = "platforms", version = "0.0.11")
 bazel_dep(name = "rules_cc", version = "0.0.15")
-bazel_dep(name = "bazel_features", version = "1.28.0")
-archive_override(
-    module_name = "bazel_features",
-    integrity = "sha256-SOPLvKDy+RN7GHKN8eFjQ+58Wx4Isj+vcXoUluBqxLo=",
-    strip_prefix = "bazel_features-59915eb2ca215c7b2266c83c49bb7522a5b6737f",
-    urls = ["https://github.com/bazel-contrib/bazel_features/archive/59915eb2ca215c7b2266c83c49bb7522a5b6737f.zip"],
-)
-
+bazel_dep(name = "bazel_features", version = "1.30.0")
 bazel_dep(name = "bazel_skylib", version = "1.6.1")
 bazel_dep(name = "protobuf", version = "27.0", repo_name = "com_google_protobuf")
 bazel_dep(name = "zlib", version = "1.3.1.bcr.5")
diff --git a/WORKSPACE b/WORKSPACE
index 6b8beb6..acd7cae 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -41,6 +41,10 @@
 
 rules_java_dependencies()
 
+load("@bazel_features//:deps.bzl", "bazel_features_deps")
+
+bazel_features_deps()
+
 load("@com_google_protobuf//bazel/private:proto_bazel_features.bzl", "proto_bazel_features")  # buildifier: disable=bzl-visibility
 
 proto_bazel_features(name = "proto_bazel_features")
@@ -63,14 +67,3 @@
 load("//test:repositories.bzl", "test_repositories")
 
 test_repositories()
-
-http_archive(
-    name = "bazel_features",
-    sha256 = "48e3cbbca0f2f9137b18728df1e16343ee7c5b1e08b23faf717a1496e06ac4ba",
-    strip_prefix = "bazel_features-59915eb2ca215c7b2266c83c49bb7522a5b6737f",
-    url = "https://github.com/bazel-contrib/bazel_features/archive/59915eb2ca215c7b2266c83c49bb7522a5b6737f.zip",
-)
-
-load("@bazel_features//:deps.bzl", "bazel_features_deps")
-
-bazel_features_deps()
diff --git a/distro/relnotes.bzl b/distro/relnotes.bzl
index 5641bf7..ce04d50 100644
--- a/distro/relnotes.bzl
+++ b/distro/relnotes.bzl
@@ -18,6 +18,16 @@
 ~~~
 
 **WORKSPACE setup**
+
+With Bazel 8.0.0 and before 8.3.0, add the following to your `.bazelrc` file:
+
+~~~
+# https://github.com/bazelbuild/bazel/pull/26119
+common --repositories_without_autoloads=bazel_features_version,bazel_features_globals
+~~~
+
+In all cases, add the following to your `WORKSPACE` file:
+
 ~~~
 load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
 http_archive(
@@ -31,6 +41,9 @@
 load("@rules_java//java:rules_java_deps.bzl", "rules_java_dependencies")
 rules_java_dependencies()
 
+load("@bazel_features//:deps.bzl", "bazel_features_deps")
+bazel_features_deps()
+
 # note that the following line is what is minimally required from protobuf for the java rules
 # consider using the protobuf_deps() public API from @com_google_protobuf//:protobuf_deps.bzl
 load("@com_google_protobuf//bazel/private:proto_bazel_features.bzl", "proto_bazel_features")  # buildifier: disable=bzl-visibility
diff --git a/java/bazel/rules/bazel_java_binary.bzl b/java/bazel/rules/bazel_java_binary.bzl
index def12d1..cbbe0de 100644
--- a/java/bazel/rules/bazel_java_binary.bzl
+++ b/java/bazel/rules/bazel_java_binary.bzl
@@ -13,6 +13,7 @@
 # limitations under the License.
 """Bazel java_binary rule"""
 
+load("@bazel_features//:features.bzl", "bazel_features")
 load("@bazel_skylib//lib:paths.bzl", "paths")
 load("@rules_cc//cc:find_cc_toolchain.bzl", "use_cc_toolchain")
 load("//java/common:java_semantics.bzl", "semantics")
@@ -202,6 +203,14 @@
 
     return ctx.actions.declare_file(executable_name)
 
+_LAUNCHER_MAKER_TOOLCHAIN_TYPE = "@bazel_tools//tools/launcher:launcher_maker_toolchain_type"
+_LAUNCHER_MAKER_TOOLCHAIN = config_common.toolchain_type(_LAUNCHER_MAKER_TOOLCHAIN_TYPE, mandatory = True)
+
+def _find_launcher_maker(ctx):
+    if bazel_features.rules._has_launcher_maker_toolchain:
+        return ctx.toolchains[_LAUNCHER_MAKER_TOOLCHAIN_TYPE].binary
+    return ctx.executable._windows_launcher_maker
+
 def _create_stub(ctx, java_attrs, launcher, executable, jvm_flags, main_class, coverage_main_class):
     java_runtime_toolchain = semantics.find_java_runtime_toolchain(ctx)
     java_executable = helper.get_java_executable(ctx, java_runtime_toolchain, launcher)
@@ -282,11 +291,13 @@
     # TODO(b/295221112): Change to use the "launcher" attribute (only windows use a fixed _launcher attribute)
     launcher_artifact = ctx.executable._launcher
     ctx.actions.run(
-        executable = ctx.executable._windows_launcher_maker,
+        executable = _find_launcher_maker(ctx),
         inputs = [launcher_artifact],
         outputs = [executable],
         arguments = [launcher_artifact.path, launch_info, executable.path],
         use_default_shell_env = True,
+        toolchain = _LAUNCHER_MAKER_TOOLCHAIN_TYPE if bazel_features.rules._has_launcher_maker_toolchain else None,
+        mnemonic = "JavaLauncherMaker",
     )
     return executable
 
@@ -308,6 +319,8 @@
         provides = [JavaInfo],
         toolchains = [semantics.JAVA_TOOLCHAIN] + use_cc_toolchain() + (
             [semantics.JAVA_RUNTIME_TOOLCHAIN] if executable or test else []
+        ) + (
+            [_LAUNCHER_MAKER_TOOLCHAIN] if bazel_features.rules._has_launcher_maker_toolchain else []
         ),
         # TODO(hvd): replace with filegroups?
         outputs = {
@@ -340,16 +353,18 @@
         ),
         "_test_support": attr.label(default = _compute_test_support),
         "_launcher": attr.label(
-            cfg = "exec",
+            cfg = "target",
             executable = True,
             default = "@bazel_tools//tools/launcher:launcher",
         ),
+    },
+    {
         "_windows_launcher_maker": attr.label(
             default = "@bazel_tools//tools/launcher:launcher_maker",
             cfg = "exec",
             executable = True,
         ),
-    },
+    } if not bazel_features.rules._has_launcher_maker_toolchain else {},
 )
 
 def make_java_binary(executable):
diff --git a/java/rules_java_deps.bzl b/java/rules_java_deps.bzl
index 0c96d8c..b32edd2 100644
--- a/java/rules_java_deps.bzl
+++ b/java/rules_java_deps.bzl
@@ -210,6 +210,15 @@
         ],
     )
 
+def bazel_features_repo():
+    maybe(
+        http_archive,
+        name = "bazel_features",
+        sha256 = "a660027f5a87f13224ab54b8dc6e191693c554f2692fcca46e8e29ee7dabc43b",
+        strip_prefix = "bazel_features-1.30.0",
+        url = "https://github.com/bazel-contrib/bazel_features/releases/download/v1.30.0/bazel_features-v1.30.0.tar.gz",
+    )
+
 def rules_java_dependencies():
     """An utility method to load non-toolchain dependencies of rules_java.
 
@@ -223,3 +232,4 @@
     zlib_repo()
     absl_repo()
     rules_license_repo()
+    bazel_features_repo()
diff --git a/test/java/bazel/BUILD.bazel b/test/java/bazel/BUILD.bazel
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/java/bazel/BUILD.bazel
diff --git a/test/java/bazel/rules/BUILD.bazel b/test/java/bazel/rules/BUILD.bazel
new file mode 100644
index 0000000..3bf8f43
--- /dev/null
+++ b/test/java/bazel/rules/BUILD.bazel
@@ -0,0 +1,3 @@
+load(":java_binary_tests.bzl", "java_binary_tests")
+
+java_binary_tests(name = "java_binary_tests")
diff --git a/test/java/bazel/rules/java_binary_tests.bzl b/test/java/bazel/rules/java_binary_tests.bzl
new file mode 100644
index 0000000..32036d8
--- /dev/null
+++ b/test/java/bazel/rules/java_binary_tests.bzl
@@ -0,0 +1,55 @@
+"""Tests for the Bazel java_binary rule"""
+
+load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite")
+load("@rules_testing//lib:util.bzl", "util")
+load("//java:java_binary.bzl", "java_binary")
+
+def _test_java_binary_cross_compilation_to_unix(name):
+    # A Unix platform that:
+    # - has a JDK
+    # - does not require a launcher
+    # - is not supported by the default C++ toolchain
+    util.helper_target(
+        native.platform,
+        name = name + "/platform",
+        constraint_values = [
+            "@platforms//os:linux",
+            "@platforms//cpu:s390x",
+        ],
+    )
+
+    util.helper_target(
+        java_binary,
+        name = name + "/bin",
+        srcs = ["java/C.java"],
+        main_class = "C",
+    )
+
+    analysis_test(
+        name = name,
+        impl = _test_java_binary_cross_compilation_to_unix_impl,
+        target = name + "/bin",
+        config_settings = {
+            "//command_line_option:platforms": [Label(name + "/platform")],
+        },
+        # Requires the launcher_maker toolchain.
+        attr_values = {"tags": ["min_bazel_9"]},
+    )
+
+def _test_java_binary_cross_compilation_to_unix_impl(env, target):
+    # The main assertion is that analysis succeeds, but verify the absence of a
+    # binary launcher for good measure. We do this by checking that the output
+    # executable is the stub script, and not a bespoke launcher
+    executable = target[DefaultInfo].files_to_run.executable.short_path
+    assert_action = env.expect.that_target(target).action_generating(executable)
+    assert_action.mnemonic().equals("TemplateExpand")
+    assert_action.substitutions().keys().contains("%jvm_flags%")
+    assert_action.inputs().contains_exactly(["java/bazel/rules/java_stub_template.txt"])
+
+def java_binary_tests(name):
+    test_suite(
+        name = name,
+        tests = [
+            _test_java_binary_cross_compilation_to_unix,
+        ],
+    )
diff --git a/test/repo/WORKSPACE b/test/repo/WORKSPACE
index eb68085..c272975 100644
--- a/test/repo/WORKSPACE
+++ b/test/repo/WORKSPACE
@@ -9,6 +9,10 @@
 
 rules_java_dependencies()
 
+load("@bazel_features//:deps.bzl", "bazel_features_deps")
+
+bazel_features_deps()
+
 load("@com_google_protobuf//bazel/private:proto_bazel_features.bzl", "proto_bazel_features")  # buildifier: disable=bzl-visibility
 
 proto_bazel_features(name = "proto_bazel_features")