fix: point runfiles libraries at the manifest when there is no runfiles tree

Where runfiles trees are not built, which is the default on Windows,
RUNFILES_DIR names a directory holding nothing but the manifest, and a runfiles
library has to read that manifest instead. Bazel does not put
RUNFILES_MANIFEST_FILE in a build action's environment
(bazelbuild/bazel#7994), so the launcher exported only RUNFILES_DIR and
@bazel/runfiles fell back to walking a tree that was not there:

    RUNFILES_MANIFEST_FILE should have been set but wasn't.
    falling back to using runfiles symlinks.
    cfg_probe.txt not found in runfiles at _main/js/private/test/js_run_binary/cfg_probe.txt

Export RUNFILES_MANIFEST_FILE from the launcher when the tree was not laid out
and the manifest is really there. //js/private/test/js_run_binary:target_cfg_test
now runs on Windows rather than failing to build, so it is no longer marked
incompatible.

The two RUNFILES_ONLY cases stay marked, with the reason corrected: they assert
that cfg_probe.txt is *absent* from the target bin directory, which only holds
for an action that cannot see files it did not declare. Other targets in that
package do build cfg_probe.txt in the target configuration, so without a sandbox
the probe finds that copy in the execroot. no_copy_to_bin_test's reason is
likewise corrected: it requires a file by a path relative to the entry point,
and require() does not consult a manifest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
diff --git a/js/private/bash.bzl b/js/private/bash.bzl
index 5341565..80dfd06 100644
--- a/js/private/bash.bzl
+++ b/js/private/bash.bzl
@@ -113,4 +113,18 @@
 # Set RUNFILES_DIR if not already set so that tools such as @bazel/runfiles
 # can locate runfiles without requiring RUNFILES to be exported.
 export RUNFILES_DIR="${RUNFILES_DIR:-$RUNFILES}"
+
+# Where runfiles trees are not built, which is the default on Windows, RUNFILES_DIR names a
+# directory holding nothing but the manifest and a runfiles library has to read that manifest
+# instead. Bazel does not put RUNFILES_MANIFEST_FILE in a build action's environment
+# (https://github.com/bazelbuild/bazel/issues/7994), so point at the manifest here when the tree
+# was not laid out. Only ever when the file is really there: a library that finds neither a tree
+# nor a manifest reports that better than one sent to a path that does not exist.
+if [ -z "${RUNFILES_MANIFEST_FILE:-}" ] && [ ! -d "$RUNFILES/$JS_BINARY__WORKSPACE" ]; then
+    if [ -f "$RUNFILES/MANIFEST" ]; then
+        export RUNFILES_MANIFEST_FILE="$RUNFILES/MANIFEST"
+    elif [ -f "${RUNFILES}_manifest" ]; then
+        export RUNFILES_MANIFEST_FILE="${RUNFILES}_manifest"
+    fi
+fi
 """
diff --git a/js/private/test/js_run_binary/BUILD.bazel b/js/private/test/js_run_binary/BUILD.bazel
index 59035b0..a729119 100644
--- a/js/private/test/js_run_binary/BUILD.bazel
+++ b/js/private/test/js_run_binary/BUILD.bazel
@@ -8,11 +8,12 @@
 
 package(default_testonly = True)
 
-# find_cfg_probe.mjs locates its probe file with the @bazel/runfiles library, so these cases need a
-# runfiles tree, which Windows does not build by default. Bazel does not set the runfiles
-# environment variables for the tool of a build action either, so without a tree the library has
-# nothing to resolve against.
-_REQUIRES_RUNFILES = select({
+# The RUNFILES_ONLY cases below assert that cfg_probe.txt is *absent* from the target bin directory,
+# which only holds for an action that cannot see files it did not declare -- a sandboxed one. Other
+# targets in this package do build cfg_probe.txt in the target configuration, so on Windows, which
+# has no sandbox, the probe finds that copy sitting in the execroot. What the launcher resolves
+# through the runfiles manifest is not the issue; target_cfg_test below runs on Windows.
+_REQUIRES_SANDBOX = select({
     "@platforms//os:windows": ["@platforms//:incompatible"],
     "//conditions:default": [],
 })
@@ -41,7 +42,7 @@
     name = "runfiles_location",
     silent_on_success = False,
     stdout = "runfiles_location.txt",
-    target_compatible_with = _REQUIRES_RUNFILES,
+    target_compatible_with = _REQUIRES_SANDBOX,
     tool = ":find_cfg_probe",
     use_execroot_entry_point = False,
 )
@@ -50,12 +51,12 @@
     name = "runfiles_test",
     actual = ":runfiles_location.txt",
     expected = "RUNFILES_ONLY",
-    target_compatible_with = _REQUIRES_RUNFILES,
+    target_compatible_with = _REQUIRES_SANDBOX,
 )
 
 label_capture(
     name = "label_capture",
-    target_compatible_with = _REQUIRES_RUNFILES,
+    target_compatible_with = _REQUIRES_SANDBOX,
     tool = ":find_cfg_probe",
 )
 
@@ -63,7 +64,7 @@
     name = "label_capture_test",
     actual = ":label_capture_stdout.txt",
     expected = "RUNFILES_ONLY",
-    target_compatible_with = _REQUIRES_RUNFILES,
+    target_compatible_with = _REQUIRES_SANDBOX,
 )
 
 # With use_execroot_entry_point = True, the tool's data dependencies are built
@@ -73,7 +74,6 @@
     name = "target_cfg_location",
     silent_on_success = False,
     stdout = "target_cfg_location.txt",
-    target_compatible_with = _REQUIRES_RUNFILES,
     tool = ":find_cfg_probe",
     use_execroot_entry_point = True,
 )
@@ -89,7 +89,6 @@
     name = "target_cfg_test",
     file1 = ":target_cfg_location.txt",
     file2 = ":expected_target_cfg_location.txt",
-    target_compatible_with = _REQUIRES_RUNFILES,
 )
 
 # Verify that dependencies passed via the srcs parameter are always built for
diff --git a/js/private/test/no_copy_to_bin/BUILD.bazel b/js/private/test/no_copy_to_bin/BUILD.bazel
index ad762ff..3431f8c 100644
--- a/js/private/test/no_copy_to_bin/BUILD.bazel
+++ b/js/private/test/no_copy_to_bin/BUILD.bazel
@@ -7,9 +7,10 @@
     data = ["//js/private/test/no_copy_to_bin/subpkg:42.js"],
     entry_point = "test_no_copy_to_bin.js",
     no_copy_to_bin = ["//js/private/test/no_copy_to_bin/subpkg:42.js"],
-    # A file kept out of the bin directory is only laid out next to the program in the runfiles
-    # tree, so there is nowhere for the program to require it from on Windows, which does not build
-    # one by default.
+    # The program requires this file by a path relative to itself, which node resolves against the
+    # entry point's own directory. A file kept out of the bin directory only sits next to the entry
+    # point in the runfiles tree, and Windows does not build one by default; a runfiles manifest is
+    # no help here since require() does not consult one.
     target_compatible_with = select({
         "@platforms//os:windows": ["@platforms//:incompatible"],
         "//conditions:default": [],
diff --git a/js/private/test/snapshots/launcher.sh b/js/private/test/snapshots/launcher.sh
index d913138..f0059a3 100644
--- a/js/private/test/snapshots/launcher.sh
+++ b/js/private/test/snapshots/launcher.sh
@@ -300,6 +300,20 @@
 # can locate runfiles without requiring RUNFILES to be exported.
 export RUNFILES_DIR="${RUNFILES_DIR:-$RUNFILES}"
 
+# Where runfiles trees are not built, which is the default on Windows, RUNFILES_DIR names a
+# directory holding nothing but the manifest and a runfiles library has to read that manifest
+# instead. Bazel does not put RUNFILES_MANIFEST_FILE in a build action's environment
+# (https://github.com/bazelbuild/bazel/issues/7994), so point at the manifest here when the tree
+# was not laid out. Only ever when the file is really there: a library that finds neither a tree
+# nor a manifest reports that better than one sent to a path that does not exist.
+if [ -z "${RUNFILES_MANIFEST_FILE:-}" ] && [ ! -d "$RUNFILES/$JS_BINARY__WORKSPACE" ]; then
+    if [ -f "$RUNFILES/MANIFEST" ]; then
+        export RUNFILES_MANIFEST_FILE="$RUNFILES/MANIFEST"
+    elif [ -f "${RUNFILES}_manifest" ]; then
+        export RUNFILES_MANIFEST_FILE="${RUNFILES}_manifest"
+    fi
+fi
+
 JS_BINARY__RUNFILES="$RUNFILES"
 export JS_BINARY__RUNFILES