Build bootclasspath in a UTF-8 environment

Copybara Import from https://github.com/bazelbuild/rules_java/pull/243

BEGIN_PUBLIC
Build bootclasspath in a UTF-8 environment (#243)

`java` and `javac` convert file and classpaths to absolute paths and thus require a UTF-8 locale to work under a path that contains non-ASCII characters.

Unblocks https://github.com/bazelbuild/bazel/pull/24457

Closes #243
END_PUBLIC

COPYBARA_INTEGRATE_REVIEW=https://github.com/bazelbuild/rules_java/pull/243 from fmeum:utf8-environment 05813e491f73a167bf5c7d5e723d2417649f3867
PiperOrigin-RevId: 700695134
Change-Id: I2f5753720ec3c838a4dd8b6aabf1050c6935ef3d
diff --git a/MODULE.bazel b/MODULE.bazel
index 706c277..b14fcbf 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -97,6 +97,7 @@
 bazel_dep(name = "rules_pkg", version = "0.9.1", dev_dependency = True)
 bazel_dep(name = "stardoc", version = "0.7.1", dev_dependency = True)
 bazel_dep(name = "rules_shell", version = "0.2.0", dev_dependency = True)
+bazel_dep(name = "rules_testing", version = "0.7.0", dev_dependency = True)
 
 test_repositories = use_extension("//test:repositories.bzl", "test_repositories_ext", dev_dependency = True)
 use_repo(test_repositories, "guava", "truth")
diff --git a/WORKSPACE b/WORKSPACE
index d5e6a7e..161fa9c 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -53,6 +53,13 @@
 
 stardoc_repositories()
 
+http_archive(
+    name = "rules_testing",
+    sha256 = "28c2d174471b587bf0df1fd3a10313f22c8906caf4050f8b46ec4648a79f90c3",
+    strip_prefix = "rules_testing-0.7.0",
+    url = "https://github.com/bazelbuild/rules_testing/releases/download/v0.7.0/rules_testing-v0.7.0.tar.gz",
+)
+
 load("//test:repositories.bzl", "test_repositories")
 
 test_repositories()
diff --git a/test/analysis/BUILD.bazel b/test/analysis/BUILD.bazel
new file mode 100644
index 0000000..754e1dd
--- /dev/null
+++ b/test/analysis/BUILD.bazel
@@ -0,0 +1,5 @@
+load(":bootclasspath_tests.bzl", "bootclasspath_tests")
+
+bootclasspath_tests(
+    name = "bootclasspath_tests",
+)
diff --git a/test/analysis/bootclasspath_tests.bzl b/test/analysis/bootclasspath_tests.bzl
new file mode 100644
index 0000000..c862d83
--- /dev/null
+++ b/test/analysis/bootclasspath_tests.bzl
@@ -0,0 +1,25 @@
+"""Tests for the bootclasspath rule."""
+
+load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite")
+load("@rules_testing//lib:truth.bzl", "subjects")
+
+def _test_utf_8_environment(name):
+    analysis_test(
+        name = name,
+        impl = _test_utf_8_environment_impl,
+        target = Label("//toolchains:platformclasspath"),
+    )
+
+def _test_utf_8_environment_impl(env, target):
+    for action in target.actions:
+        env_subject = env.expect.where(action = action).that_dict(action.env)
+        env_subject.keys().contains("LC_CTYPE")
+        env_subject.get("LC_CTYPE", factory = subjects.str).contains("UTF-8")
+
+def bootclasspath_tests(name):
+    test_suite(
+        name = name,
+        tests = [
+            _test_utf_8_environment,
+        ],
+    )
diff --git a/toolchains/BUILD b/toolchains/BUILD
index 191c311..ffbffc4 100644
--- a/toolchains/BUILD
+++ b/toolchains/BUILD
@@ -15,6 +15,7 @@
     "java_runtime_version_alias",
     "java_toolchain_alias",
 )
+load(":utf8_environment.bzl", "utf8_environment")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -282,6 +283,11 @@
     }),
 )
 
