Fix native library path separator on Windows (#383)

On Windows, java.library.path is split by ";" instead of ":".

Regression test added.

Note for Googlers: this fix is in service of b/149162146#comment19

Closes #383

COPYBARA_INTEGRATE_REVIEW=https://github.com/bazelbuild/rules_java/pull/383 from gharrma:fix-java-native-lib-handling-on-windows b0e4c3efbe33f23d05145001ca9df54ba4c6e7ed
PiperOrigin-RevId: 986888472
Change-Id: I492266c118fba328e651accf005c0ba7945ed0a3
diff --git a/java/common/rules/impl/java_binary_impl.bzl b/java/common/rules/impl/java_binary_impl.bzl
index 379c213..691d66d 100644
--- a/java/common/rules/impl/java_binary_impl.bzl
+++ b/java/common/rules/impl/java_binary_impl.bzl
@@ -169,8 +169,9 @@
     native_libs_dirs = collect_native_deps_dirs(depset(transitive = native_libs_depsets))
     if native_libs_dirs:
         prefix = "${JAVA_RUNFILES}/" + ctx.workspace_name + "/"
+        separator = ";" if helper.is_target_platform_windows(ctx) else ":"
         jvm_flags.append("-Djava.library.path=%s" % (
-            ":".join([prefix + d for d in native_libs_dirs])
+            separator.join([prefix + d for d in native_libs_dirs])
         ))
 
     jvm_flags.extend(ctx.fragments.java.default_jvm_opts)
diff --git a/test/java/common/rules/common_launcher_java_binary_tests.bzl b/test/java/common/rules/common_launcher_java_binary_tests.bzl
index c1e0128..ec403a0 100644
--- a/test/java/common/rules/common_launcher_java_binary_tests.bzl
+++ b/test/java/common/rules/common_launcher_java_binary_tests.bzl
@@ -276,6 +276,47 @@
                 "${{JAVA_RUNFILES}}/{workspace}/_solib_{cpu}/_//{package}:{test_name}/jni.so___{package}",
             ])
 
+# Regression test: the native library path separator on Windows is ";" instead of ":".
+def _test_java_binary_native_library_path_separator(name):
+    util.helper_target(
+        java_binary,
+        name = name + "/app",
+        srcs = ["DoesNotMatter.java"],
+        deps = [
+            name + "/lib1",
+            name + "/lib2",
+        ],
+    )
+
+    # Define two native libs (in different directories so java.library.path has multiple entries).
+    util.helper_target(
+        cc_binary,
+        name = name + "/lib1",
+        srcs = ["lib1.so"],
+    )
+    util.helper_target(
+        cc_binary,
+        name = name + "/lib2",
+        srcs = ["subdir/lib2.so"],
+    )
+
+    analysis_test(
+        name = name,
+        # The Starlark rules are only used with Bazel 8 onwards.
+        attr_values = {"tags": ["min_bazel_8"]},
+        attrs = {
+            "_windows_constraints": attr.label_list(default = ["@platforms//os:windows"]),
+            "_cc_toolchain": attr.label(default = Label("@bazel_tools//tools/cpp:current_cc_toolchain")),
+        },
+        impl = _test_java_binary_native_library_path_separator_impl,
+        target = name + "/app",
+    )
+
+def _test_java_binary_native_library_path_separator_impl(env, target):
+    # We should find two libraries on java.library.path (if the correct path separator was used).
+    native_library_paths = expect_that_executable.of_target(env, target).native_library_paths()
+    native_library_paths.has_size(2)
+
 # Regression test for bug 2774317: Problems with inner classes as main class.
 def _test_java_binary_inner_class(name):
     util.helper_target(
@@ -596,6 +637,7 @@
             _test_java_test_main_class_with_dot,
             _test_java_test_has_assertions_enabled,
             _test_java_binary_native_library_path_includes_transitive_deps,
+            _test_java_binary_native_library_path_separator,
             _test_java_binary_inner_class,
             _test_java_test_inner_class,
             _test_java_binary_strict_java_deps_flag,
diff --git a/test/java/testutil/binary_executable_subject.bzl b/test/java/testutil/binary_executable_subject.bzl
index df5c477..29df32d 100644
--- a/test/java/testutil/binary_executable_subject.bzl
+++ b/test/java/testutil/binary_executable_subject.bzl
@@ -3,6 +3,7 @@
 load("@rules_cc//cc/common:cc_common.bzl", "cc_common")
 load("@rules_testing//lib:truth.bzl", "subjects")
 load("//java/common:java_semantics.bzl", "semantics")
+load("//java/common/rules:java_helper.bzl", "helper")
 
 def _of_target(env, target):
     executable = target[DefaultInfo].files_to_run.executable.short_path
@@ -11,7 +12,7 @@
         java_start_class = lambda: _java_start_class_subject(action_subject),
         test_suite = lambda: _test_suite_subject(action_subject),
         jvm_flags = lambda: _jvm_flags_subject(action_subject),
-        native_library_paths = lambda: _native_library_paths_subject(action_subject),
+        native_library_paths = lambda: _native_library_paths_subject(env, action_subject),
     )
     return public
 
@@ -44,7 +45,8 @@
             desc = "jvm_flags",
         ).offset(0, factory = subjects.str).split("\t")
 
-def _native_library_paths_subject(action):
+def _native_library_paths_subject(env, action):
+    separator = ";" if helper.is_target_platform_windows(env.ctx) else ":"
     return _jvm_flags_subject(action).transform(
         filter = lambda e: e.startswith("-Djava.library.path="),
         map_each = lambda e: e.split("=", 1)[1],
@@ -54,7 +56,7 @@
         factory = lambda actual, meta: subjects.str(actual, meta.derive(
             format_str_kwargs = {"cpu": meta.ctx.attr._cc_toolchain[cc_common.CcToolchainInfo].cpu},
         )),
-    ).split(":").transform(
+    ).split(separator).transform(
         map_each = lambda e: e.replace("_U", "_").replace("_S", "/").replace("_C", ":"),
         desc = "pretty",
         format = True,