+utf8_environment(
+    name = "utf8_environment",
+    visibility = ["//visibility:private"],
+)
+
 bootclasspath(
     name = "platformclasspath",
     src = "DumpPlatformClassPath.java",
diff --git a/toolchains/default_java_toolchain.bzl b/toolchains/default_java_toolchain.bzl
index a6d200b..75d05f7 100644
--- a/toolchains/default_java_toolchain.bzl
+++ b/toolchains/default_java_toolchain.bzl
@@ -16,6 +16,7 @@
 
 load("//java/common:java_common.bzl", "java_common")
 load("//java/toolchains:java_toolchain.bzl", "java_toolchain")
+load(":utf8_environment.bzl", "Utf8EnvironmentInfo")
 
 # JVM options, without patching java.compiler and jdk.compiler modules.
 BASE_JDK9_JVM_OPTS = [
@@ -223,6 +224,7 @@
 
 def _bootclasspath_impl(ctx):
     exec_javabase = ctx.attr.java_runtime_alias[java_common.JavaRuntimeInfo]
+    env = ctx.attr._utf8_environment[Utf8EnvironmentInfo].environment
 
     class_dir = ctx.actions.declare_directory("%s_classes" % ctx.label.name)
 
@@ -243,6 +245,7 @@
         inputs = [ctx.file.src] + ctx.files.java_runtime_alias,
         outputs = [class_dir],
         arguments = [args],
+        env = env,
         execution_requirements = _SUPPORTS_PATH_MAPPING,
     )
 
@@ -281,6 +284,7 @@
         inputs = inputs,
         outputs = [bootclasspath],
         arguments = [args],
+        env = env,
         execution_requirements = _SUPPORTS_PATH_MAPPING,
     )
     return [
@@ -304,6 +308,10 @@
             cfg = "exec",
             allow_single_file = True,
         ),
+        "_utf8_environment": attr.label(
+            default = ":utf8_environment",
+            cfg = "exec",
+        ),
     },
     toolchains = [_JAVA_BOOTSTRAP_RUNTIME_TOOLCHAIN_TYPE],
 )
diff --git a/toolchains/utf8_environment.bzl b/toolchains/utf8_environment.bzl
new file mode 100644
index 0000000..5b4d554
--- /dev/null
+++ b/toolchains/utf8_environment.bzl
@@ -0,0 +1,48 @@
+# Copyright 2024 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.
+
+"""
+Determines the environment required for Java actions to support UTF-8.
+"""
+
+visibility("private")
+
+Utf8EnvironmentInfo = provider(
+    doc = "The environment required for Java actions to support UTF-8.",
+    fields = {
+        "environment": "The environment to use for Java actions to support UTF-8.",
+    },
+)
+
+# The default UTF-8 locale on all recent Linux distributions. It is also available in Cygwin and
+# MSYS2, but doesn't matter for determining the JVM's platform encoding on Windows, which always
+# uses the active code page.
+_DEFAULT_UTF8_ENVIRONMENT = Utf8EnvironmentInfo(environment = {"LC_CTYPE": "C.UTF-8"})
+
+# macOS doesn't have the C.UTF-8 locale, but en_US.UTF-8 is available and works the same way.
+_MACOS_UTF8_ENVIRONMENT = Utf8EnvironmentInfo(environment = {"LC_CTYPE": "en_US.UTF-8"})
+
+def _utf8_environment_impl(ctx):
+    if ctx.target_platform_has_constraint(ctx.attr._macos_constraint[platform_common.ConstraintValueInfo]):
+        return _MACOS_UTF8_ENVIRONMENT
+    else:
+        return _DEFAULT_UTF8_ENVIRONMENT
+
+utf8_environment = rule(
+    _utf8_environment_impl,
+    attrs = {
+        "_macos_constraint": attr.label(default = "@platforms//os:macos"),
+    },
+    doc = "Returns a suitable environment for Java actions to support UTF-8.",
+)